▄▄▄ ▄▄
▄██▀▀▀ ▄ █▄ ██
██ ▄▀█▄ ▄ ▀ ██ ██ ▄▄
██ ██ ████▄▄█▀█▄ ▄███▄ ▄██▀█ ████▄ ██ ▄███▄ ▄████
██ ▄██ ██ ██▄█▀ ██ ██ ▀███▄ ██ ██ ██ ██ ██ ██ ██
▀███▀ ▄█▀ ▀█▄▄▄▄▀███▀ █▄▄██▀ ▄████▀▄██▄▀███▀ ▀████
██
▀▀▀
05-19-2026 | pwnable.kr series
this CTF is an exercise in providing input to an ELF binary using various methods. we are given five stages and need to clear each by giving input via:
argv - the program’s argument list
stdio - file input/output
env - environment variables
file - reading with fread()
network - sockets/IPC
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char* argv[], char* envp[]){
printf("Welcome to pwnable.kr\n");
printf("Let's see if you know how to give input to program\n");
printf("Just give me correct inputs then you will get the flag :)\n");
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");
// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");
// here's your flag
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}stage 1 checks three conditions: argument count is 100, argument at
index 65 (ascii 'A') is a null byte, and argument at index
66 (ascii 'B') is the byte sequence
"\x20\x0a\x0d" (space, newline, carriage return). we cannot
pass these arguments on the command line, but the raw data can be given
by calling execv(). e.g., in python:
os.execv("/path/to/prog", args_list)note that the first argument is always the name of the binary itself (at index 0). we can use python to automate the process of passing inputs to clear each stage.
# stage 1
args = ["input2"]
args += ["A"] * 64
args += [""]
args += ["\x20\x0a\x0d"]
args += ["42069"]
args += ["A"] * 32100 string arguments are supplied, with the correct data placed at
argv['A'] and argv['B']. null bytes cannot be
directly embedded in program arguments, but one workaround is to pass an
empty string since it’s effectively just a pointer to a null byte.
ignore the 42069 for now—that’s used later.
stage 2 reads data from the standard input (file descriptor 0) and
standard error (file descriptor 2) streams into a 4-byte buffer, and
compares the data to some fixed values. stdin can be given with a
regular pipe, but stderr is a bit less intuitive. standard error is
usually only written to, not read from. but here, we can use a
combination of process substitution and input redirection to feed data
to the program’s standard error stream and write it to
buf.
# stage 2
# read 4 bytes into buf (stdin) read 4 bytes into buf (stderr)
printf "\x00\x0a\x00\xff" | python /tmp/input2/sol.py 2< <(printf "\x00\x0a\x02\xff")stage 3 simply checks for the existance of an environment variable
0xdeadbeef with the value 0xcafebabe. this
variable can’t be manually set because export rejects
non-alphanumeric/underscore characters, but we can use the
os.environb mapping object to set it in python:
# stage 3
os.environb[b"\xde\xad\xbe\xef"] = b"\xca\xfe\xba\xbe"stage 4 attempts to open a file with the unusual name
\x0a and checks whether it contains four null bytes.
actually, \x0a is the corresponding value of the the ascii
character \n, which is the file’s proper name and not
easily typed. although, such a file can be created using ANSI-C
quoting:
$ printf "\x00\x00\x00\x00" > $'\n'
$ ls $'\n'
''$'\n'
$ hexdump $'\n'
0000000 0000 0000
0000004or in python:
# stage 4
with open("\x0a", "w") as f:
f.write("\x00\x00\x00\x00")stage 5 opens a network socket, binds to a port set by
argv['C'] (argv[67]), listens and accepts
incoming connections, writes the client’s message to buf,
and compares it to 0xdeadbeef. a little bit about
sockets:
a socket is a communication endpoint that allows two processes to communicate with each other. they enable IPC (inter-process communication) and are used both locally (unix domain) and over networks (internet domain). they are set up as interfaces for each communicating process. in general, the linux socket lifecycle looks like this:
socket() - creates a communication endpoint
bind() - assigns an IP address/port to the socket
listen() - (server) waits for incoming connection
requests
connect() - (client) attempts a connection with the
listening socket
accept() - (server) creates a new socket for established
connections
send() / recv() - exchange data
close() - terminate connection and release resources
we first set the custom port 42069 on the server socket
by passing it to argv[67]. now, the server socket needs to
be created and start listening before we try to connect to it.
otherwise, the client socket will have nothing to connect to. the
problem is that the moment we call execv() to set up the
listener, the process image is replaced and we can’t set up the client
socket—one solution is to fork the process, allowing the parent to run
exec and the child to handle the client socket:
# stage 5
pid = os.fork()
if pid == 0:
time.sleep(1)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = "127.0.0.1"
port = 42069
client_socket.connect((host, port))
client_socket.send(b"\xde\xad\xbe\xef")
client_socket.close()
else:
os.execv("/home/input2/input2", args)in the child process, sleeping for a second gives input2
time to set up the listening socket and avoid the race condition. we can
bind to any available network interface since
saddr.sin_addr.s_addr is set to INADDR_ANY,
but I chose the localhost IP 127.0.0.1 on the loopback
interface for simplicity. then, connect on port 42069 and
send the payload, writing 0xdeadbeef to
buf.
one last issue is that the input2 user does not have
write permissions in the home directory, so we need to run the script
from /tmp. but this means
system("/bin/cat flag"); runs in /tmp, not in
home where the flag is—there’s a clever trick to solve this using a
symbolic link:
$ ln -s /home/input2/flag /tmp/input2/flagor in python
os.symlink("/home/input2/flag", "/tmp/input2/flag")now, the input2 user inherits permissions of the file’s
owner (input2_pwn) with setregid() and the
setuid bit active, and follows the link to
/home/input2/flag.
full script:
import os
import socket
import time
# stage 1
args = ["input2"]
args += ["A"] * 64
args += [""]
args += ["\x20\x0a\x0d"]
args += ["42069"]
args += ["A"] * 32
# stage 2
# read 4 bytes into buf (stdin) read 4 bytes into buf (stderr)
'''
$ printf "\x00\x0a\x00\xff" | python /tmp/input2/sol.py 2< <(printf "\x00\x0a\x02\xff")
'''
# stage 3
os.environb[b"\xde\xad\xbe\xef"] = b"\xca\xfe\xba\xbe"
# stage 4
with open("\x0a", "w") as f:
f.write("\x00\x00\x00\x00")
os.symlink("/home/input2/flag", "/tmp/input2/flag")
# stage 5
pid = os.fork()
if pid == 0:
time.sleep(1)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = "127.0.0.1"
port = 42069
client_socket.connect((host, port))
client_socket.send(b"\xde\xad\xbe\xef")
client_socket.close()
else:
os.execv("/home/input2/input2", args)input2@ubuntu:/tmp/input2$ printf "\x00\x0a\x00\xff" | python /tmp/input2/sol.py 2< <(printf "\x00\x0a\x02\xff")
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
Mommy_now_I_know_how_to_pa5s_inputs_in_Linux