Post

GreyCTF 2024

No Sql Injection

1
2
3
4
5
6
I asked My friend Jason to build me a new e-commerce website. We just finished the login system and there's already bugs 🤦

Author: jro

http://challs.nusgreyhats.org:33336

Phân tích qua thì bài này có các chức năng chính sau

  • /api/register/1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.post('/api/register/1', async (req, res) => {
    try {
        let { username } = req.body;

        username = decode(username);

        const token = btoa(JSON.stringify({
            name: username,
            admin: false
        }));
        console.log(token);

        await query("insert into tokens values (?)", [token]);

        res.json({ "err": false, "token": token });
    } catch (err) {
        console.log(err);
        res.json({ "err": true });
    }
})
  • /api/register/2
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
app.post('/api/register/2', async (req, res) => {
    try {
        let { password, token } = req.body;
        password = decode(password);
        token = decode(token);

        const result = await query("select 1 from tokens where token = ?", [token]);
        console.log(result);
        if (result.length != 1) {
            return res.json({ err: "Token not found!" });
        }

        await query("delete from tokens where token = ?", [token]);

        const { name, admin } = JSON.parse(atob(token));
        console.log(JSON.parse(atob(token)))

        await query("insert into users (name, password, admin) values (?, ?, ?)", [name.toString(), password, admin === true]);

        res.json({ "err": false });
    } catch (err) {
        console.log(err);
        res.json({ "err": true });
    }
})
  • /api/login
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
app.post('/api/login', async (req, res) => {
    try {
        let { password, username } = req.body;
        password = decode(password);
        username = decode(username);

        const result = await query("select admin from users where name = ? and password = ?", [username, password]);

        if (result.length != 1) {
            return res.json({ err: "Username or password did not match" });
        }

        if(result[0].admin) {
            res.json({ "err": false, "msg": config.flag});
        } else {
            res.json({ "err": false, "msg": "You've logged in successfully, but there's no flag here!"});
        }

        // Prevent too many records from filling up the database
        await query("delete from users where name = ? and password = ?", [username, password]);
    } catch (err) {
        console.log(err);
        res.json({ "err": true });
    }
})

Giải thích các api endpoint

  • /register/1 sẽ nhận username khi chúng ta post data, sau đóbằng cách thêm vào một key:value - admin:false vào và chuyển sang dạng json và base64 encode nó

image

Sau đó backend sẽ thêm token này vào bảng tokens và gửi trả ta token như trên

  • /register/2 sẽ nhận passwordtoken (cả 2 đều đã bị base64) khi chúng ta post data, sau đó decode cả 2 và kiểm tra xem token đã có trong db chưa, nếu có thì parse nó thành json và lấy dữ liệu của các trường name, password, admin

image

Sau đó backend sẽ xoá token này khỏi database và thêm các trường trên vào bảng users

  • /login thì đơn giản chỉ là check xem account có phải là admin không có thì in ra flag, không thì không in gì cả

Vậy trong này có cái gì tận dụng được để khai thác ? Thông thường so sánh của mysql là case-insensitive tức là ‘a’ == ‘A’ vậy bằng 1 cách nào đấy chúng ta có thể điều khiển cái token của chúng ta và khi parse nó thì nó sẽ ra data một cách khác thường

Từ đó token thông thường là

1
{"name":"yuu","admin":false}

Sau khi bị chúng ta chỉnh sửa và nó bị biến đổi thành

1
{"name":"yuu","admin":true}

Vì chúng ta chỉ có thể kiểm soát được giá trị của username nên chúng ta phải làm sao đó để phá dấu " để ảnh hưởng đến các đặc tính của JSON ở đây đó chính là chỉnh sửa in hoa, in thường của các ký tự đại diện cho dâu "

Sau khi phá được dấu " và viết được admin = true thì chúng ta vẫn còn 1 việc nữa phần admin = false ở phía sau vẫn còn và sẽ ghi đè giá trị admin ở trước, lúc này chúng ta có thể tiếp tục phá giá trị của admin = false ở sau bằng cách chỉnh lại ký tự in hoa -> thường của ký tự đại diện cho giá trị admin

Và có 1 điều cần lưu ý là JSON.stringify khi parse các JSON object nếu các ký tự đó không thể decode qua UTF-8 hay UTF-16 thì nó sẽ trả về unicode code và escape nó ví dụ (JSON.stringify('\uDF06') -> '"\\udf06"' )

Vậy chúng ta vừa corrupt cái base64 token và kiểm tra xem các ký tự đó có thể decode qua UTF-8 hay UTF-16 không

Dưới đây là một cách mình tìm được

1
2
safe : b'{"name":"yuu (\xac\x8aadmin\x8a:true-\xc2c\x8a3\xa2to","admin":false}',
malicious: b'{"name":"yuu ","admin":true,"c":"to","admOn":false}'

2 base64 của payload trên

1
2
safe: eyJuYW1lIjoieXV1ICisimFkbWluijp0cnVlLcJjijOidG8iLCJhZG1PbiI6ZmFsc2V9,
malicious: eyJuYW1lIjoieXV1ICIsImFkbWluIjp0cnVlLCJjIjoidG8iLCJhZG1PbiI6ZmFsc2V9

Login bằng username ở trên (trong trường hợp payload của mình là yuu ) và password ta có flag

image

This post is licensed under CC BY 4.0 by the author.