Skip to content

Back to the catalog

dfir.chwriteup

From Dangerous PHP Functions to Webshell Hunting

A DFIR practitioner deploys four PHP webshells (popen-based, proc_open-based, p0wny-shell, and Weevely) on a test web server, then tests whether common webshell YARA rulesets (nsacyber, Florian Roth's Thor rules) can detect them via Velociraptor hunts. The rules only catch p0wny-shell due to its many code-execution keywords, missing the simpler popen/proc_open shells and Weevely entirely, exposing brittleness in signature-based webshell detection. The author proposes a semi-automatic approach combining Velociraptor/DetectRaptor with manual review, web-log frequency analysis, and UAC-generated bodyfile/mactime timelines to hunt webshells more reliably.

TL;DR Testing real webshells (p0wny-shell, Weevely, popen/proc_open shells) against nsacyber/Thor YARA rules and Velociraptor shows signature-based detection largely fails, motivating manual review, log frequency analysis, and UAC timeline forensics.
Document

What it is

A DFIR walkthrough of PHP command-execution functions that survive a naive disable_functions hardening, and the detection artifacts they leave. It names the concrete bypass functions, shows working PoC webshells, and dissects the fallback logic in p0wny-shell and the on-disk obfuscation of weevely.

How it works

The default disable_functions shipped in the shown php.ini (/etc/php/8.0/apache2/php.ini) blocks only the pcntl_* family — it does not block popen, proc_open, passthru, or preg_replace. So a shell can still fork a command via popen($_REQUEST['code'],'r') or a proc_open reverse shell. p0wny-shell probes each method in order and uses the first available one, so blocking a subset is worthless:

flowchart TD
    A[executeCommand] --> B{exec?}
    B -->|no| C{shell_exec?}
    C -->|no| D{system + ob_*?}
    D -->|no| E{passthru + ob_*?}
    E -->|no| F{popen/feof/fread/pclose?}
    F -->|no| G{proc_open/stream_get_contents?}
    B & C & D & E & F & G -->|yes| H[run command]

To block execution you must disable the entire chain, not just exec/system/shell_exec.

What to do

Extend disable_functions to cover the fallback set, e.g. beyond the obvious three, add popen, proc_open, passthru, pcntl_exec, and preg_replace. The HackTricks reference the post cites is the source for the fuller (still "possibly not exhaustive") list.

For hunting, the post argues Yara rules are weak against webshells and points instead at: manual analysis, frequency analysis of web-server logs (rarely-hit endpoints), and collection with Velociraptor and UAC.

A concrete weevely signature: the generated agent.php is a phar loader whose readable header is hex/octal-escaped and truncated by __HALT_COMPILER, e.g.

<?php include "\160\x68\141\x72\72\57\57".basename(__FILE__)."\57\x78";__HALT_COMPILER(); ?>

That \160\x68\141\x72\72\57\57 decodes to phar:// — grepping for escaped phar:// plus __HALT_COMPILER in .php files is a usable hunt.

Caveats

The post itself flags that its bypass list is "possibly not exhaustive," so a disable-list approach is inherently incomplete — treat it as defense-in-depth, not a boundary. It also notes preg_replace reaches code execution indirectly (via the deprecated /e modifier calling system()), so its presence in the list is conditional on legacy PHP behavior.

Organisations named here

  • nsacyber
  • Oxdf

Technologies named here

  • disable_functions
  • PHP
  • phpinfo
  • WordPress
  • p0wny-shell
  • weevely
  • HackTricks
  • YARA
  • nsacyber webshell Yara rule set
  • Velociraptor
  • Velociraptor-Exchange Generic.Webshells hunt
  • DetectRaptor
  • Thor Webshell Yara rule
  • webshell-scan
  • UAC
  • The Sleuth Kit
  • stat
  • mactime

Techniques named here

  • Webshell upload/execution via popen
  • Webshell execution via proc_open
  • Obfuscated PHP backdoor generation
  • Webshell detection via Yara rules
  • Manual webshell analysis / hunting
  • Frequency analysis of web server logs
  • Timeline analysis via bodyfile/mactime
  • Process ancestry monitoring for webshell detection

Also named here

  • Florian Roth
  • Matthew Green