一年前挖过某系统的漏洞,但当时挖的0day有点鸡肋,能否成功利用取决于系统中用户是否自定义了某个值。最近这一年看系统经过几次版本迭代,于是下来对比了下更新内容,希望在功能上有所突破。但事与愿违,对比更新内容发现文件更新量到是多,但都是一些不痛不痒的更新。虽然最终未能挖到通杀0day,但在这次代码复审的过程中比去年多少有一点点突破,突破起于empty
函数也终于empty
。本文就此过程简单抽象出来记录一下,没准在码字的过程中就能灵光一闪有了突破口呢?
系统最终利用环境是通过构造参数形成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
,程序用该id
从SomeTable
中查询全部字段赋值于$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
的时候的参数名也不是固定的,有时候哪怕他设置了该值你也有可能无法从一堆参数中确定下来,只有一个一个的尝试。
在去年审计该系统的时候就发现该系统采用默认安装的时候,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]))
这里的GET
和POST
只要满足一个即可走我们希望走到的逻辑处。我们可不可以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
。
前面说过,从数据库中查询到的所有字段内容全部赋值于$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。
实际上该系统其它的地方也有一些利用链,但是条件都是一个比一个苛刻,空了再总结总结。文章写完了,期待的灵光一闪然并卵……