Post

mEmoji

Level 7 web exploitation challenge on Dreamhack.io

mEmoji

The Challenge

https://dreamhack.io/wargame/challenges/1236

The challenge…

This is an XSS challenge, and there’s a filter to prevent XSS, but bypassing it is trivial as we are able to use different encodings in our 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
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
54
55
56
57
58
59
60
61
62
const denyList = ["<", ">", "!", "\\x", "\\u", "#"];
const encodingList = [
    "UTF-8","UTF-16","UTF-16LE","UTF-16BE","UTF-32","UTF-32LE","UTF-32BE",
    "ISO-8859-1","ISO-8859-2","ISO-8859-3","ISO-8859-4","ISO-8859-5","ISO-8859-6","ISO-8859-7","ISO-8859-8","ISO-8859-9","ISO-8859-10","ISO-8859-13","ISO-8859-14","ISO-8859-15","ISO-8859-16",
    "CP1250","CP1251","CP1252","CP1253","CP1254","CP1255","CP1256","CP1257","CP1258",
    "KOI8-R","KOI8-U",
    "EUC-JP", "EUC-KR",
    "SHIFT_JIS",
    "GB2312",
    "GBK",
    "GB18030",
    "BIG5","BIG5-HKSCS",
    "ARMSCII-8",
    "TCVN",
    "GEORGIAN-ACADEMY", "GEORGIAN-PS",
    "PT154",
    "RK1048",
    "MULELAO-1",
    "TIS-620",
    "CP874",
    "VISCII",
    "ISO-2022-JP","ISO-2022-KR","ISO-2022-CN"
];

router.get("/create", (req, res)=>{
    res.render('create', { encodingList });
})

router.post("/create", (req, res)=>{
    const { content, sourceEncoding } = req.body;
    const random = Math.random().toString();
    const memoId = generateMD5Hash(random);

    if (content === "" || sourceEncoding === ""){
        return res.status(403).send({result: false, msg: 'Missing param'});
    }

    let isDenied = false;

    denyList.forEach((chr) => {
        if (content.indexOf(chr) !== -1) {
            isDenied = true;
        }
    })
    
    if (isDenied) {
        return res.status(403).send({result: false, msg: 'Not allowed character'});
    }

    // console.log(`[+] id ==> ${memoId}`)
    try{
        const iconv = new Iconv(sourceEncoding, 'ASCII//TRANSLIT//IGNORE');
        const contentResult = iconv.convert(content);
        createMemo(memoId, contentResult, sourceEncoding);

    }catch (e){
        return res.status(500).send({result: false, msg: e, data: {content, sourceEncoding, random}});
    }

    return res.status(200).send({result: true, msg: 'Ok'});

})

Submitting homoglyphs of < and > with UTF-8 encoding is enough to smuggle HTML tags and achieve XSS. However there’s a CSP on the webpage, so we can only execute JS code if we have the nonce on our script tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const DEFAULT_NONCE = process.env.NONCE || '30502580d81f844336d204b5f6ff1a84';
router.get('/check', (req, res) => {

    const id = req.query.id;

    if (id === undefined){
        return res.render('check', {content: undefined, sourceEncoding: undefined, msg: 'Missing param'});
    }
    if (db.memo[id] === undefined){
        return res.render('check', {content: undefined, sourceEncoding: undefined, msg: 'Memo is not available'});
    }
    const content = db.memo[id].content;
    const sourceEncoding = db.memo[id].sourceEncoding;

    const nonce = (!req.session.nonce) ? DEFAULT_NONCE : req.session.nonce;
    res.setHeader('Content-Security-Policy', `default-src 'self'; base-uri 'self'; script-src 'nonce-${nonce}'`);
    return res.render('check', {content: content, sourceEncoding: sourceEncoding});
})

The nonce is only set if the admin bot has nonce in its session already. The admin bot gets the nonce in its session from here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const page = await browser.newPage();
const response = await page.goto('http://127.0.0.1:3000/home/' + path);
if (response.status() !=200 ){
    await page.close();
    await browser.close();
    return false;
}
await sleep(1 * 1000);
await page.setCookie({
    name: "FLAG",
    value: FLAG,
    domain: '127.0.0.1',
    path: "/",
});
await page.goto(`http://127.0.0.1:3000/memo/check?id=${memoId}`);
await sleep(2 * 1000);
await page.close();
await browser.close();

On /home:

1
2
3
4
5
6
7
8
<div id="link">
    <a href="/memo/create">Create 📝</a>
    <a href="/report">Report 🧑‍💼</a>
</div>
<% if (emoji !== undefined) { %>
<img src="<%= emoji %>" alt="emoji" style="width: 250px; height: 250px;">
<% } %>
<img src="nonce.png">
1
2
3
4
5
6
7
router.get('/nonce.png', (req, res) =>{
    const random = Math.random().toString();
    const nonce = generateMD5Hash(random);
    req.session.nonce = nonce;
    res.setHeader('Content-Type','image/png');
    return res.send('ok');
})

However, as we control path, we can use a path traversal to send the admin bot somewhere else which doesn’t put nonce into the admin’s session. Something like ../report works.

We realize that we don’t actually have the memoId to send the admin bot to, as the server never gives us the memoId of the memo we create.

memoId is generated using Math.random, which is not cryptographically secure. We can also leak the generated values if we can get Iconv to throw an error. Using an invalid value for sourceEncoding, like the integer 1, can help us achieve this.

1
2
3
4
5
6
7
8
9
10
const random = Math.random().toString();
const memoId = generateMD5Hash(random);
try{
    const iconv = new Iconv(sourceEncoding, 'ASCII//TRANSLIT//IGNORE');
    const contentResult = iconv.convert(content);
    createMemo(memoId, contentResult, sourceEncoding);

}catch (e){
    return res.status(500).send({result: false, msg: e, data: {content, sourceEncoding, random}});
}

We just need 4 random values to start predicting the next value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json_data = {
    'content': 'test',
    'sourceEncoding': 1,
}
stuff=[]
for i in range(4):
    response = requests.post(
        'http://host8.dreamhack.games:15345/memo/create',
        cookies=cookies,
        headers=headers,
        json=json_data,
        verify=False,
    )
    stuff.append(float(response.json()["data"]["random"]))
print(stuff)

Getting the 4 random values

We use JSRandomnessPredictor from here for our pRNG cracking:

Predicting the next random value

Next, we use the homoglyphs + default nonce + path traversal + predicted memoId to craft a payload and win.

Win!

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