Security Research

Code Audit: DedeCMS V5.7 SP2 Vulnerability Collection

#Web Security#Code Audit
Copper-orange fractured chain links and fissures over a near-black teal-blue background, symbolizing the five DedeCMS entry points threading through front-end, back-end, and file-operation chains

Background

DedeCMS (织梦 CMS) had a string of 0days surfacing recently. With the vendor not yet patched, I took the chance to study them and turned the findings into this collection — feedback from fellow researchers very welcome.

Note: This audit is based on the DedeCMS V5.7 UTF8 SP2 (20180109) source package, carried out in January 2018. All five vulnerabilities have since been patched in subsequent releases. As of 2026, DedeCMS is still maintained, but the 2021 commercial-licensing controversy pushed many sites to migrate to other CMSes. The architectural pattern — external variable registration + blacklist filtering + weak-typed comparison — remains a worthwhile audit teaching sample, and the chained approach generalises across other Chinese-made CMSes.

Vulnerability collection overview

The whole audit reduces to one thread: user-controllable entry points in the front-end member centre, combined with PHP weak typing and the legacy variable-registration baggage, escalate step by step into back-end admin and the file system. The sections below walk through them by vulnerability type.

Environment

  • Source: DedeCMS-V5.7-UTF8-SP2-20180109
  • Problem files: uploads/member/index.php, uploads/include/memberlogin.class.php, uploads/include/helpers/cookie.helper.php
  • Type: cookie forgery → arbitrary front-end user login

Analysis

In member/index.php lines 125-166, the block that updates the recent-visitor and site-stat records will, when $vtime - $last_vtime > 3600 || !preg_match('#,'.$uid.',#i', ','.$last_vid.',') is satisfied and $last_vid is empty, assign $last_vid = $uid and then at line 164 call PutCookie('last_vid', $last_vid, 3600*24, '/') to push the cookie to the client.

DedeCMS uses external variable registration in include/common.inc.php (lines 108-117), so $uid here is user-controllable.

Following PutCookie into include/helpers/cookie.helper.php lines 21-29: line 27 concatenates the value with $cfg_cookie_encode from the config, then truncates an MD5 digest via substr(md5($cfg_cookie_encode.$value),0,16) and ships it to the client.

The GetCookie validator in the same file (lines 54-75) checks at line 65 whether the client cookie has been forged. To forge a cookie, two routes come to mind:

  1. Obtain $cfg_cookie_encode from data/config.cache.inc.php (requires an arbitrary file read/download vulnerability);
  2. Use the PutCookie path that the server itself invokes on first login to mint a cookie — such a cookie is guaranteed to pass GetCookie.

A correction to some republished versions of the original article: $cfg_cookie_encode lives in data/config.cache.inc.php, not in data/safe/inc_safe_config.php. The two files are unrelated.

Following the login code in include/memberlogin.class.php: once a valid loginuser / loginpwd pair is supplied, PutLoginInfo is invoked. That method (lines 517-540) uses PutCookie at lines 531-539 to push cookies — hence the forgery surface.

The login-state check in the same file (lines 160-241) reads DedeUserID from the cookie at line 170, and at line 185 feeds it into a database query whose result is rendered on the page.

Reproduction

Case 1: Because mid in the database is an int, forging a cookie requires registering an account whose username equals the mid of the target user (admin defaults to mid=1). For example, register the username 0001 — it maps to mid=1 in dede_member, which is admin.

Then issue this request to obtain the forged cookie:

GET http://127.0.0.1/member/index.php?uid=0001

Log in with the 0001 account, grab the unmodified post-login cookie, copy last_vid into DedeUserID and last_vid__ckMd5 into DedeUserID__ckMd5, then refresh — you are now logged in as admin.

Cookie forgery PoC

Case 2: In memberlogin.class.php line 170, the cookie is validated first, then GetNum strips anything that is not a digit or a dot, and the result is cast to int before being concatenated into SQL. Meanwhile member/index.php line 124, when uid is non-empty, require_onces DEDEMEMBER.'/inc/config_space.php'; that file at line 29 calls GetUserSpaceInfos, which at line 131 fetches user data with a LIKE clause.

