Room Banner

Looney Tunables

CVE-2023-4911: That's all Sec-Folks!

medium

60 min

Room progress ( 0% )

To access material, start machines and answer questions login.

Task 1Introduction

The CVE identifier CVE-2023-4911 has been assigned to The Qualys Threat Research Unit (TRU) on October 3, 2023, due to a critical security flaw in the GNU C Library's dynamic loader, known as ld.so. This vulnerability poses a significant risk since it allows escalating the privileges of a logged-on user and obtaining full control of the vulnerable instance.  

This vulnerability was introduced in glibc version 2.34 through commit 2ed18c. The vulnerability affects recent versions of major Linux distributions such as RHEL, Ubuntu, Fedora, Debian, Amazon Linux, Gentoo and any other distribution that uses glibc.

To deploy the attached VM, press the green Start Machine button at the top of the task and connect to the machine via SSH with the following credentials:

THM key
Usernamenopriv
PasswordPassword321
Answer the questions below
Click and continue learning!

LD.SO: The dynamic linker/loader

Whenever you execute an ELF file that depends on shared libraries (.so files), the operating system will need to load such libraries and link them to your executable so that any shared functions are available to your program during its execution. In Linux systems, this is normally managed by ld.so, an executable prepackaged as part of the glibc library.

Every ELF executable contains a section named .interp, where the location of its required loader is specified so that the operating system knows which specific loader to use. For most ELF files in Linux, this will be ld.so (probably with a slightly different name). To check the .interp section of an ELF file, you can use the following command:

Terminal
user@attackbox$ readelf /usr/bin/man -p .interp

String dump of section '.interp':
  [     0]  /lib64/ld-linux-x86-64.so.2
        

We can also check which libraries are needed to run the man command by using the ldd command to get a list of dependencies:

Terminal
user@attackbox$ ldd /usr/bin/man
        linux-vdso.so.1 (0x00007ffc699f7000)
        libmandb-2.11.2.so => /usr/lib/man-db/libmandb-2.11.2.so (0x00007f24dc8f4000)
        libman-2.11.2.so => /usr/lib/man-db/libman-2.11.2.so (0x00007f24dc8c2000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f24dc886000)
        libpipeline.so.1 => /lib/x86_64-linux-gnu/libpipeline.so.1 (0x00007f24dc874000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f24dc692000)
        libgdbm.so.6 => /lib/x86_64-linux-gnu/libgdbm.so.6 (0x00007f24dc67d000)
        libseccomp.so.2 => /lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007f24dc65d000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f24dca1d000)
        

Here, we can see that the man command depends on libz.so.1, which can be found at /lib/x86_64-linux-gnu/libz.so.1, for example.

Usually, the required libraries are searched in a specific set of locations in the system, including the default /lib directory. You can read the ld.so documentation for the complete list of searched locations.

Using DT_RPATH to Influence the Library Search Path

When compiling a program, you can specify additional paths where you want ld.so to look for libraries. These directories will be embedded in your program's ELF file and parsed and used by ld.so when loading the executable. This allows you to override the default library search path and have your program search in an alternate location first. For example, you could compile a program with the following options to that end:

Terminal
user@attackbox$ gcc -Wl,--enable-new-dtags -Wl,-rpath=/tmp -o myapp myapp.c
        

Whenever ld.so loads your executable, it will first check /tmp/ for libraries, then default to the regular library search path.

Modifying glibc Behaviour via GLIBC_TUNABLES

To better understand how this vulnerability works, let's have a look at how the dynamic linker (ld.so) briefly works. Once it gets executed, it checks specific environment variables called GLIBC_TUNABLES. Those features, aka Tunables, allow developers to dynamically alter the runtime library behaviour.

One example to understand how this process works is the following:

GLIBC_TUNABLES="malloc.check=1:malloc.tcache_max=128"

The environment variable GLIBC_TUNABLES sets the maximum size chunk that may be stored in a tcache (in bytes).

