前言
这实际上是i春秋的一道题,上网搜到POC
后,我就直接一把梭地打过去,没有去深究漏洞产生的原因。
这里利用的是缓存文件没有合理过滤造成的漏洞,从而前台getshell
了。因为没有遇到过这种情况,所以这里也就复现一下这个漏洞。
php5.6+deepin5.11
onethink1.0
代码审计
这套cms
是基于thinkphp3
进行的二次开发
./Application/Home/Model/MemberModel.class.php--function login() line 35
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
|
public function login($uid){ $user = $this->field(true)->find($uid); if(!$user){ $Api = new UserApi(); $info = $Api->info($uid); $user = $this->create(array('nickname' => $info[1], 'status' => 1)); $user['uid'] = $uid; if(!$this->add($user)){ $this->error = '前台用户信息注册失败,请重试!'; return false; } } elseif(1 != $user['status']) { $this->error = '用户未激活或已禁用!'; return false; }
$this->autoLogin($user);
action_log('user_login', 'member', $uid, $uid);
return true; }
|
这个函数是用于登录指定用户时被调用的,由于我们之前已经将注册过了,所以用户信息已经被存入数据库中。
login()
的参数是$uid
,这个参数来自于数据库中的记录,在数据库中查找该用户的id,正确情况下跟踪到函数$this->autoLogin($user);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private function autoLogin($user){ $data = array( 'uid' => $user['uid'], 'login' => array('exp', '`login`+1'), 'last_login_time' => NOW_TIME, 'last_login_ip' => get_client_ip(1), ); $this->save($data);
$auth = array( 'uid' => $user['uid'], 'username' => get_username($user['uid']), 'last_login_time' => $user['last_login_time'], );
session('user_auth', $auth); session('user_auth_sign', data_auth_sign($auth));
} }
|
调用函数get_username()
,跟踪这个函数
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
|
function get_username($uid = 0){ static $list; if(!($uid && is_numeric($uid))){ return session('user_auth.username'); }
if(empty($list)){ $list = S('sys_active_user_list'); }
$key = "u{$uid}"; if(isset($list[$key])){ $name = $list[$key]; } else { $User = new User\Api\UserApi(); $info = $User->info($uid); if($info && isset($info[1])){ $name = $list[$key] = $info[1]; $count = count($list); $max = C('USER_MAX_CACHE'); while ($count-- > $max) { array_shift($list); } S('sys_active_user_list', $list); } else { $name = ''; } } return $name; }
|
关键代码如下
1 2 3 4 5 6 7 8 9 10 11
| $User = new User\Api\UserApi(); $info = $User->info($uid); if($info && isset($info[1])){ $name = $list[$key] = $info[1]; $count = count($list); $max = C('USER_MAX_CACHE'); while ($count-- > $max) { array_shift($list); } S('sys_active_user_list', $list);
|
这里调用了S()
函数,可以看到$name=>'sys_active_user_list',$value=>$list=>$info[1]
,其中$info[1]
来自数据库查询的结果,查询结果中的nickname
。可以看到这里调用了函数set()
。
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
| function S($name,$value='',$options=null) { static $cache = ''; if(is_array($options) && empty($cache)){ $type = isset($options['type'])?$options['type']:''; $cache = Think\Cache::getInstance($type,$options); }elseif(is_array($name)) { $type = isset($name['type'])?$name['type']:''; $cache = Think\Cache::getInstance($type,$name); return $cache; }elseif(empty($cache)) { $cache = Think\Cache::getInstance(); } if(''=== $value){ return $cache->get($name); }elseif(is_null($value)) { return $cache->rm($name); }else { if(is_array($options)) { $expire = isset($options['expire'])?$options['expire']:NULL; }else{ $expire = is_numeric($options)?$options:NULL; } return $cache->set($name, $value, $expire); } }
|
跟进函数set()
,缓存文件的操作类./Library/Think/Cache/Driver/File.class.php
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
|
public function set($name,$value,$expire=null) { N('cache_write',1); if(is_null($expire)) { $expire = $this->options['expire']; } $filename = $this->filename($name); $data = serialize($value); if( C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) { $data = gzcompress($data,3); } if(C('DATA_CACHE_CHECK')) { $check = md5($data); }else { $check = ''; } $data = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>"; $result = file_put_contents($filename,$data); if($result) { if($this->options['length']>0) { $this->queue($name); } clearstatcache(); return true; }else { return false; } }
|
这个函数用于写入缓存。关键代码如下:
1 2 3 4
| $filename = $this->filename($name); $data = serialize($value); $data = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>"; $result = file_put_contents($filename,$data);
|
这个filename()
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
private function filename($name) { $name = md5($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; }
|
我们查看./ThinkPHP/Conf/convertion.php
1 2 3 4 5
| .... 'DATA_CACHE_PATH' => TEMP_PATH, 'DATA_CACHE_SUBDIR' => false, 'DATA_PATH_LEVEL' => 1,
|
查看./ThinkPHP/ThinkPHP.php
1
| defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH.'Temp/');
|
所以最终文件存放位置就是./Runtime/temp/md5($).php
由于这里的$name
为sys_active_user_list
,知道文件存放位置后,我们就可以去构造payload了。
1
| $data = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>";
|
我们构造payload:%0aphpinfo();#
%0
使得我们传入的参数不被注释,#
使得序列化后的字符失效。
注册一个用户,使得用户名为payload,不过在注册时,要抓包,将%0a
解码一下。
登录该用户,使得用户名为payload,不过在登录时,要抓包,将%0a
解码一下。
然后查看缓存文件
总结
漏洞不是很复杂,但是对tp的框架的理解还有待提高