Unveiling the Sudo Heap Overflow Vulnerability (CVE-2021–3156): A Critical Security Flaw Reappears

TutorialBoy
19 min readJul 22, 2023

--

Introduction

In this blog post, we dive into the intricacies of the Sudo Heap Overflow Vulnerability (CVE-2021–3156). On January 26, 2021, Qualys Research Labs discovered a flaw in sudo. When sudo parses the command line parameters, the truncation character is wrongly judged, which leads to the attacker maliciously constructing a payload, causing sudo to overflow the heap. This vulnerability can cause Local privilege escalation.

Environment

environment version

• ubuntu 20.04

• sudo-1.8.31p2

Use the following command to compile and install

cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix = /home/pwn/sudo CFLAGS = "-O0 -g " make && make install

Vulnerability

#poc
./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111

Executing the above POC and executing sudoedit will display the words malloc(): invalid size, which is a typical exception caused by a heap overflow.

Vulnerability Analysis

The Source code analysis

set_cmnd function 
File : plugins\sudoers\sudoers . c
800 : static int
801 : set_cmnd ( void )
802 : { ... 819 : if ( sudo_mode & ( MODE_RUN | MODE_EDIT | MODE_CHECK )) { //Need to meet the setting of the flag bit To enter the escape process ... 845 : 846 : /* set user_args */ 847 : if ( NewArgc > 1 ) { 848 :



char * to , * from , ** av ;
849 : size_t size , n ;
850 :
851 : /* Alloc and build up user_args. */
852 : for ( size = 0 , av = NewArgv + 1 ; * av ; av ++ ) //Traverse each parameter
853 : size += strlen ( * av ) + 1 ; // Calculate the length of each parameter
854 : if ( size == 0 || ( user_args = malloc ( size )) == NULL ) { // Dynamically allocate a section of memory through malloc to store parameter content
855 : sudo_warnx ( U_ ( "%s: %s" ), __func__ , U_ ( "unable to allocate memory" ));
856 : debug_return_int ( - 1 );
857 : }
858 : if ( ISSET ( sudo_mode , MODE_SHELL | MODE_LOGIN_SHELL )) { //The setting of the flag bit needs to be satisfied to enter the escape process
859 : /*
860: * When running a command via a shell, the sudo front-end
861: * escapes potential meta chars. We unescape non-spaces
862: * for sudoers matching and logging purposes.
863: */
864 : for ( to = user_args , av = NewArgv + 1 ; ( from = * av ); av ++ ) { //Traverse each environment variable and copy the content to the memory
865 : while ( * from ) { /* The vulnerability point, when scanning the parameter content, encounter \ needs to be escaped, such as '\t' , '\n', etc., so sudo only judges whether \ is followed by a space character, that is, the isspace function is used to judge . The characters included in isspace are as follows: ' ' (0x20) space (SPC) space character '\t' (0x09) horizontal tab (TAB) horizontal tab character '\n' (0x0a) newline (LF) newline character '\v' (0x0b) vertical tab (VT) vertical tab character '\f' (0x0c) feed (FF) form feed character '\r' (0x0d) carriage return (CR) does not include '' above carriage return . The parameters are separated by '', so when '\\' is followed by '', from++ will cause the next parameter to be copied in, and finally cause the heap block to overflow. */ 866 :




== '\\' && ! isspace (( unsigned char ) from [ 1 ]))
867 : from ++ ;
868 : * to ++ = * from ++ ;
869 : }
870 : * to ++ = ' ' ;
871 : }
872 : *-- to = '' ;

Therefore, the vulnerability lies in the need to escape the escape character when entering the set_cmnd function, but the function does not judge the escape character as the end of the parameter, that is, \ + \x00

parse_args function

The parse_args function is used to reverse escaping, that is, if there are escape characters in the parameter, a \ will be added before each escape character

File : src\parse_args . c 
592 : if ( ISSET ( mode , MODE_RUN ) && ISSET ( flags , MODE_SHELL )) { //The setting of the flag bit needs to be satisfied before entering the reverse escape process
593 : char ** av , * cmnd = NULL ;
594 : int ac = 1 ;
595 :
596 : if ( argc != 0 ) {
597: /* shell -c "command" */
598 : char * src , * dst ;
599 : size_t cmnd_size = ( size_t ) ( argv [ argc - 1 ] - argv [ 0 ]) +
600 : strlen ( argv [ argc - 1 ]) + 1 ;
601 :
602 : cmnd = dst = reallocarray ( NULL , cmnd_size , 2 );
603 : if ( cmnd == NULL )
604 : sudo_fatalx ( U_ ( "%s: %s" ), __func__ , U_ ( "unable to allocate memory" ));
605 : if ( ! gc_add ( GC_PTR , cmnd ))
606 : exit ( 1 );
607 :
608 : for ( av = argv ; * av != NULL ; av ++ ) {
609 : for ( src = * av ; * src != '' ; src ++ ) {
610 : /* quote potential meta characters */
611 : if ( ! isalnum (( unsigned char ) * src ) && * src != '_' && * src != '-' && * src != '$' )
612 : * dst ++ = '\\' ;
613 : * dst ++ = * src ;
614 : }
615 : * dst ++ = ' ' ;
616 : }
617 : if ( cmnd != dst )
618 : dst -- ; /* replace last space with a NUL */
619: * dst = '' ;
620 :
621 : ac += 2 ; /* -c cmnd */
622 : }

This is why the set_cmnd function needs to escape the parameters, so if the parse_args function is first used to reverse the parameters, and then the set_cmnd function is used to escape, then there will be no loopholes in sudo

Bypass inspection

So how to bypass the set_cmnd function and directly enter the parse_args function is the key factor for the vulnerability to be successfully triggered

The first is how to enter the set_cmnd function, sudo will go through double detection

  • sudo_mode needs to have the flag bit of MODE_RUN, MODE_EDIT or MODE_CHECK
  • sudo_mode needs to have the flag bit of MODE_SHELL or MODE_LOGIN_SHELL
File : plugins\sudoers\sudoers . c ... 819 :      if ( sudo_mode & ( MODE_RUN | MODE_EDIT | MODE_CHECK )) { //The setting of the flag bit needs to be satisfied to enter the escape process ... 858 : if ( ISSET ( sudo_mode , MODE_SHELL | MODE_LOGIN_SHELL )) { // Need to meet the setting of the flag bit to enter the escape process If you want to get the flag bit of MODE_SHELL, you need to set the -s parameter, at this time through SET ( flags , MODE_SHELL )




, set the flag to MODE_SHELL, and the default mode is NULL, so setting the -s parameter can make the flag set both MODE_SHELL and MODE_RUN.
File : src\parse_args . c
479 : case 's' :
480 : sudo_settings [ ARG_USER_SHELL ]. value = "true" ;
481 : SET ( flags , MODE_SHELL );
482 : break ; ... 534 : if ( ! mode ) 535 : mode = MODE_RUN


; /* running a command */
536 : }

But if you use sudo -s, it will cause the flag to set MODE_SHELL and MODE_RUN, and you will enter the process of the parse_args function, which will add a ‘\’ in front of all non-alphanumeric characters, which will cause us to fail. Construct the vulnerability character of ‘’ + ‘\x00’, so if we want to exploit the vulnerability successfully, we don’t need the program to enter the set_cmd function, but we cannot enter the parse_args function

File : src\parse_args . c 
592 : if ( ISSET ( mode , MODE_RUN ) && ISSET ( flags , MODE_SHELL )) { //The setting of the flag bit needs to be satisfied before entering the anti-escaping process ... 608 : for ( av = argv ; * av != NULL ; av ++ ) { 609 : for ( src = * av ; * src !=


'' ; src ++ ) {
610 : /* quote potential meta characters */
611 : if ( ! isalnum (( unsigned char ) * src ) && * src != '_' && * src != '-' && * src != '$' )
612 : * dst ++ = '\\' ;
613 : * dst ++ = * src ;
614 : } ... 622 : }

At the beginning of the parse_args function, it will detect whether it is called by sudo or sudoedit. If it is called by sudoedit, it will directly set MODE_EDIT to the mode, thus bypassing the mode==NULL. When the flag needs to be set to MODE_RUN, so use sudoedit -s, you can set the flag to set MODE_EDIT and set MODE_SHELL

File : src\parse_args . c ... 265 :      proglen = strlen ( progname ); 266 :      if ( proglen > 4 && strcmp ( progname + proglen - 4 , "edit" ) == 0 ) { 267 : progname = "sudoedit " ; 268 : mode = MODE_EDIT ; 269 : sudo_settings [

ARG_SUDOEDIT ]. value = "true" ;
270 : }

The second path to enter set_cmnd is to set the flag to MODE_EDIT | MODE_SHELL. Such input can bypass the parse_args function and prohibit entry to the set_cmd function. This is why the heap overflow of sudo needs to be triggered by sudoedit -s instead of sudo — the s

File : plugins\sudoers\sudoers . c ... 819 :      if ( sudo_mode & ( MODE_RUN | MODE_EDIT | MODE_CHECK )) { //The setting of the flag bit needs to be satisfied to enter the escape process ... 858 : if ( ISSET ( sudo_mode , MODE_SHELL | MODE_LOGIN_SHELL )) { // Need to meet the setting of the flag bit to enter the escape process

Exploit

Vulnerability Exploitation Analysis

Since the program has an obvious heap overflow vulnerability, it is necessary to sort out how the heap overflow is exploited.

• Find a heap block whose value will affect the flow of program execution, which is called an available heap block.

• Find an operation that can freely control the location of the heap block, and deploy the heap block applied by the vulnerable function above the exploitable heap block. When the heap overflow is triggered, the value of the exploitable heap block can be rewritten to our expected value.

Available Heaps

NSS is used to parse and obtain different types of name information, such as how to obtain user information by name, and NSS needs to be called when sudo needs to obtain user information.

When using NSS to obtain information, it actually performs corresponding behaviors through different dynamic link libraries, and the file names of these libraries exist in the configuration file of /etc/nsswitch.conf

For example, if you want to query the passwd file, you need to use libnss_files.so and libnss_systemed.so

So how to load these dynamic link libraries needs to depend on the nss_load_library function, and this related information is stored in the service_user structure, which is stored in the heap memory.

Then it is necessary to study whether the value of the structure will affect the execution flow of the program. The code is as follows.

File : nsswitch .c 327 : static int 328 : 
nss_load_library ( service_user * ni ) 329 : { 330 : if ( ni -> library == NULL ) 331 : { 332 : / * This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335:

holds the head of the service_library list made from the
336: default configuration. */
337 : static name_database default_table ;
338 : ni -> library = nss_new_service ( service_table ? : & default_table ,
339 : ni -> name ); //if ni If the value of ->library is NULL, a new ni->library will be created and all members will be initialized
340 : if ( ni -> library == NULL )
341 : return - 1 ;
342 : }
343 :
344 : if ( ni -> library -> lib_handle == NULL ) //Since ni->library is newly created, ni->library->lib_handle must be NULL
345 : {
346 : /* Load the shared library. */
347 : size_t shlen = ( 7 + strlen ( ni -> name ) + 3
348 : + strlen ( __nss_shlib_revision ) + 1 );
349 : int saved_errno = errno ;
350 : char shlib_name [ shlen ];
351 :
352 : /* Construct shared object name. */
353 : __st***y ( __st***y ( __st* **y ( __st***y ( shlib_name ,
354 : "libnss_" ),
355 : ni -> name ),
356: ".so" ), //shalib_name is obtained according to splicing
357 : __nss_shlib_revision );
358 :
359 : ni -> library -> lib_handle = __libc_dlopen ( shlib_name ); //load dynamic link library

The key point of the above code is that the program will use __libc_dlopen to open the dynamic link library specified by shalib_name, and shalib_name is obtained by a series of splicing through ni->name, and ni->name is stored in the structure service_user In *ni, the structure is stored in the heap memory. Then we found the key value ni->name, which is a key variable that can modify the execution flow of the program.

For example, if we change ni->name to X/test, then the final splicing result will get libnss_X/test.so, then if we create a new libnss_X in the current directory and create a test.so in the directory The dynamic link library, then sudo will load and execute the code in our dynamic link library. So far we have found the first key factor of utilization, which is the use of heap blocks.

The operation of arranging blocks

Since we have found the available heap blocks, if we can deploy the heap overflowed heap blocks above the available heap blocks, and use the heap overflow to modify ni->name, the effect of arbitrary code execution can be completed.

In the main function of sudo, the setlocate function will be executed. setlocale is a function used to set the locale of the program, and there are corresponding implementations in many programming languages ​​and operating systems.

Regional settings refer to the collection of related information such as the language, region, date format, currency symbol, etc. used by the program at runtime. By setting the locale, the program can adapt to localization needs according to different regions and language environments.

export LC_ALL=en_US.UTF-8@XXXX

In the setlocal function, a lot of heap block allocation and release operations are involved. When calling setlocal(LC_ALL,””), the program will search for the value of the locale through the value set by the environment variable, and the search for the environment variable depends on _nl_find_locale function.

_nl_find_locale function 
File : locale\findlocale . c
101 : struct __locale_data *
102 : _nl_find_locale ( const char * locale_path , size_t locale_path_len ,
103 : int category , const char ** name )
104 : { ... 184 : /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier]
187:
188: Besides the first all of them are allowed to be missing. If the
189: full specified locale is not found, the less specific one are
190: looked for. The various part will be stripped off according to
191: the following order:
192: (1) codeset
193: (2) normalized codeset
194: (3) territory
195: (4) modifier
196: */ /* The format of the area is C_en_US.UTF-8@XXXXXX _nl_explode_name is used for judgment (1 )(2)(3)(4)Which part exists and which part is missing */
197: mask = _nl_explode_name ( loc_name , & language , & modifier , & territory ,
198 : & codeset , & normalized_codeset );
199 : if ( mask == - 1 )
200 : /* Memory allocate problem. */
201 : return NULL ;
202 : //locale_file is dynamic for regional settings Memory allocation 205 : locale_file = _nl_make_l10nflist ( & _nl_locale_file_list [ category ], 206 :


locale_path , locale_path_len , mask ,
207 : language , territory , codeset ,
208 : normalized_codeset , modifier ,
209 : _nl_category_names_get ( category ), 0 ); // return NULL
210 :
211 : if ( locale_file == NULL )
212 : {
213 : /* Find status record for addressed locale file. We have to search
214: through all directories in the locale path. */
215 : locale_file = _nl_make_l10nflist ( & _nl_locale_file_list [ category ],
216 : locale_path , locale_path_len , mask ,
217 : language , territory , code set ,
218 : normalized_codeset , modifier ,
219 : _nl_category_names_get ( category ), 1 );
220: if ( locale_file == NULL )
221 : /* This means we are out of core. */
222 : return NULL ;
223 : }
}

_nl_make_l10nflist* *function**

_nl_make_l10nflist will allocate heap blocks according to the value we pass in.

File : intl\l10nflist . c 
150 : struct loaded_l10nfile *
151 : _nl_make_l10nflist ( struct loaded_l10nfile ** l10nfile_list ,
152 : const char * dirlist , size_t dirlist_len ,
153 : int mask , const char * language , const char * territory ,
154 : const char * codeset , const char * normalized_codeset ,
155 : const char * modifier ,
156 : const char * filename , int do_allocate )
157 : { ... 165 : //Dynamic allocation according to the length of the area value we pass in 166 : abs_filename = ( char * ) malloc ( dirlist_len 167 : + strlen ( language )

168 : + (( mask & XPG_TERRITORY ) != 0
169 : ? strlen ( territory ) + 1 : 0 )
170 : + (( mask & XPG_CODESET ) != 0
171 : ? strlen ( codeset ) + 1 : 0 )
172 : + (( mask & XPG_NORM_CODESET ) != 0
173 : ? strlen ( normalized_codeset ) + 1 : 0 )
174 : + (( mask & XPG_MODIFIER ) != 0
175 : ? strlen ( modifier ) + 1 : 0 )
176 : + 1 + strlen ( filename ) + 1 );
177 : ... 292 : }

setlocale* *function**

The overall operation of the setlocale function is to read the value of the environment variable to obtain the value of the regional setting, and allocate the heap block size according to the value of the regional setting. If there is any specification that does not meet the regional value, all previously applied heap blocks will be released. .

File : locale\setlocale . c 
334 : while ( category -- > 0 )
335 : if ( category != LC_ALL )
336 : { //Use the _nl_find_locale function to obtain the value of the environment variable and store it in newdata[category] 337 : newdata [ category ] = _nl_find_locale ( locale_path , locale_path_len , 338 : category , 339 : & newnames


[ category ]);
340 : ... 364 : else 365 : { //Use the __strdup function to allocate space in the heap memory and copy newdata[category] into it 366 : newnames [ category ] = __strdup ( newnames [ category ] ); 367 : if ( newnames [ category ] == NULL ) 368 : break ; 369 : } ... 393 : if (


category != LC_ALL && newnames [ category ] != _nl_C_name
394 : && newnames [ category ] != _nl_global_locale .__ names [ category ])
395 : free (( char * ) newnames [ category ]) ; The primitive, as long as there is a regional setting value that does not conform to the specification, all previously applied heap blocks will be released

Therefore, the size of the heap can be controlled by the area value, and then a wrong area value is set at the end to control the position of the heap. So far we have found an operation that can control the heap.

LC_IDENTIFICATION = C.UTF-8@XX..XX #If the length is 0x10, then malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX, #If the length is 0X20, then malloc(0x20) LC_TELEPHONE = XXXX #If it does not conform to the specification of the area value, free() will be called analysis of exp

Since we need to control the heap block of server_user, we need to know the size of the heap block. Through debugging, we can see that it is a heap block of 0x40, so use setlocate to release a few more heap blocks of 0x40, then server_user will use what we released pile of blocks.

Immediately after that, allocate the vulnerability heap block above the server_user heap block. Since the server_user heap block is constructed by ourselves, we only need to release the vulnerability heap block while releasing the heap block, and the application for the vulnerability heap block can be set according to the length of the parameter

Set the function of setting the area value as the primitive of heap block allocation and release, and use the character after @ to control the size of the heap block

Freeing heap blocks with wrong region values

Finally, how to fill in the exploitable heap block. Here, heap overflow is used, and the filling string is constructed in the environment variable, so that the exploitable heap block can overwrite the content value of the exploitable heap block. However, it should be noted here that we need to Ni->library is filled with \x00, and \x00 cannot be directly input into the environment variable, so it is necessary to observe how the vulnerable function copies characters again. According to the code analysis, as long as ‘’ is followed by ‘\x00’, then we can directly copy the value of \x00 to the heap memory. Then modify ni->name to the dynamic link library we think is constructed.

File : plugins\sudoers\sudoers . c 
866 : if ( from [ 0 ] == '\\' && ! isspace (( unsigned char ) from [ 1 ])) //if '\' is followed by '\x00'
867 : from ++ ; //At this point from will point to \x00
868 : * to ++ = * from ++ ; //Use \x00 to copy the value
869 : }

Set multiple environment variables so that there are multiple ‘’ + ‘\x00’ in the memory, so use ‘\x00’ to cover the memory value of the heap.

The demonstration effect is as follows

Bug Fixes

The repair of the vulnerability is to make an additional judgment on the flag bit of MODE_EDIT, and add a check of ‘’ after ‘’

---  a / plugins / sudoers / sudoers . c Sat Jan 23 08 : 43 : 59 2021 - 0700       
+++ b / plugins / sudoers / sudoers . c Sat Jan 23 08 : 43 : 59 2021 - 0700
@@ - 547 , 7 + 547 , 7 @@

/* If run as root with SUDO_USER set, set sudo_user.pw to that user. */ / * XXX - causes confusion when root is not listed in sudoers */ - if ( sudo_mode & ( MODE_RUN | MODE_EDIT ) && prev_user != NULL ) { + if ( ISSET ( sudo_mode , MODE_RUN | MODE_EDIT ) && prev_user != NULL ) { if ( user_uid == 0 && strcmp ( prev_user ,



"root" ) != 0 ) { struct passwd * pw ; @@ - 932 , 8 + 932 , 8 @@ if ( user_cmnd == NULL ) user_cmnd = NewArgv [ 0 ]; - if ( sudo_mode & ( MODE_RUN | MODE_EDIT | MODE_CHECK )) { - if ( ISSET ( sudo_mode , MODE_RUN |


MODE_CHECK )) {
+ if ( ISSET ( sudo_mode , MODE_RUN | MODE_EDIT | MODE_CHECK )) {
+ if ( ! ISSET ( sudo_mode , MODE_EDIT )) { // extra judgment on MODE_EDIT const char * runchroot = user_runchroot ; if ( runchroot == NULL && def_runchroot != NULL && strcmp ( def_runchroot , "*"


) != 0 )
@@ - 961 , 7 + 961 , 8 @@ sudo_warnx ( U_ ( "%s: %s" ), __func__ , U_ ( "unable to allocate memory" )); debug_return_int ( NOT_FOUND_ERROR ); } - if ( ISSET ( sudo_mode , MODE_SHELL | MODE_LOGIN_SHELL )) { + if ( ISSET ( sudo_mode ,


MODE_SHELL | MODE_LOGIN_SHELL ) &&
+ ISSET ( sudo_mode , MODE_RUN )) { // sudo -s is required to escape
/* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non- spaces @@ -969,10 +970,22 @@ */ for ( to = user_args , av = NewArgv + 1 ; ( from = * av ); av ++ ) { while ( * from ) {

if ( from [ 0 ] == '\\' && ! isspace (( unsigned char ) from [ 1 ]))
+ if ( from [ 0 ] == '\\' && from [ 1 ] != '' && / / Added the judgment of ''
+ ! isspace (( unsigned char ) from [ 1 ])) { from ++ ; + } + if


( size - ( to - user_args ) < 1 ) {
+ sudo_warnx ( U_ ( "internal error, %s overflow" ),
+ __func__ );
+ debug_return_int ( NOT_FOUND_ERROR );
+ } * to ++ = * from ++ ; } + if ( size - ( to - user_args ) < 1 ) { + sudo_warnx



( U_ ( "internal error, %s overflow" ),
+ __func__ );
+ debug_return_int ( NOT_FOUND_ERROR );
+ } * to ++ = ' ' ; } *-- to = '' ;

Summarize

Sudo heap overflow attack process

First, use setlocate as the primitive for heap block allocation and release, and construct a suitable heap layout to ensure that the server_user heap block is as close as possible to the heap block created by the vulnerable code.

Secondly, the heap overflow is used to overwrite the ni->name value of the server_user heap block, and the overwritten value is a maliciously constructed dynamic link library name.

Finally, wait for the dynamic link library to be loaded and executed.

Limitations of Sudo Heap Overflow Exploitation

Since the sudo heap overflow depends on the layout of the heap, different versions of sudo or the operating system will affect the exploitation of the vulnerability.

--

--

TutorialBoy
TutorialBoy

Written by TutorialBoy

Our mission is to get you into information security. We'll introduce you to penetration testing and Red Teaming. We cover network testing, Active Directory.