Answer the questions below
Click and continue learning!

In line 282, the code looks for any variables with the name GLIBC_TUNABLES in the environment variables (envp) and then copies them into new_envNext, the method parse_tunables(), called (at line 286), processes and sanitizes the data in new_env to ensure that the settings in GLIBC_TUNABLES won't result in any problems or vulnerabilities.

The processed copy new_env is substituted for the original GLIBC_TUNABLES variable at line 288.

269 void
270 __tunables_init (char **envp)
271 {
272   char *envname = NULL;
273   char *envval = NULL;
274   size_t len = 0;
275   char **prev_envp = envp;
...
279   while ((envp = get_next_env (envp, &envname, &len, &envval,
280                                &prev_envp)) != NULL)
281     {
282       if (tunable_is_name ("GLIBC_TUNABLES", envname))
283         {
284           char *new_env = tunables_strdup (envname);
285           if (new_env != NULL)
286             parse_tunables (new_env + len + 1, envval);
287           /* Put in the updated envval.  */
288           *prev_envp = new_env;
289           continue;
290         }

Inside parse_tunables(), the code examines the copied GLIBC_TUNABLES to break it down into individual name-value pairs. It does this by searching for equal signs (=) and colons (:) in the copied data.

Here's where it gets interesting. The code is careful about security. It removes "dangerous tunables", specifically those marked as SXID_ERASEThose security-related tunables instruct GLIBC to modify security attributes or permissions from a process. These attributes could include changing elevated privileges, typically associated with set-user-ID (SUID) or set-group-ID (SGID) programs. 

The vulnerability occurs when GLIBC_TUNABLES contains unexpected input, like tunable1=tunable2=AAA. In this case, instead of gracefully handling it, the code copies the entire input as if it were a valid setting.  This issue occurs when the tunables are of type SXID_IGNORE, which should not be removed. During the first iteration of the loop, the entire tunable1=tunable2=AAA is copied to tunestr, filling it up. Later, at lines 247-248, the code fails to increment the pointer (p) because no colon (':') was found. As a result, p still points to the value of tunable1, i.e., tunable2=AAA. During the second iteration of the loop, tunable2=AAA is incorrectly appended to tunestr, causing a buffer overflow because tunestr is already full.

162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168   char *p = tunestr;
169   size_t off = 0;
170 
171   while (true)
172     {
173       char *name = p;
174       size_t len = 0;
175 
176       /* First, find where the name ends.  */
177       while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178         len++;
179 
180       /* If we reach the end of the string before getting a valid name-value
181          pair, bail out.  */
182       if (p[len] == '\0')
183         {
184           if (__libc_enable_secure)
185             tunestr[off] = '\0';
186           return;
187         }
188 
189       /* We did not find a valid name-value pair before encountering the
190          colon.  */
191       if (p[len]== ':')
192         {
193           p += len + 1;
194           continue;
195         }
196 
197       p += len + 1;
198 
199       /* Take the value from the valstring since we need to NULL terminate it.  */
200       char *value = &valstring[p - tunestr];
201       len = 0;
202 
203       while (p[len] != ':' && p[len] != '\0')
204         len++;
205 
206       /* Add the tunable if it exists.  */
207       for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208         {
209           tunable_t *cur = &tunable_list[i];
210 
211           if (tunable_is_name (cur->name, name))
212             {
...
219               if (__libc_enable_secure)
220                 {
221                   if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222                     {
223                       if (off > 0)
224                         tunestr[off++] = ':';
225 
226                       const char *n = cur->name;
227 
228                       while (*n != '\0')
229                         tunestr[off++] = *n++;
230 
231                       tunestr[off++] = '=';
232 
233                       for (size_t j = 0; j < len; j++)
234                         tunestr[off++] = value[j];
235                     }
236 
237                   if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238                     break;
239                 }
240 
241               value[len] = '\0';
242               tunable_initialize (cur, value);
243               break;
244             }
245         }
246 
247       if (p[len] != '\0')
248         p += len + 1;
249     }
250 }
Answer the questions below
Click and continue learning!

This section will dig more into the exploitation phase using the following PoC.


  1. Environment Variables Setup

    The exploit creates different arrays (line 41), which will be used later to store the
    GLIBC_TUNABLES environment variables and trigger the buffer overflow in glibc when the program is executed. 

  2. Environment Variables Crafting

    filler: This variable is created to pad away the loader's read-write section. It's filled with a long sequence of 'F' characters ('F'). (lines 17-23)
    kv: This variable is the payload that will trigger the buffer overflow. It's filled with a long sequence of 'A' characters ('A'). (lines 27-33)
    filler2: Similar to filler, this variable is used to pad away any extra portions. It's also filled with a sequence of 'F' characters. (lines 37-43)
    dt_rpath: This variable is used to craft a specific value -0x14ULL to overwrite memory regions in the exploitation process. (lines 47-53)

    All those variables will be copied to the envp array, which contains environment variables. 

  3. Weaponization

    The payload in
    GLIBC_TUNABLES does the following to ld.so: It overwrites a segment of the stack with an index pointing to a string with " as its content. The intention here is to manipulate the stack so that the index points to a specific memory address where the character " (a double quote) is stored. The stack is a region of memory used for storing local variables and function call information during program execution. To add to the exploit's complexity, modern operating systems randomize the stack's location, making it difficult to predict where the index will end up in memory.

  4. Exploitation:
    The exploit uses a trial-and-error approach to account for the randomness of the stack's location. It repeatedly runs the program (forking and executing it) until it gets lucky and the fixed address 0x7ffdfffff030 matches the address where the stack index points to ". This effectively manipulates the library search path to point to the directory named " which will contain a malicious version of lib6.so.
    The process for generating the forged libc6.so consists of copying libc6.so but replacing the __libc_start_main function with a custom shellcode that does setuid(0) + setgid(0) + exec('/bin/sh'). You can find the Python script for this here.
    The program enters a loop where it forks a child process: In the child process, it attempts to execute /usr/bin/su with --help. If the child process takes longer than one second, it assumes that it is returning from a shell, indicating a potential successful exploit.
Answer the questions below
Click and continue learning!

Now, it is time to practice this vulnerability! Deploy the machine attached to this task, and try to escalate your privileges.

In the attached VM, you will find the POC files required for exploitation.

First, you must execute the Python script to generate the malicious libc.so.6.

Terminal
nopriv@looneytunes:~$ python3 gen_libc.py
[*]
'/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
        

You will then need to compile the exploit using gcc:

Terminal
nopriv@looneytunes:~$ gcc -o exp exp.c
        

Finally, you can launch the exploit and be patient since the exploitation may take some time (5 to 20 mins).

Terminal
nopriv@looneytunes:~$ ./exp
try 100
try 200
[snip]
try 3700
try 3800
# whoami
root
        
Answer the questions below
What's the value of the flag in /root/root.txt?

In this room, we have analyzed the complexity of exploiting a buffer overflow flaw in the GNU C Library by messing with the loader (ld.so) and its environment variables. By abusing this vulnerability, an attacker can quickly escalate his privileges, becoming root in the target system. For this reason, all the Linux vulnerable systems should be patched and updated to the latest version of glibc. You can check the fixed versions issued for each distro in their respective security advisories/repositories:

Remember that similar strategies may also apply to other system libraries, highlighting the importance of preventative security measures in cyber security.

Answer the questions below
Click and continue learning!

Room Type

Free Room. Anyone can deploy virtual machines in the room (without being subscribed)!

Users in Room

4,462

Created

679 days ago

Ready to learn Cyber Security? Create your free account today!

TryHackMe provides free online cyber security training to secure jobs & upskill through a fun, interactive learning environment.

Already have an account? Log in

We use cookies to ensure you get the best user experience. For more information contact us.

Read more