How we broke PHP, hacked Pornhub and earned $20,000

How we broke PHP, hacked Pornhub and earned $20,000

Original text by Ruslan Habalov

It all started by auditing Pornhub, then PHP and ended in breaking both…


  • We have gained remote code execution on and have earned a $20,000 bug bounty on Hackerone.
  • We have found two use-after-free vulnerabilities in PHP’s garbage collection algorithm.
  • Those vulnerabilities were remotely exploitable over PHP’s unserialize function.
  • We were also awarded with $2,000 by the Internet Bug Bounty committee (c.f. Hackerone).


This project was realized by Dario Weißer (@haxonaut), cutz and Ruslan Habalov (@evonide).
Many thanks go out to cutz for co-authoring this article.

Pornhub’s bug bounty program and its relatively high rewards on Hackerone caught our attention. That’s why we have taken the perspective of an advanced attacker with the full intent to get as deep as possible into the system, focusing on one main goal: gaining remote code execution capabilities. Thus, we left no stone unturned and attacked what Pornhub is built upon: PHP.

Bug discovery

After analyzing the platform we quickly detected the usage of unserialize on the website. Multiple paths (everywhere where you could upload hot pictures and so on) were affected for example:


In all cases a parameter named “cookie” got unserialized from POST data and afterwards reflected via Set-Cookie headers. Example Request:

Bug discovery

After analyzing the platform we quickly detected the usage of unserialize on the website. Multiple paths (everywhere where you could upload hot pictures and so on) were affected for example:
In all cases a parameter named “cookie” got unserialized from POST data and afterwards reflected via Set-Cookie headers. Example Request:

This could be further verified by sending a specially crafted array that contained an object:


Response layout:

0=exception 'Exception' in /path/to/a/file.php:1337
 Stack trace:
 #0 /path/to/a/file.php(1337): unserialize('a:1:{i:0;O:9:"E...')
 #1 {main}

This might strike as a harmless information disclosure at first sight, but generally it is known that using user input on unserialize is a bad idea:

Standard exploitation techniques require so called Property-Oriented-Programming (POP) that involve abusing already existing classes with specifically defined “magic methods” in order to trigger unwanted and malicious code paths. Unfortunately, it was difficult for us to gather any information about Pornhub’s used frameworks and PHP objects in general. Multiple classes from common frameworks have been tested — all without success.

Bug description

The core unserializer alone is relatively complex as it involves more than 1200 lines of code in PHP 5.6. Further, many internal PHP classes have their own unserialize methods. By supporting structures like objects, arrays, integers, strings or even references it is no surprise that PHP’s track record shows a tendency for bugs and memory corruption vulnerabilities. Sadly, there were no known vulnerabilities of such type for newer PHP versions like PHP 5.6 or PHP 7, especially because unserialize already got a lot of attention in the past (e.g. phpcodz). Hence, auditing it can be compared to squeezing an already tightly squeezed lemon. Finally, after so much attention and so many security fixes its vulnerability potential should have been drained out and it should be secure, shouldn’t it?

Fuzzing unserialize

To find an answer Dario implemented a fuzzer crafted specifically for fuzzing serialized strings which were passed to unserialize. Running the fuzzer with PHP 7 immediately lead to unexpected behavior. This behavior was not reproducible when tested against Pornhub’s server though. Thus, we assumed a PHP 5 version.

However, running the fuzzer against a newer version of PHP 5 just generated more than 1 TB of logs without any success. Eventually, after putting more and more effort into fuzzing we’ve stumbled upon unexpected behavior again. Several questions had to be answered: is the issue security related? If so can we only exploit it locally or also remotely? To further complicate this situation the fuzzer did generate non-printable data blobs with sizes of more than 200 KB.

Analyzing unexpected behavior

A tremendous amount of time was necessary to analyze potential issues. After all, we could extract a concise proof of concept of a working memory corruption bug — a so called use-after-free vulnerability! Upon further investigation we discovered that the root cause could be found in PHP’s garbage collection algorithm, a component of PHP that is completely unrelated to unserialize. However, the interaction of both components occurred only after unserialize had finished its job. Consequently, it was not well suited for remote exploitation. After further analysis, gaining a deeper understanding for the problem’s root causes and a lot of hard work a similar use-after-free vulnerability was found that seemed to be promising for remote exploitation.

Vulnerability links:

The high sophistication of the found PHP bugs and their discovery made it necessary to write separate articles. You can read more details in Dario’s fuzzing unserialize write-up.

In addition, we have written an article about Breaking PHP’s Garbage Collection and Unserialize.


Even this promising use-after-free vulnerability was considerably difficult to exploit. In particular, it involved multiple exploitation stages.
Since our main goal was to execute arbitrary code we needed to somehow compromise the CPU’s instruction pointer referred to as RIP on x86_64. This usually involves the following obstacles:

  1. The stack and heap (which also include any potential user-input) as well as any other writable segments are flagged non-executable (c.f. Executable space protection).
  2. Even if you are able to control the instruction pointer you need to know what you want to execute i.e. you need to have a valid address of an executable memory segment. For this it is common to call the libc function system which will execute a shell command. In PHP context it is often enough to execute zend_eval_string which usually gets executed e.g. when you write “eval(‘echo 1337;’);” in a PHP script i.e. it allows us to execute arbitrary PHP code without having to transition into other involved libraries.

The first problem can be overcome by using Return-oriented programming (ROP) where you can utilize already existing and executable memory fragments from the binary itself or its libraries. The second problem, however, requires to find the correct address of zend_eval_string. Usually, when a dynamically linked program gets executed the loader will map the process to 0x400000 which is the standard load address on x86_64. In case you somehow already obtained the correct PHP executable (e.g. by finding the exact package that is shipped by the target) you can just locally lookup the offset for any function you wantWe discovered that Pornhub was using a customly compiled version of php5-cgi, therefore making it difficult to determine the exact PHP version as well as getting any information at all about the memory layout of the whole PHP process.

Leaking the PHP binary and required pointers

Exploiting use-after-frees in PHP usually follows the same rules. As soon as you’re able to fill freed memory that later on gets reused as an internal PHP variable — so called zvals — you can generate vectors that allow reading from arbitrary memory as well as triggering code execution.

Preparing the memory disclosure

As previously mentioned we were required to obtain more information about Pornhub’s PHP binary. Therefore, the first step was to abuse the use-after-free to inject a zval that represents a PHP string. The definition of the zval structure looks like the following for PHP 5.6:

