太菜了 学习一下thinkphp……

数据获取

通过input方式来获取数据 其主要调用request类的request方法如input('get.a') 过滤函数为filterExp:

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 其他安全过滤
}

会添加一个空格来让其失效 主要是针对后面数据操作的做的一个过滤 所以在输出值时 要自行对其进行转移 防止XSS

可以在应用控制其上要添加一个处理函数 可以在./extra/目录下创建一个function文件 添加公共函数:

1
2
3
4
5
6
7
8
9
10
function html($string){
if(is_array($string)){
foreach ($string as $key=>$value){
$string[$key]=html($value);
}
}else{
$string=htmlspecialchars($string);
}
return $string;
}

在数据输出位置 使用函数来对数据中特殊字符进行进行转移
使用方式可以是 thinkphp中请求方式自带的一种过滤方式 :Request::instance()->filter('htmlspecialchars')或者Request::instance()->get('name','','htmlspecialchars')或者input("get.a/id","","htmlspecialchars,strip_tags") (input请求以/分割参数 过滤以,分割)

其中request是核心 他可以获得很多请求信息 :

1
2
3
4
5
6
7
$request->url() //url地址
$request->root() //root地址
$request->path() //path信息
$request->header() //header信息
$request->ip() //ip信息
$request->only([name]) //请求参数仅包含 同input('param.name')
$request->except([name]) //同理 排除参数

thinkphp有一种叫参数绑定的请求方式 如http://serverName/index.php/index/blog/archive/year/2016/month/06 此种请求的意思是 为archive方法绑定 yearmonth参数
还有一种形参接收参数 如:

1
2
3
4
public function index($user)
{
var_dump($user);
}

这样就可以直接接收post get等方式的user传参 同时支持接收数组 而input(“param.user”)是不支持接受参数的

数据库操作

thinkphp使用的是PDO的查询方式 很大方面增加了安全性 就学习一下他的数据库操作
首先来看看如何连接数据库 在application/目录下database.php配置数据库信息 就可以直接掉用db()助手函数来实例化数据库 如:

1
2
3
$username = input('get.user');
$info = db('a')->where(array('id'=> $username))->select();
var_dump($info);

接下来就会进行一串复杂的操作 (真的菜…) 这里先引入别人的一张图:
Alt text

参考:http://blog.csdn.net/u011069013/article/details/53096102
http://seaii-blog.com/index.php/2017/10/02/71.html

可以看到 Connection作为连接器 Query作为查询器 Builder作为sql生成器 这里主要关注Builder的代码

select

Query要准备查询时 他会分析查询表达式 即$options = $this->parseExpress()where table field data join union等信息分析传入option数组 然后会调用$sql = $this->builder->select($options)这一重要操作

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

这里为什么我们要重点关注parseWhere 因为parseWhere操作比较多 比较复杂 而其他生成函数相对较简单 大多是直接从$option数组读出数据绑定
parseWhere主要调用parseWhereItem函数
先说一下where表达式的格式 最主要包括三部分 字段field,关系表达式exp,数据value 进入函数后 会进行

1
2
3
4
if (!is_array($val)) {
$val = ['=', $val];
}
list($exp, $value) = $val;

获取exp,value 所以 我们在传数组时 要满足数组[0]为表达式 数组[1]为数据值 但是其实我们是无法直接通过接收数组来赋值表达时的 因为上面数据获取那一块提到 get,post等传参都会经过filterExp函数 而一旦检测到关系表达式 就会在后面添加有个空格 导致在parseWhereItem函数检测exp时 错误退出:

1
2
3
4
5
6
7
8
if (!in_array($exp, $this->exp)) {
$exp = strtolower($exp);
if (isset($this->exp[$exp])) {
$exp = $this->exp[$exp];
} else {
throw new Exception('where express error:' . $exp);
}
}

导致传输的exp无效 所以是无法通过请求来传输数组 控制表达式
这里顺便复现一下 5.0.10版本鸡肋的注入 :
注入产生的原因正是因为绕过了filterExp 而导致可以控制表达式 即NOT LIKE 而此表达式在拼接时:

