This challenge was solved by several teams during the contest, however it seems that most teams didn’t have the intended solution, so here it is 😉
The source, binary and exploit for this challenge can be found on our github here!
smtpwn was a very simple local SMTP service. Basically you write a message to its stdin
, and it’ll write a file to /tmp/
with the following content:
Message-ID: <urandom_hex_string>rn
From: "fromuser" <fromuser@hostname>rn
To: "touser" <touser@hostname>rn
Date: current_datetimern
X-Flag: the_flagrn
Subject: Read this message to get the flag!rn
rnuser_inputrn.rn
Where you control or known every variable except the urandom_hex_string
used as a message-id, and the flag.
Now the program works as follows:
- retrieve all variables used in the message in the following order: the message on
stdin
, the username, hostname, current date, the flag, and the random message-id. - create the output file, which name is
/tmp/mail_<time>_<message-id>
, and write the message to it. - print the message id and the output file checksum
- destroy the output file
The new file can only be read by its owner, which is the setuid one. So you cannot just make stdout
blocking, read the message-id and read the file before unlink is called.
The random string is problematic, because you can’t guess it nor brute-force it. The trick here lies in the way error handling was performed: the message “An error occured” is printed but the program doesn’t exit.
if (/* ... snip ...*/
/* read flag */
((flag_fd = open(FLAG_PATH, O_RDONLY)) == -1) ||
(read(flag_fd, msg->flag, FLAG_SIZE) < 0) ||
/* generate random Message-ID */
((urandom_fd = open(RANDOM_PATH, O_RDONLY)) == -1) ||
(read(urandom_fd, random, sizeof(random)) != sizeof(random)) ||
(hexdigest(msg->id, random, sizeof(random)))
) {
fail();
}
close(flag_fd);
close(urandom_fd);
Because the flag is opened before /dev/urandom
and not closed yet, you can cause a file descriptor exhaustion bug to happen, which would leave the random message-id initialized with null bytes.
That can be trivially done using ulimit (or setting RLIMIT_NOFILE
to 4 with setrlimit()
).
Many teams just stopped there and then performed a race condition by creating a hardlink so they can read the flag file. Which I didn’t really think of (fail). However there’s a “cleaner” solution, which involves ulimit
/ setrlimit
again.
Because we now know the message-id (null bytes), the only thing left that we don’t know is the flag itself. Remember that the checksum of the file written to disk is provided. We can use that information to leak the flag byte by byte, proceeding as follows:
- set
RLIMIT_NOFILE
to 4 to bypass/dev/urandom
- set
RLIMIT_FSIZE
so that the program cannot write more thanX+1
bytes,X
being the number of bytes before the flag - launch
smtpwn
and check out the checksum - bruteforce the last byte (255 possibilities) until we match the checksum
- repeat with
RLIMIT_FSIZE += 1
The exploit can be found on github here.
This allows to retrieve the flag quickly (and reliably):
[+] I
[+] IN
[+] INS
[+] INS{
[+] INS{M
[+] INS{M4
* snip *
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3l
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}
Final flag: INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}