struct _zval_struct {
    zvalue_value value;       /* value */
    zend_uint refcount__gc;
    zend_uchar type;          /* active type */
    zend_uchar is_ref__gc;

Whereas the zvalue_value field is defined as a union, hence making type juggling (and type confusions) easily possible.

typedef union _zvalue_value {
    long lval;          /* long value */
    double dval;        /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;      /* hash table value */
    zend_object_value obj;
    zend_ast *ast;

A PHP variable of type string is a zval of type 6. Consequently, it treats the union as a structure that contains a char pointer and a length field. So crafting a string zval with an arbitrary starting point and arbitrary length creates a powerful infoleak that gets triggered when Pornhub’s setcookie() reflects the injected zval in the response header.

Finding PHP’s image base

Usually, one can start by leaking the binary, which as stated before, begins at 0x400000. Unfortunately, Pornhub’s server used protection mechanisms like PIE and ASLR which randomize the image base of the process and its shared libraries. This also has become the default as more and more distributions ship packages that enable position independent code.

The next challenge was on: finding the correct loading address of the binary.

The first difficulty was to somehow obtain a single valid address we could start leaking from. Here it was helpful to know some details about PHP’s internal memory management. In particular, once a zval is freed PHP will overwrite its first eight bytes with an address to the previously freed chunk. Hence, a trick to obtain a first valid address is to create an integer zval, free this integer zval and finally use a dangling pointer to this zval to obtain its current value.

Since php-cgi implements multiple workers that simply get forked from a master process, the memory layout never really changes between different requests, as long as you keep sending data of the same size. That’s also why we could send request after request, each time leaking a different portion of memory by letting the fake zval string begin at different addresses. However, obtaining the heap address of a freed chunk is by its own right not enough to get any clues about the executable location. This is due to a lack of any useful information in the surroundings of that chunk.

To get interesting addresses, there is a relatively complicated technique which requires multiple frees and allocations of PHP structures during the unserialization process (c.f. ROP in PHP applications Slide 67). Due to the nature of our bug and to keep the complexity as low as possible we have used our own trick.

By using a serialized string like “i:0;a:0:{}i:0;a:0:{}[…]i:0;a:0:{}” as part of our overall unserialize payload we could force unserialize to create many empty arrays and free them once it terminated. When initializing an array PHP consecutively allocates memory for its zval and hashtable. One default hashtable entry for empty arrays is the uninitialized_bucket symbol. Overall, we were able to obtain a memory fragment that looked similar to the following:

0x7ffff7fc2fe0: 0x0000000000000000 0x0000000000eae040
0x7ffff7fc3010: 0x00007ffff7fc2b40 0x0000000000000000
0x7ffff7fc3020: 0x0000000100000000 0x0000000000000000
0x7ffff7fc3030: # <--------- This address was leaked in a previous request.
0x7ffff7fc3040: 0x00007ffff7fc2f48 0x0000000000000000
0x7ffff7fc3050: 0x0000000000000000 0x0000000000000000
0x7ffff7fc30a0: 0x0000000000eae040 0x00000000006d5820
(gdb) x/xg 0x0000000000eae040
0xeae040 <uninitialized_bucket>: 0x0000000000000000

The address 0xeae040 is PHP’s uninitialized_bucket symbol address and directly points into PHP’s BSS segment. You can see that it occurs multiple times in the neighborhood of the lastly freed chunk. As stated before, many empty arrays were freed. Thus, by abusing the circumstance that some hashtable entries remained unchanged in the heap we were able to leak this specific symbol.

Finally, we could apply a page-wise backwards scan starting from the uninitialized_bucket symbol address to find the ELF header:

$start &= 0xfffffffffffff000;
$pages += 0x1000 while leak($start - $pages, 4) !~ /^\x7fELF/;
return $start - $pages;
Leaking interesting PHP binary segments

At this point our situation further complicated things as we were only able to leak 1 KB of data per request (this is due to enforced header size limitations by Pornhub’s web server). A PHP binary can take up to about 30 MB of size. Assuming one request per second the leaking would have taken about 8 hours and 20 minutes to complete. As we were afraid that our exploitation process could get interrupted at any time it was essential to act as fast and as stealthy as possible. This is why we were required to implement some heuristics to guess/filter likely interesting sections in advance. Nevertheless, we could resolve any structure that was referenced in the ELF’s string and symbol table. There are other techniques like ret2dlresolve that allow omitting the whole leaking process, but they weren’t entirely applicable here since they require crafting more data structures and require knowledge about different memory locations.

To get the address of zend_eval_string you’d first have to find the ELF program headers which are at offset 32, then scan forward until you find a program header entry of type 2 (PT_DYNAMIC) to get the ELF’s dynamic section. This section finally contains a reference to the string and symbol table (type 5 and 6) which you can completely dump by using their size fields and grab any function whose virtual address you desire. Alternatively, you can also use the hashtable (DT_HASH) to find functions more quickly, but in this scenario it doesn’t matter much since you can quickly traverse the tables locally anyway. In addition to zend_eval_stringwe were interested in further symbols and the location of our POST variables (because they were supposed to be used as a ROP stack later on).

Leaking the address of our POST data

To get the address of the supplied POST data you can just leak some more pointers by reading from:

(*(*(php_stream_temp_data *)(sapi_globals.request_info.request_body.abstract)).innerstream).readbuf

Traversing this chain looks complicated, but you just need to dereference a few pointers with the correct offset and you’ll quickly find the stdin:// stream which points to the POST data inside the heap.

Preparing the ROP payload

The second part deals with actually taking control over the PHP process and gaining code execution. For this to happen we need to discuss how one can modify the instruction pointer first.

Taking over the instruction pointer

We adjusted our payload to contain a fake object (instead of the previously used string zval) with a pointer to a specially crafted zend_object_handlers table. This table is, in its essence, an array of function pointers whose structure definition can be found in:

struct _zend_object_handlers {
    zend_object_add_ref_t add_ref;

When creating such a faked zend_object_handlers table we can simply setup add_ref however we prefer. The function behind this pointer usually handles the incrementation of the object’s reference counter. Once our created fake object gets passed as a parameter to “setcookie” the following things happen:

#0  _zval_copy_ctor
#1  0x0000000000881d01 in parse_arg_object_to_string
#5  0x00000000008845ca in zend_parse_parameters (num_args=2, type_spec=0xd24e46 "s|slssbb")
#6  0x0000000000748ad5 in zif_setcookie
#14 0x000000000093e492 in main

Here, according to “s|sl[…]” one can see that “setcookie” is expecting a string as its first and second parameter (| marks the start of optional parameters). Hence, it will try to cast our object which is passed as the second parameter into a string. Finally, _zval_copy_ctor will then execute:

ZEND_API void _zval_copy_ctor_func(zval *zvalue ZEND_FILE_LINE_DC)
        case IS_OBJECT:
                Z_OBJ_HT_P(zvalue)->add_ref(zvalue TSRMLS_CC);

Here, according to “s|sl[…]” one can see that “setcookie” is expecting a string as its first and second parameter (| marks the start of optional parameters). Hence, it will try to cast our object which is passed as the second parameter into a string. Finally, _zval_copy_ctor will then execute:

ZEND_API void _zval_copy_ctor_func(zval *zvalue ZEND_FILE_LINE_DC)
        case IS_OBJECT:
                Z_OBJ_HT_P(zvalue)->add_ref(zvalue TSRMLS_CC);

In particular, this will make a call to the provided add_ref function with the address of our object as a parameter (c.f. PHP Internals Book – Copying zvals to see an explanation). The corresponding assembly looks like:

<_zval_copy_ctor_func+288>: mov    0x8(%rdi),%rax
<_zval_copy_ctor_func+292>: callq  *(%rax)

Here, RDI is the first argument to the _zval_copy_ctor_func  function which also is the address of our fake object zval (zvalue in the source code above). As previously seen in the definition of the  _zvalue_valuetypedef, an object contains an element called obj of type zend_object_value which is defined as follows:

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;

Thus, 0x8(%rdi) will point to the second entry in  _zend_object_value which corresponds to the address of our first zend_object_handlers entry. As mentioned before, this entry is our custom add_ref function and explains why we have direct control over RAX, too.

To bypass the previously discussed non-executable memory problem we had to obtain further information. In particular, we needed to collect useful gadgets and prepare stack pivoting for our ROP chain since there wasn’t enough control over the stack yet.

Leaking ROP gadgets

Now we could setup the add_ref pointer, or RAX respectively, to take over the instruction pointer. Although this gives you a starting point it doesn’t ensure that all of your provided ROP gadgets are executed because the CPU will pop the next instruction’s address from the current stack once returning from the first gadget. We don’t have any control over this stack, so consequently, it was necessary to pivot the stack into our ROP chain. This is why the next step was to copy RAX into RSP and continue ropping from there. Using a locally compiled version of PHP we scanned for good candidates for stack pivoting gadgets and found that php_stream_bucket_split contained the following piece of code:

<php_stream_bucket_split+381>: push %rax    # <------------
<php_stream_bucket_split+382>: sub $0x31,%al
<php_stream_bucket_split+384>: rcrb $0x41,0x5d(%rbx)
<php_stream_bucket_split+388>: pop %rsp     # <------------
<php_stream_bucket_split+389>: pop %r13
<php_stream_bucket_split+391>: pop %r14
<php_stream_bucket_split+393>: retq

This was used to nicely modify RSP to point to our by POST data provided ROP chain, effectively chaining all provided gadget calls.

According to the x86_64 calling convention the first two parameters of a function are RDI and RSI, so we had to find a pop %rdi and pop %rsi gadget, tooThose are pretty common and thus easily found. However, we still had no idea if those gadgets actually existed on Pornhub’s version of PHP. Therefore, we had to manually verify their presence.

Verifying the presence of the required ROP gadgets

The infoleak vector allowed us to quickly dump the disassembly of php_stream_bucket_split and check if our stack pivoting gadget was available on the remote version. Fortunately, only little corrections of the gadgets’ offsets were necessary. Finally, we implemented some checks to confirm that all addresses were correct:

my $pivot  = leak($php_base + 0x51a71f, 13);
my $poprdi = leak($php_base + 0x2b904e, 2);
my $poprsi = leak($php_base + 0x50ee0c, 2);
die '[!] pivot gadget doesnt seem to be right', $/
    unless ($pivot eq "\x50\x2c\x31\xc0\x5b\x5d\x41\x5c\x41\x5d\x41\x5e\xc3");
die '[!] poprdi gadget doesnt seem to be right', $/
    unless ($poprdi eq "\x5f\xc3");
die '[!] poprsi gadget doesnt seem to be right', $/
    unless ($poprsi eq "\x5e\xc3");
Crafting the ROP stack

The final ROP payload that effectively executed zend_eval_string(code); exit(0); looked like the following snippet:

my $rop = "";
$rop .= pack('Q', $php_base + 0x51a71f);              # pivot rsp
$rop .= pack('Q', 0xdeadbeef);                        # junk
$rop .= pack('Q', $php_base + 0x2b904e);              # pop rdi
$rop .= pack('Q', $post_addr + length($rop) + 8 * 7); # pointing to $php_code
$rop .= pack('Q', $php_base + 0x50ee0c);              # pop rsi
$rop .= pack('Q', 0);                                 # retval_ptr
$rop .= pack('Q', $zend_eval_string);                 # zend_eval_string
$rop .= pack('Q', $php_base + 0x2b904e);              # pop rdi
$rop .= pack('Q', 0);                                 # exit code
$rop .= pack('Q', $exit);                             # exit
$rop .= $php_code . "\x00";

Because the stack pivot contained a pop %r13 and pop %r14 the 0xdeadbeef padding inside the remaining chain was necessary to continue with setting RDI. As the first parameter to zend_eval_string RDI is required to reference the code that is to be executed. This code is located right after the ROP chain. It was also required to keep sending the exact same amount of data between each request so that all calculated offsets stayed correct. This was achieved by setting up different paddings wherever it was necessary.

The next step was to finally trigger code execution by returning back into the PHP interpreter. Actually, other techniques like return2libc are quite applicable as well but create a few other problems that are easier dealt with when staying in PHP context.

Returning into PHP

Being able to execute arbitrary PHP code is an important step, but being able to view its output is equally important, unless one wants to deal with side channels to receive responses. So the remaining tricky part was to somehow display the result on Pornhub’s website.

Clean termination of PHP

Usually php-cgi forwards the generated content back to the web server so that it’s displayed on the website, but wrecking the control flow that badly creates an abnormal termination of PHP so that its result will never reach the HTTP server. To get around this problem we simply told PHP to use direct unbuffered responses that are usually used for HTTP streaming:

my $php_code = 'eval(\'
    header("X-Accel-Buffering: no");
    header("Content-Encoding: none");
    header("Connection: close");
    echo file_get_contents("/etc/passwd");

This finally allowed us to directly fetch every output the PHP payload generated without having to worry about the cleanup routines that are usually involved when the CGI process sends data to the web server. This further increased the stealthiness factor by minimizing the number of potential errors and crashes.

To summarize, our payload contained a fake object with its add_ref function pointer pointing to our first ROP gadget. The following diagram visualizes this concept:

Together with our ROP stack which was provided over POST data our payload did the following things:

  1. Created our fake object which was later on passed as a parameter to “setcookie”.
  2. This caused a call to the provided add_ref function i.e. it allowed us to gain program counter control.
  3. Our ROP chain then prepared all registers/parameters as discussed.
  4. Next, we were able to execute arbitrary PHP code by making a call to zend_eval_string.
  5. Finally, we caused a clean process termination while also fetching the output from the response body.

Once running the above code we were in and got a nice view of Pornhub’s ‘/etc/passwd’ file. Due to the nature of our attack we would have also been able to execute other commands or actually break out of PHP to run arbitrary syscalls. However, just using PHP was more convenient at this point. Finally, we dumped a few details about the underlying system and immediately wrote and submitted a report to Pornhub over Hackerone.


Here is the timeline of the disclosure process:

  • 2016-05-30 Hacked Pornhub and submitted the issue over Hackerone. Hours later Pornhub quickly fixed the issue by removing calls to unserialize
  • 2016-06-14 Received a reward of $20,000
  • 2016-06-16 Submitted issues to
  • 2016-06-21 Both bugs got fixed in PHP’s security repository
  • 2016-06-27 Received Hackerone IBB reward of $2,000 ($1,000 for each vulnerability)
  • 2016-07-22 Pornhub resolved the issue on Hackerone


We gained remote code execution and would’ve been able to do the following things:

  • Dump the complete database of including all sensitive user information.
  • Track and observe user behavior on the platform.
  • Leak the complete available source code of all sites hosted on the server.
  • Escalate further into the network or root the system.

Of course none of the above things were done and very careful attention was paid to respect the scope and limitations of the bug bounty program.
Further, we were able to find two zero day vulnerabilities in PHP’s garbage collection algorithm. Those vulnerabilities, although being in a very different PHP context, could be reliably and remotely exploited in an unserialize context, too.

It is well-known that using user input on unserialize is a bad idea. In particular, about 10 years have passed since its first weaknesses have become apparent. Unfortunately, even today, many developers seem to believe that unserialize is only dangerous in old PHP versions or when combined with unsafe classes. We sincerely hope to have destroyed this misbelief. Please finally put a nail into unserialize’s coffin so that the following mantra becomes obsolete.

You should never use user input on unserialize. Assuming that using an up-to-date PHP version is enough to protect unserialize in such scenarios is a bad idea. Avoid it or use less complex serialization methods like JSON.

The newest PHP versions contain fixes by now. Hence, you should update your PHP 5 and PHP 7 versions accordingly.

Many thanks to the Pornhub team for:

  • Very polite and competent responses.
  • Actually caring about security (and not just pretending like many other companies do nowadays).
  • Being very generous regarding the bounty of $20,000.
    According to Sinthetic Labs’s Public Hackerone Reports last update we are grateful to see that this submission seems to be heads on with the ShellShock vulnerability submission for being one of the highest paid public bounties on Hackerone so far.

Further, many thanks go out to the PHP developers for quickly deploying the fix and the Internet Bug Bounty committee for awarding us with $2,000.

Finally, we want to highlight the necessity of such programs. As you can see, offering high bug bounties can motivate security researchers to find bugs in underlying software. This positively impacts other sites and unrelated services as well.

Please don’t forget to checkout our two other write-ups regarding the PHP bugs and their discovery.

Introducing RPC Investigator

Introducing RPC Investigator

Original text by Aaron LeMasters

Trail of Bits is releasing a new tool for exploring RPC clients and servers on Windows. RPC Investigator is a .NET application that builds on the NtApiDotNet platform for enumerating, decompiling/parsing and communicating with arbitrary RPC servers. We’ve added visualization and additional features that offer a new way to explore RPC.

RPC is an important communication mechanism in Windows, not only because of the flexibility and convenience it provides software developers but also because of the renowned attack surface its implementers afford to exploit developers. While there has been extensive research published related to RPC servers, interfaces, and protocols, we feel there’s always room for additional tooling to make it easier for security practitioners to explore and understand this prolific communication technology.

Below, we’ll cover some of the background research in this space, describe the features of RPC Investigator in more detail, and discuss future tool development.

If you prefer to go straight to the code, check out RPC Investigator on Github.


Microsoft Remote Procedure Call (MSRPC) is a prevalent communication mechanism that provides an extensible framework for defining server/client interfaces. MSRPC is involved on some level in nearly every activity that you can take on a Windows system, from logging in to your laptop to opening a file. For this reason alone, it has been a popular research target in both the defensive and offensive infosec communities for decades.

A few years ago, the developer of the open source .NET library NtApiDotNet, James Foreshaw, updated his library with functionality for decompiling, constructing clients for, and interacting with arbitrary RPC servers. In an excellent blog post—focusing on using the new 

 functionality via powershell scripts and cmdlets in his 
 package—he included a small section on how to use the powershell scripts to generate C# code for an RPC client that would work with a given RPC server and then compile that code into a C# application.

We built on this concept in developing RPC Investigator (RPCI), a .NET/C# Windows Forms UI application that provides a visual interface into the existing core RPC capabilities of the 


  • Enumerating all active ALPC RPC servers
  • Parsing RPC servers from any PE file
  • Parsing RPC servers from processes and their loaded modules, including services
  • Integration of symbol servers
  • Exporting server definitions as serialized .NET objects for your own scripting

Beyond visualizing these core features, RPCI provides additional capabilities:

  • The Client Workbench allows you to create and execute an RPC client binary on the fly by right-clicking on an RPC server of interest. The workbench has a C# code editor pane that allows you to edit the client in real time and observe results from RPC procedures executed in your code.
  • Discovered RPC servers are organized into a library with a customizable search interface, allowing you to pivot RPC server data in useful ways, such as by searching through all RPC procedures for all servers for interesting routines.
  • The RPC Sniffer tool adds visibility into RPC-related Event Tracing for Windows (ETW) data to provide a near real-time view of active RPC calls. By combining ETW data with RPC server data from 
    , we can build a more complete picture of ongoing RPC activity.


Disclaimer: Please exercise caution whenever interacting with system services. It is possible to corrupt the system state or cause a system crash if RPCI is not used correctly.

Prerequisites and System Requirements

Currently, RPCI requires the following:

By default, RPCI will automatically discover the Debugging Tools for Windows installation directory and configure itself to use the public Windows symbol server. You can modify these settings by clicking 

Edit -&gt; Settings
. In the Settings dialog, you can specify the path to the debugging tools DLL (dbghelp.dll) and customize the symbol server and local symbol directory if needed (for example, you can specify the path 

If you want to observe the debug output that is written to the RPCI log, set the appropriate trace level in the Settings window. The RPCI log and all other related files are written to the current user’s application data folder, which is typically 

. To view this folder, simply navigate to 
View -&gt; Logs
. However, we recommend disabling tracing to improve performance.

It’s important to note that the bitness of RPCI must match that of the system: if you run 32-bit RPCI on a 64-bit system, only RPC servers hosted in 32-bit processes or binaries will be accessible (which is most likely none).

Searching for RPC servers

The first thing you’ll want to do is find the RPC servers that are running on your system. The most straightforward way to do this is to query the RPC endpoint mapper, a persistent service provided by the operating system. Because most local RPC servers are actually ALPC servers, this query is exposed via the 

File -> All RPC ALPC Servers…
 menu item.

The discovered servers are listed in a table view according to the hosting process, as shown in the screenshot above. This table view is one starting point for navigating RPC servers in RPCI. Double-clicking a particular server will open another tab that lists all endpoints and their corresponding interface IDs. Double-clicking an endpoint will open another tab that lists all procedures that can be invoked on that endpoint’s interface. Right-clicking on an endpoint will open a context menu that presents other useful shortcuts, one of which is to create a new client to connect to this endpoint’s interface. We’ll describe that feature in a later section.

You can locate other RPC servers that are not running (or are not ALPC) by parsing the server’s image by selecting 

File -&gt; Load from binary…
 and locating the image on disk, or by selecting 
File-&gt;Load from service…
 and selecting the service of interest (this will parse all servers in all modules loaded in the service process).

Exploring the Library

The other starting point for navigating RPC servers is to load the library view. The library is a file containing serialized .NET objects for every RPC server you have discovered while using RPCI. Simply select the menu item 

Library -> Servers
 to view all discovered RPC servers and 
Library -> Procedures
 to view all discovered procedures for all server interfaces. Both menu items will open in new tabs. To perform a quick keyword search in either tab, simply right-click on any row and type a search term into the textbox. The screenshot below shows a keyword search for “()” to quickly view procedures that have zero arguments, which are useful starting points for experimenting with an interface.

The first time you run RPCI, the library needs to be seeded. To do this, navigate to 

Library -&gt; Refresh
, and RPCI will attempt to parse RPC servers from all modules loaded in all processes that have a registered ALPC server. Note that this process could take quite a while and use several hundred megabytes of memory; this is because there are thousands of such modules, and during this process the binaries are re-mapped into memory and the public Microsoft symbol server is consulted. To make matters worse, the Dbghelp API is single-threaded and I suspect Microsoft’s public symbol server has rate-limiting logic.

You can periodically refresh the database to capture any new servers. The refresh operation will only add newly-discovered servers. If you need to rebuild the library from scratch (for example, because your symbols were wrong), you can either erase it using the menu item 

Library -&gt; Erase
 or manually delete the database file (
) inside the current user’s roaming application data folder. Note that RPC servers that are discovered by using the 
File -&gt; Load from binary…
File -&gt; Load from service…
 menu items are automatically added to the library.

You can also export the entire library as text by selecting 

Library -&gt; Export as Text

Creating a New RPC Client

One of the most powerful features of RPCI is the ability to dynamically interact with an RPC server of interest that is actively running. This is accomplished by creating a new client in the Client Workbench window. To open the Client Workbench window, right-click on the server of interest from the library servers or procedures tab and select 

New Client

The workbench window is organized into three panes:

  • Static RPC server information
  • A textbox containing dynamic client output
  • A tab control containing client code and procedures tabs

The client code tab contains C# source code for the RPC client that was generated by 

. The code has been modified to include a “Run” function, which is the “entry point” for the client. The procedures tab is a shortcut reference to the routines that are available in the selected RPC server interface, as the source code can be cumbersome to browse (something we are working to improve!).

The process for generating and running the client is simple:

  • Modify the “Run” function to call one or more of the procedures exposed on the RPC server interface; you can print the result if needed.
  • Click the “Run” button.
  • Observe any output produced by “Run”

In the screenshot above, I picked the “Host Network Service” RPC server because it exposes some procedures whose names imply interesting administrator capabilities. With a few function calls to the RPC endpoint, I was able to interact with the service to dump the name of what appears to be a default virtual network related to Azure container isolation.

Sniffing RPC Traffic with ETW Data

Another useful feature of RPCI is that it provides visibility into RPC-related ETW data. ETW is a diagnostic capability built into the operating system. Many years ago ETW was very rudimentary, but since the Endpoint Detection and Response (EDR) market exploded in the last decade, Microsoft has evolved ETW into an extremely rich source of information about what’s going on in the system. The gist of how ETW works is that an ETW provider (typically a service or an operating system component) emits well-structured data in “event” packets and an application can consume those events to diagnose performance issues.

RPCI registers as a consumer of such events from the Microsoft-RPC (MSRPC) ETW provider and displays those events in real time in either table or graph format. To start the RPC Sniffer tool, navigate to 

Tools -&gt; RPC Sniffer…
 and click the “play” button in the toolbar. Both the table and graph will be updated every few seconds as events begin to arrive.

The events emitted by the MSRPC provider are fairly simple. The events record the results of RPC calls between a client and server in RpcClientCall and RpcServerCall start and stop task pairs. The start events contain detailed information about the RPC server interface, such as the protocol, procedure number, options, and authentication used in the call. The stop events are typically less interesting but do include a status code. By correlating the call start/stop events between a particular RPC server and the requesting process, we can begin to make sense of the operations that are in progress on the system. In the table view, it’s easier to see these event pairs when the ETW data is grouped by ActivityId (click the “Group” button in the toolbar), as shown below.

The data can be overwhelming, because ETW is fairly noisy by design, but the graph view can help you wade through the noise. To use the graph view, simply click the “Node” button in the toolbar at any time during the trace. To switch back to the table view, click the “Node” button again.

A long-running trace will produce a busy graph like the one above. You can pan, zoom, and change the graph layout type to help drill into interesting server activity. We are exploring additional ways to improve this visualization!

In the zoomed-in screenshot above, we can see individual service processes that are interacting with system services such as Base Filtering Engine (BFE, the Windows Defender firewall service), NSI, and LSASS.

Here are some other helpful tips to keep in mind when using the RPC Sniffer tool:

  • Keep RPCI diagnostic tracing disabled in Settings.
  • Do not enable ETW debug events; these produce a lot of noise and can exhaust process memory after a few minutes.
  • For optimum performance, use a release build of RPCI.
  • Consider docking the main window adjacent to the sniffer window so that you can navigate between ETW data and library data (right-click on a table row and select 
    Open in library
     or click on any RPC node while in the graph view).
  • Remember that the graph view will refresh every few seconds, which might cause you to lose your place if you are zooming and panning. The best use of the graph view is to take a capture for a fixed time window and explore the graph after the capture has been stopped.

What’s Next?

We plan to accomplish the following as we continue developing RPCI:

  • Improve the code editor in the Client Workbench
  • Improve the autogeneration of names so that they are more intuitive
  • Introduce more developer-friendly coding features
  • Improve the coverage of RPC/ALPC servers that are not registered with the endpoint mapper
  • Introduce an automated ALPC port connector/scanner
  • Improve the search experience
  • Extend the graph view to be more interactive

Related Research and Further Reading

Because MSRPC has been a popular research topic for well over a decade, there are too many related resources and research efforts to name here. We’ve listed a few below that we encountered while building this tool:

If you would like to see the source code for other related RPC tools, we’ve listed a few below:

If you’re unfamiliar with RPC internals or need a technical refresher, we recommend checking out one of the authoritative sources on the topic, Alex Ionescu’s 2014 SyScan talk in Singapore, “All about the RPC, LRPC, ALPC, and LPC in your PC.”

Patch Tuesday -> Exploit Wednesday: Pwning Windows Ancillary Function Driver for WinSock (afd.sys) in 24 Hours

Patch Tuesday -> Exploit Wednesday: Pwning Windows Ancillary Function Driver for WinSock (afd.sys) in 24 Hours

Original text by By Valentina Palmiotti co-authored by Ruben Boonen

‘Patch Tuesday, Exploit Wednesday’ is an old hacker adage that refers to the weaponization of vulnerabilities the day after monthly security patches become publicly available. As security improves and exploit mitigations become more sophisticated, the amount of research and development required to craft a weaponized exploit has increased. This is especially relevant for memory corruption vulnerabilities.

However, with the addition of new features (and memory-unsafe C code) in the Windows 11 kernel, ripe new attack surfaces can be introduced. By honing in on this newly introduced code, we demonstrate that vulnerabilities that can be trivially weaponized still occur frequently. In this blog post, we analyze and exploit a vulnerability in the Windows Ancillary Function Driver for Winsock, 

, for Local Privilege Escalation (LPE) on Windows 11. Though neither of us had any previous experience with this kernel module, we were able to diagnose, reproduce, and weaponize the vulnerability in about a day. You can find the exploit code here.

Patch Diff and Root Cause Analysis

Based on the details of CVE-2023-21768 published by the Microsoft Security Response Center (MSRC), the vulnerability exists within the Ancillary Function Driver (AFD), whose binary filename is 

. The AFD module is the kernel entry point for the Winsock API. Using this information, we analyzed the driver version from December 2022 and compared it to the version newly released in January 2023. These samples can be obtained individually from Winbindex without the time-consuming process of extracting changes from Microsoft patches. The two versions analyzed are shown below.

  • AFD.sys / Windows 11 22H2 / 10.0.22621.608 (December 2022)
  • AFD.sys / Windows 11 22H2 / 10.0.22621.1105 (January 2023)

Ghidra was used to create binary exports for both of these files so they could be compared in BinDiff. An overview of the matched functions is shown below.

Figure 2 — Binary comparison of AFD.sys

Only one function appeared to have been changed, 

. This significantly sped up our analysis of the vulnerability. We then compared both of the functions. The screenshots below show the changed code pre- and post-patch when looking at the decompiled code in Binary Ninja.


afd.sys version 10.0.22621.608

Figure 3 — afd!AfdNotifyRemoveIoCompletion pre-patch


afd.sys version 10.0.22621.1105

Figure 4 — afd!AfdNotifyRemoveIoCompletion post-patch

This change shown above is the only update to the identified function. Some quick analysis showed that a check is being performed based on 

<a href="" target="_blank" rel="noreferrer noopener">PreviousMode</a>
. If 
 is zero (indicating that the call originates from the kernel) a value is written to a pointer specified by a field in an unknown structure. If, on the other hand, 
 is not zero then 
<a href="" target="_blank" rel="noreferrer noopener">ProbeForWrite</a>
 is called to ensure that the pointer set out in the field is a valid address that resides within user mode.

This check is missing in the pre-patch version of the driver. Since the function has a specific switch statement for 

, the assumption is that the developer intended to add this check but forgot (we all lack coffee sometimes !).

From this update, we can infer that an attacker can reach this code path with a controlled value at 

 of the unknown structure. If an attacker is able to populate this field with a kernel address, then it’s possible to create an arbitrary kernel Write-Where primitive. At this point, it is not clear what value is being written, but any value could potentially be used for a Local Privilege Escalation primitive.

The function prototype itself contains both the 

 value and a pointer to the unknown structure as the first and third arguments respectively.

Figure 5 — afd!AfdNotifyRemoveIoCompletion function prototype

Reverse Engineering

We now know the location of the vulnerability, but not how to trigger the execution of the vulnerable code path. We’ll do some reverse engineering before beginning to work on a Proof-of-Concept (PoC).

First, the vulnerable function was cross-referenced to understand where and how it was used.

Figure 6 — afd!AfdNotifyRemoveIoCompletion cross-references

A single call to the vulnerable function is made in 


We repeat the process, looking for cross-references to 

. We find no direct calls to the function, but its address appears above a table of function pointers named 

Figure 7 — afd!AfdIrpCallDispatch

This table contains the dispatch routines for the AFD driver. Dispatch routines are used to handle requests from Win32 applications by calling 

<a href="" target="_blank" rel="noreferrer noopener">DeviceIoControl</a>
. The control code for each function is found in 

However, the pointer above is not within the 

 table as we expected. From Steven Vittitoe’s Recon talk slides, we discovered that there are actually two dispatch tables for AFD. The second being 
. By calculating the distance between the start of this table and where the pointer to 
 is stored, we can calculate the index into the 
 which shows the control code for the function is 

Figure 8 — afd!AfdIoctlTable

It’s worth noting that it’s the last input/output control (IOCTL) code in the table, indicating that 

 is likely a new dispatch function that has been recently added to the AFD driver.

At this point, we had a couple of options. We could reverse engineer the corresponding Winsock API in a user space to better understand how the underlying kernel function was called, or reverse engineer the kernel code and call into it directly. We didn’t actually know which Winsock function corresponded to 

, so we opted to do the latter.

We came across some code published by x86matthew that performs socket operations by calling into the AFD driver directly, forgoing the Winsock library. This is interesting from a stealth perspective, but for our purposes, it is a nice template to create a handle to a TCP socket to make IOCTL requests to the AFD driver. From there, we were able to reach the target function, as evidenced by reaching a breakpoint set in WinDbg while kernel debugging.

Figure 9 — afd!AfdNotifySock breakpoint

Now, refer back to the function prototype for 

, through which we call into the AFD driver from user space. One of the parameters, 
, is a user mode buffer. As mentioned in the previous section, the vulnerability occurs because the user is able to pass an unvalidated pointer to the driver within an unknown data structure. This structure is passed in directly from our user mode application via the lpInBuffer parameter. It’s passed into 
 as the fourth parameter, and into 
 as the third parameter.

At this point, we don’t know how to populate the data in 

, which we’ll call 
, in order to pass the checks required to reach the vulnerable code path in 
. The remainder of our reverse engineering process consisted of following the execution flow and examining how to reach the vulnerable code.

Let’s go through each of the checks.

The first check we encounter is at the beginning of 


Figure 10 — afd!AfdNotifySock size check

This check tells us that the size of the 

 should be equal to 
 bytes, otherwise the function fails with 

The next check validates values in various fields in our structure:

Figure 11 — afd!AfdNotifySock structure validation

At the time we didn’t know what any of the fields correspond to, so we pass in a 

 byte array filled with 
 bytes (

The next check we encounter is after a call to 

<a rel="noreferrer noopener" href="" target="_blank">ObReferenceObjectByHandle</a>
. This function takes the first field of our input structure as its first argument.

Figure 12 — afd!AfdNotifySock call nt!ObReferenceObjectByHandle

The call must return success in order to proceed to the correct code execution path, which means that we must pass in a valid handle to an 

. There is no officially documented way to create an object of that type via Win32 API. However, after some searching, we found an undocumented NT function 
<a href="" target="_blank" rel="noreferrer noopener">NtCreateIoCompletion</a>
 that did the job.

Afterward, we reach a loop whose counter was one of the values from our struct:

Figure 13 — afd!AfdNotifySock loop

This loop checked a field from our structure to verify it contained a valid user mode pointer and copied data to it. The pointer is incremented after each iteration of the loop. We filled in the pointers with valid addresses and set the counter to 1. From here, we were able to finally reach the vulnerable function 


Figure 14 — afd!AfdNotifyRemoveIoCompletion call

Once inside 

, the first check is on another field in our structure. It must be non-zero. It’s then multiplied by 0x20 and passed into 
 along with another field in our struct as the pointer parameter. From here we can fill in the struct further with a valid user mode pointer (
) and field 
dwLen = 1
 (so that the total size passed to 
 is equal 0x20), and the checks pass.

Figure 15 — afd! Afd!AfdNotifyRemoveIoCompletion field check

Finally, the last check to pass before reaching the target code is a call to 

 which must return 0 (

This function will block until either:

  • A completion record becomes available for the 
  • The timeout expires, which is passed in as a parameter of the function

We control the timeout value via our structure, but simply setting a timeout of 0 is not sufficient for the function to return success. In order for this function to return with no errors, there must be at least one completion record available. After some research, we found the undocumented function 

<a rel="noreferrer noopener" href="" target="_blank">NtSetIoCompletion</a>
, which manually increments the I/O pending counter on an 
. Calling this function on the 
 we created earlier ensures that the call to 

Figure 16 — afd!AfdNotifyRemoveIoCompletion check return nt!IoRemoveIoCompletion

Triggering Arbitrary Write-Where

Now that we can reach the vulnerable code, we can fill the appropriate field in our structure with an arbitrary address to write to.  The value that we write to the address comes from an integer whose pointer is passed into the call to 

 sets the value of this integer to the return value of a call to 

Figure 17 — nt!KeRemoveQueueEx return value
Figure 18 — nt!KeRemoveQueueEx return use

In our proof of concept, this write value is always equal to 

. We speculated that the return value of 
 is the number of items removed from the queue, but did not investigate further. At this point, we had the primitive we needed and moved on to finishing the exploit chain. We later confirmed that this guess was correct, and the write value can be arbitrarily incremented by additional calls to 
 on the 


With the ability to write a fixed value (0x1) at an arbitrary kernel address, we proceeded to turn this into a full arbitrary kernel Read/Write. Because this vulnerability affects the latest versions of Windows 11(22H2), we chose to leverage a Windows I/O ring object corruption to create our primitive. Yarden Shafir has written a number of excellent posts on Windows I/O rings and also developed and disclosed the primitive that we leveraged in our exploit chain. As far as we are aware this is the first instance where this primitive has been used in a public exploit.

When an I/O Ring is initialized by a user two separate structures are created, one in user space and one in kernel space. These structures are shown below.

The kernel object maps to 

 and is shown below.

Figure 19 — nt!_IORING_OBJECT initialization

Note that the kernel object has two fields, 

, which are zeroed on initialization. The count indicates how may I/O operations can possibly be queued for the I/O ring. The other parameter is a pointer to a list of the currently queued operations.

On the user space side, when calling 

<a rel="noreferrer noopener" href="" target="_blank">kernelbase!CreateIoRing</a>
 you get back an I/O Ring handle on success. This handle is a pointer to an undocumented structure (HIORING). Our definition of this structure was obtained from the research done by Yarden Shafir.

typedef struct _HIORING {

    HANDLE handle;


    ULONG IoRingKernelAcceptedVersion;

    PVOID RegBufferArray;

    ULONG BufferArraySize;

    PVOID Unknown;

    ULONG FileHandlesCount;

    ULONG SubQueueHead;

    ULONG SubQueueTail;


If a vulnerability, such as the one covered in this blog post, allows you to update the 

 fields, then it is possible to use standard I/O Ring APIs to read and write kernel memory.

As we saw above, we are able to use the vulnerability to write 

 at any kernel address that we like. To set up the I/O ring primitive we can simply trigger the vulnerability twice.

In the first trigger we set the 


Figure 20 — nt!_IORING_OBJECT first time triggering the bug

And in the second trigger we set 

 to an address that we can allocate in user space (like 

Figure 21 — nt!_IORING_OBJECT second time triggering the bug

All that remains is to queue I/O operations by writing pointers to forged 

 structures at the user space address (
). The number of entries should be equal to 
. This process is highlighted in the diagram below.

Figure 22 — Setting up user space for I/O Ring kernel R/W primitive

One such 

 is shown in the screenshot below. Note that the destination of the operation is a kernel address (
) and that the size of the operation, in this case, is 
 bytes. It is not possible to tell from the structure if this is a read or write operation. The direction of the operation depends on which API was used to queue the I/O request. Using 
<a rel="noreferrer noopener" href="" target="_blank">kernelbase!BuildIoRingReadFile</a>
 results in an arbitrary kernel write and 
 results in an arbitrary kernel read.

Figure 23 — Example faked I/O Ring operation

To perform an arbitrary write, an I/O operation is tasked to read data from a file handle and write that data to a Kernel address.

Figure 24 — I/O Ring arbitrary write

Conversely, to perform an arbitrary read, an I/O operation is tasked to read data at a kernel address and write that data to a file handle.

Figure 25 – I/O Ring arbitrary read


With the primitive set up all that remains is using some standard kernel post-exploitation techniques to leak the token of an elevated process like System (PID 4) and overwrite the token of a different process.

Exploitation In the Wild

After the public release of our exploit code, Xiaoliang Liu (@flame36987044) from 360 Icesword Lab disclosed publicly for the first time, that they discovered a sample exploiting this vulnerability in the wild (ITW) earlier this year. The technique utilized by the ITW sample differed from ours. The attacker triggers the vulnerability using the corresponding Winsock API function, 

, instead of calling into the 
 driver directly, like in our exploit.  

The official statement from 360 Icesword Lab is as follows:  
“360 IceSword Lab focuses on APT detection and defense. Based on our 0day vulnerability radar system, we discovered an exploit sample of CVE-2023-21768 in the wild in January this year, which differs from the exploits announced by @chompie1337 and @FuzzySec in that it is exploited through system mechanisms and vulnerability features. The exploit is related to 

 gets the number of times 
 is called, so we use this to change the privilege count.”  

Conclusion and Final Reflections

You may notice that in some parts of the reverse engineering our analysis is superficial. It’s sometimes helpful to only observe some relevant state changes and treat portions of the program as a black box, to avoid getting led down an irrelevant rabbit hole. This allowed us to turn around an exploit quickly, even though maximizing the completion speed was not our goal.

Additionally, we conducted a patch diffing review of all the reported vulnerabilities in 

 indicated as “Exploitation More Likely”. Our review revealed that all except two of the vulnerabilities were a result of improper validation of pointers passed in from user mode. This shows that having a historical knowledge of past vulnerabilities, particularly within a specific target, can be fruitful for finding new vulnerabilities. When the code base is expanded – the same mistakes are likely to be repeated. Remember, new C code == new bugs . As evidenced by the discovery of the aforementioned vulnerability being exploited in the wild, it is safe to say that attackers are closely monitoring new code base additions as well. 

The lack of support for Supervisor Mode Access Protection (SMAP) in the Windows kernel leaves us with plentiful options to construct new data-only exploit primitives. These primitives aren’t feasible in other operating systems that support SMAP. For example, consider CVE-2021-41073, a vulnerability in Linux’s implementation of I/O Ring pre-registered buffers, (the same feature we abuse in Windows for a R/W primitive). This vulnerability can allow overwriting a kernel pointer for a registered buffer, but it cannot be used to construct an arbitrary R/W primitive because if the pointer is replaced with a user pointer, and the kernel tries to read or write there, the system will crash.

Despite best efforts by Microsoft to kill beloved exploit primitives, there are bound to be new primitives to be discovered that take their place. We were able to exploit the latest version of Windows 11 22H2 without encountering any mitigations or constraints from Virtualization Based Security features such as HVCI.


Exploiting CVE-2023-23397: Microsoft Outlook Elevation of Privilege Vulnerability

Microsoft Outlook Elevation of Privilege Vulnerability windows

Original text by Dominic Chell

Today saw Microsoft patch an interesting vulnerability in Microsoft Outlook. The vulnerability is described as follows:

Microsoft Office Outlook contains a privilege escalation vulnerability that allows for a NTLM Relay attack against another service to authenticate as the user.

However, no specific details were provided on how to exploit the vulnerability.

At MDSec, we’re continually looking to weaponise both private and public vulnerabilities to assist us during our red team operations. Having recently given a talk on leveraging NTLM relaying during red team engagements at FiestaCon, this vulnerability particularly stood out to me and warranted further analysis.

While no particular details were provided, Microsoft did provide a script to audit your Exchange server for mail items that might be being used to exploit the issue.

Review of the audit script reveals it is specifically looking for the PidLidReminderFileParameterproperty inside the mail items and offers the option to “clean” it if found:

Diving in to what this property is, we find the following definition:

This property controls what filename should be played by the Outlook client when the reminder for the mail item is triggered. This is of course particularly interesting as it implies that the property accepts a filename, which of could potentially be a UNC path in order to trigger the NTLM authentication.

Following further analysis of the available properties, we also note the PidLidReminderOverride property which is described as follows:

With this in mind, we should likely set the PidLidReminderOverride property in order to trigger Outlook to parse our malicious UNC inside PidLidReminderFileParameter.

Let’s begin to build an exploit….

The first step to exploit this issue is to create an Outlook MSG file; these files are compound files in CFB format. To speed up the generation of these files, I leveraged the .NET MsgKit library.

Reviewing the MsgKit library, we find that the Appointment class defines a number of properties to add to the mail item before the MSG file is saved:

To create our malicious calendar appointment, I extended the Appointment class to add our required PidLidReminderOverride and PidLidReminderFileParameter properties, as shown above.

From that point, we simply need to create a new appointment and save it, before sending to our victim:

This vulnerability is particularly interesting as it will trigger NTLM authentication to an IP address (i.e. a system outside of the Trusted Intranet Zone or Trusted Sites) and this occurs immediately on opening the e-mail, irrespective of whether the user has selected the option to load remote images or not.

This one is worth patching as a priority as its incredibly easy to exploit and will no doubt be adopted by adversaries fast.

Here’s a demonstration of our exploit which will relay the incoming request to LDAP to obtain a shadow credential:

Gitpod remote code execution 0-day vulnerability via WebSockets

Gitpod remote code execution 0-day vulnerability via WebSockets

Original text by Elliot Ward


This article walks us through a current Snyk Security Labs research project focusing on cloud based development environments (CDEs) — which resulted in a full workspace takeover on the Gitpod platform and extended to the user’s SCM account. The issues here have been responsibly disclosed to Gitpod and were resolved within a single working day!

Cloud development environments and Gitpod

As more and more companies begin to leverage cloud-based development environments for benefits such as improved performance, developer experience, consistent development environments, and low setup times, we couldn’t help but wonder about the security implications of adopting these cloud based IDEs.

First, let’s provide a brief overview of how CDEs operate so we can understand the difference between cloud-based and traditional, local-workstation based development — and how it changes the developer security landscape.

In contrast to traditional development, CDEs run on a cloud hosted machine with an IDE backend. This typically provides a web application version of the IDE and support for integrating with a locally installed IDE over SSH, giving users a seamless and familiar experience. When using a CDE, the organization’s code and any supporting services, such as a development database, are hosted within the cloud. Check out the following diagram for a visual representation of information flow in a CDE.

The security risks of locally installed development environments are not new. However, they historically haven’t received much attention from developers. In May 2021, Snyk disclosed vulnerable VS Code extensions that lead to a 1-click data leak or arbitrary command execution. These traditional workstation-based development environments, such as a local instance of VS Code or IntelliJ, carry other information security concerns — including hardware failure, data security, and malware. While these concerns can be addressed by employing Full Disk Encryption, version control, backups, and anti-malware systems, many questions remain unanswered with the adoption of cloud-based development environments: 

  • What happens if a cloud IDE workspace is infected with malware?
  • What happens when access controls are insufficient and allow cross-user or even cross-organization access to workspaces?
  • What happens when a rogue developer exfiltrates company intellectual property from a cloud-hosted machine outside the visibility of the organization’s data loss prevention or endpoint security software?

In a current security research project here at Snyk, we examine the security implications of adopting cloud based IDEs. In this article we present a case study of one of the vulnerabilities discovered during our initial exploration in the Gitpod platform. 

Examining the Gitpod platform

Disclaimer: When it came to looking at cloud IDE solutions for our research, we settled on either self-hosted or cloud-based solutions, where the vendor has a clearly defined security policy providing safe harbor for researchers. 

One of the most popular CDE’s is Gitpod. Its wide adoption and extended feature set — including automated backups, Git integration out of the box, and multiple IDE backends — ensured that Gitpod was among the first products we looked into.

The first stage of our research involved becoming familiar with the basic workflows of Gitpod, setting up an organization, and experimenting with the product while capturing traffic using Burpsuite to observe the various APIs and transactions. We then pulled the Gitpod source code from GitHub to study the inner workings of its APIs, and reviewed any relevant architecture documentation to better understand each of the components and their function. A great resource for this was a video that provided an initial deep dive into the architecture of Gitpod. At a high level, Gitpod leverages multiple microservices deployed in a Kubernetes environment, where each user workspace is deployed to a dedicated ephemeral pod. 

Gitpod’s primary set of external components are concerned with the dashboard, authentication, and the creation and management of workspaces, organizations, and accounts. At its core, the main component here is aptly named server, a TypeScript application that exposes a JSONRPC API over WebSocket that is consumed by a React frontend called dashboard

From the dashboard, it’s easy to integrate with a SCM provider, such as GitHub or Bitbucket, to import a repository and spin up a development environment — which then serves the source code and provides a working Git environment. Once the workspace is provisioned, it is made accessible via 

 on a subdomain of (i.e 
) through a Golang-based component called ws-proxy

The security vulnerability that was discovered through our research relates primarily to the server component and the JSONRPC served over a WebSocket connection, which ultimately led to a workspace takeover in Gitpod.

Technical details

WebSockets and Same Origin Policy

WebSocket is a technology that allows for real-time, two-way communication between a client (typically a web browser) and a server. It enables a persistent connection between the client and server, allowing for continuous “real-time” data transfer without the need for repeated HTTP requests. 

An interesting aspect of WebSockets from a security perspective, is that a browser security mechanism, the Same Origin Policy (SOP), does not apply. This is the security control which prevents a website from issuing an AJAX request to another website and being able to read the response. If this were possible, it would present a security concern because browsers typically submit cookies along with every request (even for Cross Origin requests, such as CSRF related attacks). Without SOP, any website would be able to issue requests to foreign websites and obtain your data from other domains.

This leads us to the vulnerability class known as Cross-Site WebSocket Hijacking. This attack is similar to a combination of a Cross-Site Request Forgery and CORS misconfiguration. When a WebSocket handshake relies solely upon HTTP cookies for authentication, a malicious website is able to instantiate a new WebSocket connection to the vulnerable application, allowing an attacker to both send and receive data through the connection.

When reviewing an application with WebSocket connections, it’s always worth examining this in depth. Let’s take a look at the WebSocket request for the Gitpod server.

In normal circumstances, the connection is successfully upgraded to a WebSocket and communication begins. There was no additional authentication taking place within the WebSocket exchange itself, and the JSONRPC can be invoked via the WebSocket connection.

So far, we’ve found no additional authentication taking place within the established channel — a good sign for any potential attackers. Now, let’s verify that no additional Origin checks are taking place by taking a handshake we have observed and tampering with the Origin header.

This looks promising! It seems that the domain
 is able to issue Cross-Origin WebSocket requests to
. However, another security mechanism introduces a challenge.

SameSite Cookie bypass

SameSite cookies are a fairly recent addition, providing partial mitigation against Cross-Site Request Forgery (CSRF) attacks. While not everyone has adopted them, most popular browsers have made the default value for all cookies which do not explicitly disable 

 to be 
. So while the underlying vulnerability is present, without a bypass for 
 cookies our attack would largely be theoretical and only work against a subset of outdated and niche browsers.

So what is a 

 in the context of 
? Simply put, the site corresponds to the combination of the scheme and the registrable domain (if any) of the origin’s host. If we look at the specifications for 
 we can see that subdomains are not considered. This is more relaxed than the specification of an Origin used by the Same Origin Policy, which is comprised of a scheme + host (including subdomains) + port (eg:

Earlier, we observed that the workspace was exposed via a subdomain on
. In the context of 
, the workspace URL is considered to be the same site as
. So, it should be possible for one workspace to issue a cross-domain 
 request to a 
 domain with the original user’s cookies attached. Let’s see if we can leverage an attacker controlled workspace to serve a WebSocket Hijacking payload. 

To first verify that the cookies are indeed transmitted and the WebSocket communication is successfully achieved, let’s open the browser console from a workspace and attempt to initiate a WebSocket connection.

As we can see in the above screenshot, we attempt to open a new WebSocket connection and, once open, submit a JSONRPC request. We can see in the console output that a message has been received containing the result of our request, confirming that the cookies are submitted and the origin is permitted to open a websocket to

We now need a way to serve JavaScript from the workspace that can be accessed by a Gitpod user. When looking through the features available, it is possible to expose ports in the workspace and make them accessible using a command line utility called 

 — available on the path inside the workspace by typing 
. We can invoke 
gp ports expose 8080
, and then set up a basic Python web server using 
python -m http.server 8080
. This in turn creates a new subdomain where the exposed port can be accessed as shown below.

However, the connection failed. This is somewhat concerning and requires more investigation. Here we open the source code and start looking for what could be causing the problem. We found the following regular expression pattern, which appears to be used to extract the workspace name from the URL.

The way this matching is performed results in the wrong workspace name being extracted, as demonstrated in the following screenshot:

So, it looks like we can’t serve our content from an exposed port inside the Gitpod hosted workspace and we need another way. By now we already know that we have privileged access to a machine that’s running the VS Code service and is serving requests issued to our workspace URL — so can we abuse this in some way?

The initial idea was to terminate the 

 process and start a Python web server to serve an HTML file. Unfortunately, this did not work and resulted in the workspace being restarted. This appeared to be performed by a local service 
. While testing this approach we noticed that when we terminated the process without binding another process to the VS Code port, the 
 service will automatically restart the 
 process, resulting in a brief hang to the UI without a full restart of the workspace.

This brought a promising idea. Can we patch VS Code to serve a built-in exploit for us?

Patching VS Code was relatively easy. By comparing the original VS Code server source code to the distributed version, we quickly found a convenient location to serve the exploit.

VS Code contains an API endpoint at 

, which returns the commit of the current version:

We modified it so that the correct 

 and the contents of an HTML file were returned. Now, we terminated the 
 process, allowing our newly introduced changes to load into a newly spawned VS Code process instance:

Finally, we can leverage the JSONRPC methods 

, and 
 to build a payload that grants us full control over the user’s workspaces when an unsuspecting Gitpod user visits our link!

Here it is in action:

We can see that we’ve been able to extract some sensitive information about the user account, and are notified that our SSH key has been added to the account. Let’s see if we can SSH to the workspace:

Mission successful! As shown above, we have full access to the user’s workspaces after they’ve visited a link we sent them!


  • Mon, Feb. 13, 2023 — Vulnerability disclosed to vendor
  • Mon, Feb. 13, 2023 — Vendor acknowledges vulnerability
  • Tue, Feb. 14, 2023 — New version released and deployed to production SaaS Gitpod instance
  • Tue, Feb. 22, 2023 — CVE-2023-0957 assigned
  • Wed, Mar. 1, 2023 — Vendor releases new version for Gitpod Self-Hosted and issues advisory


In this post, we presented the first findings from our current research into Cloud Development Environments (CDEs) — which allowed a full account takeover through visiting a link, exploiting a commonly misunderstood vulnerability (WebSocket Hijacking), and leveraging a practical SameSite cookie bypass. As cloud developer workspaces are becoming increasingly popular, it’s important to consider the additional risks that are introduced.

We would like to praise Gitpod for their fantastic turnaround on addressing this security vulnerability, and look forward to presenting more of our findings on cloud-based remote development solutions in the near future.

BlackLotus Becomes First UEFI Bootkit Malware to Bypass Secure Boot on Windows 11

BlackLotus Becomes First UEFI Bootkit Malware to Bypass Secure Boot on Windows 11

Original text by Ravie Lakshmanan

A stealthy Unified Extensible Firmware Interface (UEFI) bootkit called BlackLotus has become the first publicly known malware capable of bypassing Secure Boot defenses, making it a potent threat in the cyber landscape.

«This bootkit can run even on fully up-to-date Windows 11 systems with UEFI Secure Boot enabled,» Slovak cybersecurity company ESET said in a report shared with The Hacker News.

UEFI bootkits are deployed in the system firmware and allow full control over the operating system (OS) boot process, thereby making it possible to disable OS-level security mechanisms and deploy arbitrary payloads during startup with high privileges.

Offered for sale at $5,000 (and $200 per new subsequent version), the powerful and persistent toolkit is programmed in Assembly and C and is 80 kilobytes in size. It also features geofencing capabilities to avoid infecting computers in Armenia, Belarus, Kazakhstan, Moldova, Romania, Russia, and Ukraine.

Details about BlackLotus first emerged in October 2022, with Kaspersky security researcher Sergey Lozhkin describing it as a sophisticated crimeware solution.

«This represents a bit of a ‘leap’ forward, in terms of ease of use, scalability, accessibility, and most importantly, the potential for much more impact in the forms of persistence, evasion, and/or destruction,» Eclypsium’s Scott Scheferman noted.

BlackLotus, in a nutshell, exploits a security flaw tracked as CVE-2022-21894 (aka Baton Drop) to get around UEFI Secure Boot protections and set up persistence. The vulnerability was addressed by Microsoft as part of its January 2022 Patch Tuesday update.

A successful exploitation of the vulnerability, according to ESET, allows arbitrary code execution during early boot phases, permitting a threat actor to carry out malicious actions on a system with UEFI Secure Boot enabled without having physical access to it.

«This is the first publicly known, in-the-wild abuse of this vulnerability,» ESET researcher Martin Smolár said. «Its exploitation is still possible as the affected, validly signed binaries have still not been added to the UEFI revocation list

«BlackLotus takes advantage of this, bringing its own copies of legitimate – but vulnerable – binaries to the system in order to exploit the vulnerability,» effectively paving the way for Bring Your Own Vulnerable Driver (BYOVD) attacks.

Besides being equipped to turn off security mechanisms like BitLocker, Hypervisor-protected Code Integrity (HVCI), and Windows Defender, it’s also engineered to drop a kernel driver and an HTTP downloader that communicates with a command-and-control (C2) server to retrieve additional user-mode or kernel-mode malware.

The exact modus operandi used to deploy the bootkit is unknown as yet, but it starts with an installer component that’s responsible for writing the files to the EFI system partition, disabling HVCI and BitLocker, and then rebooting the host.

The restart is followed by the weaponization of CVE-2022-21894 to achieve persistence and install the bootkit, after which it is automatically executed on every system start to deploy the kernel driver.

While the driver is tasked with launching the user-mode HTTP downloader and running next-stage kernel-mode payloads, the latter is capable of executing commands received from the C2 server over HTTPS.

This includes downloading and executing a kernel driver, DLL, or a regular executable; fetching bootkit updates, and even uninstalling the bootkit from the infected system.

«Many critical vulnerabilities affecting security of UEFI systems have been discovered in the last few years,» Smolár said. «Unfortunately, due the complexity of the whole UEFI ecosystem and related supply-chain problems, many of these vulnerabilities have left many systems vulnerable even a long time after the vulnerabilities have been fixed – or at least after we were told they were fixed.»

«It was just a matter of time before someone would take advantage of these failures and create a UEFI bootkit capable of operating on systems with UEFI Secure Boot enabled.»