Castiel's Blog

一条被empty函数终结的利用链

一条被empty函数终结的利用链
2020-04-04 · 6 min read

0x00 前言

一年前挖过某系统的漏洞,但当时挖的0day有点鸡肋,能否成功利用取决于系统中用户是否自定义了某个值。最近这一年看系统经过几次版本迭代,于是下来对比了下更新内容,希望在功能上有所突破。但事与愿违,对比更新内容发现文件更新量到是多,但都是一些不痛不痒的更新。虽然最终未能挖到通杀0day,但在这次代码复审的过程中比去年多少有一点点突破,突破起于empty函数也终于empty。本文就此过程简单抽象出来记录一下,没准在码字的过程中就能灵光一闪有了突破口呢?

0x01 利用链

系统最终利用环境是通过构造参数形成SQL注入,只要有了SQL注入即可控制整个系统。先看看最终这个SQL注入的代码,我个人觉得还是比较有意思:

public function Insert($DataArray, $DataTable)
    {
        foreach ($DataArray as $k => $v) {
            $cols[] = $k;
            if (!is_array($v)) {
                $values[] = ":" . $k;
                $sqlarr[$k] = $v;
            } else {
                $values[] = $v[0];
            }
        }
        $query = "INSERT INTO `" . $DataTable . "` (`" . implode("`,`", $cols) . "`" . ") VALUES (" . implode(",", $values) . ");";
    $this->db->insertId($query, $sqlarr);

SQL语句使用PDO预处理查询,默认可以多语句执行。从以上代码可以看出明显的注入,只需要控制$DataArray中任意一项目的值为一个数组即可,例如$DataArray['aaa']=array("SQL Inject Here")。但$DataArray的定义比较严格,其中只有一项可控,暂且称为$DataArray["test"]

想要控制$DataArray["test"]这个值可谓困难重重,首先得前台提交一个id,程序用该idSomeTable中查询全部字段赋值于$SomeThing,然后经过各种验证判断(此处省略一万行代码……)。再判断 $SomeThing['m_name']是否为空,不为空则使用GET或者POST获取 $SomeThing['m_name']把值赋给 $SomeThing['m_value']。最终$DataArray["test"] = isset($SomeThing['m_value']) ? $SomeThing['m_value'] : 0

但实际在利用过程中往往SomeTable中是没有设置m_name字段的,这就造成了利用链比较鸡肋,取决于m_name是否有设置。而且这个值是自定义的,所以GET或者POST的时候的参数名也不是固定的,有时候哪怕他设置了该值你也有可能无法从一堆参数中确定下来,只有一个一个的尝试。

0x02 第一个empty

在去年审计该系统的时候就发现该系统采用默认安装的时候,SomeTable会插入一条默认的记录,但这条记录的所有字段值均为0。当时挖掘到上诉的利用方式之后也试图利用这条默认值来摆脱受用户自定义字段名的限制,但当时在第一处对前台提交id值做验证的时候就卡主没有继续下去了,先来看看抽象代码:

public function getSomething()
    {
		$fieldName = getFieldName();
		if (!$fieldName) {
			$fieldName = "id";
		}
        if (isset($_GET[$fieldName]) && !empty($_GET[$fieldName]) || isset($_POST[$fieldName]) && !empty($_POST[$fieldName])) {
            $fieldVlue = get_post($fieldName);
        } else {
            ......省略代码
        }
        ......从数据库中查询$fieldVlue对应的记录
    }

前面说过SomeTable中默认记录的所有字段值都为0,所以我们想利用他的话这里这个验证无法过,比如我们前台提交id=0,在这里!empty($_GET[$fieldName]值始终为false;这里参考php官方文档,empty判断一个变量是否被认为是空的。当一个变量并不存在,或者它的值等同于FALSE,那么它会被认为不存在,如下图所述:

所以这里我们提交的id=0刚好满足字符串"0"这个条件,所以直接这样是过不了的。但是细心的师傅们肯定能发现这里的问题if (isset($_GET[$fieldName]) && !empty($_GET[$fieldName]) || isset($_POST[$fieldName]) && !empty($_POST[$fieldName])) 这里的GETPOST只要满足一个即可走我们希望走到的逻辑处。我们可不可以GET提交一个值POST再提交一个值,让其中任意一个来绕过此处这个验证,剩下的就是看$fieldVlue是如何取值的了,继续看看get_post的实现:

public static function get_post($key, $dop = "")
    {
    ……省略部分代码
        if (isset($_GET[$key])) {
            $val = process($_GET[$key], $dop);
        } else {
            if (isset($_POST[$key])) {
                $val = process($_POST[$key], $dop);
            } else {
                $val = "";
            }
        }
        return $val;
    }

代码是不是很完美?程序先判断$_GET再判断$_POST,使用$_POST提交id=1来绕过上面的验证,$_GET提交id=0来对$fieldVlue赋值,如此便能从数据库中查询默认记录的数据了。但是利用之路往往都是充满坎坷的……,且看第二个empty

0x03 第二个empty

前面说过,从数据库中查询到的所有字段内容全部赋值于$SomeThing,再验证$SomeThing['m_name']是否为空,来看如下代码:

if (!empty($SomeThing["m_name"]) && get_post($SomeThing["m_name"]) !== false) {
		$SomeThing["m_value"] = get_post($SomeThing["m_name"]);
	}

这里的这个empty便是死亡empty了,因为数据库查询出来的m_name字段内容为0,且不可控,所以这里也无思路可绕过了。此处也是最终环节,能绕过此处验证,给$SomeThing["m_value"]赋值便是完美的通杀0day。

0x04 总结

实际上该系统其它的地方也有一些利用链,但是条件都是一个比一个苛刻,空了再总结总结。文章写完了,期待的灵光一闪然并卵……