直接进入关键

首发先知嗷 -> https://xz.aliyun.com/t/10755

分析

下载后打开发现是个14w行的PHP,通过文本搜索大致搜索了一下,能够确定以下信息:

  • source只有一个,是__destruct函数
  • sink点在readfile,而不是call_user_func

那么只要找到source到sink的一条路径就行了。

call graph

对函数内进行分析就十分简单,直接遍历AST,查找函数名为readfileFunctionCall节点就好了。但是从source到sink往往会跨好几个函数,并且AST没有过程间的信息,所以需要分析补充。这里通过粗略分析,只需要函数调用信息即可。

直接调用

这是最简单的调用,例如:

1
@$this->QsIFY2PS->xly0ZQT($Q0CGxlEy);

直接去寻找方法名为xly0ZQTClassMethod节点即可,找到了就可以从当前的ClassMethod创建call边指向目标,不用细到从调用的那一行引出。

__call

如果没找到方法名,例如:

1
@$this->oS9D89Gt->Ws2xymT($NOCGzO);

是找不到Ws2xymT这个类方法的,这样就得通过__call来调用。

image

所以要判断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方法的类。

image

所以要判断$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_encodebase64_decode出现的次数,两者相等即可。但是rot13出结果了,因为懒就没写计数器了。

对于第二种,直接:变量状态[b] = 变量状态[a]$a有时候是凭空出现的,并没有定义,变量状态[a]就为false。

source to sink

call graph建立之后,就可以进行路径查找了。

image

实现

上述内容可能存在文字没表达清楚,直接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