直接进入关键
首发先知嗷 -> https://xz.aliyun.com/t/10755
分析
下载后打开发现是个14w行的PHP,通过文本搜索大致搜索了一下,能够确定以下信息:
- source只有一个,是
__destruct
函数 - sink点在
readfile
,而不是call_user_func
那么只要找到source到sink的一条路径就行了。
call graph
对函数内进行分析就十分简单,直接遍历AST,查找函数名为readfile
的FunctionCall
节点就好了。但是从source到sink往往会跨好几个函数,并且AST没有过程间的信息,所以需要分析补充。这里通过粗略分析,只需要函数调用信息即可。
直接调用
这是最简单的调用,例如:
|
|
直接去寻找方法名为xly0ZQT
的ClassMethod
节点即可,找到了就可以从当前的ClassMethod
创建call
边指向目标,不用细到从调用的那一行引出。
__call
如果没找到方法名,例如:
|
|
是找不到Ws2xymT
这个类方法的,这样就得通过__call
来调用。
所以要判断call_user_func
中用到的变量,在上面extract
是创建了的。
从__call出去
因为__call
中存在call_user_func
调用了其他方法,目标方法名是从extract
中来的,通过粗略的分析,__call
中全部都是一个extract
和一个call_user_func
,所以就省略call_user_func
中的参数分析,直接将extract
中的硬编码字符串作为目标方法名去查找。
__invoke
例如:@call_user_func($this->WHB5xkK7, ['LUlnpp' => $RwGAFc8G]);
上述两种方法都是找不到目标方法的,但是存在拥有__invoke
方法的类。
所以要判断$key
的值是不是上面call_user_func
参数中的值,也就是base64_decode
中的参数是不是call_user_func
中的参数base64
编码后的值。
简单的污点传播
因为构建call graph
时遍历AST查找调用点,污点分析也要遍历,索性放一起好了,顺便也能减少步骤。
在查找调用点的时候,顺便判断下变量是否可控,不可控就结束建立call graph
。这样只要有call
边的,说明涉及到的变量都是可控的。
本题中,涉及的变量的产生基本为两种:
- 参数传入
- 赋值
并且变量都是在函数中用的,没有全局变量,所以分析一个方法前,可以创建一个变量状态Map,保存变量的状态。
参数传入
因为存在call
边的,变量都是可控的,所以默认参数就是可控的。
所以:变量状态[参数名] = true
赋值
本题中的赋值存在两种情况:
$b = foo($a)
,$a
为参数$b = $a
对于第一种,右边是函数调用的判断函数是否是sanitizer
,如果是那就:变量状态[b] = false
,这里sanitizer
我选择了crypt md5 sha1
以及base64_encode
,为什么base64_encode
也是呢?其实可以添加一个计数器,统计路径上的base64_encode
与base64_decode
出现的次数,两者相等即可。但是rot13
出结果了,因为懒就没写计数器了。
对于第二种,直接:变量状态[b] = 变量状态[a]
,$a
有时候是凭空出现的,并没有定义,变量状态[a]
就为false。
source to sink
当call graph
建立之后,就可以进行路径查找了。
实现
上述内容可能存在文字没表达清楚,直接show you the code
依托github.com/VKCOM/php-parser
解析php8,基于go语言写了个辅助工具(为什么不用PHP?因为go写多了,顺手就用了)
https://github.com/LuckyC4t/sctf-fumo-tree-go
后续想法
个人感觉是能通过CodeQL来编写查找,因为PHP动态性质,CodeQL估计不能分析出本题较为动态的Call Graph,需要手动补充flow。留给有兴趣的师傅们探索了。寄了,写文章的时候没看CodeQL文档,发先知之后才发现CodeQL不支持PHP,Orz