Code Audit: DuomiCms Global Filter Bypass
Background
While browsing the CNVD vulnerability database I noticed someone had reported a front-end SQL injection in a certain CMS. The description pointed at the cardpwd parameter, so I decided to audit that version of the CMS myself.

Audit Process
Locating cardpwd
After deploying the source locally, I loaded it into the Seay source audit tool and searched for the keyword cardpwd. The hit landed in member/mypay.php, where the parameter is received via POST.

Inside member/mypay.php (lines 26 and 38), receiving cardpwd requires a logged-in session and $dm=='mypay'. Tracing $dm led to duomiphp/common.php (lines 52-55), which imports GET, POST, and COOKIE keys as variables — a pattern that also hints at variable overwrite (out of scope here).

So reaching this feature needs two things: a registered, logged-in account; and dm=mypay submitted via GET, POST, or COOKIE.
The First Regex Filter
After registering and visiting /member/mypay.php, I submitted 1,2 to confirm cardpwd was reachable. Reading on through lines 43-63, the handling of cardkey and cardpwd runs a regex check:
[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}
Testing this regex showed that /*!50000 xxxx*/ slips past it — MySQL’s versioned comment syntax, where SQL inside the comment executes when the server version is ≥ 5.00.00. It counts as a legal comment (defeating naive /* regexes) while still letting keywords like union reach the parser. But this only clears the mypay.php regex — CheckSql is still waiting downstream.

GetOne and Execute
The filter turned out not to be that simple. Line 63 runs the SQL through GetOne. Located in duomiphp/sql.class.php (lines 277-300), GetOne strips trailing , and ;, then appends limit 0,1; so the query returns a single row — the comment reads “execute a SQL statement, return the previous or only one record.”

It then calls SetQuery (line 288) and Execute (line 290). Following Execute (lines 234-269) reveals the key method CheckSql, with a comment explicitly stating it is “for SQL security checks.”

CheckSql: The 80sec Global Filter
CheckSql lives in duomiphp/sql.class.php (lines 537-642). The comment notes it is “an SQL statement filter provided by 80sec, with modifications here.” Testing showed that line 598 hard-matches /* (string comparison, not regex), so it cannot be bypassed — the /*!50000 xxxx*/ payload from the previous step dies here.

With UNION injection off the table, I tried subqueries. That also failed against the regex at line 628:
~\([^)]*?select~s
It matches any select wrapped in parentheses (i.e., a subquery), and the s modifier makes . match newlines. MySQL has no parenthesless subquery form — the official documentation requires parentheses around subqueries.
Re-reading the CNVD Description
When you run out of ideas, go back and re-read everything you have already collected — small surprises tend to get left behind on the road.
The CNVD description said “the system does not filter variables.” I half-jokingly wondered whether I had downloaded the wrong source. Could 'or'1 — the classic arbitrary-recharge trick — really earn a CNVD ID? (On this CMS, submitting any card number at /member/mypay.php with 'or'1 as the password recharges the account.)
I added a recharge card in the admin backend, registered a front-end account, logged in, and submitted cardpwd = 'or'1. It worked. A bug, sure, but not the kind that convinces me.

The Bypass
A Small Detail from Gray-Box Testing
I switched to gray-box testing on cardpwd: the input was caught by the regex (lines 43-63), caught by CheckSql (lines 537-642), and the unbalanced ' threw a SQL error.
But one detail stood out — a stray ' caused a SQL error, and the error message echoed back the cardpwd value I had submitted, instead of the “request filtered” alert. That meant something was off. Two possibilities:
- The SQL ran before being filtered (×)
- The extra
'caused the injection to slip past the filter (√)
The “Full SQL Check” Block
To pin down where things happened, I inserted echo statements at key points to print cardpwd as it flowed through processing. With /*!50000union*/, the response was Safe Alert: Request Error step 2! — tracing that string led to duomiphp/sql.class.php line 635, inside CheckSql.

Lines 561-586 contain a block labeled “full SQL check” that I had been ignoring while focused on the filter rules. Because this CMS transmits everything in plaintext, password fields can contain characters that trip the SQL detection rules. To avoid false positives, the developers wrote this block to replace content wrapped in single-quote pairs.

I inserted echo $clean; at line 589 and confirmed: anything between single quotes had been turned into $s$. Then echo $db_string; at line 640 printed the data that actually passed the checks.

Data Flow and Bypass Principle
The very mechanism designed to protect password fields from false positives is what made the bypass possible. The data flow:
cardpwd → $pwd → GetOne() → Execute() → CheckSql() → $clean → $db_string
The $clean → $db_string step works like this: the original data is first sanitized by the “full SQL check” and assigned to $clean, which is then fed through every SQL injection detection rule. If all rules pass, the original data is returned as $db_string. Crucially, none of the filter rules touch `, ', or ".
So the attack only needs to leverage the “single-quoted content becomes $s$” behavior: place sensitive keywords inside a single-quote pair, and they get replaced during detection (passing the check) while $db_string still carries the original payload at execution time.
Crafting the Payload
To wrap characters in single quotes without breaking SQL semantics, the single quotes themselves must be “escaped” — forced to pair up in a way that leaves the keyword-bearing fragment inside a quote pair. Methods to escape a single quote:
- SQL comment:
/*'*/or/*!60000'*/ - Backtick:
` ' ` - Double quote:
" ' "
This yields the first payload (double quote plus backtick):
bypass'or"'or extractvalue(1,(select group_concat(0x3a,name,0x3a,password) from duomi_admin`'`))or '1
The processed result ($db_string), printed for readability:

A second variant using two double quotes:
bypass'or"'or extractvalue(1,(select group_concat(0x3a,name,0x3a,password) from duomi_admin))or "'=

Final Thoughts
The overall flow is clear and the audit itself is not hard. The time sink was fixating on cracking the filter regexes head-on. When a blacklist refuses to bend, look at the surrounding code instead — this time, creatively abusing the “full SQL check” bypassed the global SQL defense entirely.
The CNVD entry covers only one parameter (cardpwd’s 'or'1 recharge), so the reporter’s technique clearly differs from this post. What this post bypasses is the global check — every call site of CheckSql() shares the same flaw.
One closing thought: blacklist-style global filtering is fundamentally a patching contest with attackers. No matter how tight the rules, some function or encoding form will slip through. The only reliable defense is parameterized queries, which structurally isolate data from SQL syntax. Thanks to everyone who helped brainstorm along the way.
(Note: the CMS version covered here has long been unmaintained. Payloads are for learning purposes only.)