抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

0X01 漏洞概况

本次漏洞存在于 Builder 类的 parseData 方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生。漏洞影响版本: 5.0.13<=ThinkPHP<=5.0.155.1.0<=ThinkPHP<=5.1.5

实验环境

  1. composer版本1.8.5

    1
    composer create-project --prefer-dist topthink/think=5.0.15 thinkphp5015
  2. 将 composer.json 文件的 require 字段设置成如下:

    1
    2
    3
    4
    "require": {
    "php": ">=5.4.0",
    "topthink/framework": "5.0.15"
    }
  3. 执行update

1
composer update

可以直接用phpstudy带的composer

  1. 修改 application/index/controller/Index.php 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php
    namespace app\index\controller;

    class Index
    {
    public function index()
    {
    $username = request()->get('username/a');
    db('users')->insert(['username' => $username]);
    return 'Update success';
    }
    }
  2. 创建数据库

    1
    2
    3
    4
    create table users(
    id int primary key auto_increment,
    username varchar(50) not null
    );
  3. 配置数据库

    application/database.php 文件中配置数据库相关信息,并开启 application/config.php 中的 app_debug 和 app_trace

  4. 配置完成

0X02 漏洞复现

POC

1
http://<yoursite>/public/index.php/index/index?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1

参数部分包含三个键值对:

  1. username[0]=inc:设置 username[0] 参数的值为 inc
  2. username[1]=updatexml(1,concat(0x7,user(),0x7e),1):设置 username[1] 参数的值为 updatexml(1,concat(0x7,user(),0x7e),1)
  3. username[2]=1:设置 username[2] 参数的值为 1

其中,username[1] 的值,是一个试图利用 updatexml 函数进行 SQL 注入的示例

1
updatexml(1, concat(0x7, user(), 0x7e), 1)

updatexml是 MySQL 中的一个函数,用于更新 XML 数据。在这里利用它来执行任意 SQL 查询。

  • concat(0x7, user(), 0x7e)concat 函数用于连接字符串。0x70x7e 是十六进制表示的字符,分别是 BEL~(换成别的什么乱七八糟的也可以)。它们会与当前数据库用户的名字(通过 user() 函数获取)拼接在一起。

由于 updatexml 函数的设计,这种拼接的字符串会被插入到 XML 文档中,并返回错误消息,其中包含拼接的内容。这样,攻击者可以通过错误消息获取敏感信息,如当前数据库用户。


user()获取当前数据库用户的名字

version() 获取当前数据库版本

0X03 代码分析

调用链

漏洞函数

application/index/controller/Index.php

1
2
3
4
5
6
public function index()
{
$username = request()->get('username/a');
db('users')->insert(['username' => $username]);
return 'Update success';
}
  1. request()->get('username/a'):从请求中获取 username 参数,并假设它是一个数组。
  2. db('users')->insert(['username' => $username]):将获取到的 username 参数插入到 users 表中。
  3. return 'Update success':返回一个简单的成功消息。

**跟进insert **

1
db('users')->insert(['username' => $username]);

进来后跟进生成sql语句,看\think\db\Query::insert函数如何处理数据

跟进\think\db\Builder::insert

数据会先经过\think\db\Builder::parseData进行处理

跟进\think\db\Builder::parseData

获取字段列表

1
2
3
4
5
6
$bind = $this->query->getFieldsBind($options['table']);
if ('*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}

getFieldsBind 获取 $options['table']表的字段绑定信息,并存储在 $bind 中。如果 $options['field'] 等于 '*',表示选择所有字段,则将 $bind 中的所有键(字段名)存储在 $fields 中。否则,将 $options['field'] 的值存储在 $fields 中。

处理数据

  • result 初始化创建一个空数组用于存放结果数据
  • foreach 循环遍历传入的数据 data,用以处理键值对
  • 如果键值对中的值val 是一个对象,并且存在toString 的方法,则调用该方法,将其转换为字符串。
  • 检查键 key 是否包含”.”,且是否是fields字段列表中的。
  • 如果不在,并且是在严格模式 ($options['strict']true) ,抛出异常。
  • 如果值为空,则返回到结果数组中,置为NULL
  • 如果不为空,则对 $val[0]进行判断,分三支为 ‘exp’、’inc’、’dec’【根据数组的第一个元素(表示操作类型)进行不同处理】
    • 'exp': 表达式,直接使用值。
      • $val 是一个数组且 $val[0]'exp' 时,直接将 $val[1] 赋值给 $result[$item]'exp' 通常表示一个原生的数据库表达式,因此不需要进一步处理。
      • 例如: $val = ['exp', 'NOW()'],则 $result[$item] 将被赋值为 'NOW()',表示在 SQL 中使用当前时间。
    • 'inc': 递增操作,解析键并构造递增表达式。
      • $val 是一个数组且 $val[0]'inc' 时,表示一个递增操作。$val[1] 是要递增的字段名,$val[2] 是递增的值。
      • 例如:如果 $val = ['inc', 'updatexml(0,concat(0x7,version(),0x7e),1)', 1],并且 parseKey('updatexml(0,concat(0x7,version(),0x7e),1)') 返回 'updatexml(0,concat(0x7,version(),0x7e),1)',则 $result[$item] 将被赋值为 'updatexml(0,concat(0x7,version(),0x7e),1) + 1'
    • 'dec': 递减操作,解析键并构造递减表达式。

跟进 \think\db\Builder::parseKey

$val 没有进行任何过滤处理,怎么进去的,怎么出来的。并且直接拼接 $val[2]

1
2
3
4
protected function parseKey($key, $options = [])
{
return $key;
}

所以从parseData 方法处理后,出来的数据是 updatexml(0,concat(0x7,version(),0x7e),1) + 1

**回到\think\db\Builder::insert**,向下跟进

生成一个完整的sql语句

1
INSERT INTO `users` (`username`) VALUES (updatexml(0,concat(0x7,version(),0x7e),1)+1) 

0X04 修复方案

修复漏洞的原理在于避免直接将用户输入的$val[1]值拼接到 SQL 语句中,而是使用安全的方式来构建 SQL 语句。

修复前:

1
2
3
case 'inc':
$result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
break;

在这段代码中,$val[1] 是用户输入的值,并通过 $this->parseKey 方法进行处理后,直接拼接到 $result[$item] 中。这种直接拼接方式很容易受到 SQL 注入攻击的影响。

修复后:

1
$result[$item] = $item . '+' . floatval($val[2]);
  1. **使用 $item 代替 parseKey($val[1])**:
    • $item 是通过 $this->parseKey($key, $options) 方法解析后的安全字段名。($iterm的值为username)
    • 这样可以确保 $item 是一个安全的字段名,避免直接使用用户输入的值。
  2. floatval($val[2])$item 拼接
    • floatval($val[2]) 将用户输入的值转换为浮点数,以确保它是一个有效的数值,从而避免了字符串或其他类型的注入。

所以最终修复后的结果👇

评论