PHP代码审计之WEB安全系列基础文章(四)之代码执行篇
【如需转载,请详细表明来源,请勿设置原创】
嗨,大家好,我是闪石星曜CyberSecurity创始人Power7089。
欢迎大家扫描下方二维码关注 “闪石星曜CyberSecurity” 公众号,这里专注分享渗透测试,Java代码审计,PHP代码审计等内容,都是非常干的干货哦。

今天为大家带来PHP代码审计基础系列文章第四篇之代码执行篇。
这是【炼石计划@PHP代码审计】知识星球第二阶段的原创基础系列文章,拿出部分课程分享给零基础的朋友学习。本系列原创基础文章涵盖了PHP代码审计中常见的十余种WEB漏洞,是匹夫老师精心的创作,欢迎关注我的公众号跟着一起学习。
【炼石计划@PHP代码审计】是一个系统化从入门到提升学习PHP代码审计的成长型知识星球。这里不仅注重夯实基础,更加专注实战进阶。强烈推荐加入我们,一起来实战提升PHP代码审计。

PHP代码审计之代码执行
1.代码执行原理
为了代码的灵活性与简洁性,会适当调用PHP中的一些代码执行函数来完成一些系统的功能,而代码执行的函数相当于可以直接调用PHP中任意代码来执行,更重要的是代码执行可以通过调用命令执行的函数来执行系统命令,来达到控制后台甚至我们的服务器,这就是我们所说的RCE(远程代码/命令执行漏洞)的由来。
1.1 代码执行示例代码
eval.php
以最常见的一句话木马为例,我们通过代码执行函数eval()来进行演示,假设我们eval()中的参数可控,我们就可以控制我们传入的参数来造成RCE。
1 | |
由于这里的value参数可控,我们传入PHP中的phpinfo()函数即可输出PHP配置信息。值得注意的是eval()中传入的函数必须要用分号来结尾,否则会报出致命性错误,导致执行中断。

也可以传入我们PHP中命令执行的函数来执行系统命令,这就是我们上篇所说的代码执行也可以当作命令执行的原理。

在**eval()**函数中可以传入多个函数来执行,每个函数也必需要用分号进行分割。

2.代码执行相关函数
eval()
将传入的字符串当作PHP代码来执行,代码示例如上图。
assert()
其与eval()类似,传入的内容会被当做代码来执行,不同的是eval中传入的值可以不用分号来结尾。
示例代码:
1 | |

同样也可以调用命令执行函数来执行系统命令

assert()函数也可进行拆分调用,这是与eval()函数很大的不同点,有时可以用来绕过一些WAF的防御。
示例代码:
1 | |

preg_replace()
该函数有三个参数,用于执行一个正则表达式的搜索和替换
搜索 subject 中匹配 pattern 的部分, 如果匹配成功以 replacement 进行替换
语法:
1 | |
1 | |
示例代码:
1 | |
由于使用/e传入的phpinfo()会被当做代码来执行

create_function()
PHP中的匿名函数,相当于定义了一个没有名字的函数,该函数直接用变量进行调用。
第一个参数参数是函数传递的参数,第二个参数相当于函数中的函数体。
语法:
1 | |
代码示例:
第二个参数会在内部执行eval(),在这里也就是执行后面的return语句
1 | |
上面的代码可以等价于以下代码只不过匿名函数是用变量来调用的,而非匿名变量是通过函数名来调用的。
1 | |
显然他们的计算结果是相同的


代码示例:
当create_function()中传入的参数可控时就会造成代码执行
1 | |
由于$a在被调用时才会触发执行匿名函数,所以我们通过传入的参数闭合该匿名函数的},就造成了代码执行,实际情况中要根据具体闭合条件进行闭合,从而使我们想要执行的代码独立出来。
// 注释当前行代码(单行注释)
/* 注释之后所有代码(多行注释)

array_map()
array_map() 函数返回用户自定义函数作用后的数组。回调函数接受的参数数目应该和传递给 array_map() 函数的数组数目一致。
语法:
1 | |
代码示例:
array_map()将调用sum_num()函数,去执行该函数,这也就是上面所说的函数回调。且后面传入的参数要与回调的函数参数数目保持一致。
1 | |

代码示例:
当传入的参数可控时就会造成代码执行
1 | |
当我们传入assert,array_map()函数就会回调assert()函数去执行我们后面传入的参数phpinfo(),相当于assert(phpinfo())

call_user_func()
第一个参数作为回调函数调用, 其余参数是回调函数的参数:
语法:
1 | |
代码示例:
第一个参数welcome作为回调函数进行调用,后面则需要传入我们回调函数所需的参数,如下面的$city。
1 | |

示例代码:
当我们传入的参数可控时就会造成RCE
1 | |


call_user_func_array()
把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入,与call_user_func()函数不同的是,该函数传入的参数是以数组的形式传入的。
语法:
1 | |
示例代码:
这里需要注意的是参数必须以数组形式传入,如下面的$a。
1 | |

示例代码:
当传入的参数可控时就会造成RCE
1 | |
这里的arg2传入的参数就是以数组形式就行传递的

如果直接不以数组形式就行传递就会导致致命错误

array_filter()
把输入数组中的每个键值传给回调函数,与call_user_func_array()不同的是,该函数的第一个参数为回调函数的参数,而第二个参数则是传入的回调函数。
代码示例:
当传入的参数可控时就会造成RCE
1 | |

ob_start()
用于打开输出控制缓冲,如果参数可控也可造成RCE。
1 | |

usort()
使用用户自定义的比较函数对数组中的元素进行排序,该函数第二个参数是用户自定义的回调函数。
语法:
1 | |
代码示例:
1 | |
回调用户自定义my_sort()函数对$a数组中的值进行排序

代码示例:
如果usort()函数参数可控,也可能可造成RCE。PHP版本>=5.6可实现
1 | |

array_walk()
使用用户自定义函数对数组中的每个元素做回调处理,该函数的第二个参数也为回调的自定义函数。
语法:
1 | |
代码示例:
1 | |
回调上面的myfunction()函数,对$a赋值的键值对进行处理。

代码示例:
如果array_walk()函数参数可控,也可能可造成RCE
1 | |
由于传入的参数为数组,所以形参需在后面加[]

动态函数
PHP函数直接由字符串拼接,且拼接内容可控就会导致RCE
示例代码:
1 | |
这种动态函数代码执行的方式相信大家很好理解,传入的第一个参数则为函数名,而第二个参数则传入参数值。如下面的system为函数,calc.exe为函数值,从而拼接后执行system(calc.exe);弹出计算器。

3.代码执行代码审计总结
1 | |