12-PHP代码审计——ThinkPHP5.0.15 update注入分析
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了12-PHP代码审计——ThinkPHP5.0.15 update注入分析相关的知识,希望对你有一定的参考价值。
环境:
thinkphp_5.0.15_full
poc:
level[0]=inc&level[1]=updatexml(1,concat(0x7,user(),0x7e),1)&level[2]=1
下载ThinkPHP5.0.15解压到www.tptest.com目录下,并在phpstudy中将域名站点的网站目录改为ThinkPHP5.0.15的public目录。
在application目录的config.php文件中开启跟踪调试模式,如下所示:
// 应用调试模式
'app_debug' => true,
// 应用Trace
'app_trace' => true,
然后在application目录的database.php文件中配置数据库
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'thinkphp32',
// 用户名
'username' => 'root',
// 密码
'password' => '123456',
// 端口
'hostport' => '3306',
如果出现以下画面说明配置正常
update注入示例程序:
class Index
{
public function index()
{
///a表示以数组接收level
$level = input("level/a");
$data = db("users")->where("id" , 1)->update(["level"=>$level]);
dump($data);
}
}
thinkphp提供了input函数来接收数据,那么问题是input函数是如何接收提交的参数?
访问网址提交poc:
网页返回了错误信息,直接把数据库的用户信息爆出来了。
在input函数中,内部有两个函数比较重要:filterValue和typeCast
public function input($data = [], $name = '', $default = null, $filter = ''){
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name,提取数据
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
} else {
$type = 's';
}
// 按.拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
if (is_object($data)) {
return $data;
}
}
//后面的操作都是对数据过滤,强转
// 解析过滤器
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter)) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
//提取数据并过滤
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制数据类型转换
$this->typeCast($data, $type);
}
return $data;
}
filterValue函数内部实际是使用了filterExp函数过滤特殊关键字,如下所示:
public function filterExp(&$value){
// 过滤查询特殊字符
if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
}
如果提交的数据中出现了以上这些关键字的话,都会被过滤掉
typeCast函数内部会会对数据进行数据类型转换,支持以下数据类型转换
private function typeCast(&$data, $type){
switch (strtolower($type)) {
// 数组
case 'a':
$data = (array) $data;
break;
// 数字
case 'd':
$data = (int) $data;
break;
// 浮点
case 'f':
$data = (float) $data;
break;
// 布尔
case 'b':
$data = (boolean) $data;
break;
// 字符串
case 's':
default:
if (is_scalar($data)) {
$data = (string) $data;
} else {
throw new \\InvalidArgumentException('variable type error:' . gettype($data));
}
}
}
thinkphp5.0.15操作数据库操作流程:
db() --> where() --> update()
db()函数主要是实例化数据库的一些初始化操作,where函数内部是一些赋值操作,我们重点从update函数开始分析:
public function update(array $data = []){
//提取数据进行赋值
$options = $this->parseExpress();
$data = array_merge($options['data'], $data);
$pk = $this->getPk($options);
if (isset($options['cache']) && is_string($options['cache']['key'])) {
$key = $options['cache']['key'];
}
if (empty($options['where'])) {
// 如果存在主键数据 则自动作为更新条件
if (is_string($pk) && isset($data[$pk])) {
$where[$pk] = $data[$pk];
if (!isset($key)) {
$key = 'think:' . $options['table'] . '|' . $data[$pk];
}
unset($data[$pk]);
} elseif (is_array($pk)) {
// 增加复合主键支持
foreach ($pk as $field) {
if (isset($data[$field])) {
$where[$field] = $data[$field];
} else {
// 如果缺少复合主键数据则不执行
throw new Exception('miss complex primary data');
}
unset($data[$field]);
}
}
if (!isset($where)) {
// 如果没有任何更新条件则不执行
throw new Exception('miss update condition');
} else {
$options['where']['AND'] = $where;
}
} elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {
$key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind);
}
// 生成UPDATE SQL语句,这里又调用了一次update函数
$sql = $this->builder->update($data, $options);
// 获取参数绑定
$bind = $this->getBind();
if ($options['fetch_sql']) {
// 获取实际执行的SQL语句
return $this->connection->getRealSql($sql, $bind);
} else {
// 检测缓存
if (isset($key) && Cache::get($key)) {
// 删除缓存
Cache::rm($key);
} elseif (!empty($options['cache']['tag'])) {
Cache::clear($options['cache']['tag']);
}
// 执行操作
$result = '' == $sql ? 0 : $this->execute($sql, $bind);
if ($result) {
if (is_string($pk) && isset($where[$pk])) {
$data[$pk] = $where[$pk];
} elseif (is_string($pk) && isset($key) && strpos($key, '|')) {
list($a, $val) = explode('|', $key);
$data[$pk] = $val;
}
$options['data'] = $data;
$this->trigger('after_update', $options);
}
return $result;
}
}
update函数内部又调用了一个update函数
update函数中接收的数组data是一个数组level,其实就是之前我们提交的poc,写成数组的目的是为了绕过后面的过滤。
在这个update函数中有几个函数比较关键
public function update($data, $options){
//提取数据
$table = $this->parseTable($options['table'], $options);
//解析data,这里的data是level数组
$data = $this->parseData($data, $options);
if (empty($data)) {
return '';
}
foreach ($data as $key => $val) {
$set[] = $key . '=' . $val;
}
//过滤的核心函数
$sql = str_replace(
['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
[
$this->parseTable($options['table'], $options),
implode(',', $set),
$this->parseJoin($options['join'], $options),
$this->parseWhere($options['where'], $options),
$this->parseOrder($options['order'], $options),
$this->parseLimit($options['limit']),
$this->parseLock($options['lock']),
$this->parseComment($options['comment']),
], $this->updateSql);
return $sql;
}
parseTable函数会把表拿出来赋值,parseData函数会对level的内容进行解析,这里我们要重点分析parseData函数对level数组做了哪些处理。
分析parseData函数:
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 = [];
//取出level数组中的数据,分别放入key和val
foreach ($data as $key => $val) {
//解析key
$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';
//val是否为数组
} 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;
}
parseData函数将data数组中的内容取出来放入val中然后过滤,此时的val仍然是一个数组。
val当满足条件后,会取出val的第一个元素进行匹配,如果匹配到inc就会执行图中这行代码,parseKey函数解析了val[1]和val[2]
这行代码执行时会把val[1]和val[2]的内容拼接起来,$result[$item]中拼接成的内容是这样的updatexml(1,concat(0x7,user(),0x7e),1)+1。
因为parseKey函数虽然对key的内容做了过滤,但这里只过滤了特殊字符然后将key返回,所以这里key的内容仍然还是:updatexml(1,concat(0x7,user(),0x7e),1),之前的poc用数组的方式提交目的是为了在这里绕过过滤。
继续跟进parseKey函数如何解析val的内容
protected function parseKey($key, $options = []){
$key = trim($key);
//这个if没有过滤
if (strpos($key, '$.') && false === strpos($key, '(')) {
// JSON字段支持
list($field, $name) = explode('$.', $key);
$key = 'json_extract(' . $field . ', \\'$.' . $name . '\\')';
} elseif (strpos($key, '.') && !preg_match('/[,\\'\\"\\(\\)`\\s]/', $key)) {
list($table, $key) = explode('.', $key, 2);
if ('__TABLE__' == $table) {
$table = $this->query->getTable();
}
if (isset($options['alias'][$table])) {
$table = $options['alias'][$table];
}
}
//过滤特殊字符
if (!preg_match('/[,\\'\\"\\*\\(\\)`.\\s]/', $key)) {
$key = '`' . $key . '`';
}
if (isset($table)) {
if (strpos($table, '.')) {
$table = str_replace('.', '`.`', $table);
}
$key = '`' . $table . '`.' . $key;
}
return $key;
}
接下来还调用了核心过滤函数str_replace,但是str_replace函数内部没有对$result[$item]的内容进行过滤,它只过滤了where子单元的内容,这样sql语句就绕过了后台的过滤。
update函数中返回的sql语句是这样的:
execute函数在执行该sql语句会报错,同时还会爆出数据库的用户信息。
到此,漏洞分析结束。
以上是关于12-PHP代码审计——ThinkPHP5.0.15 update注入分析的主要内容,如果未能解决你的问题,请参考以下文章