关于ThinkPHP的SQL注入漏洞
一.前言
最近拖延症又犯难了,导致很久都没有时间来写这个文章。而且因为一些奇怪的缘故,导致自己的MySQL数据库中间崩了好几次,导致这次的漏洞复现磕磕绊绊的。同时这也是我自己第一次做代码审计,效率着实算不上高,就以此来记录一下自己这次的学习(考古)过程吧。
二.正文
1.数据处理流程
环境搭建方面,就用thinkphp官网上的历史遗留版本,然后再随便找一个能用的数据库就可以了,最后用phpstudy将这几个东西联系起来就行了,环境搭建不是重要的地方,就不过多赘述了,下面直接开始正文。
测试之前要去thinkphp的配置文件中将数据库的设置进行相应的填写,以便ThinkPHP架构可以正常的进行工作。
接下来,要将/Application/Home/Controller/IndexController.class.php文件进行一些改动,以便我们方便测试,我们需要让index文件可以接受一个参数。改成如下的格式就可以了
class IndexController extends Controller
{
public function index()
{
$data = M('users')->find(I('GET.id'));
var_dump($data);
}
}
然后我们向index中传入任意一个参数值,就比如id=2之类的,就可以到本地页面上的返回数值(也就是数据库里面的内容)。
要想知道这个漏洞的成因,我们就必须来观察他的程序运行顺序,以及每一个步骤他对数据的处理结果。
我们通过传入一个参数,步步调试来看一下程序的运行过程。
我们首先会进入M函数,但是M函数对于我们这次的调试目标并没有什么太大的帮助,因为它的主要运行目的是在初始化模型,我们直接跟进到I函数
前三个判断语句没什么用,就是简单的处理了一下从indexcontroller那边传来的数据,我们直接来到下面。
这里会给过滤器也就是filter设置一个初值,也就是DEFAULT_FILTER,当然从监视器我们可以看到,这个默认的过滤器就是htmlspecialchars
继续往下看,代码的逻辑很明确,他就是将上面定义的过滤器对每一个传入其中的数据都应用了一遍(这个默认过滤器实测没啥用)。接下来我们继续跟进下面的逻辑,它又对数据应用了一次过滤器这次的过滤器名为think_filter。
这个过滤器还是很有用的,我们单步跟进看一下,他都过滤了哪些东西。
他过滤东西从明面上就能看出来,很少。如果打过CTF的话,就知道真正的过滤器是可以排满两整行的(x。接下来我们将正式进入find函数的执行过程。
这里是对where字段进行赋值,以用于之后的查询语句拼接的操作,并没有什么特别值得关注的地方,接下来的一大段代码都在处理复合主键的东西,因为这里用的数据库只有唯一的一个主键,所以下面的步骤可以直接跳过。我们直接来到了下面这块代码段,其中调用了_parseOptions()函数。
我们直接单步进入这个函数。
前面同样是在获取参数,处理相关的数据,关键是要看下面对于where字段的处理方面的相关代码。
单步进入这个_parseType()函数看一下中间处理的过程。
这里要说明的一点是,如果这里的id规定的是int型参数的话,我们的程序执行会直接进入intval()函数,这样就会导致我们所输入的参数值直接变成一个整型变量,根本无法注入,所以我们出于复现的目的,要先将自己数据库中的id改成varchar格式,以便它可以正常的往下运行。我们这时候将输入的变量变成一个非法的请求参数,也就是我们常用的注入手段,id=1’ or 1=1.来看一下这一串字符串在经过处理后会变成什么样子。(果然没有进入intval,要不然就直接寄了)
通过左边的监视器中我们可以发现,在该字符串经过parseType()后并没有被转义,所以这里没有发生过滤,我们接着往下调。我们又回到了find()函数,紧接着要执行的是这个程序会生成sql语句的程序段。
在这里我们执行代码的时候突然出现了问题,就是在执行完buildSelectSql()这个函数之后,我们传入参数中的单引号被转义了。
所以一切的原因就都聚焦于这个函数了,我们这一次在这个函数上设置断点进行单步调试。
我们看到其中有一个parseSql()函数很是吸引人的视线,我们单步进入看一看。
我们这个参数是要传给where字段的,所以我们直接跟进parsewhere函数进行进一步调试。
我们看到数据中间跳过了一段很长的判断语句,数据直接传到了最下面。
我们进入parseWhereItem()函数中,调试一步过后我们同上次一样直接来到了下面。
再次单步进入这个函数中
函数的第三行有点长我直接将他粘到这里吧
$value = strpos($value, ':') === 0 && in_array($value, array_keys($this->bind)) ? $this->escapeString($value) : '\'' . $this->escapeString($value) . '\'';
其中的escapeString()函数是关键,我们单步进入看一下。
emm,直接用addslashes进行了转移,看来通过一般的办法是无法奏效了,但是整体走下来其实感觉这个框架的过滤方面仍有一个隐患,就是过滤的地方太少了,感觉仍可以形成注入。
2.注入点一,数组注入
我们这次传入的参数如果是以数组的形式传进去的话,会发生什么呢?
这次我们传入参数id[where]=1。来看一下第一个分歧点:
在刚进入find函数的时候,我们这一次传入的参数是无法进入第一个判断语句的,这会影响到后面语句的判断,我们接着往下看。
在进入_parseOption()函数后我们来到下面这个判断语句
这时候options参数为
他不满足is_array(option[‘where’]),所以它无法进入这个判断语句,它也就无法触发parsetype()函数,在上面的分析中我们知道在parsetype()里面有一个intval(),如果我们传入数组的话他就不会触发这个函数,所以我们就可以在id的类型为int的时候用数组来绕过这个过滤。
我们直接快进到查询语句的拼接。(如下为拼接效果)
所以我们基于此,就可以得到我们的第一个payload
?id[where]=id=0%20union%20select%201,2,database()
3.注入点二,exp注入
我们这次换一个思路进行SQL注入,我们先将IndexController中的index函数改成如下的代码。
public function index(){
$User = D('Users');
$map = array('username' => $_GET['username']);
// $map = array('username' => I('username'));
$user = $User->where($map)->find();
var_dump($user);}
ok,我们这时候先往本地请求一行参数,然后开始调试
?username[0]=exp&username[1]=1
前面的where函数没有什么好看的我们直接来到find函数里面,看看真正的“硬货”。
再一次来到_parseOption()函数的面前,我们往下调试的时候就发现了不对劲的地方,我们之前重视这个函数,关键是里面有一个_parsetype()函数。但这一次不一样了,我们来看一下相关的代码段
is_scalar()这个函数是用来判断传入值是不是标量的,很显然的一个问题就来了,现在的val不是标量,而是一个数组。
所以它根本就不会进入到这个函数里面,基于这种现实,我们就只得把目光放在后面的调试过程了。
我们这一次来到SQL查询语句构建的地方来
紧接着我们要像上一节的步骤那样,直接调试到对where的单独处理那部分。
单步进入这个函数,为了方便我直接将代码贴到下面了
protected function parseWhereItem($key, $val)
{
$whereStr = '';
if (is_array($val)) {
if (is_string($val[0])) {
$exp = strtolower($val[0]);
if (preg_match('/^(eq|neq|gt|egt|lt|elt)$/', $exp)) {
// 比较运算
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
} elseif (preg_match('/^(notlike|like)$/', $exp)) {
// 模糊查找
if (is_array($val[1])) {
$likeLogic = isset($val[2]) ? strtoupper($val[2]) : 'OR';
if (in_array($likeLogic, array('AND', 'OR', 'XOR'))) {
$like = array();
foreach ($val[1] as $item) {
$like[] = $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($item);
}
$whereStr .= '(' . implode(' ' . $likeLogic . ' ', $like) . ')';
}
} else {
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
}
} elseif ('bind' == $exp) {
// 使用表达式
$whereStr .= $key . ' = :' . $val[1];
} elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
我们的参数会经过is_array(),is_string()这两个判断语句,最终会来到
elseif ('exp' == $exp)
这也是我们为什么要在第一个参数中给username[0]设置为exp的原因。
我们接着往下看,wherestr这个变量将会被赋值为key与val[1]的拼接。
也就是username 1
我们直接来看一下这个SQL语句最终会成为什么样子?
我们的wherestr直接被放在where字段的后面,并且没有进一步的过滤细节。所以基于这种情况我们又可以构造出一个SQL注入的payload
?username[0]=exp&username[1]==1%20union%20select%201,2,database()
后记
参考的博客
feng师傅
Y4师傅
这一次在最后复现bind注入的时候phpstudy的环境突然就不行了。哎,怎么说呢集成的环境确实用起来简单又方便,但是如果一旦出错,也是真的麻烦。整了一晚上我都没有把这玩意搞好,真是长记性了,以后还是自己搭环境了。sql注入要暂时先放一放了,等环境搞好吧,想趁着这段时间回去吧SSTI的原理搞一搞。