本文于10月13日发表在我的博客。
前几天在『代码审计』知识星球里发了一个小挑战:https://t.zsxq.com/13bFX1N8F
<?php
$password = trim($_REQUEST['password'] ?? '');
$name = trim($_REQUEST['name'] ?? 'viewsource');
function viewsource() {show_source(__FILE__);}
if (strcmp(hash('sha256', $password), 'ca572756809c324632167240d208681a03b4bd483036581a6190789165e1387a') === 0) {
function readflag() {
echo 'flag';
}
}
$name();
?>
执行环境是PHP7.4,目标是读取到flag。
这段代码非常简单,我加了一些迷惑因素,比如trim
、strcmp
、hash
之类的函数,但实际上核心与这些干扰因素没关系,我们来简单做个分析。
PHP脚本执行过程理解
我并不是C语言和PHP底层原理的专家,这里只能用一些简单的语言来描述PHP脚本编译执行的过程。
就如其他大部分脚本语言一样,PHP的执行分为两部分:
源代码编译成Zend虚拟机指令(PHP中叫opline)的过程
Zend虚拟机执行机器指令的过程
其中前者又会被分为下面几个步骤:
调用
zendparse
完成词法分析、语法分析,生成AST树调用
init_op_array
,zend_compile_top_stmt
来完成AST到opline数组的转化调用
pass_two
完成编译时到运行时信息的转化,设置每个opcode对应的handler
后者拿到编译完成后的opline array,依次执行每个opcode,其实就是执行每个opcode对应的handler,完成PHP脚本的执行。我们参考我在『代码审计』星球里分享过的远程调试ZendVM的方法,找到zend_execute_scripts
函数,你即可看到大致的逻辑:
我们要关注的是PHP代码的编译阶段。PHP在编译“函数定义”的时候,会使用zend_compile_func_decl
函数:
void zend_compile_func_decl(znode *result, zend_ast *ast, zend_bool toplevel) /* {
{
{ */
{
...
zend_ast_decl *decl = (zend_ast_decl *) ast;
zend_bool is_method = decl->kind == ZEND_AST_METHOD;
if (is_method) {
zend_bool has_body = stmt_ast != NULL;
zend_begin_method_decl(op_array, decl->name, has_body);
} else {
zend_begin_func_decl(result, op_array, decl, toplevel);
if (decl->kind == ZEND_AST_ARROW_FUNC) {
find_implicit_binds(&