So you can register a username like bala1bala (any name containing the target mid digit will do in practice), then pass %1% in the uid parameter so GetUserSpaceInfos still returns data and execution reaches PutCookie to mint the forged cookie. Then swap the cookie:

DedeUserID=%1%;
DedeUserID__ckMd5=8983265c65c8d1ca;

GetNum(GetCookie("DedeUserID")) casts to int 1, which is then concatenated into SQL — and you are logged in as admin.

Cookie forgery key code

Vuln 2: Arbitrary Front-End Password Change

Environment

  • Problem files: uploads/member/resetpassword.php, uploads/member/inc/inc_pwd_functions.php
  • Type: arbitrary user password change

Analysis

In member/resetpassword.php lines 95-96, the expression $row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer is the crux. By default $row['safequestion'] is 0 in the database and $row['safeanswer'] is empty; both $safeanswer and $safequestion are user-controllable, and the comparison uses == — a classic weak-typing flaw.

The line if(empty($safequestion)) $safequestion = ''; requires empty($safequestion) to be false while $row['safequestion'] == $safequestion is true. The string "0.0" satisfies both.

Once past the check, execution enters the sn method. Following it into member/inc/inc_pwd_functions.php lines 150-172, the method calls newmail, which at lines 73-123 issues the password-reset link when the incoming $send is N.

Reproduction

First, request a reset key:

GET http://127.0.0.1/member/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=1

Then follow the redirect link to reset the password:

GET http://127.0.0.1/member/resetpassword.php?dopost=getpasswd&id=1&key=UXqCX4lO

Vuln 3: Arbitrary Back-End Password Reset

Environment

  • Problem file: uploads/member/edit_baseinfo.php
  • Type: arbitrary back-end user password reset

Analysis

In member/edit_baseinfo.php lines 118-123, when an admin user logged in to the front end changes their password, the back-end admin password is overwritten at the same time. This is a side effect of DedeCMS sharing one user table across the front-end and back-end account systems — the front-end password-change endpoint does not distinguish the “front-end password” field from the “back-end password” field.

Reproduction

First, use Vuln 2 to reset admin’s front-end password to admin123. Then use Vuln 1 to forge a cookie and log in to the front end as admin, and visit the password-change page:

GET http://127.0.0.1/member/edit_baseinfo.php

Submit the old password admin123, a new password 123456, and an email. After the change, the back-end login accepts 123456 directly. This is the link that chains Vuln 1 and Vuln 2 — front-end impersonation plus silent back-end overwrite yields full back-end admin takeover.

Front-end password change overwriting the back-end password

Vuln 4: Front-End Arbitrary File Deletion

Environment

  • Problem files: uploads/member/album_add.php, uploads/member/archives_do.php, uploads/member/inc/inc_batchup.php
  • Type: arbitrary file deletion

Analysis

The issue lives in member/album_add.php lines 88-103. Line 88 includes /inc/archives_check.php, which initialises $litpic. Then line 100 reassigns it via $litpic = $litpicname;, but $litpicname was never initialised — so a variable-overwrite payload can control it directly.

Line 94 requires $formhtml==1 before reaching $litpic = $litpicname. But $formhtml itself is assigned when empty (the same variable-registration baggage), so it can be set via overwrite too.

In member/archives_do.php lines 161-162, DelArc is called when $row['issystem']!=-1, and DelArcSg when $row['issystem']==-1. The default issystem is 1, so we follow DelArc into member/inc/inc_batchup.php lines 20-129. Lines 72-76 fetch the litpic column from the database, build the path with $litpic = DEDEROOT.$licp['litpic'];, check only whether the file exists — never its type — and then delete it. Hence arbitrary file deletion.

Reproduction

In the member centre, navigate to “Content Centre → System Model Content → Album” and craft a request that adds formhtml=1 and litpicname=/1.txt (relative to the site root):

