{"id":200,"date":"2021-02-19T23:39:59","date_gmt":"2021-02-19T23:39:59","guid":{"rendered":"http:\/\/lockedbyte.com\/blog\/?p=200"},"modified":"2021-02-20T00:13:14","modified_gmt":"2021-02-20T00:13:14","slug":"cve-2021-3156-sudo-heap-based-overflow-leads-to-lpe","status":"publish","type":"post","link":"https:\/\/lockedbyte.com\/blog\/index.php\/2021\/02\/19\/cve-2021-3156-sudo-heap-based-overflow-leads-to-lpe\/","title":{"rendered":"CVE-2021-3156 &#8211; sudo heap-based overflow leading to privilege escalation (PoC development)"},"content":{"rendered":"\n<p>On 26th of January, a new sudo vulnerability came out reported by Qualys (Baron Samedit).<br><br>The advisory is available <a href=\"https:\/\/www.qualys.com\/2021\/01\/26\/cve-2021-3156\/baron-samedit-heap-based-overflow-sudo.txt\">here.<\/a><br><br>The vulnerability is present in the sudo code for 10 years, which attracts a lot, as a ton sudo versions are affected.<\/p>\n\n\n\n<!--more Continue reading-->\n\n\n\n<p><br>The vulnerability consists on a heap-based overflow when an argv[] parameter ends with a backslash.<br><br>Let&#8217;s analyze the root cause at set_cmnd() @ plugins\/sudoers\/sudoers.c:<\/p>\n\n\n\n<pre class=\"prettyprint\">&#47;* set user_args *&#47;\nif (NewArgc &gt; 1) {\n    char *to, *from, **av;\n    size_t size, n;\n\n    &#47;* Alloc and build up user_args. *&#47;\n    for (size = 0, av = NewArgv + 1; *av; av++)\n\tsize += strlen(*av) + 1;\n    if (size == 0 || (user_args = malloc(size)) == NULL) {\n\tsudo_warnx(U_(&#34;%s: %s&#34;), __func__, U_(&#34;unable to allocate memory&#34;));\n\tdebug_return_int(-1);\n    }\n    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {\n\t&#47;*\n\t * When running a command via a shell, the sudo front-end\n\t * escapes potential meta chars.  We unescape non-spaces\n\t * for sudoers matching and logging purposes.\n\t *&#47;\n\tfor (to = user_args, av = NewArgv + 1; (from = *av); av++) {\n\t    while (*from) {\n\t\tif (from&#91;0&#93; == &#39;\\\\&#39; &amp;&amp; !isspace((unsigned char)from&#91;1&#93;))\n\t\t    from++;\n\t\t*to++ = *from++;\n\t    }\n\t    *to++ = &#39; &#39;;\n\t}\n\t*--to = &#39;\\0&#39;;\n    } else {\n\tfor (to = user_args, av = NewArgv + 1; *av; av++) {\n\t    n = strlcpy(to, *av, size - (to - user_args));\n\t    if (n &gt;= size - (to - user_args)) {\n\t\tsudo_warnx(U_(&#34;internal error, %s overflow&#34;), __func__);\n\t\tdebug_return_int(-1);\n\t    }\n\t    to += n;\n\t    *to++ = &#39; &#39;;\n\t}\n\t*--to = &#39;\\0&#39;;\n    }\n}\n}\n<\/pre>\n\n\n\n<p><br><br>As we can see, first it iterates over the available argv[] arguments and strlen() them, the addition of the whole strlen() results is used as the malloc() request size.<\/p>\n\n\n\n<p>Then if the ISSET() conditions are met, it enters in the for loop.<br><br>As we can see, the while loop condition is *from, so when *from is NULL, we can deduce it breaks out from the while loop.<\/p>\n\n\n\n<p>But in the if conditional we can see that if the character is a backslash, it increments from by one, so if the backslash is the last character in the argv, the next byte will be a NULL byte, then it will copy the NULL byte to the to content, and increments it again. That means the while loop condition is still met as we bypassed the *from being a NULL byte at loop condition.<\/p>\n\n\n\n<p>Now, let&#8217;s suppose there is a second argv[] after it. It will start copying it&#8217;s bytes until it finishes (if not finishing with a backslash too lol).<br><br>Once we reach a NULL byte, the for loop will point from to the second argv[] start, which will start copying bytes again from the second argv, bypassing the size calculation with strlen() and writing bytes out of bounds.<br><br>Also, the environment variables are contiguous to the argv parameters, as explained by the Qualys advisory:<\/p>\n\n\n\n<pre class=\"prettyprint\">\n------------------------------------------------------------------------\nenv -i &#39;AA=a\\&#39; &#39;B=b\\&#39; &#39;C=c\\&#39; &#39;D=d\\&#39; &#39;E=e\\&#39; &#39;F=f&#39; sudoedit -s &#39;1234567890123456789012\\&#39;\n------------------------------------------------------------------------\n\n--|--------+--------+--------+--------|--------+--------+--------+--------+--\n  |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|\n--|--------+--------+--------+--------|--------+--------+--------+--------+--\n              size  &lt;---- user_args buffer ----&gt;  size      fd       bk\n\n<\/pre>\n\n\n\n<p>But\u2026 how can we exploit this bug?<br><br>Sudo has all the protections enabled in their binary, so no classic attacks, also we are overflowing the heap, so it&#8217;s time to corrupt internal program data and structures to reach something interesting.<br><br>The Qualys advisory pointed out various methods to exploit it.<br><br>The first method is to get a chunk before an struct that holds a function pointer, also it has fully compatible arguments with execv() (first a string, and then an address to a NULL pointer.<br><br>But ASLR is enabled\u2026<br><br>Hopefully, that pointers points to sudoers.so, which in a near location, has a PLT entry for execv()<br><br>Then to bypass ASLR we can partial overwrite the function pointer and reuse the arguments to execute a binary with the name of the rdi pointed string as root.<br><br>The struct to be overflowed is the following:<\/p>\n\n\n\n<pre class=\"prettyprint\">&#47;* Singly linked hook list. *&#47;\nstruct sudo_hook_entry {\n    SLIST_ENTRY(sudo_hook_entry) entries;\n    union {\n\tsudo_hook_fn_t generic_fn;\n\tsudo_hook_fn_setenv_t setenv_fn\n\tsudo_hook_fn_unsetenv_t unsetenv_fn;\n\tsudo_hook_fn_getenv_t getenv_fn;\n\tsudo_hook_fn_putenv_t putenv_fn;\n    } u;\n    void *closure;\n};\n<\/pre>\n\n\n\n<p>This is the code that will call the corrupted function to let us jump to execv():<\/p>\n\n\n\n<pre class=\"prettyprint\">&#47;* NOTE: must not anything that might call getenv() *&#47;\nint\nprocess_hooks_getenv(const char *name, char **value)\n{\n    struct sudo_hook_entry *hook;\n    char *val = NULL;\n    int rc = SUDO_HOOK_RET_NEXT;\n\n    &#47;* First process the hooks. *&#47;\n    SLIST_FOREACH(hook, &amp;sudo_hook_getenv_list, entries) {\n\trc = hook-&gt;u.getenv_fn(name, &amp;val, hook-&gt;closure);\n\tif (rc == SUDO_HOOK_RET_STOP || rc == SUDO_HOOK_RET_ERROR)\n\t    break;\n    }\n    if (val != NULL)\n\t*value = val;\n    return rc;\n}\n<\/pre>\n\n\n\n<p><br>The second method was a lot more clean, and did not need bruteforce. I find it more stable aswell.<br><br>There is another code at nss\/nsswitch.c in libc which will help us a bit\u2026<br><br>It loads some services in the form: &#8220;libnss_&#8221; + ni->name + &#8220;.so&#8221;<\/p>\n\n\n\n<pre class=\"prettyprint\">&#47;* Construct shared object name.  *&#47;\n__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,\n\t\t\t\t      &#34;libnss_&#34;),\n\t\t\t    ni-&gt;name),\n\t\t  &#34;.so&#34;),\n\t__nss_shlib_revision);\n<\/pre>\n\n\n\n<p>Well, but it retrives it from an struct called ni\u2026<\/p>\n\n\n\n<pre class=\"prettyprint\">typedef struct service_user\n{\n  &#47;* And the link to the next entry.  *&#47;\n  struct service_user *next;\n  &#47;* Action according to result.  *&#47;\n  lookup_actions actions&#91;5&#93;;\n  &#47;* Link to the underlying library object.  *&#47;\n  service_library *library;\n  &#47;* Collection of known functions.  *&#47;\n  void *known;\n  &#47;* Name of the service (`files&#39;, `dns&#39;, `nis&#39;, ...).  *&#47;\n  char name&#91;0&#93;;\n} service_user;\n<\/pre>\n\n\n\n<p>What if we can corrupt the name array to an string called &#8220;whatever\/whatever&#8221;?<br><br>It will concat it and will finally be: &#8220;libnss_whatever\/whatever.so&#8221;<br><br>So if we enter a custom library with a constructor popping a shell in that directory with that name we would be able to pop a shell as root.<br><br>As that modified string will be passed to _libc_dlopen().<br><br>The only requirement is:<\/p>\n\n\n\n<pre class=\"prettyprint\">&#47;* Load library.  *&#47;\nstatic int\nnss_load_library (service_user *ni)\n{\n  if (ni-&gt;library == NULL)\n    {\n      &#47;* This service has not yet been used.  Fetch the service\n\t library for it, creating a new one if need be.  If there\n\t is no service table from the file, this static variable\n\t holds the head of the service_library list made from the\n\t default configuration.  *&#47;\n      static name_database default_table;\n      ni-&gt;library = nss_new_service (service_table ?: &amp;default_table,\n\t\t\t\t     ni-&gt;name);\n      if (ni-&gt;library == NULL)\n\treturn -1;\n    }\n\n\n  if (ni-&gt;library-&gt;lib_handle == NULL)\n    {\n      &#47;* Load the shared library.  *&#47;\n      size_t shlen = (7 + strlen (ni-&gt;name) + 3\n\t\t      + strlen (__nss_shlib_revision) + 1);\n      int saved_errno = errno;\n<\/pre>\n\n\n\n<p>To avoid a crash, we would have to make ni-&gt;library NULL so it calls nss_new_service() again and in the if below a crash does not happen when trying to access ni-&gt;library-&gt;lib_handle.<\/p>\n\n\n\n<p><br>Now we know the attack vectors for it, and how to trigger the vulnerability, but we must now try to get a chunk where we want.<br><br>Finally, as the Qualys advisory says, I created a bruteforce script fuzz.py which gave me a bunch of crashes to accelerate the Heap Feng Shui methodology.<br><br>Initial fuzz.py version: https:\/\/github.com\/lockedbyte\/CVE-Exploits\/blob\/master\/CVE-2021-3156\/fuzz.py<br><br>@bl4sty made an improved version of it available here: https:\/\/github.com\/lockedbyte\/CVE-Exploits\/tree\/master\/CVE-2021-3156\/fuzz2<br><br>Finally after some time fuzzing, some interesting crashes appeared, meaning that the chunks gone just where we want to, before interesting pointers that trigger crashes when overwritten.<br><br>One of them was the nss_load_library() one, and the other was the process_hooks_getenv()<br><br>I got a few crashes for both of them and selected the ones that fits better the layout I needed.<\/p>\n\n\n\n<pre class=\"prettyprint\">Program received signal SIGSEGV, Segmentation fault.\n0x0000555555566502 in ?? ()\nrax            0x0                 0\nrbx            0x555555583650      93824992425552\nrcx            0x7                 7\nrdx            0x4242424242424242  4774451407313060418\nrsi            0x7fffffffe770      140737488349040\nrdi            0x7ffff797464d      140737347274317\nrbp            0x7ffff797464d      0x7ffff797464d\nrsp            0x7fffffffe770      0x7fffffffe770\nr8             0x7ffff7f61081      140737353486465\nr9             0x7fffffffe6d0      140737488348880\nr10            0xffffffff          4294967295\nr11            0x202               514\nr12            0x7fffffffe770      140737488349040\nr13            0x7fffffffe7b0      140737488349104\nr14            0x7fffffffe818      140737488349208\nr15            0x2                 2\nrip            0x555555566502      0x555555566502\neflags         0x10206             &#91; PF IF RF &#93;\ncs             0x33                51\nss             0x2b                43\nds             0x0                 0\nes             0x0                 0\nfs             0x0                 0\ngs             0x0                 0\nk0             0x0                 0\nk1             0x0                 0\nk2             0x0                 0\nk3             0x0                 0\nk4             0x0                 0\nk5             0x0                 0\nk6             0x0                 0\nk7             0x0                 0\n=&gt; 0x555555566502:\tcallq  *0x8(%rbx)\n(gdb) i r rbx\nrbx            0x555555583650      93824992425552\n(gdb) x&#47;8x 0x555555583650\n0x555555583650:\t0x42424242\t0x42424242\t0x42424242\t0x42424242\n0x555555583660:\t0x42424242\t0x42424242\t0x42424242\t0x42424242\n(gdb) \n<\/pre>\n\n\n\n<p>As you can see after reproducing one of the crashes, the struct hook contains attacker controlled data, which allows us to corrupt the pointer we need.<\/p>\n\n\n\n<p><br>When I finally calculated the needed offsets successfully, I entered at the end the partial overwrite and debugged a bit through the PoC to see if working:<\/p>\n\n\n\n<pre class=\"prettyprint\">$rax   : 0x0               \n$rbx   : 0x000055607b638b90  \u2192  0x20208a0420002042 (&#34;B &#34;?)\n$rcx   : 0x7               \n$rdx   : 0x0               \n$rsp   : 0x00007ffcb5b1de58  \u2192  0x0000556079797505  \u2192   lea edx, &#91;rax+0x1&#93;\n$rbp   : 0x00007f1f0bbe564d  \u2192  &#34;SUDO_EDITOR&#34;\n$rsi   : 0x00007ffcb5b1de60  \u2192  0x0000000000000000\n$rdi   : 0x00007f1f0bbe564d  \u2192  &#34;SUDO_EDITOR&#34;\n$rip   : 0x7f1f0b008a04    \n$r8    : 0x00007f1f0c1d2081  \u2192  &#34;-&gt; %s @ %s:%d&#34;\n$r9    : 0x00007ffcb5b1ddc0  \u2192  0x0000003000000028 (&#34;(&#34;?)\n$r10   : 0xffffffff        \n$r11   : 0x202             \n$r12   : 0x00007ffcb5b1de60  \u2192  0x0000000000000000\n$r13   : 0x00007ffcb5b1dea0  \u2192  0x0000000000000000\n$r14   : 0x00007ffcb5b1df08  \u2192  0x802a75732b4ae100\n$r15   : 0x4               \n$eflags: &#91;zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification&#93;\n$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n0x00007ffcb5b1de58\u2502+0x0000: 0x0000556079797505  \u2192   lea edx, &#91;rax+0x1&#93; \u2190 $rsp\n0x00007ffcb5b1de60\u2502+0x0008: 0x0000000000000000 \u2190 $rsi, $r12\n0x00007ffcb5b1de68\u2502+0x0010: 0x802a75732b4ae100\n0x00007ffcb5b1de70\u2502+0x0018: 0x0000000000000000\n0x00007ffcb5b1de78\u2502+0x0020: 0x00007ffcb5b1def0  \u2192  0x00007f1f0bbee9e8  \u2192  &#34;..&#47;..&#47;..&#47;plugins&#47;sudoers&#47;rcstr.c&#34;\n0x00007ffcb5b1de80\u2502+0x0028: 0x00007f1f0bbe564d  \u2192  &#34;SUDO_EDITOR&#34;\n0x00007ffcb5b1de88\u2502+0x0030: 0x00007ffcb5b1df98  \u2192  0x0000000000000000\n0x00007ffcb5b1de90\u2502+0x0038: 0x00007ffcb5b1df08  \u2192  0x802a75732b4ae100\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n&#91;!&#93; Cannot disassemble from $PC\n&#91;!&#93; Cannot access memory at address 0x7f1f0b008a04\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n&#91;#0&#93; Id 1, Name: &#34;sudoedit&#34;, stopped 0x7f1f0b008a04 in ?? (), reason: SIGSEGV\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ngef\u27a4\n<\/pre>\n\n\n\n<p><br>As we can see the RIP was partially modified with two arbitrary bytes plus a NULL byte which is appended by the copying function.<br><br>Now we need aproximately 4096 tries to success on the jump to the address we want (execv@PLT).<br><br>As we can see the arguments are fully compatible with the execv ones, we have an string pointed by rdi called &#8220;SUDO_EDITOR&#8221; that coincides with the path variable in the execv:<br><br>$rdi : 0x00007f1f0bbe564d \u2192 &#8220;SUDO_EDITOR&#8221;<br><br>and then, rsi points to a NULL pointer<\/p>\n\n\n\n<p><br>$rsi : 0x00007ffcb5b1de60 \u2192 0x0000000000000000<\/p>\n\n\n\n<p>Those arguments are fully compatible with execv(), which is a good condition to execute a binary called &#8220;SUDO_EDITOR&#8221; (the callback) in the same directory, which will finally be executed as root.<br><br>The second method can be triggered just by corrupting the argument, and overwriting ni->library with a NULL pointer, thus avoiding the crash when trying to access it&#8217;s content once a new service pointer is given by nss_new_service().<\/p>\n\n\n\n<p><br>You can find the exploits for both of the explained methods here: <a href=\"https:\/\/github.com\/lockedbyte\/CVE-Exploits\/tree\/master\/CVE-2021-3156\">here.<\/a><br><br>Also, I did a small speech about the vulnerability and the exploitation, you can find the slides <a href=\"https:\/\/github.com\/lockedbyte\/slides\/blob\/main\/Exploiting%20sudo%20CVE-2021-3156_%20%20From%20heap-based%20overflow%20to%20LPE_EoP.pdf\">here.<\/a><br><\/p>\n","protected":false},"excerpt":{"rendered":"<p>On 26th of January, a new sudo vulnerability came out reported by Qualys (Baron Samedit). The advisory is available here. The vulnerability is present in the sudo code for 10 years, which attracts a lot, as a ton sudo versions are affected.<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[7],"tags":[],"_links":{"self":[{"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/200"}],"collection":[{"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=200"}],"version-history":[{"count":38,"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/200\/revisions"}],"predecessor-version":[{"id":241,"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/200\/revisions\/241"}],"wp:attachment":[{"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=200"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=200"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lockedbyte.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=200"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}