▄▄▄                                    ▄▄
▄██▀▀▀                   ▄          █▄    ██
██ ▄▀█▄ ▄                ▀          ██    ██          ▄▄
██   ██ ████▄▄█▀█▄ ▄███▄    ▄██▀█   ████▄ ██ ▄███▄ ▄████
██  ▄██ ██   ██▄█▀ ██ ██    ▀███▄   ██ ██ ██ ██ ██ ██ ██
 ▀███▀ ▄█▀   ▀█▄▄▄▄▀███▀   █▄▄██▀  ▄████▀▄██▄▀███▀ ▀████
                                                      ██
                                                    ▀▀▀
    

05-19-2026 | pwnable.kr series

input2

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"] * 32

100 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
0000004

or 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/flag

or 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