catfish注入分析及THINkPHP5初学习

看一篇关于catfishcms的注入审计 比较有意思 用来复现分析学习 cms是使用的thinkphp5的框架 没有接触过 所以记录一下他的db处理(是真的菜~~)

0x00 thinkphp数据处理流程学习

主要文件:

1
2
3
library\think\db\Builder.php
library\think\db\Query.php
library\think\db\Connection.php

thinkphp5使用的是PDO的数据处理 主要流程:

1
获取数据-》生成语句=》PDO处理数据

主要难点是在生成语句这里 主要在Builder.php中完成 很绕 看的稀里糊涂 大致是 获取一个基本模板 如insert:

1
$insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';

通过处理数据 然后替换:

1
2
3
4
5
6
7
8
9
$sql = str_replace(
['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseTable($options['table']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($options['comment']),
], $this->insertSql);

这样生成一句sql预编译语句 再传入$bind,$sql两个主要成分 执行预编译-》绑定数据-》执行。
$bind中包含着数据字段 数据值 数据类型
$sql为预编译语句 进过各类分析(这里看的很头疼) 最关键的点处理$data ,$option时 确定需要绑定的点

0x01 catfish注入分析

上面的给出的注入点 控制点来自对cookieuser_id的控制 cms在这没有对这个数据进行处理 直接赋值给sessionuser_id 导致后面调用sessionuser_id时产生注入
文中给出的payload为:

1
think:["exp","1 or updatexml(1,concat(0x3e,user()),0)"]

其中think:与session获取数据有关 这里就不分析了 分析一下后面一部分
文中使用的是isert注入 在thinkphp5中使用注入 就要在预编译是就讲数据绑定在$sql上 这样注入才可利用 如果在后绑定 PDO处理你所构造的语句是无效的 所以 我们来看看insert他是如何生成语句的
上面给出了insert的例子:

1
2
3
4
5
6
7
8
9
$sql = str_replace(
['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseTable($options['table']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($options['comment']),
], $this->insertSql);

可以看出$value是我们注入操作的核心 $value变量是我们从处理后的$data
中获取 来看看他对$data的处理 关键点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected function parseData($data, $options)
{
if (empty($data)) {
return [];
}
// 获取绑定信息
$bind = $this->query->getFieldsBind($options);
if ('*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($key);
if (!in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif (isset($val[0]) && 'exp' == $val[0]) {
$result[$item] = $val[1];
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} elseif (is_scalar($val)) {
// 过滤非标量数据
if ($this->query->isBind(substr($val, 1))) {
$result[$item] = $val;
} else {
$this->query->bind($key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
$result[$item] = ':' . $key;
}
}
}
return $result;

主要有3种情况 一种是键值对应原值 一种是键值对应空值 一种是键值对应绑定键值 最后总的返回给$data 所以我们要产生注入 就要控制到第一种情况中 最后在替换时 就直接将语句绑定到预编译语句上 而满足第一种情况 需要值为数组 且第一个值为exp 这就是payload的来源

这里因为是cookie传参控制的 所以避开了 thinkphp的Request.php中对数据的处理 他其中对get,post传参都进行了

1
2
3
4
5
6
7
8
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 .= ' ';
}
// TODO 其他安全过滤
}

开始对框架不熟悉 没看这个 卡了半天(关键是调试那个空格还不明显~~ 简直菜)

会想既然insert可以在预编译语句上绑定数据 select那些可以吗
来看看select的数据处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$sql = str_replace(
['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($options['table']),
$this->parseDistinct($options['distinct']),
$this->parseField($options['field']),
$this->parseJoin($options['join']),
$this->parseWhere($options['where'], $options),
$this->parseGroup($options['group']),
$this->parseHaving($options['having']),
$this->parseOrder($options['order']),
$this->parseLimit($options['limit']),
$this->parseUnion($options['union']),
$this->parseLock($options['lock']),
$this->parseComment($options['comment']),
$this->parseForce($options['force']),
], $this->selectSql);

主要是parseWhere 其中最关键的是buildWhere, parseWhereItem两个函数
其中parseWhereItem中有个关键有个关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
} 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) {
$bind[$bindName . '_in_' . $k] = [$v, $bindType];
$array[] = ':' . $bindName . '_in_' . $k;
}
$this->query->bind($bind);
$zone = implode(',', $array);
} else {
$zone = implode(',', $this->parseValue($value, $field));
}
$whereStr .= $key . ' ' . $exp . ' (' . $zone . ')';
}

p师傅有分析

当执行prepare预编译时 报错就不会进入下面操作 如果没有开启调试报错 也无法通过报错获取数据

0x02 最后

太菜了~~~~