第四届阿里云伏魔挑战赛PHP WebShell记录
LionTree WEB安全 6025浏览 · 2025-04-02 03:25

伏魔对webshell的检测主要基于模拟污点引擎,虽然介绍中也提到了AI检测和动态沙箱执行检测等其他手段,但测试中感知并不强。模拟污点引擎也是一个类似zend的虚拟机,对AST进行解释执行,从而在比较精确地获取变量值和函数调用链的同时规避动态沙箱对环境依赖及版本碎片化等问题,原理可以参考WebShell检测之「模拟污点引擎」首次公测,邀你来战!模拟执行在恶意文本检测中的最佳实践

动态函数调用

动态调用是PHP webshell最常利用的特性,首先来测试下针对动态调用的检测规则:

$a($b)调用system命令执行为例

$b明确为用户可控,此时$a在运行过程中的任意时刻(每行代码执行后)都不能包含system等敏感函数

$b的值来自file_get_contents等返回值不确定的内置函数行为也是一样的

如果直接写成$a($_GET['x'])这种的话无论$a是啥都是black,估计是直接写了个正则

$b"whoami"等敏感命令,此时对$a的每次赋值不允许包含"system",但拼接的话只要最终结果不为"system"即可

$b为其他安全的常量,此时$a的值无所谓

对于诸如 file_get_contents()phpinfo() 这类返回值不确定的内置函数调用,其返回值会被视为污点值,待遇等同"system"字符串。因此,如果想利用动态函数调用,我们需要让引擎获取到一个它认为是确定的安全值,但实际上却是恶意值。

Parser

php5和php7的语法规则存在一定的差异,有一些语法是不兼容的。如果污点模拟引擎采用的parser只支持某个版本就会存在特定版本的绕过。基于这个思路我去查看了下最常见的PHP-Parser对php各版本的支持情况,即使伏魔使用的不是PHP-Parser也可能存在类似的问题(感觉用的应该是魔改版?)。

https://github.com/nikic/PHP-Parser/blob/7d3039c37823003d576247868fe755f3d7ec70b8/doc/0_Introduction.markdown 中可以看到PHP-Parser在对于变量表达式的解析只支持PHP7的规则,测试发现伏魔同样存在这个问题,仅仅按照PHP7的方式进行了求值。

在PHP7中$$foo[0]先获取了$$foo的值,然后再访问索引。PHP5中则是先获取了$foo[0],最终获取了$b的值。

ini_set

伏魔对能够精确求值的内置函数进行了建模(猜测应该是直接调用了这些内置函数进行求值?),但测试发现其中一些内置函数的行为没有考虑ini配置项的影响,通过ini_set可以构造出模拟执行和真实执行的差异。

通过ini_set('bcmath.scale', 4);使得模拟执行引擎无法正确模拟bcadd的行为

ini_set动态将zend.enable_gc设置为0,影响gc_enabled()的返回值误导模拟执行引擎

伏魔对输出缓冲区中的值也进行了精确的模拟,但没考虑到通过error_append_string可以在报错信息中包含system

arg_separator.output其实是被考虑到了,但ini_set($_GET['x'],"st");这样的形式没有被禁止,此时污点模拟引擎没有模拟出是哪个配置项被设置为了"st"

不过第一个参数为$_GET['x']时伏魔会去检查第二个参数是否可能是某个回调函数,因此像unserialize_callback_func没有办法用这种方式利用

precisionarg_separator.output类似,需要用$_GET['x']来设置。这会影响print_r浮点数部分的结果,输出后再从缓冲区中取出。

将pcre.backtrack_limit设置为较低的值,使得preg_match正则回溯超过限定次数,$matches[2]$matches[1]为空。模拟执行引擎误以为两者为aaatesting

文件包含

相关的限制:

对最终包含的路径值有一个类似^\/tmp\/[\s\S]+$的正则检测,符合的话就是black,不过并不严格,允许/tmp前出现../

include_path也有类似限制

会检测sess_防止包含session文件

禁止包含__FILE__

包含自身

尽管__FILE__被禁用,但仍然可以直接包含自身文件名制造出模拟执行和真实执行的差异

写文件后包含

污点模拟引擎无法真实模拟文件的写入操作,因此如果能够实现一个内容可控的文件写入 WebShell,就可以包含写入的文件升级为一个完整的webshell。

直接error_log....

RecursiveDirectoryIterator没有被禁,可以起到类似通配符的效果,直接包含文件上传的临时文件

使用pgsqlCopyToFile写文件再包含执行

使用php://temp写入临时文件,再包含/proc/self/fd/获取systemsession_id

利用soap缓存写入恶意代码包含执行

soap缓存文件的命名非常好预测,见https://github.com/php/php-src/blob/2f1398dad934086b605073c51af3118c8eff28b1/ext/soap/php_sdl.c#L3216

恶意服务器上写一个名为<的合法wsdl文件,最终的缓存内容会包含请求的url,也就会带上<?=system(session_id(session_start()));?>

这个因为SoapClient不是默认安装的扩展被忽略了,不过这个拓展还挺常见的

控制流

对于条件非常量的条件分支语句,污点模拟引擎会确保每个分支都被遍历。对于循环语句,则仅模拟循环次数为常量的情况;当循环次数不确定时,循环中的赋值结果将直接被视为污点值。

测试发现污点模拟引擎还存在很强的容错性,即使出现了在真实php中会报出fatal error的语句也会继续执行,可以利用这一特性制造出与真实执行时不同的控制流。

类似的通过修改max_execution_time来制造fatal error

其他

使用PDOStatement::debugDumpParams将带有"system"的字符串输入到输出缓冲区中,之后再取出。这个就是伏魔单纯没考虑到PDOStatement::debugDumpParams对输出缓冲区的影响。

和jsp类似,php其实也支持多种编码,在lexer和parser的代码中很明显可以看到在zend.multibyte开启的情况下对编码的各种处理。zend.multibyte需要通过.user.ini.htaccess打开(php文档写错了说它是INI_ALL的,实际上看源码是INI_PERDIR的),伏魔也没有禁止写入这两个文件。

这里的declare(encoding='HTML-ENTITIES');效果和直接设置zend.script_encoding是类似的,后者印象中很多年前在ctf里就出现过,前者倒似乎没见人提过。

可以将真正的恶意代码/命令执行放在其他地方,php仅仅作为一个跳板。比如说fpm环境下可以仅仅构造一个SSRF。除了curl外伏魔并没有将网络相关的内置函数作为sink,类似的样本应该还可以写出不少。


0 条评论
某人
表情
可输入 255