POST /member/album_add.php HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=---------------------------223472707522220
Cookie: PHPSESSID=kublnhoscak1n73fseggmmmb33; DedeUserID=8; DedeUserID__ckMd5=03ad72531b31e585;

-----------------------------223472707522220
Content-Disposition: form-data; name="dopost"

save
-----------------------------223472707522220
Content-Disposition: form-data; name="channelid"

2
-----------------------------223472707522220
Content-Disposition: form-data; name="title"

1
-----------------------------223472707522220
Content-Disposition: form-data; name="litpic"; filename="1.png"
Content-Type: image/png

PNG
-----------------------------223472707522220
Content-Disposition: form-data; name="formhtml"

1
-----------------------------223472707522220
Content-Disposition: form-data; name="litpicname"

/1.txt
-----------------------------223472707522220--

Then in the album listing, delete the just-published post — the file pointed to by litpicname is removed:

GET /member/index.php?dopost=save HTTP/1.1
Host: 127.0.0.1
Cookie: PHPSESSID=kublnhoscak1n73fseggmmmb33; DedeUserID=8; DedeUserID__ckMd5=03ad72531b31e585;

Arbitrary file deletion PoC

Vuln 5: Back-End Arbitrary File Upload

Environment

  • Problem file: uploads/include/dialog/select_images_post.php
  • Type: back-end arbitrary file upload

Analysis

In include/dialog/select_images_post.php lines 33-40, line 34 substitutes anything matched by a regex in the filename with empty, and line 36 checks whether the filename contains a whitelisted format. Neither approach actually parses the file suffix — hence the bypass.

In the same file lines 55-62, the suffix is extracted for concatenation and upload. The detection logic and the file-naming logic disagree, and that disagreement is exactly what gets exploited.

Following $cfg_imgtype to data/config.cache.inc.php line 18 reveals the upload-type restriction. Constructions like xxx.jpg.p%php or xxx.jpg.p*hp bypass it, as long as the image part matches the whitelist in config.cache.inc.php.

Reproduction

This vulnerability requires the member feature to be enabled. Then the upload restriction can be bypassed from the editor inside the member centre:

POST /include/dialog/select_images_post.php?CKEditor=body&CKEditorFuncNum=2&langCode=zh-cn HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=---------------------------2029356716975
Cookie: PHPSESSID=b602af1f688b3422d78ac6e9b0adcec3; DedeUserID=8;

-----------------------------2029356716975
Content-Disposition: form-data; name="upload"; filename="1.png.p*hp"
Content-Type: image/png

PNG
-----------------------------2029356716975--

Arbitrary file upload PoC

Final Thoughts

Looking back at these five vulnerabilities, they are not isolated points but a chain stitched together by the legacy PHP style of “weak typing + variable registration + blacklist filtering”: cookie forgery yields front-end admin → weak-typed bypass resets the front-end password → the front-end password change silently overwrites the back-end password → back-end takeover enables arbitrary file upload for RCE, with variable-overwrite file deletion as a fallback. None of the steps is critical in isolation, but linked end to end they take an anonymous visitor all the way to RCE.

DedeCMS’ code reflects a common pattern in Chinese PHP projects from around 2010: the residual mindset of register_globals, the abuse of ==, blacklist-based suffix filtering, and detection logic divorced from the landing logic. Even as the vendor patches each instance, the patch-bypass-repatch tug-of-war continues — as long as the architectural style stays the same, new vulnerabilities of the same family keep surfacing. When auditing legacy CMSes, what is worth memorising is not any single payload but the scanning routine of “hunt uninitialised variables → hunt weak comparisons → hunt detection/landing inconsistency”. It transfers cleanly to other Chinese CMSes.

To be clear, the reproduction here is purely an audit exercise on the 2018 source package — affected sites have long since been patched. If a production environment is still on this version, upgrade to a currently maintained DedeCMS release, or evaluate migrating to a maintained CMS — the marginal cost of further patching has grown too high. Stay vigilant out there.