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
Host: 221.141.3.112
[...]

fname=test_name&lname=last_name&username=login_name27393&password=password&email=asdf@example.com'),('name','surname','Administrator',MD5('mypassword'),'asdf@example.com')%23


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.


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

Solution:

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
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
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:02C8
[...]
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
seg000:0331

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
                                               ;di=pointer
                                               ;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
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:

MLJKE
FKBKF
DNKHG
NDIFI
ECAFJ
GFHEK
BCIBL
KAHAN

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
;
.data
     item dd 0
     MIN_CHAR db 40h
     MAX_CHAR db 50h
     temp_passwd db 10h dup(' '),0
     password_ok db 2,' gate'
.code
start:
     call main
     exit


main proc
     cls
     mov ebx, offset temp_passwd
     mov edi, 5
     mov al, 20h
  loc_27F:
     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


  tryPassword:
     xor eax, eax
     xor ebx, ebx
     xor ecx, ecx
     xor edx, edx
     mov cl, 10h
     xor dx, dx
     mov esi, offset temp_passwd
     cld
  enc_loop:
     lodsb
     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


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


end_main:
     ret


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
     retn
checkPWD endp
end start