{"repro_id":"REPRO-2026-00307","version":6,"title":"vBulletin runtime template runMaths pre-auth RCE","repro_type":"security","status":"published","severity":"critical","description":"Provisional research ticket for a pre-authentication RCE described by SSD Disclosure as 'vbulletin runtime template runMaths preauth rce'. The SSD page itself was not extractable in this environment (robot challenge), so the ticket is chained from public corroboration: vBulletin's template runtime exposes vB_Template_Runtime::runMaths(), the template engine allows conditional/function evaluation, and public search results point to a new pre-auth vBulletin RCE family with public PoC disclosure in 2026. The likely root cause is unsafe evaluation of attacker-controlled template/runtime expressions in the template rendering path, enabling code execution without authentication. This ticket should be validated against the exact affected versions, entrypoint, and patch level once a reproducible source or archived copy of the SSD article is available.","root_cause":"# Root Cause Analysis Report\n\n## Summary\n\nCVE-2026-61511 is a pre-authentication remote code execution vulnerability in vBulletin's template runtime. The public `ajax/render/pagenav` route merges unauthenticated POST data into template variables. The stock `pagenav` template assigns `pagenav[pagenumber]` to `pagenav.currentpage` and embeds it in a `{vb:math}` expression. `vB5_Template_Runtime::runMaths()` removes characters with a deny-style regular expression but retains digits, parentheses, concatenation, bitwise XOR, and invocation syntax, then passes the result to PHP `eval()`. A phpfuck-style expression can therefore synthesize `system` and an attacker-selected command using only permitted characters. This run reproduced arbitrary command execution through the real HTTP route without a session.\n\n## Impact\n\n- **Affected component:** vBulletin 5/6 presentation-layer template engine, specifically `includes/vb5/template/runtime.php`, the `pagenav` template, and the public AJAX renderer.\n- **Affected versions:** The published CVE record identifies vBulletin 5.0.0 through 5.7.5 and 6.0.0 through 6.2.1. The exact current-run target was vBulletin 5.1.5.\n- **Tested runtime:** vBulletin 5.1.5 source and original database from digest-pinned image manifest `cck1/vbulletin:v2` (`sha256:df5a9fa87186c49101db79577a0401c07f1239bf79dd1e5d066328147f6727f8`), served by Apache 2.4.25 with PHP 7.0.32. The PHP runtime came from the digest-pinned `wordpress:4.9.8-php7.0-apache` image manifest (`sha256:60cbd13178ed7f629098b1b7a0104ce038d55729f89784739e1bc2eca372f64a`); only its Apache/PHP runtime was used, not WordPress code.\n- **Risk:** Critical. An unauthenticated network attacker can run commands as the web-service account. Consequences include application/database disclosure, site modification, persistence, credential theft, and potential host compromise depending on service privileges.\n- **Repository scope note:** The ticket-mandated `https://github.com/quangbahoa/vbulletin` repository was inspected at commit `6126d15ed2a4023e9efa672c387f98c3e9905d4c`, but it contains vBulletin 3.8.11 and has no `runMaths`, `{vb:math}`, or `ajax/render` implementation. It is outside the CVE's affected 5.x/6.x code family and could not be the runtime target.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Unauthenticated remote arbitrary PHP/code execution.\n- **Reproduced impact:** Unauthenticated attacker-selected OS command execution via `system()` through `POST /` with `routestring=ajax/render/pagenav` and `pagenav[pagenumber]`.\n- **Parity:** `full`\n- **Not demonstrated:** No privilege escalation beyond the web-service account, persistence, outbound connectivity, or destructive post-exploitation was attempted.\n\n## Root Cause\n\nThe public light application recognizes any route beginning with `ajax/render` and dispatches it without an authentication gate. `vB5_Frontend_Controller_Ajax::actionRender()` combines `$_GET`, `$_POST`, and supplied data, then renders the caller-selected server template. In the stock `pagenav` template, a request-provided `pagenav[pagenumber]` value becomes `pagenav.currentpage`; that value is then inserted into math expressions such as:\n\n```text\n{vb:math {vb:var pagenav.currentpage} - 1}\n```\n\n`vB5_Template_Runtime::runMaths()` attempts to make `eval()` safe with this filter:\n\n```php\n$str = preg_replace('#([^+\\-*=/\\(\\)\\d\\^<>&|\\.]*)#', '', $str);\n$status = @eval(\"\\$str = $str;\");\n```\n\nThe filter is not a safe arithmetic parser. The retained character set supports numeric literals, string concatenation, XOR-based string construction, parenthesized expression invocation, and function calls on PHP 7+. The exploit synthesizes the string `CHR`, uses it to build `system` and the command byte-by-byte, then invokes the resulting function name. The request is processed more than once in the template, which explains the duplicate response marker; one successful invocation is already sufficient for RCE.\n\nThe vulnerable source excerpts captured from the exact tested package are:\n\n- `bundle/logs/repro/vb515-runmaths-source.txt`\n- `bundle/logs/repro/vb515-pagenav-source.txt`\n\nThe vendor fix is available as vBulletin 6.2.2 and as Patch Level 1 packages for 6.2.1, 6.2.0, and 6.1.6. The patches are license-gated at `https://members.vbulletin.com/patches.php`; no public source commit was found. Vendor references:\n\n- https://forum.vbulletin.com/forum/vbulletin-announcements/vbulletin-announcements_aa/4509358-security-patch-released-for-vbulletin-6-2-1-6-2-0-and-6-1-6\n- https://forum.vbulletin.com/forum/vbulletin-announcements/vbulletin-announcements_aa/4509404-vbulletin-6-2-2-is-available\n\n## Reproduction Steps\n\n1. Run `bash -n bundle/repro/reproduction_steps.sh` to validate shell syntax.\n2. Start an isolated affected vBulletin instance with a compatible PHP 7+ runtime, then invoke:\n\n   ```bash\n   TARGET_URL=http://127.0.0.1:18081/ bash bundle/repro/reproduction_steps.sh\n   ```\n\n3. The script:\n   - verifies service reachability;\n   - generates a fresh nonce and a phpfuck/XOR payload using only characters accepted by the vulnerable filter;\n   - sends a benign numeric request through the same unauthenticated endpoint and verifies that it does not contain the nonce;\n   - sends the malicious `pagenav[pagenumber]` request;\n   - requires the exact fresh response marker before exiting successfully;\n   - writes `bundle/repro/runtime_manifest.json` and request, response, payload, identity, and proof logs.\n4. Expected success evidence is exit code 0 and:\n\n   ```text\n   CONFIRMED: unauthenticated pagenav input executed an attacker-selected command through runMaths().\n   ```\n\nThe script exits 1 when the endpoint is reached but command execution is absent and exits 2 for a service/setup blocker.\n\n## Evidence\n\nPrimary current-run positive evidence (two consecutive self-contained runs):\n\n- `bundle/logs/repro/durable-positive-3-proof.log` and `bundle/logs/repro/durable-positive-4-proof.log`\n  - `response_marker=true`\n  - `target_file_marker=true`\n  - `benign_control_reached_math=true`\n  - `benign_control_clean=true`\n  - `authentication_material_sent=false`\n  - `CONFIRMED: unauthenticated pagenav input executed an attacker-selected command through runMaths().`\n- `bundle/logs/repro/durable-positive-3-exploit-request.trace` and `bundle/logs/repro/durable-positive-4-exploit-request.trace` — byte-level HTTP traces of unauthenticated exploit requests.\n- `bundle/logs/repro/durable-positive-3-exploit-response.txt` and `bundle/logs/repro/durable-positive-4-exploit-response.txt` — each contains its independent fresh marker:\n\n  ```text\n  __PRUVA_BEGIN__PRUVA_RUNMATHS_durable_positive_3__PRUVA_END__\n  __PRUVA_BEGIN__PRUVA_RUNMATHS_durable_positive_4__PRUVA_END__\n  ```\n\n- `bundle/logs/repro/durable-positive-3-marker.txt` and `bundle/logs/repro/durable-positive-4-marker.txt` — independent target-local files written by the command.\n- `bundle/logs/repro/durable-positive-3-baseline-response.txt` and `bundle/logs/repro/durable-positive-4-baseline-response.txt` — benign arithmetic controls through the same route, rendering page values 1 and 3 and no nonce.\n- `bundle/logs/repro/durable-positive-3-health.txt` and `bundle/logs/repro/durable-positive-4-health.txt` — reachable vBulletin service and PHP 7.0.32 headers.\n- `bundle/repro/runtime_manifest.json` — structured current-run endpoint and proof-artifact manifest.\n\nCompatibility negative control:\n\n- `bundle/logs/repro/manual-positive-1-exploit-response.txt` and `bundle/logs/repro/php55-negative-control.txt` show that the same product route under PHP 5.5.9 returned `Invalid math expression`; PHP 5.5 lacks the PHP 7 uniform-variable syntax used by the public payload. This constrains exploitability of this payload but does not negate the confirmed affected configuration.\n\nSource and provenance evidence:\n\n- `bundle/logs/repro/vb515-runmaths-source.txt`\n- `bundle/logs/repro/vb515-pagenav-source.txt`\n- `bundle/logs/repro/CVE-2026-61511-cve-record.json`\n- `bundle/logs/repro/KIS-2026-13.html`\n- `bundle/logs/repro/ssd-post-10702.json`\n\nNovelty classification is `duplicate`: KIS-2026-13, SSD Secure Disclosure, the CVE record, and the public PoC disclose this exact pagenav-to-runMaths route and payload mechanism.\n\n## Recommendations / Next Steps\n\n- Upgrade to vBulletin 6.2.2 or later, or apply the official Patch Level 1 package for a supported 6.2.1, 6.2.0, or 6.1.6 installation immediately.\n- Replace `eval()` with a small, explicitly bounded arithmetic parser. A character filter is not an adequate substitute for grammar validation.\n- Do not merge arbitrary unauthenticated request arrays into server-template data. Use route-specific schemas and scalar type/length limits.\n- Restrict public template rendering to an explicit allowlist and audit every allowed template for request-to-executable-context flows.\n- Add regression tests that submit the published payload and nearby character-set variants to every public math-bearing template; assert no command marker, no function call, and a safe validation error.\n- Review logs for large `pagenav[pagenumber]` values and requests selecting `ajax/render/pagenav`.\n- Treat disabling dangerous PHP functions only as defense in depth, not the primary fix.\n\n## Additional Notes\n\n- **Idempotency:** The proof uses a unique nonce and marker path per run. Repeating it does not depend on a previous marker and the benign control always runs first. The final self-contained script completed twice consecutively with exit code 0 (`durable-positive-3` and `durable-positive-4`), producing distinct markers.\n- **PHP limitation:** The public phpfuck payload relies on PHP 7's uniform variable syntax. The exact vBulletin 5.1.5 instance under PHP 5.5 reached the sink but returned an invalid-expression control result. PHP 7.0.32 produced command execution.\n- **Comparator limitation:** The vendor's fixed package is license-gated and was not available in this environment. The PHP 5.5 parser control and benign same-route control were both reached, but they are not substitutes for testing vBulletin 6.2.2. Variant analysis should test the official fixed release if licensed bytes become available.\n- The local command was deliberately limited to a unique `/tmp` marker plus a matching response marker; no external network target or third-party vBulletin site was touched.\n","cve_id":"CVE-2026-61511","reproduced_at":"2026-07-27T17:08:24.902780+00:00","duration_secs":8351.0,"tool_calls":826,"handoffs":3,"total_cost_usd":69.440791,"agent_costs":{"coding":4.973425,"discovery":0.698606,"judge":0.677622,"learning_policy":0.01859,"repro":33.783744,"support":1.523991,"vuln_variant":27.764813},"cost_breakdown":{"coding":{"gpt-5.6-sol":4.973425},"discovery":{"gpt-5.6-sol":0.698606},"judge":{"gpt-5.6-sol":0.677622},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.01859},"repro":{"gpt-5.6-sol":33.783744},"support":{"gpt-5.6-sol":1.523991},"vuln_variant":{"gpt-5.6-sol":27.764813}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"discovery_hunt","schema_version":2,"stages":["support","discovery","claim_contract","repro","judge","vuln_variant","coding"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-27T17:08:26.060614+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10373,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":21959,"category":"reproduction_script"},{"path":"bundle/coding/proposed_fix.diff","filename":"proposed_fix.diff","size":13567,"category":"patch"},{"path":"bundle/logs/coding/coding-live-3-variant-benign.json","filename":"coding-live-3-variant-benign.json","size":942,"category":"other"},{"path":"bundle/logs/coding/coding-live-3-variant-attack.json","filename":"coding-live-3-variant-attack.json","size":39369,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":807,"category":"other"},{"path":"bundle/logs/coding/coding-live-3-http.log","filename":"coding-live-3-http.log","size":153,"category":"log"},{"path":"bundle/logs/coding/coding-live-3-source-identity.txt","filename":"coding-live-3-source-identity.txt","size":467,"category":"other"},{"path":"bundle/logs/coding/coding-live-3-parent-benign.json","filename":"coding-live-3-parent-benign.json","size":466,"category":"other"},{"path":"bundle/logs/coding/coding-live-3-parent-attack.json","filename":"coding-live-3-parent-attack.json","size":526,"category":"other"},{"path":"bundle/logs/repro/php70-positive-1-proof.log","filename":"php70-positive-1-proof.log","size":193,"category":"log"},{"path":"bundle/logs/repro/php70-positive-1-marker.txt","filename":"php70-positive-1-marker.txt","size":31,"category":"other"},{"path":"bundle/logs/repro/php70-positive-1-exploit-response.txt","filename":"php70-positive-1-exploit-response.txt","size":15650,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1687,"category":"other"},{"path":"bundle/logs/repro/vb515-runmaths-source.txt","filename":"vb515-runmaths-source.txt","size":2205,"category":"other"},{"path":"bundle/logs/repro/vb515-pagenav-source.txt","filename":"vb515-pagenav-source.txt","size":2702,"category":"other"},{"path":"bundle/repro/discovery_manifest.json","filename":"discovery_manifest.json","size":2649,"category":"other"},{"path":"bundle/logs/repro/durable-positive-4-exploit-response.txt","filename":"durable-positive-4-exploit-response.txt","size":15654,"category":"other"},{"path":"bundle/logs/repro/durable-positive-4-source-identity.txt","filename":"durable-positive-4-source-identity.txt","size":911,"category":"other"},{"path":"bundle/logs/repro/durable-positive-4-health.txt","filename":"durable-positive-4-health.txt","size":423,"category":"other"},{"path":"bundle/logs/repro/durable-positive-4-baseline-request.trace","filename":"durable-positive-4-baseline-request.trace","size":1917,"category":"other"},{"path":"bundle/logs/repro/durable-positive-4-payload.txt","filename":"durable-positive-4-payload.txt","size":83145,"category":"other"},{"path":"bundle/logs/repro/durable-positive-4-mysql.log","filename":"durable-positive-4-mysql.log","size":283,"category":"log"},{"path":"bundle/logs/repro/durable-positive-4-apache-stdout.log","filename":"durable-positive-4-apache-stdout.log","size":237,"category":"log"},{"path":"bundle/logs/repro/durable-positive-4-apache-error.log","filename":"durable-positive-4-apache-error.log","size":505,"category":"log"},{"path":"bundle/logs/repro/durable-positive-4-apache-access.log","filename":"durable-positive-4-apache-access.log","size":264,"category":"log"},{"path":"bundle/coding/verify_fix.sh","filename":"verify_fix.sh","size":12017,"category":"other"},{"path":"bundle/coding/summary_report.md","filename":"summary_report.md","size":6034,"category":"documentation"},{"path":"bundle/logs/coding/coding-live-3-proof.log","filename":"coding-live-3-proof.log","size":410,"category":"log"},{"path":"bundle/logs/coding/coding-live-3-observation.json","filename":"coding-live-3-observation.json","size":278,"category":"other"},{"path":"bundle/logs/coding/coding-live-3-unit.log","filename":"coding-live-3-unit.log","size":1419,"category":"log"}]}