1
2
3
4
5
6
7
8
9
if (is_array($value)) {
foreach ($value as $item) {
$array[] = $key . ' ' . $exp . ' ' . $this->parseValue($item, $field);
}
$logic = isset($val[2]) ? $val[2] : 'AND';
$whereStr .= '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')';
} else {
$whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field);
}

可以注意到$val[2]是为做处理的 直接拼接到语句中 导致注入(要产生注入 就要在绑定数据之前生成 注入语句 即预编译时产生注入)
继续说说上面的 不同表达式 会进行不同的拼接 在拼接数据值时 会注意到一个关键函数parseValue:

1
2
3
4
5
6
7
8
9
10
11
12
protected function parseValue($value, $field = '')
{
if (is_string($value)) {
$value = strpos($value, ':') === 0 && $this->query->isBind(substr($value, 1)) ? $value : $this->connection->quote($value);
} elseif (is_array($value)) {
$value = array_map([$this, 'parseValue'], $value);
} elseif (is_bool($value)) {
$value = $value ? '1' : '0';
} elseif (is_null($value)) {
$value = 'null';
}
return $value;

对数据值进行quote处理 通过查询 quote的作用是为值附上引号 或转移 可以说是一个安全处理函数 所以

顺便记录p师傅blog的鸡肋sql注入 只不过在5.0.10版本已经修复了 (基本都跟传入数组相关 至少要能传入数组)

insert

在进行insert操作时 会在Builder中insert方法生成语句 其中有一个关键操作$data = $this->parseData($data, $options); 来进行$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
37
38
39
40
41
42
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 (isset($val[0]) && 'exp' == $val[0]) {
$result[$item] = $val[1];
} 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;
}

其中有一步会判断$value是否为数组 数组[0]是否为exp 如果是 则直接将\$value[1]赋值给\$result 即最后的$data 这里就可能导致注入 参考catfish注入

可能导致的问题

Display导致任意文件包含(效果类似包含)

在做swpu比赛的时候了解到这个地方可能产生漏洞 下来分析了一下
当时问了一下出题的师傅 题目版本是tp3的 dispaly函数:

1
2
3
4
5
6
7
8
9
10
11
public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
G('viewStartTime');
// 视图开始标签
Hook::listen('view_begin',$templateFile);
// 解析并获取模板内容
$content = $this->fetch($templateFile,$content,$prefix);
// 输出模板内容
$this->render($content,$charset,$contentType);
// 视图结束标签
Hook::listen('view_end');
}

在调用display时会调用fetch函数 fetch函数在运行时 会有一个读取文件内容的操作 :
Alt text
再编译模板内容:

1
2
3
$tmplContent = $this->compiler($tmplContent);
Storage::put($tmplCacheFile,trim($tmplContent),'tpl');
return $tmplCacheFile;

这个过程没怎么看懂 但是将读取的内容 写入到了一个tmp文件中 格式:

1
<?php if (!defined('THINK_PATH')) exit(); phpinfo();?>

最后编译文件 最后调用read函数 来输出内容 效果:
Alt text

再来看一下tp5 tp5的display比tp3多了一种渲染输出的方式 一种是fetch 一种是display(这里说的是渲染输出的方法) 先来关注fetch函数 他调用Template.php的fetch方法时:

1
2
3
4
5
6
7
if ($template) {
$cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.');
if (!$this->checkCache($cacheFile)) {
// 缓存无效 重新模板编译
$content = file_get_contents($template);
$this->compiler($content, $cacheFile);
}

同tp3相似 读取编译 最后 再调用read函数输出内容 达到包含的效果
display渲染输出是直接将$this->display()中值作为内容输出 然后编译输出 所以 可以直接:
Alt text

所以$this->display()的值是一旦被用户可控 就可能造成危险

缓存漏洞

前面的文章提及到了 主要是Cache::set函数 的前两个参数 如果可控 就可能造成缓存getshell

注入

注入 前面提到了 主要是如果用户可以通过其他途径传入可控数组 就有可能导致注入

最后

还是太菜了 只是初步认识了一下thinkphp 在以后的学习中 再继续学习…..