ThinkPHP5 SQL注入漏洞 && 敏感信息泄露
介绍:
ThinkPHP是一个快速、兼容而且简单的轻量级国产PHP开发框架,诞生于2006年初,原名FCS,2007年元旦正式更名为ThinkPHP,遵循Apache2开源协议发布,从Struts结构移植过来并做了改进和完善,同时也借鉴了国外很多优秀的框架和模式,使用面向对象的开发结构和MVC模式,融合了Struts的思想和TagLib(标签库)、RoR的ORM映射和ActiveRecord模式。
Poc : http://172.20.0.3/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1
影响: <5.0.9
前言
这是第一篇漏洞复现文章,学了php但是在之前还没有接触过thinkphp框架
一个鸡肋的sql注入漏洞
以下是执行大概流程,一遍看一边调试
开始
先在kali安装vulhub
教程文章
第一次用打开发现出问题了,去网上搜索了半天没发现解决方法,就去安全群问了一下发现是没有启动docker
打开复现环境执行payload
先看漏洞的形成原因,‘控制了预编译SQL语句中的键名’
搭建本地环境用于调试 这里用的demo 是
主要分析以上的内容
$ids = input('ids/a');
Ids/a 是将input接受到的数据强行转换成数组
$ids值 就是上面浮窗的数组
$t 是以下
下断点进入
$t调用了where和select 函数,先进入select函数分析 位置在(thinkphp\library\think\db\Query.php)
重点是2306行的生成sql语句 不过在这之前先看个变量后面才好理解
$opthins =
分析成了用于方便sql的对象
进入2306行
$sql = $this->builder->select($options);
进入了(thinkphp\library\think\db\Builder.php
)
会执行637行的代码进入parseWhere函数
224行调用了buildWhere函数
进入243行的buildWhere函数
buildWhere函数中主要有以下重要的(主要是IN查询)
$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 . ' ' . $this->parseClosure($value);
} else {
$value = is_array($value) ? $value : explode(',', $value);
if (array_key_exists($field, $binds)) {
$bind = [];
$array = [];
foreach ($value as $k => $v) {
if ($this->query->isBind($bindName . '_in_' . $k)) {
$bindKey = $bindName . '_in_' . uniqid() . '_' . $k;
} else {
$bindKey = $bindName . '_in_' . $k;
}
$bind[$bindKey] = [$v, $bindType];
$array[] = ':' . $bindKey;
}
$this->query->bind($bind);
$zone = implode(',', $array);
} else {
$zone = implode(',', $this->parseValue($value, $field));
}
$whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
}
}
....
return $whereStr;
}
一开始他是有对$bindName进行过滤的,但是$value是数组他会用foreach把$bindName和键名进行拼接
最后形成
导致在编译SQL语句的时候发生错误,从而产生报错
为什么叫鸡肋漏洞呢,查了原因是因为thinkphp用了PDO来连接数据库的
通常,PDO预编译执行过程分三步:
- prepare($SQL) 编译SQL语句
- bindValue($param, $value) 将value绑定到param的位置上
- execute() 执行
我们在第二部的$param那里用报错语句,在执行到第二时直接抛出错误无法执行到第3,
在$SQL中也一样 ,如果$SQL等于报错语句就会直接报错无法执行二和三
还有一个主要原因是thinkphp默认配置PDO::ATTR_EMULATE_PREPARES => false
引用大佬的话:
这个选项涉及到PDO的“预处理”机制:因为不是所有数据库驱动都支持SQL预编译,所以PDO存在“模拟预处理机制”。如果说开启了模拟预处理,那么PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行;
远程执行代码漏洞
Poc:
index?s=index/\think\container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
版本:5.0.5-5.0.22
5.1.0-5.1.30
调试注意要点:因为提前设置了断点导致提前跳转到固定的函数中,导致调试失败参数失败无法运行
复现
下载5.0.22的thinkphp
下载地址:
http://www.thinkphp.cn/down/1260.html
http://localhost/thinkphp_5.0.22_with_extend/public/index.php?s=captcha
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami
_method=__construct&filter[]=system&method=get&get[]=whoami
这里2个都能达到同样的效果,如果是调试建议第2个
一开始复现的时候以为要demo,结果不用
设置断点进入App.php
创建了一个http请求实例
继续往下
来到了115行,这里会进去路由检测传入的参数是,新建的http实例,和配置信息
参数赋值在79和81
进入后发现$request->path()函数
我们进入path函数在 433行
这里this是http实例$request调用的
437会进入用this调用进入pathinfo函数
进入后发现会用(isset($_GET[Config::get('var_pathinfo')]))
进行检测
这里会进入,说明一下Config::get('var_pathinfo')是配置文件中的设置的参数,默认值为s,从GET中获取键值这里给的是captcha
这里把给this.pathinfo赋值
这里把返回值赋值给$pathinfo , 有赋值给this.path,最后return this.path
继续执routeCheck函数,会在642行进入,check函数
在Route.php中第839行进入check
857会调用method函数
进入method,
在524中的 isset($_POST[Config::get('var_method')]) var_method
和上面的一样,配置文件中的设置的参数 获取_method的值,这里是__construct
给this->method
赋值大写的__construct
,然后用this调用构造函数参数为发送post数据的数组形式
进入__construct函数,这里的foreach会遍历所有变量,把http实例中存在的覆盖,不存在的添加 method为get
返回method函数,把getthis->method返回,也就是get
返回check函数,最后执行并返回
返回内容
在这调用完成之后routeCheck将返回method赋值给dispatch
继续执行run到,138行会执行exec
到exec函数里,这里会因为type为method执行相应的分支。
468行,有调用param函数 634
突然发现这里的method已经是返回执行whoami后的数据了,
不过不能显示出来
继续执行到了,661 执行了input函数
要执行array_walk_recursive函数
这里用system做参数,$tada为null,执行filterValue
Foreach循环 $
$value值为whoami $key是0 $filters是
到1083就是一个回调函数,$filter当作函数,$value当作参数执行
完成
首先$filters是一个数组而且至少2个值而且值要是一个函数或者方法才能执行call_user_func函数
往上
这里$data也要是数组才能执行array_walk_recursive函数
这里this要指向一开始创建的http实例,然后$filter要是一个空值
- 通过
- 未通过
0 投票者