
CVE-2021-3156 POC and Docker and Analysis write up
[toc]
Vulnerability ID: CVE-2021-3156
Vulnerability Score:
Affected Product: Linux sudo
Affected Versions: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1
Exploitation Conditions: Linux local; sudo is suid and executable
Impact: Local privilege escalation
Source Code: https://www.sudo.ws/getting/source/
Docker Environment: chenaotian/cve-2021-3156
The Docker I set up provides:
Everything is in the /root directory:
image-20220124223312224
Testing the exploit:``` cd exp su test ./exp whoami
For debugging related content, see later [some debugging commands](#一些调试命令)
## Vulnerability Principle
Vulnerability trigger payload```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`
Source code analysis (sudo-1.8.21): First is the main function in sudo.c (sudo.c: 133):```c int main(int argc, char *argv[], char *envp[]) { int nargc, ok, status = 0; char **nargv, **env_add; char **user_info, **command_info, **argv_out, **user_env_out; struct sudo_settings *settings; struct plugin_container *plugin, *next; sigset_t mask; debug_decl_vars(main, SUDO_DEBUG_MAIN)
··· ···
··· ···
/* Parse command line arguments. */
//在这里处理输入参数,设置sudo_mode
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
··· ···
··· ···
switch (sudo_mode & MODE_MASK) {
··· ···
··· ···
case MODE_EDIT:
case MODE_RUN:
ok = policy_check(&policy_plugin, nargc, nargv, env_add,
&command_info, &argv_out, &user_env_out);
··· ···
··· ···
}
··· ···
··· ···
}
- First, the `parse_args` function is called to process the input parameters. In fact, we only input `-s` here, nothing much to set, setting `sudo_mode` to `MODE_EDIT` and `MODE_SHELL`.
- Then, depending on the `sudo_mode`, `MODE_EDIT` will call `policy_check`.
Next is the `policy_check` function in `sudo.c` (sudo.c: 1136):```c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
char *env_add[], char **command_info[], char **argv_out[],
char **user_env_out[])
{
··· ···
··· ···
ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
argv_out, user_env_out);
···
}
调用了回调函数 plugin->u.policy->check_policy ,可以调试查看这个函数的真实函数:
image-20220123113326096
调用的是policy.c 中的 sudoers_policy_check 函数(policy.c: 760):```c static int sudoers_policy_check(int argc, char * const argv[], char *env_add[], char **command_infop[], char **argv_out[], char **user_env_out[]) { ··· ···
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, &exec_args);
··· ···
··· ···
}
Then called the sudoers_policy_main function in sudoers.c (sudoers.c: 224):```c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
void *closure)
{
··· ···
··· ···
/*
* Make a local copy of argc/argv, with special handling
* for pseudo-commands and the '-i' option.
*/
if (argc == 0) {
··· ···
} else {
/* Must leave an extra slot before NewArgv for bash's --login */
NewArgc = argc;
NewArgv = reallocarray(NULL, NewArgc + 2, sizeof(char *));
··· ···
}
memcpy(++NewArgv, argv, argc * sizeof(char *));
NewArgv[NewArgc] = NULL;
··· ···
}
}
··· ···
cmnd_status = set_cmnd();
··· ···
··· ···
··· ···
}
Some global variables are set here, NewArgc and NewArgv as shown below; they are essentially the passed parameters.
image-20220123113819116
Then it enters the set_cmnd function in sudoers.c (sudoers.c: 796):```c static int set_cmnd(void) { ··· ··· ··· ···
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
//根据参数总长度计算size, 后续malloc 申请,没有问题
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
* for sudoers matching and logging purposes.
*/
//将所有参数拷贝到一起放到堆中,逻辑是遇到'\'加非空格类型字符则只拷贝非空格字符
//但这里\x00 并不算空格类型字符
//他没有考虑参数如果只有一个'\'或以'\'结尾并且下两个字符后就是另一个字符串情况
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
*--to = '\0';
}
··· ···
}
}
··· ···
··· ···
}
The overflow also occurs here. According to the comments in the code, the heap overflow occurs when copying to the heap. The original intention of this code is not difficult to understand: it copies all the arguments in NewArgv to the heap, separated by spaces. When encountering `\` followed by a non-space character, it only copies that character.
**But it does not consider a situation where a NewArgv element ends with `\`, resulting in a `\` + `\x00` structure. Since `\x00` is not a space character (absurd), this means that after copying `\x00` to the heap, the `from` variable is incremented again (incremented twice in one loop), directly bypassing the opportunity to check the while loop's end marker `\x00`, so it assumes the argument has not been fully copied and continues copying until encountering the next `\x00`.**
In this scenario, you can see that `\` + `\x00` is immediately followed by the next argument `A*80`, so it will continue copying to the end of `A*80`. But don't forget that the program will then process the `A*80` argument normally and copy it again. So here `A*80` is copied twice in total, but the chunk was allocated based on the size of only one `A*80` string, far exceeding the allocated chunk length.
image-20220123113907744
Then an overflow occurs. Before copying:
image-20220123114036691
After copying:
image-20220123114137794
The overall vulnerability trigger path (you can set breakpoints directly on these functions when debugging) is:
- sudo.c : main
- sudo.c : policy_check
- policy.c : sudoerrs_policy_check
- sudoers.c : sudoers_policy_main
- sudoers.c : set_cmnd
- sudoers.c : 859
## Vulnerability Exploitation Principle
Referenced [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156) , **but his heap layout method is not always achievable. Here we analyze the heap layout method in detail.** By passing environment variables `LC_*` to lay out the heap, then making the overflowing chunk exactly overwrite the `service_user` structure needed by `nss_load_library` to load a shared object. Overwrite the shared object name string in this structure, then make the program load the specified shared object to achieve arbitrary code execution.
Although the logic seems clear, the details to handle are still troublesome:
1. Related data structures and mechanisms in nss_load_library
2. How setlocale uses the environment variable `LC_*` for heap layout