Login write up

Ans:ACSC{fake}

這題進入頁面後,是一個 Login 畫面。

image

老方法一樣先猜 admin:admin

image

顯然他沒那麼笨,所以我先看看題目給的 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 去登入他,但是我們加上 []

image

這會造成我們輸入的東西是以 陣列 去執行,JS 會將陣列 ['guest'] 自動轉成字串 'guest',所以查詢成功,在資料庫中會變成這樣:

USER_DB[['guest']]

我們的帳密都是 guest,這裡資料庫就會通過,但為何後面不是走 guest 分支,而是炸 user 的內容還不會被擋下來,有兩點:

所以利用 JS 的隱式轉換會把 username[]=guest 轉成陣列 ['guest'],正是本題漏洞核心,因此 else 被執行,回傳 flag。

這裡就會變成:

image