Login write up
Ans:ACSC{fake}
這題進入頁面後,是一個 Login 畫面。

老方法一樣先猜 admin:admin。

顯然他沒那麼笨,所以我先看看題目給的 source code。
完整程式碼
在資料庫的地方他有兩筆資料,分別是 guest 跟 user,只是 user 的密碼是 32 字元長的隨機十六進位字串。
接著檢查 username 有沒有超過 6 個字元,不然就報錯。
接著會去資料庫裡面,對比 username 去找資料,然後對比帳密。
是 guest 就回傳文本,反之就是 flag。
我們現在要做的,就是用程式邏輯去混淆他。這裡 user 的密碼都是亂數生成,爆破根本沒意義,但如果讓 guest 成為可以讀 flag 的角色呢?
一樣用 guest:guest 去登入他,但是我們加上 []。

這會造成我們輸入的東西是以 陣列 去執行,JS 會將陣列 ['guest'] 自動轉成字串 'guest',所以查詢成功,在資料庫中會變成這樣:
我們的帳密都是 guest,這裡資料庫就會通過,但為何後面不是走 guest 分支,而是炸 user 的內容還不會被擋下來,有兩點:
- 陣列長度為 1,不超過 6,因此使用者字元長度限制會被繞過。
== 其實是寬鬆比對,在 JS 中會先將陣列轉成字串,導致結果相等,進而通過密碼驗證,但後續判斷使用者名稱不成立,是因為我們傳的是陣列 ['guest'],不等於字串 'guest'。
所以利用 JS 的隱式轉換會把 username[]=guest 轉成陣列 ['guest'],正是本題漏洞核心,因此 else 被執行,回傳 flag。
這裡就會變成:
- 資料庫確定有這支帳號,且同意我們登入。
- else 條件滿足,因此吐 flag 出來。

Login write up
這題進入頁面後,是一個 Login 畫面。
老方法一樣先猜
admin:admin。顯然他沒那麼笨,所以我先看看題目給的 source code。
完整程式碼
const express = require('express'); const crypto = require('crypto'); const FLAG = process.env.FLAG || 'flag{this_is_a_fake_flag}'; const app = express(); app.use(express.urlencoded({ extended: true })); const USER_DB = { user: { username: 'user', password: crypto.randomBytes(32).toString('hex') }, guest: { username: 'guest', password: 'guest' } }; app.get('/', (req, res) => { res.send(` <html><head><title>Login</title><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head> <body> <section> <h1>Login</h1> <form action="/login" method="post"> <input type="text" name="username" placeholder="Username" length="6" required> <input type="password" name="password" placeholder="Password" required> <button type="submit">Login</button> </form> </section> </body></html> `); }); app.post('/login', (req, res) => { const { username, password } = req.body; if (username.length > 6) return res.send('Username is too long'); const user = USER_DB[username]; if (user && user.password == password) { if (username === 'guest') { res.send('Welcome, guest. You do not have permission to view the flag'); } else { res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`); } } else { res.send('Invalid username or password'); } }); app.listen(5000, () => { console.log('Server is running on port 5000'); });在資料庫的地方他有兩筆資料,分別是 guest 跟 user,只是 user 的密碼是 32 字元長的隨機十六進位字串。
const USER_DB = { user: { username: 'user', password: crypto.randomBytes(32).toString('hex') }, guest: { username: 'guest', password: 'guest' } };接著檢查 username 有沒有超過 6 個字元,不然就報錯。
app.post('/login', (req, res) => { const { username, password } = req.body; if (username.length > 6) return res.send('Username is too long');接著會去資料庫裡面,對比 username 去找資料,然後對比帳密。
const user = USER_DB[username]; if (user && user.password == password) {是 guest 就回傳文本,反之就是 flag。
if (username === 'guest') { res.send('Welcome, guest. You do not have permission to view the flag'); } else { res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`); } } else { res.send('Invalid username or password'); } });我們現在要做的,就是用程式邏輯去混淆他。這裡 user 的密碼都是亂數生成,爆破根本沒意義,但如果讓 guest 成為可以讀 flag 的角色呢?
一樣用
guest:guest去登入他,但是我們加上[]。這會造成我們輸入的東西是以 陣列 去執行,JS 會將陣列
['guest']自動轉成字串'guest',所以查詢成功,在資料庫中會變成這樣:USER_DB[['guest']]我們的帳密都是 guest,這裡資料庫就會通過,但為何後面不是走 guest 分支,而是炸 user 的內容還不會被擋下來,有兩點:
==其實是寬鬆比對,在 JS 中會先將陣列轉成字串,導致結果相等,進而通過密碼驗證,但後續判斷使用者名稱不成立,是因為我們傳的是陣列['guest'],不等於字串'guest'。所以利用 JS 的隱式轉換會把
username[]=guest轉成陣列['guest'],正是本題漏洞核心,因此 else 被執行,回傳 flag。這裡就會變成: