Knock-knock | DiceCTF 2022

Antoine The Conqueror.

[+] Source
1. Initial reconnaissance:
- Ở challenge này chúng ta có một web app tên là "knock-knock" trông khá giống pastebin:

- Nhập vào bất kỳ cái gì như trên rồi ấn "Create", "knock-knock" sẽ tạo ra cho chúng ta một cái note với nội dung là những gì chúng ta vừa nhập vào. Mỗi cái note như thế này sẽ được đánh dấu bởi 2 url parameter: "id" và "token".

- Mỗi lần tạo 1 note mới thì "id" sẽ cộng thêm 1, còn "token" có vẻ như sẽ được generate ngẫu nhiên, như vậy thì các note khác nhau sẽ có "id" và "token" khác nhau.
2. Review source code:
- Chúng ta sẽ check source code để xem "id" và "token" được generate như thế nào. Đầu tiên là
Dockerfile:
FROM node:17.4.0-buster-slim
RUN mkdir -p /app
WORKDIR /app
COPY package.json .
RUN yarn
COPY . .
USER node
CMD ["node", "index.js"]
Dockerfilecho chúng ta biết "Knock-knock" được code bằng NodeJS bản 17.4.0.COPY package.json .có nghĩa là docker sẽ copy filepackage.jsontừ source vào container. Trong source mà tác giả đã cho không cópackage.jsonnên chúng ta đành tự tạo vậy.package.jsonlà file cấu hình của npm, giúp cho npm hiểu nó cần phải cài đặt cái gì, thông tin về ứng dụng, phiên bản,… Bạn có thể tham khảo tìm hiểu sâu hơn về file này tại đây để biết cách viết nó hoặc lấy filepackage.jsonmẫu ở đây như mình (vì mình lười xD).Sau đó là nội dung chính của challenge này
index.js. Về cơ bản cái app "knock-knock" này chỉ có 2 API chính làPOST /createdùng để tạo note vàGET /notedùng để xem note:
app.post('/create', (req, res) => {
const data = req.body.data ?? 'no data provided.';
const { id, token } = db.createNote({ data: data.toString() });
res.redirect(`/note?id=${id}&token=${token}`);
});
app.get('/note', (req, res) => {
const { id, token } = req.query;
const note = db.getNote({
id: parseInt(id ?? '-1'),
token: (token ?? '').toString(),
});
if (note.error) {
res.send(note.error);
} else {
res.send(note.data);
}
});
- Chúng ta đang tìm hiểu xem "id" và "token" được generate như thế nào, do đó tập trung vào
POST /createtrước. Sau khidatađược user nhập vào thì nó được truyền vào hàmcreateNotecủa objectdb- 1 instance của classDatabase. Check xem hàmcreateNotehoạt động như thế nào:
class Database {
constructor() {
this.notes = [];
this.secret = `secret-${crypto.randomUUID}`;
}
createNote({ data }) {
const id = this.notes.length;
this.notes.push(data);
return {
id,
token: this.generateToken(id),
};
}
// ...
}
- Biến
idsẽ được gán bằng độ dài của arraynotes, mỗi note do user tạo ra sẽ được đẩy vào array này:this.notes.push(data);, mỗi lầndatađược đẩy vào độ dài array tăng thêm 1, có nghĩa là id cũng tăng thêm 1. Cơ mà khoan đã, hàmcreateNotenày không chỉ được gọi trong apiPOST /create, nó còn được gọi ngay từ đầu trước cả khi client gọi đến API này:
const db = new Database();
db.createNote({ data: process.env.FLAG });
- Điều này có nghĩa flag chắc chắn sẽ nằm ở note có
id=0(note đầu tiên). Thayidở bước Initial reconnaissance thành 0 thì:

- Vấn đề còn nằm ở
token. Mỗiidkhác nhau sẽ có mộttokenkhác nhau, nó được tạo ra như sau:
generateToken(id) {
return crypto
.createHmac('sha256', this.secret)
.update(id.toString())
.digest('hex');
}
- Tác giả sử dụng hàm băm sha256 với key là
secret-${crypto.randomUUID}, data cần được hash chính là biếnid. Đã sử dụng sha256 lại còn random key của hàm băm vớicrypto.randomUUIDnữa, nhìn có vẻ rất là "hard core crypto", nhưng lỗ hổng lại nằm chính ngay đây xD
this.secret = `secret-${crypto.randomUUID}`;
- Nếu
secretđược gán bằngsecret-${crypto.randomUUID()}thì bài này "no hope", vì UUID random nên token được generate ra cũng random:

- Nhưng
secretđược gán bằngsecret-${crypto.randomUUID}nên:

crypto.randomUUIDkhi concat với string "secret-" sẽ trả về 1 string rất dài là implementation của hàmcrypto.randomUUID. Mà cái string này cố định chứ không random nên chuỗi hash sau khi băm số "0" cũng sẽ cố định luôn, hàm băm sha256 sẽ trở nên vô dụng.
3. Generate token and get flag:
- Ý tưởng khai thác của chúng ta sẽ là sử dụng chính hàm
generateTokencủa tác giả để generatetokentừidcó giá trị là 0, sau đó truy cập vào note cóid=0cùng với token vừa mới được tạo ra thì nó chắc chắn sẽ in ra flag thay vì "invalid token".
const crypto = require('crypto');
secret = `secret-${crypto.randomUUID}`;
function generateToken(id) {
return crypto
.createHmac('sha256', this.secret)
.update(id.toString())
.digest('hex');
}
console.log(generateToken(0))
- Vì máy của mình đã có sẵn node nên mình định copy nguyên đoạn code trên 1 phát lấy token ăn luôn, cơ mà không dễ như vậy :)

- Sau một hồi quay cuồng thì mình phát hiện ra lí do khá là ngớ ngẩn. Theo Dockerfile thì tác giả đang sử dụng node bản 17.4.0, còn của mình là bản 14.17.6. Mà mỗi bản node khác nhau thì hàm
crypto.randomUUIDsẽ được implement khác nhau, do đó key sẽ khác nhau.

- Chúng ta cần phải chạy đúng bản node của tác giả thì mới ra đúng key. Cơ mà tải node về khá là lâu, nên mình quyết định chạy docker cho nhanh. Chúng ta chỉ tạo 1 folder có đủ từng này file mô phỏng theo như đề bài, ở đây mình đặt tên folder là "knock-knock":

- Sau đó
docker build -t knock-knock .tại chính folder "knock-knock" này để tạo image "knock-knock", sau đódocker run knock-knockđể tạo một container từ image này. Lưu ý trong quá trình tạo container các bạn có thể gặp bug như sau:

- Bug trên xảy ra là do chúng ta chưa cài module "express" cho cái app này. Để cài module này thì tại file
package.json"tự chế" chúng ta cần bổ sung thêm 1 trường như sau:
"dependencies": {
"express": "^4.17.2"
}
- Fix nốt bug kia là chúng ta đã có thể chạy một container ngon nghẻ:

- Sau đó
docker exec -it 4336f31704f1 /bin/bashđể vào shell của container, dùng node cli để chạy exploit code phía trên và lấy token:

- Truy cập và lấy flag:

Flag: dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}

![[CVE-2022-35649] 1-Click RCE in Moodle v4.0.1](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1679245391465%2F241f5fef-ceeb-4770-a7c6-5d4916cd6846.avif&w=3840&q=75)


