gwillcox-r7 (579)
Last Login: June 23, 2023
gwillcox-r7's Latest (20) Contributions
Technical Analysis
Doing a patch diff between a Windows 10 1607 x86 version of win32kfull.sys prior to the patch and after the patch shows that only one function changed: xxxEnableMenuItem
.
Looking at the code diff for this with BinDiff shows that two new blocks were added. These appear to be doing some validation of data and then optionally also incrementing a different data element.
After some time getting proper structures defined in Binary Ninja, I was able to come up with the following HLIL graph showing the code for xxxEnableMenuItem
before the patch was issued:
And here is the code after the patch was issued:
Of note is the fact that the new edition of the code is checking that pRealMenu
exists and then if it does, its incrementing that object’s cObjLock
count by one. This extra lock appears to then be undone in a call to pRealMenu = ThreadUnlock1()
, which was also added in.
Further analysis is available at https://www.numencyber.com/cve-2023-29336-win32k-analysis/ which explains how this bug is a UAF, which makes sense given that a check was added into update the lock count appropriately. As explained in that writeup, prior to the patch, the code purely locked the window object pointed to by pMenu->spwndNotify
and failed to also lock the menu object nested within the window object, aka the pRealMenu
object returned by the call to MenuItemState
.
Tracing the call flow backwards we can see that xxxEnableMenuItem()
is called by NtUserEnableMenuItem()
. According to the writeup, the menu item passed in via the first argument to xxxEnableMenuItem()
, aka pMenu
, would typically be locked inside of inside of a higher level function such as NtUserEnableMenuItem()
. However they raise the question of “okay so you have a menu object you need to lock, but which menu item would need locking?”
So why this question in particular? Surely we are just dealing with one menu object which means one menu itself right? Well as the writeup explains, the object returned by a function such as MenuItemState()
may encompass not only the main menu within a window object but also its submenus, and even subsubmenus in some cases.
This is becuase MenuItemState()
will call MNLookupItem()
which will in turn recursively call itself until it finds the object its looking for, meaning that pMenu
may actually point to a nested set of menu objects. This looks like a potential case where assumptions could lead to some incorrect results.
In the exploit they create 4 menus, namely MenuA, MenuB, MenuC and MenuD, with MenuB being a child of MenuA, MenuC being a child of MenuB, and so on. They set the ID of MenuD to be 0xF060, aka SC_CLOSE. As noted at https://stackoverflow.com/questions/10101742/difference-between-wm-close-and-sc-close, this is sent when the user presses the close button or otherwise uses some menu control to close the window. They remove the class ID of SC_CLOSE from MenuA. They then release MenuC by deleting all references to it within MenuB, in order to ensure proper memory deallocation.
When xxxRedrawTitle()
is called from within xxxEnableMenuItem()
, a transfer will be made to a user mode callback via a call to xxxSendMessage()
, at which point the exploit removes the reference between MenuC and MenuB, effectively releasing MenuC. At this point the reference to MenuC within xxxEnableMenuItem()
will become invalid. Keep in mind in the code MenuC is effectively var_pMenu_spwndNotify
so thats where the relationship comes into play.
Hope that helps!
Technical Analysis
Oracle Weblogic 12.2.1.3.0, 12.2.1.4.0 and 14.1.1.0.0 prior to the April 2023 security update are vulnerable to an unauthenticated remote code execution vulnerability due to a post deserialization vulnerability via IIOP/T3. The bug has been described in detail in a writeup by 14m3ta7k of the company GobySec at https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md, though as of the time of writing no PoC is available.
The bug is fairly similar to CVE-2023-21839 so if you haven’t read that assessment over at https://attackerkb.com/assessments/6a410f31-83c5-416b-92fd-986f2b9b2c61, I’d recommend reading that one first and then coming back here.
When receiving a request, Weblogic will parse incoming data using BasicServerRef
’s invoke()
method, which will in turn call _invoke()
, which then calls resolve_any()
based on the passed in method name of resolve_any
.
In resolve_any()
, the incoming binding name is resolved using resolveObject()
, which will then call lookup()
based on the context information.
Based on the context information, further lookup()
calls are made in several classes such as WLContextImpl
, WLEventContextImpl
, RootNamingNode
, ServerNamingNode
and BasicNamingNode
. All of this will eventually lead to a resolveObject()
call in BasicNamingNode
class.
At this point the resolveObject()
method will realize the obj
parameter passed into it is not an instance of the BasicnamingNode
class and the mode
parameter passed in will be 1
so it will call getObjectInstance()
in WLNamingManager
.
Finally getObjectInstance()
in WLNamingManager
will call getReferent()
method inside the passed in object based on the object interface type, as can be seen in the screenshot below, taken from the writeup.
For CVE-2023-21931 we specifically are interested in the LinkRef
interface type, instead of the OpaqueReference
interface that is used by CVE-2023-21839. In particular the following code will be called when WLNamingManager
’s getObjectInstance()
method finds an object that is an implementation of the LinkRef
interface. Code was taken from the writeup at https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md.
package weblogic.jndi.internal; public final class WLNamingManager { public static Object getObjectInstance(Object boundObject, Name name, Context ctx, Hashtable env) throws NamingException { if (boundObject instanceof ClassTypeOpaqueReference) { ...... } else if (boundObject instanceof LinkRef) { String linkName = ((LinkRef)boundObject).getLinkName(); InitialContext ic = null; try { ic = new InitialContext(env); boundObject = ic.lookup(linkName); // vulnerability trigger point } catch (NamingException var15) { ...... } finally {......} } } }
Basically if an object is passed in as the boundObject
variable that is an instance of the LinkRef
interface, then String linkName = ((LinkRef)boundObject).getLinkName();
is called to get the link name, before it is later passed on to a lookup call with the line boundObject = ic.lookup(linkName)
where the remote JNDI loading occurs.
What is important to note is that the LinkRef
interface contains a constructor that allows one to set the linkAddrType
variable that the getLinkName()
function will return, so the input string, aka linkName
, in the remote JNDI loading operation boundObject = ic.lookup(linkName)
, is entirely attacker controlled.
For reference here is what the constructor for a LinkRef class looks like, taken from https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md
package javax.naming; public class LinkRef extends Reference { static final String linkClassName = LinkRef.class.getName(); static final String linkAddrType = "LinkAddress"; public LinkRef(Name linkName) { super(linkClassName, new StringRefAddr(linkAddrType, linkName.toString())); } public LinkRef(String linkName) { super(linkClassName, new StringRefAddr(linkAddrType, linkName)); } public String getLinkName() throws NamingException { if (className != null && className.equals(linkClassName)) { RefAddr addr = get(linkAddrType); if (addr != null && addr instanceof StringRefAddr) { return (String)((StringRefAddr)addr).getContent(); } } throw new MalformedLinkException(); } }
The code shows that its possible to instantiate a new LinkRef class with a linkName
string and that getLinkName()
will return the contents of addr
, aka the linkName
that was passed in.
Whilst there is presently no in the wild exploitation of this vulnerability, given CVE-2023-21839 is almost identical to this vulnerability and is being exploited in the wild, I’d imagine that this vulnerability will likely also be exploited in the wild shortly, particularly given that this vulnerability was only recently patched in April 2023, whereas CVE-2023-21839 was patched back in January 2023.
Technical Analysis
Oracle Weblogic 12.2.1.3.0, 12.2.1.4.0 and 14.1.1.0.0 prior to the Jan 2023 security update are vulnerable to an unauthenticated remote code execution vulnerability due to a post deserialization vulnerability via IIOP/T3. A PoC for this vulnerability was originally posted by 4ra1n at https://github.com/4ra1n/CVE-2023-21839, and about a week or so later a writeup came out by 14m3ta7k of the company GobySec at https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md which explained the vulnerability in depth with detailed code walkthroughs and explanations.
As noted in the writeup, CVE-2023-21839 is not your typical deserialization vulnerability. Most deserialization vulnerabilities lead to code execution at the same time that deserialization of the attacker’s object occurs. However for CVE-2023-21839, the vulnerability occurs not in the deserialization of the attacker’s object, but rather the fact that an attacker is able to create an object in the target’s memory and then later trigger another function that utilizes that deserialized object without performing proper sanity checks.
As the writeup explains, we can abuse some behavior of the lookup()
method. When receiving a request, Weblogic will parse incoming data using BasicServerRef
’s invoke()
method, which will in turn call _invoke()
, which then calls resolve_any()
based on the passed in method name of resolve_any
.
In resolve_any()
, the incoming binding name is resolved using resolveObject()
, which will then call lookup()
based on the context information.
Based on the context information, further lookup()
calls are made in several classes such as WLContextImpl
, WLEventContextImpl
, RootNamingNode
, ServerNamingNode
and BasicNamingNode
. All of this will eventually lead to a resolveObject()
call in BasicNamingNode
class.
At this point the resolveObject()
method will realize the obj
parameter passed into it is not an instance of the BasicnamingNode
class and the mode
parameter passed in will be 1
so it will call getObjectInstance()
in WLNamingManager
.
Finally getObjectInstance()
in WLNamingManager
will call getReferent()
method inside the passed in object based on the object interface type, as can be seen in the screenshot below, taken from the writeup.
The bug for CVE-2023-21839 occurs specifically because the ForeignOpaqueReference
is an instance of the OpaqueReference
interface that the code above handles. The ForeignOpaqueReference
class defines two constructors, and the parameterized one allows a user to specify the env
and remoteJNDIName
parameters, which are internally assigned to the private variables jndiEnvironment
and remoteJNDIName
respectively.
Additionally the class ForeignOpaqueReference
defines a getReferent()
method that overrides the OpaqueReference
interface’s default definition. This function will take the remoteJNDIName
private variable of the ForeignOpaqueReference
class and call retVal = context.lookup(this.remoteJNDIName);
, which will perform a remote JNDI loading operation. The specific code for the getRefent()
function can be seen below, and is taken from the writeup at https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md:
package weblogic.jndi.internal; public class ForeignOpaqueReference implements OpaqueReference, Serializable { private Hashtable jndiEnvironment; private String remoteJNDIName; ...... public ForeignOpaqueReference(String remoteJNDIName, Hashtable env) { this.remoteJNDIName = remoteJNDIName; this.jndiEnvironment = env; } public Object getReferent(Name name, Context ctx) throws NamingException { InitialContext context; if (this.jndiEnvironment == null) { context = new InitialContext(); } else { Hashtable properties = this.decrypt(); context = new InitialContext(properties); } Object retVal; try { retVal = context.lookup(this.remoteJNDIName); // vulnerability point } finally { context.close(); } return retVal; } ...... }
This implementation of OpaqueReference
’s getReferent()
method inside of ForeignOpaqueReference
will be called by the WLNamingManager
class’s getObjectInstance()
method, as mentioned previously. Specifically the getObjectInstance()
code will see that the passed in boundObject
, which will be a ForeignOpaqueReference
object, is an instance of the OpaqueReference
interface, and will call boundObject = ((OpaqueReference)boundObject).getReferent(name, ctx);
This will end up leading to a remote JNDI loading operation using the location specified in the remoteJNDIName
class variable of the ForeignOpaqueReference
object the attacker passed in. Remote JNDI loading operations are particularly dangerous since they are essentially an application reaching out to a given target, usually a remote server in the case of remote JNDI loading, and saying “Hey, I’m interested in this particular class. Serve me up the class file for that object and I’ll instantiate a local copy on my server”. The problem with this is that you can define code in Java within a static
block in a class definition that will be run whenever the class is created.
The Metasploit module at https://github.com/rapid7/metasploit-framework/pull/17946 takes advantage of this to trigger a JNDI connection to a LDAP server we control. The LDAP server will then respond with a remote reference response that points to a HTTP server that we control, where the malicious Java class file will be hosted. Oracle Weblogic will then make a HTTP request to retrieve the malicious Java class file, at which point our HTTP server will serve up the malicious class file and Oracle Weblogic will instantiate an instance of that class, granting us RCE as the oracle
user.
Overall the risk of this vulnerability is fairly high since most of Oracle Weblogic runs as the oracle
user, so this allows an unauthenticated attacker who has access to IIOP/T3 ports on a given target to gain full control over the server. It can be mitigated to some extent by not exposing IIOP/T3 ports to the public, however the risk will still remain that those on the internal network might be able to abuse this vulnerability to gain access to the server. Whilst public exploit code is available that makes exploitation easier, there are a number of details about this vulnerability that would take an attacker some time to reverse engineer.
Additionally, here are some other limitations that are important to take into account:
- JDK 8u121 introduces the “trustURLCodebase” property which makes it so that we cannot load remote classes via RMI, and later this was expanded to also apply to LDAP connections such as the ones used in this vulnerability. The only way to get around this now is to take advantage of gadget chains within the application itself and essentially do a deserialization attack using gadgets from existing Java class files available in the class path.
- For some reason in my tests the Java class name had to be exactly 11 characters long, otherwise things started to get wonky and would fail.
- When sending the URL to visit as part of the LDAP response to the JDNI request, you have to redirect to the root, aka
/
, of a target web server. Requesting a page or a subdirectory won’t work. Anchor tags after the/
are fine though.
- You must compile the target class to send with Java 8 SDK. Compiling it with a later JDK will result in a class that uses a bytecode version later than 52.0, which may not be able to be loaded by the target depending on which Java version they have installed.
A final point which is of important note is that this is quite similar to CVE-2023-21931, the only difference is that CVE-2023-21931 targets the getObjectInstance()
method of the WLNamingManager
class using an object that is an instance of the LinkRef
interface instead of the OpaqueReference
interface, and uses different internal methods to perform the remote JNDI load.
Technical Analysis
The GPO Watcher endpoint at /api/agent/tabs/agentGPOWatcherData
on Zoho ManageEngine ADAudit Plus before 7006 is vulnerable to a directory traversal in the Html_fileName
parameter of a formulated POST request. By sending this request with the Html_fileName
containing a directory traversal to write to the alert_scripts
directory and setting the htmlReport
parameter to the contents of a malicious command that the attacker wishes to execute, the attacker can create a malicious alert script file on the target computer.
Once this malicious alert script is created, they can then create an alert profile that will trigger on failed logins, and set the script to be executed on failed logins to the malicious script file that was created. At this point, all the attacker needs to do is to attempt to login with invalid credentials, and the malicious script file they created will be loaded and any of the commands contained within it will be executed as the user running Zoho ManageEngine ADAudit Plus.
The one limitation of this vulnerability is that a user will need to have valid credentials to the server as a user who has the ability to create alert profiles, so this does limit the usefulness of this vulnerability somewhat. However should the attacker possess these credentials, it is pretty easy for them to exploit this vulnerability to gain access to the target server.
If you would like more details on the specifics of this vulnerability, there is a great writeup at https://www.ctfiot.com/71659.html that goes into more detail.
Technical Analysis
This is a pretty cool vulnerability that affects Hitachi Vantara Pentaho Business Analytics Server prior to versions 9.4.0.1 and 9.3.0.2, including 8.3.x. In a nutshell, Pentaho offers some endpoints where administrators are able to test and configure LDAP connections. As mentioned in https://research.aurainfosec.io/pentest/pentah0wnage/, this is done by creating an XML-based bean definition using a combination of a base file and user provided parameters, which are then injected into the properties file without sanitization.
Unfortunately this properties file can be parsed by a variety of parsers, including the built in Thymeleaf parser if a Thymeleaf template is included. By injecting a Thymeleaf template into one of the user parameters, one can utilize the ability of Thymeleaf templates to execute methods associated with Java classes to achieve arbitrary code execution by using the statement (java.lang.Runtime).getRuntime().exec()
to get the existing Java runtime, and then call its exec()
method to execute arbitrary code on the host.
This can then be used to exploit the target via a URL such as the following, which will open notepad.exe
on the target:
http://127.0.0.1:8080/pentaho/api/ldap/config/ldapTreeNodeChildren/?url=%23{T(java.lang.Runtime).getRuntime().exec('notepad.exe')}&mgrDn=a&pwd=a
Note that the parameters mgrDn
and pwd
are set to a
but they can be whatever you like and this will still work fine.
Now with that being said, normally this vulnerability would be limited in impact as you need to be an administrator to access these pages. However this can be worked around via https://attackerkb.com/topics/JGGe0nRNNv/cve-2022-43939 aka CVE-2022-43939, which allows one to do an authentication bypass to access this page by appending require.js
to the end of the URL. So the PoC would become this:
http://127.0.0.1:8080/pentaho/api/ldap/config/ldapTreeNodeChildren/require.js?url=%23{T(java.lang.Runtime).getRuntime().exec('notepad.exe')}&mgrDn=a&pwd=a
Note that this is not the only URL that works. Please refer to https://attackerkb.com/topics/JGGe0nRNNv/cve-2022-43939 for more details on the regex in use and other potential ways CVE-2022-43939 could work, as URLS such as http://127.0.0.1:8080/pentaho/api/ldap/config/ldapTreeNodeChildren/requireAjs?url=%23{T(java.lang.Runtime).getRuntime().exec('notepad.exe')}&mgrDn=a&pwd=a
could also be used to exploit this vulnerability as an unauthenticated user.
The impact of exploiting this vulnerability is that one will get code execution as the user running Hitachi Vantara Pentaho Business Analytics Server. Typically this user is a local administrator or one of the service accounts, which tend to have a fair amount of access to the underlying system. Whilst not the same as SYSTEM, it was noticed that the service account that one gains access to on a Windows device has sufficient permissions that one could elevate their privileges to SYSTEM using Named Pipe Impersonation (Printer Spooler Variant), aka technique 5 of Metasploit’s getsystem
command as detailed at https://github.com/rapid7/metasploit-framework/blob/01204106e98aacf4eb5a0537c21d5de46a240f0b/documentation/modules/post/windows/escalate/getsystem.md?plain=1#L73C9-L81
Overall this is a fairly severe vulnerability that could potentially lead to SYSTEM level compromise under the right conditions by an unauthenticated attacker. I would expect to see this being exploited in the wild soon if it is not already being exploited, so it is highly recommend to patch affected systems as soon as possible.
Technical Analysis
This is an authentication bypass in Hitachi Vantara Pentaho Business Analytics Server versions before 9.4.0.1 and 9.3.0.2, including 8.3.x due to the use of access checks which are done primarily through the applicationContext-spring-security.xml
file. Within this file are various regexes which are used to control access to various pages, however the one that stands out is this one:
334 | <sec:intercept-url pattern="\A/[^\?]*(require)(-js)?(-cfg)?.js(\?.*)?\Z" access="Anonymous,Authenticated" />
This regex allows anonymous and authenticated access to any page that starts with a /
, followed by any character other than ?
, then the word require
followed optionally by -js
or -cfg
, then any character, followed by js
and then optionally a ?
followed by any characters you like until the end of the line.
So following this we can formulate a few URLs that could bypass authentication:
/pentaho/api/ldap/config/ldapTreeNodeChildren/require.js
/pentaho/api/ldap/config/ldapTreeNodeChildren/require-cfg.js
/pentaho/api/ldap/config/ldapTreeNodeChildren/require-js.js
/pentaho/api/ldap/config/ldapTreeNodeChildren/requireBjs.js
/pentaho/api/ldap/config/ldapTreeNodeChildren/requireKjs.js?asdf=22&bbb=44&ccc=55
Whilst the article at https://research.aurainfosec.io/pentest/pentah0wnage/ where this was originally written up seems to suggest that only require.js
is possible, we can see that this is not the case based on the regex above.
Combining this with CVE-2022-43769 as discussed at https://attackerkb.com/topics/hy6nWcCo30/cve-2022-43769 can lead to unauthenticated RCE as the user running the Hitachi Vantara Pentaho Business Analytics Server, which will typically be a privileged user such as a local administrator or local service account, which is where the real risk comes into play.
Given this context, it is highly recommended that this vulnerability be patched as soon as possible. If this was just an authentication bypass without the ability to perform anything useful, then we’d recommend patching it as part of your normal patch cycle, but given the ability to combine this with CVE-2022-43769, and the ease of exploitation of both vulnerabilities to get RCE on the server as an unauthenticated user, patching should be done as soon as possible as its likely these bugs will be exploited in the wild soon if they have not been so already.
Technical Analysis
Overview
This is a pretty cool vulnerability in vBulletin version 5.6.9, 5.6.8 and 5.6.7 prior to PL1 for each respective version. The vulnerability occurs due to improper handing of non-scalar data in vBulletin’s Object-Relational Mapper (ORM), which leads to deserialization of user input without appropriate validation.
There is a great writeup on this bug at https://www.ambionics.io/blog/vbulletin-unserializable-but-unreachable but I’ll try summarize some of the important points here; I’d recommend reading the writeup though as its fairly short and to the point; something that is rare for a technical post of this nature.
The gist of what is going on here is that vBulletin stores non-scalar data in its database in a serialized format using functions like serialize()
, and then will call unserialize()
to unserialize that data when it needs to retrieve it from the database and use it. Each item to be serialized or deserialized will declare a 3 field structure stating its class, whether its a required field for that class, and a function to verify that the value is correct and modify it if necessary.
The researchers found that the searchprefs
property of the vB_DataManager_User
class is verified by the verify_serialized()
function and is of type vB_Cleaner::TYPE_NOCLEAN
, meaning it has no type restricitons. Looking at verify_serialized()
we see the following code:
function verify_serialized(&$data) { if ($data === '') { $data = serialize(array()); return true; } else { if (!is_array($data)) { $data = unserialize($data); // <--------- PROBLEM HERE!!!! if ($data === false) { return false; } } $data = serialize($data); } return true; }
The problem in this code is that to verify that the data is actually serialized, we take the untrusted user data in the $data
variable and just check that its not an array or a blank string, and if we pass this criteria then we blindly pass it into an unserialize()
call, before checking that unserialize()
didn’t return false
. If unserialize()
didn’t return false
then its assumed everything went okay and we reserialize the data using serialize()
, save that into $data
and return true
.
The issue here is that no validation is actually done to ensure the serialized data is using expected classes and isn’t just a malicious serialized object. Additionally as we will see later on, just checking that unserialize()
doesn’t return false
isn’t sufficient; we should also be checking that the object returned is of the expected type.
Whilst the researchers tried to exploit this vulnerability using their normal methods of gadget chains and abusing the applications code, they found this was an issue as vBulletin has a lot of vB_Trait_NoSerialize
traits on objects, making them impossible to deserialize without raising an exception. Additionally the one library that they did find some gadget chains in isn’t loaded by default, since the googlelogin
package isn’t enabled by default in vBulletin despite it being installed, so they couldn’t use they normal Monolog chain without a bit of tweaking.
What tweaking you may ask? Well it turns out that unserialize()
has some interesting behavior. If the class name it receives isn’t valid, then it will return a __PHP_Incomplete_Class
object. This will not cause the above code to fail though since it isn’t a false
value. Noticing this, the researchers then took a look into the autoloader behavior in vBulletin. The code for the autoloader can be seen below:
spl_autoload_register(array('vB', 'autoload')); class vB { public static function autoload($classname, $load_map = false, $check_file = true) { $fclassname = strtolower($classname); // [0] $segments = explode('_', $fclassname); // [0-1] switch($segments[0]) // [1] { case 'vb': $vbPath = true; $filename = VB_PATH; // ./vb/ break; case 'vb5': $vbPath = true; $filename = VB5_PATH; // ./vb5/ break; default: $vbPath = false; $filename = VB_PKG_PATH; // ./packages/ break; } if (sizeof($segments) > ($vbPath ? 2 : 1)) { $filename .= implode('/', array_slice($segments, ($vbPath ? 1 : 0), -1)) . '/'; // [2] } $filename .= array_pop($segments) . '.php'; // [3] if(file_exists($filename)) require($filename); // [4] } }
They noticed that vBulletin has an autoloader at vB::autoload()
which will be called whenever an unknown class is attempted to be accessed during deserialization. This code will take in a classname, split it on the _
character, check what the first part of the path contains and will append the appropriate directory name to the beginning of the path, and then takes the rest of the path minus the last segment, and squishes it together, using /
to separate each part of the final path name. Finally it takes the last item of the split and uses this as the filename to be accessed, appending .php
to the end of it before adding it to the final path name. If a file exists at this resulting path, it is then loaded using a require()
statement. No validation is done to see if this is an expected file path or similar though, so as long as the file exists in one of the three expected directories (./vb/
, ./vb5/
, or ./packages/
), it will be possible to load it via require()
with this code.
The researchers then realized that they could abuse this to load the autoloader of the Monolog library that they had a gadget chain in, such that the autoloader would be loaded into memory allowing them to use any classes within Monolog since any unknown ones will now be loaded by the Monolog autoloader. Keep in mind this is possible because the plugin is normally disabled but still installed, so all that’s needed to use it is for some code to load some of its initializers into memory so that PHP knows where to find the classes in the deserialization gadget chain. With the Monolog autoloader now in place to help load any Monolog classes that the gadget chain may need that aren’t already in memory, the researchers now had everything they needed to make their Monolog deserialization gadget chain work again.
PoC Code
The final PoC can be seen over at https://github.com/ambionics/vbulletin-exploits/blob/main/vbulletin-rce-cve-2023-25135.py and looks roughly like the following code:
POST /ajax/api/user/save HTTP/1.1 Host: 172.17.0.2 Content-Type: application/x-www-form-urlencoded Content-Length: 666 securitytoken=guest &options= &adminoptions= &userfield= &userid=0 &user[email]=pown@pown.net &user[username]=toto &password=password &user[password]=password &user[searchprefs]=a:2:{i:0;O:27:"googlelogin_vendor_autoload":0:{}i:1;O:32:"Monolog\\Handler\\SyslogUdpHandler":1:s:9:"\x00*\x00socket";O:29:"Monolog\\Handler\\BufferHandler":7:{s:10:"\x00*\x00handler";r:4;s:13:"\x00*\x00bufferSize";i:-1;s:9:"\x00*\x00buffer";a:1:{i:0;a:2:{i:0;s:[LEN]:"[COMMAND]";s:5:"level";N;}}s:8:"\x00*\x00level";N;s:14:"\x00*\x00initialized";b:1;s:14:"\x00*\x00bufferLimit";i:-1;s:13:"\x00*\x00processors";a:2:i:0;s:7:"current";i:1;s:6:"system";}}}}
Final Notes
I was unfortunately unable to determine what user the code will ultimately end up executing as; I presume it would be the user that vBulletin is running as though. However the fact that this is a unauthenticated deserialization bug that can be remotely exploited with no prior knowledge of the target makes it a pretty severe issue. The one saving grace is that it appears this bug may have been limited to only three editions of vBulletin, however if your running any of these versions its highly advisable to upgrade and to also perform a check to see if you have potentially been compromised by this vulnerability. I expect to see more widespread exploitation of this bug in the future given its ease of exploitation.
Technical Analysis
Interesting bug that was announced on Twitter at https://twitter.com/chompie1337/status/1633498392125997056 and later posted on GitHub at https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768. Bug occurs in afd.sys
which is the Windows Ancillary Function Driver for WinSock, and allows privilege escalation from a local user to NT AUTHORITY\SYSTEM
.
Whats interesting about this bug though is that unlike other EoP bugs, this only seems to affect Windows 11 and Windows Server 2022 according to https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2023-21768. Additionally the exploit at https://github.com/chompie1337/Windows_LPE_AFD_CVE-2023-21768 is noted to only work on vulnerable Windows 11 22H2 systems, which raises the question of if there were some specific items that needed to be hardcoded for a specific version of Windows, such as offsets or similar.
The current exploit that is available, from my brief look at things, appears to use the I/O Ring R/W primitive that Yarden Shafir talked about at https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/ and later released a PoC on at https://github.com/yardenshafir/IoRingReadWritePrimitive. I’m somewhat surprised that Microsoft still hasn’t patched this several months later given how strict they have been at trying to fix such items in the past, but these things typically require complicated change to the OS so perhaps I shouldn’t be all too surprised, particularly given its dealing with I/O buffers which are heavily utilized.
After leaking some addresses it appears to do the standard SYSTEM token replacement using the leaked EPROCESS address of the SYSTEM process and then takes that security token and replaces the chosen processes’s security token with the SYSTEM security token. Note I say chosen here as the exploit takes a PID as an argument and uses that PID to find out which process’s token it should replace with the SYSTEM security token.
Looking closer at the exploit to perform some of the leaks there seems to be a reference to NtQuerySystemInformation
which is commonly used for leaking information, however I don’t actually see that being used anywhere, so it looks like it is potentially left over code.
The main vulnerability exploit code appears to be centralized into ArbitraryKernelWrite0x1
in https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768/blob/master/Windows_AFD_LPE_CVE-2023-21768/exploit.c, which is then used to overwrite some entries in the IO ring buffer to transform it from arbitrary kernel write to arbitrary kernel read and write.
Looking into ArbitrarykernelWrite0x1
it seems it takes in a pointer to the address to overwrite, then creates an IPv4 extended TCP socket attribute structure, creates an IoCompletion object using NtCreateIoCompletion
that allows for both querying and modifying the state, and allows one thread of concurrent access to the object.
We then call NtSetIoCompletion
(documented to some degree at http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/IoCompletion/NtSetIoCompletion.html) on the same object with a completion key of 0x1337
and pass it the empty IO_STATUS_BLOCK
object IoStatusBlock
so we can keep track of the IO status of this IoCompletion object. We set the completion status to 0, aka not completed, and set the number of bytes transferred in a manually finished I/O operation to 0x100. Not sure why this value was chosen but eh.
If this completes successfully, then we go ahead and make a UNICODE_STRING called ObjectFilePath
that holds the string \\Device\\Afd\\Endpoint
, which according “Reverse Engineering Windows AFD.sys” by Steven Vittitoe which was presented at Recon 2015 at https://recon.cx/2015/slides/recon2015-20-steven-vittitoe-Reverse-Engineering-Windows-AFD-sys.pdf, is an endpoint that allows access to 70+ IOCTLs that are defined within the afd.sys
driver.
Looking more at this paper we can also see that AFD.sys handles everything from TCP/IP to SAN and that its listed as the “Ring 0 entrypoint for WinSock”, aka all WinSock calls will end up going through this driver which executes at the kernel level. This is backed up by their point that it handles all socket()
calls.
Anyway getting back to this we can see we also set an OBJECT_ATTRIBUTES object to the name of this string as the ObjectName
parameter, and then set its attributes to 0x40, aka OBJ_CASE_INSENSITIVE as noted at http://www.jasinskionline.com/technicalwiki/Print.aspx?Page=Constants-Windows-API&AspxAutoDetectCookieSupport=1, as I guess we need case insensitive operations for this? Idk though without further info.
Finally we call NtCreateFile
to create this file and save the handle into hSocket
. We ask for the maximium permissions possible on this object, pass in the ObjectAttributes
object attributes object we created earlier so that we use the \\Device\\Afd\\Endpoint
and use case insensitive naming, pass in the IoStatusBlock
for the I/O completion object, allow read and write sharing, pass in nothing for the creation options since this device should already be created, and pass in 1 aka FILE_OPEN so we open the existing file. Finally we pass in bExtendedAttributes
which will hold the hardcoded extended attributes for a IPv4 TCP socket.
If all goes well then we should now have an file handle in hSocket
however the AFD driver still isn’t fully aware of this socket. To complete this we then create a new Data
object of type AFD_NOTIFYSOCK_DATA
. This object type is not publicly documented anywhere as far as I can tell, and appears to have been guessed at via reverse engineering. In this structure we can see that we have a few pointers, a handle to hCompletion
which explains our earlier completion object creation call, and some DWORDs.
The next few lines of the code will set the hCompletion
parameter of this structure to the hCompletion
object we created earlier, and after this we set pData1
and pData2
to 0x2000 byte long heap buffers that are readable and writeable and which have been reserved and committed in memory.
We then set dwCounter
to 1 to indicate one entry, and set dwLen
to 1. Not sure what dwLen
controls though but we’ll have to wait for the blog for more details. We interestingly set dwTimeout
to an insanely large value of 100000000
. Its possible this may be related to an overflow which leads to the out of bound write, or it could be set this way to hold the connection open whilst the exploit happens and prevent timeouts. Finally we see that the address we wish to overwrite is placed into Data.pPwnPtr
suggesting that this structure is responsible for the arbitrary overwrite and perhaps doesn’t validate that the address is actually a kernel address and not a user address like it should be.
Finally we create an event using CreateEvent
and then call IOCTL 0x12127 on AFD, which the exploit notes as AFD_NOTIFYSOCK_IOCTL. My guess is that this processes the malicious Data
structure of type AFD_NOTIFYSOCK_DATA
and then fails to notice that the Data.pPwnPtr
is out of the expected address range, allowing for an arbitrary write vulnerability.
Technical Analysis
This was an interesting vulnerability first found by Vinicius, and which then had a nice writeup on how to exploit it published by Y4er at https://xz.aliyun.com/t/11578. It was subsequently patched by Zoho and the patch can be found at https://archives2.manageengine.com/passwordmanagerpro/12101/ManageEngine_PasswordManager_Pro_12100_to_12101.ppm, whilst the advisory can be found at https://www.manageengine.com/products/passwordmanagerpro/advisory/cve-2022-35405.html.
By sending a XML-RPC serialized message via a POST request to the /xmlrpc
endpoint as an unauthenticated user, the specs of which can be found at http://xmlrpc.com/spec.md, it is possible to gain RCE as the SYSTEM
user that Zoho Password Manager runs as. Note that the name of the method being called does not have to be valid, and the name of the parameter passed to that method also does not have to be valid. All that matters is that the value of the parameter is marked as a serializable object that is Base64 encoded. This can be seen in the Metasploit module at https://github.com/rapid7/metasploit-framework/pull/16852/files#diff-eaa6a1c5246f1059f414cda95a9c5c4e3e1d0adc4373ce64f7165fefe7576ec6R129-R157
Additionally, since the target will also respond with Failed to read result object: null
if you send the endpoint an empty string and its vulnerable to deserialization attacks, it makes it really easy to put together a full exploit for this vulnerability that not only can check if the target is vulnerable but can also reliably exploit it. The last step was to use the CommonsBeanutils1 deserialization chain and then supply the command we want to execute and boom, we can go from an unauthenticated user to SYSTEM remotely and without authentication.
Now what are the implications of this? Well it depends on the product. In the case of ManageEngine Access Manager Plus you need authentication to exploit this issue which may negate some of the risk, however one still needs to consider that successful exploitation will result in high privileged user access. However with Zoho ManageEngine Password Manager Pro and PAM360, no authentication is needed yet you will still get very high privileged user access.
Secondly one needs to consider the position of where these products will be placed in the network. Zoho ManageEngine Password Manager Pro will likely be internally facing as there is likely not a need to make it externally accessible, or if it is it will be accessible via a VPN. On the other hand ManageEngine Access Manager Plus and PAM360 are access management solutions so it is feasible, particularly in the world of remote work that we live in today, that these solutions would be accessible over the internet.
In the worse case scenario this would mean an unauthenticated attacker could potentially connect to a target server remotely over the internet, and with no authentication get SYSTEM level access on that server, which will also be controlling sensitive operations via access management controls, or will be holding user’s passwords, which could then be used to gain further access into the target network.
More realistically though is the scenario that these are internally facing and an internal attacker uses this vulnerability to gain control over access management software to avoid detection or grant themselves access to sensitive resources, or steals passwords to gain further access into the target network.
In either case the risk of this vulnerability is quite high and given the incredibly easy exploitation of this issue combined with known exploited in the wild activity, this should be patched as soon as possible and you should investigate your servers for any suspicious activity if you haven’t patched already.
Without further details this is speculation at best and is more likely to fall into the line of fear mongering than actionable intelligence. So far the only technical details that have been shared that I could find online was at https://www.fortiguard.com/psirt/FG-IR-22-377 which lists that FortiOS, FortiProxy and FortiSwitchManager may allow an unauthenticated attacker to perform operations on the administrative interface via specially crafted HTTP or HTTPS requests.
No details have actually been provided on what this HTTP or HTTPS request actually looks like though. The only details the advisory lists are that the device’s logs contain the string user="Local_Process_Access"
and that the request comes through the HTTP/HTTPS administrative interface on these devices.
Without further info we have no details on if this is part of the request itself or something else, if the request requires tampering with some complex serial data format which would make this harder to exploit or if it is a simple switch in some parameter, or any of the prerequisite information that might need to be gathered as an unauthenticated user to fully take advantage of this vulnerability.
There is a plan for them to release the blog post detailing more about this vulnerability but I think that may take some time whilst they wait for people to patch. Until then the only other hope is that someone does a patch diff of the code to see what was actually changed and then determine from that what the real issue was that was fixed.
Analysis above is technically correct however the real risk is a little higher as upon rereading the article at https://seclists.org/fulldisclosure/2017/Sep/23 which is the one that https://packetstormsecurity.com/files/144097/Hikvision-IP-Camera-Access-Bypass.html copies off of, I saw this key important message:
Hikvision camera API includes support for proprietary HikCGI protocol, which exposes URI endpoints through the camera's web interface. The HikCGI protocol handler checks for the presence of a parameter named "auth" in the query string and if that parameter contains a base64-encoded "username:password" string, the HikCGI API call assumes the idntity of the specified user. The password is ignored
Note specifically that they state that the password is ignored. This is the true risk of this vulnerability. You can specify any user that is a valid user on the system and because the logic simply checks for the presence of an auth
parameter in the URL to HikCGI pages that contains a base64 encoded string that matches the pattern username:password
, then ignores the password and uses the username for impersonation, you then become that user.
The original PoCs used hardcoded Base64 string of YWRtaW46MTEK
, which decodes toadmin:11
, as the username and password however so at first glance this seemed like a hardcoded credential issue, however this is not the case. admin
is the default user on the vast majority, but not all HikVision devices.
The impact however is still the same as admin
is the highest privileged user on these devices so you can do anything you want, including flashing the device with new firmware to backdoor the device, downloading the configuration file containing usernames and passwords (which are supposedly encrypted albeit with weak encryption that can be cracked due to the use of a weak password protecting the encryption key).
Hope that helps!
Hey @h00die-gr3y this is a great writeup and I appreciate you adding the Metasploit module in. Would you be interested in submitting the Metasploit module as a PR to be included into Metasploit? We would love to add a module in for this! If your having trouble setting up the environment to submit a PR we can also create a new PR for you if that would be easier :)
Technical Analysis
Based on updated analysis from Maurizio Agazzini over at https://security.humanativaspa.it/zyxel-authentication-bypass-patch-analysis-cve-2022-0342/, it appears the impact of this vulnerability is a lot higher than initially anticipated. I think what our analysis and possibly others failed to realize is that a lot of this vulnerability is dependent on the actual port that you send the request to.
In the analysis listed above they noted that the following ports are associated with Apache:
- 8008
- 54088
- 80
- 4433
- 443
And if we access the port 8008 we get a VPN Authorize authentication prompt, which appears to be related to 2FA. However note that unlike @jbaines-r7’s assessment, the prompt isn’t exactly the same and contains the text “VPN Authorize” at the top.
From this they then realized that since this must be related to 2FA, there is a configuration file at /var/zyxel/service_conf/httpd_twofa.conf
which controls this. Looking at the configuration showed it was set up to listen on port 8008, the same port accessed earlier, and gave some more information on setup.
They they accessed port 54088 whilst exploring the other ports and noticed it contained what looked to be a block page. This is common on firewalling/website blocking apps where they will run a website on a port and redirect to that port to show the block page whenever a website that is deemed to be “bad” is attempted to be accessed by a user.
Looking at this and noticing it was a blockpage lead them to /var/zyxel/service_conf/cf_blockpage_https.conf
which showed that port 54088 was being used for the cf_blockpage service,.
They then noticed both of these configuration files referenced /usr/local/apache/cgi-bin
using the Directory configuration directive as described at https://httpd.apache.org/docs/2.4/mod/core.html#directory, which would have the configuration option SSLOptions +StdEnvVars
which as described at https://httpd.apache.org/docs/trunk/mod/mod_ssl.xml#ssloptions, sets “the standard set of SSL related CGI/SSI environment variables are created. This per default is disabled for performance reasons, because the information extraction step is a rather expensive operation. So one usually enables this option for CGI and SSI requests only.” So this is interesting as it indicates we might be using CGI scripts on these endpoints.
At this point they then looked at /usr/local/zyxel-gui/httpd.conf
and found that the /usr/local/apache/cgi-bin
directory has a ScriptAlias for /cgi-bin/
, meaning one can access the scripts in this directory by browsing to /cgi-bin/
. Because this was configured in a global area though, this means that all the CGIs inside the /usr/local/apache/cgi-bin
directory are accessible on every different virtual host the server provides.
As a final point they show that they could get around some of the authentication errors by sending a CGI request not to port 443, but instead to port 8008, and the same request worked fine, which allowed them to export the startup-config.conf
file. This is just one example though as now they can essentially access any CGI endpoints on the server without authentication and are limited only by what the CGI script allows them to do.
Technical Analysis
There is a nice writeup on this at https://medium.com/@frycos/searching-for-deserialization-protection-bypasses-in-microsoft-exchange-cve-2022-21969-bfa38f63a62d. The bug appears to be a deserialization bug that occurs when loading a specific file, however according to the demo video at https://gist.github.com/Frycos/a446d86d75c09330d77f37ca28923ddd it seems to be more of a local attack. That being said it would grant you an LPE to SYSTEM if you were able to trigger it. Furthermore Frycos mentions that he thinks Microsoft didn’t fix the root issue when he wrote the blog (as of January 12th 2022), so its possible the root issue wasn’t fixed, though Frycos mentioned he didn’t look into this further.
From https://twitter.com/MCKSysAr/status/1524518517990727683 it does seem like at the very least some exploitation attempts have been made to try exploit this although writing to C:\Program Files\Microsoft\Exchange Server\V15\UnifiedMessaging\voicemail
to trigger the bug via making it process a voicemail has proven to be difficult to do. It does however note my tip, shown later in this writeup, of how to bypass the deny list by using System.Runtime.Remoting.ObjRef
as was pointed out online, was valid.
What follows below is some of my notes that I wrote up a while back and never published. Hopefully they are of help to someone.
Overview
Background info
Deserialization vulnerability leading to RCE potentially.
Got a CVSS 3.1 score of 9.0 with a temporal score metric score of 7.8.
Interesting that it mentions the attack vector is Adjacent and the article notes that this may be only cause of the way that he exploited it and may indicate they didn’t fix the root issue.
Low attack complexity and low privileges required seems to indicate it may be authenticated but you don’t need many privileges??? I need to check on this further.
High impact on everything else suggest this is a full compromise; this would be in line with leaking the hash.
Affected
- Microsoft Exchange Server 2019 Cumulative Update 11 prior to January 2022 security update.
- Microsoft Exchange Server 2019 Cumulative Update 10 prior to January 2022 security update.
- Microsoft Exchange Server 2016 Cumulative Update 22 prior to January 2022 security update.
- Microsoft Exchange Server 2016 Cumulative Update 21 prior to January 2022 security update.
- Microsoft Exchange Server 2013 Cumulative Update 23 prior to January 2022 security update.
Fixed By
KB5008631
Other vulns fixed in same patch
CVE-2022-21846 <– NSA reported this one.
CVE-2022-21855 <– Reported by Andrew Ruddick of MSRC.
Writeup Review
Original writeup: https://www.instapaper.com/read/1487196325
We have well known sinks in [[.NET]] whereby one can make deserialization calls from unprotected formatters such as BinaryFormatter
. These formatters as noted in [[CVE-2021-42321]] don’t have any SerializationBinder
or similar binders attached to them, which means that they are open to deserialize whatever they like, without any binder limiting them to what they can deserialize.
Initial search for vulnerabilities took place around Exchange’s Rpc
functions, which use a binary protocol created by Microsoft for communication instead of using normal HTTP requests.
Looking around we can see Microsoft.Exchange.Rpc.ExchangeCertificates.ExchangeCertificateRpcServer
contains several function prototypes:
// Microsoft.Exchange.Rpc.ExchangeCertificate.ExchangeCertificateRpcServer using System; using System.Security; using Microsoft.Exchange.Rpc; internal abstract class ExchangeCertificateRpcServer : RpcServerBase { public unsafe static IntPtr RpcIntfHandle = (IntPtr)<Module>.IExchangeCertificate_v1_0_s_ifspec; public abstract byte[] GetCertificate(int version, byte[] pInBytes); public abstract byte[] CreateCertificate(int version, byte[] pInBytes); public abstract byte[] RemoveCertificate(int version, byte[] pInBytes); public abstract byte[] ExportCertificate(int version, byte[] pInBytes, SecureString password); public abstract byte[] ImportCertificate(int version, byte[] pInBytes, SecureString password); public abstract byte[] EnableCertificate(int version, byte[] pInBytes); public ExchangeCertificateRpcServer() { } }
These are then implemented in Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServer
.
// Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServer using System; using System.Security; using System.Security.AccessControl; using System.Security.Principal; using Microsoft.Exchange.Management.SystemConfigurationTasks; using Microsoft.Exchange.Rpc; using Microsoft.Exchange.Rpc.ExchangeCertificate; using Microsoft.Exchange.Servicelets.ExchangeCertificate; internal class ExchangeCertificateServer : ExchangeCertificateRpcServer { internal const string RequestStoreName = "REQUEST"; private static ExchangeCertificateServer server; public static bool Start(out Exception e) { e = null; SecurityIdentifier securityIdentifier = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); FileSystemAccessRule accessRule = new FileSystemAccessRule(securityIdentifier, FileSystemRights.Read, AccessControlType.Allow); FileSecurity fileSecurity = new FileSecurity(); fileSecurity.SetOwner(securityIdentifier); fileSecurity.SetAccessRule(accessRule); try { server = (ExchangeCertificateServer)RpcServerBase.RegisterServer(typeof(ExchangeCertificateServer), fileSecurity, 1u, isLocalOnly: false); return true; } catch (RpcException ex) { RpcException ex2 = (RpcException)(e = ex); return false; } } public static void Stop() { if (server != null) { RpcServerBase.StopServer(ExchangeCertificateRpcServer.RpcIntfHandle); server = null; } } public override byte[] CreateCertificate(int version, byte[] inputBlob) { return ExchangeCertificateServerHelper.CreateCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob); } public override byte[] GetCertificate(int version, byte[] inputBlob) { return ExchangeCertificateServerHelper.GetCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob); } public override byte[] RemoveCertificate(int version, byte[] inputBlob) { return ExchangeCertificateServerHelper.RemoveCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob); } public override byte[] ExportCertificate(int version, byte[] inputBlob, SecureString password) { return ExchangeCertificateServerHelper.ExportCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob, password); } public override byte[] ImportCertificate(int version, byte[] inputBlob, SecureString password) { return ExchangeCertificateServerHelper.ImportCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob, password); } public override byte[] EnableCertificate(int version, byte[] inputBlob) { return ExchangeCertificateServerHelper.EnableCertificate(ExchangeCertificateRpcVersion.Version1, inputBlob); } }
Examining these functions we can see a lot of them take a byte array input named byte[] inputBlob
. If we follow the ImportCertificate()
function here as an example we can see that the implementation will call into Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServerHelper
, as is also true for the other functions.
// Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServerHelper using System; using System.Collections.Generic; using System.Management.Automation; using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Exchange.Data; using Microsoft.Exchange.Data.Common; using Microsoft.Exchange.Data.Directory; using Microsoft.Exchange.Data.Directory.Management; using Microsoft.Exchange.Data.Directory.SystemConfiguration; using Microsoft.Exchange.Extensions; using Microsoft.Exchange.Management.FederationProvisioning; using Microsoft.Exchange.Management.Metabase; using Microsoft.Exchange.Management.SystemConfigurationTasks; using Microsoft.Exchange.Management.Tasks; using Microsoft.Exchange.Net; using Microsoft.Exchange.Security.Cryptography.X509Certificates; using Microsoft.Exchange.Servicelets.ExchangeCertificate; internal class ExchangeCertificateServerHelper { ... public static byte[] ImportCertificate(ExchangeCertificateRpcVersion rpcVersion, byte[] inputBlob, SecureString password) { bool flag = false; ExchangeCertificateRpc exchangeCertificateRpc = new ExchangeCertificateRpc(rpcVersion, inputBlob, null); if (string.IsNullOrEmpty(exchangeCertificateRpc.ImportCert)) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateDataInvalid, ErrorCategory.ReadError); } Server server = null; ITopologyConfigurationSession topologyConfigurationSession = DirectorySessionFactory.Default.CreateTopologyConfigurationSession(ConsistencyMode.IgnoreInvalid, ADSessionSettings.FromRootOrgScopeSet(), 1159, "ImportCertificate", "d:\\dbs\\sh\\e19dt\\1103_100001\\cmd\\c\\sources\\Dev\\Management\\src\\ServiceHost\\Servicelets\\ExchangeCertificate\\Program\\ExchangeCertificateServer.cs"); try { server = ManageExchangeCertificate.FindLocalServer(topologyConfigurationSession); } catch (LocalServerNotFoundException) { flag = true; } if (flag || !ManageExchangeCertificate.IsServerRoleSupported(server)) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.RoleDoesNotSupportExchangeCertificateTasksException, ErrorCategory.InvalidOperation); } X509Store x509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine); try { x509Store.Open(OpenFlags.ReadWrite | OpenFlags.OpenExistingOnly); } catch (CryptographicException) { x509Store = null; } List<ServiceData> installed = new List<ServiceData>(); GetInstalledRoles(topologyConfigurationSession, server, installed); try { byte[] array = null; if (CertificateEnroller.TryAcceptPkcs7(exchangeCertificateRpc.ImportCert, out var thumbprint, out var untrustedRoot)) { X509Certificate2Collection x509Certificate2Collection = x509Store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false); if (x509Certificate2Collection.Count > 0) { if (!string.IsNullOrEmpty(exchangeCertificateRpc.ImportDescription)) { x509Certificate2Collection[0].FriendlyName = exchangeCertificateRpc.ImportDescription; } ExchangeCertificate exchangeCertificate = new ExchangeCertificate(x509Certificate2Collection[0]); UpdateServices(exchangeCertificate, server, installed); exchangeCertificateRpc.ReturnCert = exchangeCertificate; } return exchangeCertificateRpc.SerializeOutputParameters(rpcVersion); } if (untrustedRoot) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateUntrustedRoot, ErrorCategory.ReadError); } try { array = Convert.FromBase64String(exchangeCertificateRpc.ImportCert); } catch (FormatException) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateBase64DataInvalid, ErrorCategory.ReadError); } X509Certificate2 x509Certificate = null; try { X509KeyStorageFlags x509KeyStorageFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet; bool flag2 = password == null || password.Length == 0; X509Certificate2Collection x509Certificate2Collection2 = new X509Certificate2Collection(); if (exchangeCertificateRpc.ImportExportable) { x509KeyStorageFlags |= X509KeyStorageFlags.Exportable; } x509Certificate2Collection2.Import(array, flag2 ? null : password.AsUnsecureString(), x509KeyStorageFlags); x509Certificate = ManageExchangeCertificate.FindImportedCertificate(x509Certificate2Collection2); } catch (CryptographicException) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateDataInvalid, ErrorCategory.ReadError); } if (x509Certificate == null) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateDataInvalid, ErrorCategory.ReadError); } if (!string.IsNullOrEmpty(exchangeCertificateRpc.ImportDescription)) { x509Certificate.FriendlyName = exchangeCertificateRpc.ImportDescription; } if (x509Store.Certificates.Find(X509FindType.FindByThumbprint, x509Certificate.Thumbprint, validOnly: false).Count > 0) { return ExchangeCertificateRpc.SerializeError(rpcVersion, Strings.ImportCertificateAlreadyExists(x509Certificate.Thumbprint), ErrorCategory.WriteError); } x509Store.Add(x509Certificate); ExchangeCertificate exchangeCertificate2 = new ExchangeCertificate(x509Certificate); UpdateServices(exchangeCertificate2, server, installed); exchangeCertificateRpc.ReturnCert = exchangeCertificate2; } finally { x509Store?.Close(); } return exchangeCertificateRpc.SerializeOutputParameters(rpcVersion); } ...
We can see from this that most functions appear to be calling Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.ExchangeCertificateRpc()
. This has some interesting code relevant to deserialization:
// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc using System.Collections.Generic; using Microsoft.Exchange.Rpc.ExchangeCertificate; public ExchangeCertificateRpc(ExchangeCertificateRpcVersion version, byte[] inputBlob, byte[] outputBlob) { inputParameters = new Dictionary<RpcParameters, object>(); if (inputBlob != null) { switch (version) { case ExchangeCertificateRpcVersion.Version1: inputParameters = (Dictionary<RpcParameters, object>)DeserializeObject(inputBlob, customized: false); break; case ExchangeCertificateRpcVersion.Version2: inputParameters = BuildInputParameters(inputBlob); break; } } outputParameters = new Dictionary<RpcOutput, object>(); if (outputBlob != null) { switch (version) { case ExchangeCertificateRpcVersion.Version1: outputParameters = (Dictionary<RpcOutput, object>)DeserializeObject(outputBlob, customized: false); break; case ExchangeCertificateRpcVersion.Version2: outputParameters = BuildOutputParameters(outputBlob); break; } } }
Here we can see that the byte[] inputBlob
from earlier is passed to DeserializeObject(inputBlob, customized: false)
in the case that ExchangeCertificateRpcVersion
parameter passed in is ExchangeCertificateRpcVersion.Version1
.
Okay so already we know we have one limitation in that we need to set the version
parameter here to ExchangeCertificateRpcVersion.Version1
somehow.
Keeping this in mind lets explore further and look at the Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.DeserializeObject(inputBlob, customized:false)
call implementation.
// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc using System.IO; using Microsoft.Exchange.Data.Serialization; using Microsoft.Exchange.Diagnostics; private object DeserializeObject(byte[] data, bool customized) { if (data != null) { using (MemoryStream serializationStream = new MemoryStream(data)) { bool strictModeStatus = Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus(DeserializeLocation.ExchangeCertificateRpc); return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ExchangeCertificateRpc, strictModeStatus, allowedTypes, allowedGenerics).Deserialize(serializationStream); } } return null; }
Interesting so we can see that we create a new MemoryStream
object from our byte[] data
parameter and use this to create a serialization stream of type MemoryStream
. We then check using Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus
to see if DeserializeLocation.ExchangeCertificateRpc
requires strict mode for deserialization or not and we set the boolean strictModeStatus
to this result.
Finally we create a binary formatter using ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ExchangeCertificateRpc, strictModeStatus, allowedTypes, allowedGenerics)
and then call its Deserialize()
method on the serialized MemoryStream
object we created earlier using byte[] data
.
Note that before the November 2021 patch, this DeserializeObject
function actually looked like this:
// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc using System.IO; using Microsoft.Exchange.Data.Serialization; using Microsoft.Exchange.Diagnostics; private object DeserializeObject(byte[] data, bool customized) { if (data != null) { using (MemoryStream serializationStream = new MemoryStream(data)) { BinaryFormatter binaryFormatter = new BinaryFormatter(); if (customized) { binaryFormatter.Binder = new CustomizedSerializationBinder(); } return binaryFormatter.Deserialize(memoryStream); } } return null; }
As we can see the earlier code here was using BinaryFormatter
to deserialize the payload without using a proper SerializationBinder
or really any protection at all for that matter.
Looking At DeserializeObject() Deeper
Lets look at the November 2022 edition of Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.DeserializeObject(inputBlob, customized:false)
again:
// Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc using System.IO; using Microsoft.Exchange.Data.Serialization; using Microsoft.Exchange.Diagnostics; private object DeserializeObject(byte[] data, bool customized) { if (data != null) { using (MemoryStream serializationStream = new MemoryStream(data)) { bool strictModeStatus = Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus(DeserializeLocation.ExchangeCertificateRpc); return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.ExchangeCertificateRpc, strictModeStatus, allowedTypes, allowedGenerics).Deserialize(serializationStream); } } return null; }
What we want to check here now is the ExchangeBinaryFormatterFactor.CreateBinaryFormatter
call. What does the code for this look like?
// Microsoft.Exchange.Diagnostics.ExchangeBinaryFormatterFactory using System.Runtime.Serialization.Formatters.Binary; public static BinaryFormatter CreateBinaryFormatter(DeserializeLocation usageLocation, bool strictMode = false, string[] allowList = null, string[] allowedGenerics = null) { return new BinaryFormatter { Binder = new ChainedSerializationBinder(usageLocation, strictMode, allowList, allowedGenerics) }; }
Ah our good old friend ChainedSerializationBinder
and BinaryFormatter
. Looks like we will need to create a BinaryFormatter
serialized payload and ChainedSerializationBinder
will be the validator.
As mentioned in the article to bypass this logic we need to ensure that strictMode
is set to False
and that we are not using any fully qualified assembly name in the deny list defined in Microsoft.Exchange.Diagnostics.ChainedSerializationBinder.GlobalDisallowedTypesForDeserialization
, which will pretty much kill all publicly known .NET deserialization gadgets from ysoserial.NET.
For reference this is the code for ChainedSerializationBinder
in November 2021 Update:
// Microsoft.Exchange.Diagnostics.ChainedSerializationBinder using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Microsoft.Exchange.Diagnostics; public class ChainedSerializationBinder : SerializationBinder { private const string TypeFormat = "{0}, {1}"; private static readonly HashSet<string> AlwaysAllowedPrimitives = new HashSet<string> { typeof(string).FullName, typeof(int).FullName, typeof(uint).FullName, typeof(long).FullName, typeof(ulong).FullName, typeof(double).FullName, typeof(float).FullName, typeof(bool).FullName, typeof(short).FullName, typeof(ushort).FullName, typeof(byte).FullName, typeof(char).FullName, typeof(DateTime).FullName, typeof(TimeSpan).FullName, typeof(Guid).FullName }; private bool strictMode; private DeserializeLocation location; private Func<string, Type> typeResolver; private HashSet<string> allowedTypesForDeserialization; private HashSet<string> allowedGenericsForDeserialization; private bool serializationOnly; protected static HashSet<string> GlobalDisallowedTypesForDeserialization { get; private set; } = BuildDisallowedTypesForDeserialization(); protected static HashSet<string> GlobalDisallowedGenericsForDeserialization { get; private set; } = BuildGlobalDisallowedGenericsForDeserialization(); public ChainedSerializationBinder() { serializationOnly = true; } public ChainedSerializationBinder(DeserializeLocation usageLocation, bool strictMode = false, string[] allowList = null, string[] allowedGenerics = null) { this.strictMode = strictMode; allowedTypesForDeserialization = ((allowList != null && allowList.Length != 0) ? new HashSet<string>(allowList) : null); allowedGenericsForDeserialization = ((allowedGenerics != null && allowedGenerics.Length != 0) ? new HashSet<string>(allowedGenerics) : null); typeResolver = typeResolver ?? ((Func<string, Type>)((string s) => Type.GetType(s))); location = usageLocation; } public override void BindToName(Type serializedType, out string assemblyName, out string typeName) { string text = null; string text2 = null; InternalBindToName(serializedType, out assemblyName, out typeName); if (assemblyName == null && typeName == null) { assemblyName = text; typeName = text2; } } public override Type BindToType(string assemblyName, string typeName) { if (serializationOnly) { throw new InvalidOperationException("ChainedSerializationBinder was created for serialization only. This instance cannot be used for deserialization."); } Type type = InternalBindToType(assemblyName, typeName); if (type != null) { ValidateTypeToDeserialize(type); } return type; } protected virtual Type InternalBindToType(string assemblyName, string typeName) { return LoadType(assemblyName, typeName); } protected Type LoadType(string assemblyName, string typeName) { Type type = null; try { type = Type.GetType($"{typeName}, {assemblyName}"); } catch (TypeLoadException) { } catch (FileLoadException) { } if (type == null) { string shortName = assemblyName.Split(',')[0]; try { type = Type.GetType($"{typeName}, {shortName}"); } catch (TypeLoadException) { } catch (FileLoadException) { } if (type == null) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); IEnumerable<Assembly> source = assemblies.Where((Assembly x) => shortName == x.FullName.Split(',')[0]); Assembly assembly = (source.Any() ? source.First() : null); if (assembly != null) { type = assembly.GetType(typeName); } else { Assembly[] array = assemblies; foreach (Assembly assembly2 in array) { try { type = assembly2.GetType(typeName); if (!(type != null)) { continue; } return type; } catch { } } } } } return type; } protected virtual void InternalBindToName(Type serializedType, out string assemblyName, out string typeName) { assemblyName = null; typeName = null; } protected void ValidateTypeToDeserialize(Type typeToDeserialize) { if (typeToDeserialize == null) { return; } string fullName = typeToDeserialize.FullName; bool flag = strictMode; try { if (!strictMode && (allowedTypesForDeserialization == null || !allowedTypesForDeserialization.Contains(fullName)) && GlobalDisallowedTypesForDeserialization.Contains(fullName)) { flag = true; throw new InvalidOperationException($"Type {fullName} failed deserialization (BlockList)."); } if (typeToDeserialize.IsConstructedGenericType) { fullName = typeToDeserialize.GetGenericTypeDefinition().FullName; if (allowedGenericsForDeserialization == null || !allowedGenericsForDeserialization.Contains(fullName) || GlobalDisallowedGenericsForDeserialization.Contains(fullName)) { throw new BlockedDeserializeTypeException(fullName, BlockedDeserializeTypeException.BlockReason.NotInAllow, location); } } else if (!AlwaysAllowedPrimitives.Contains(fullName) && (allowedTypesForDeserialization == null || !allowedTypesForDeserialization.Contains(fullName) || GlobalDisallowedTypesForDeserialization.Contains(fullName)) && !typeToDeserialize.IsArray && !typeToDeserialize.IsEnum && !typeToDeserialize.IsAbstract && !typeToDeserialize.IsInterface) { throw new BlockedDeserializeTypeException(fullName, BlockedDeserializeTypeException.BlockReason.NotInAllow, location); } } catch (BlockedDeserializeTypeException ex) { DeserializationTypeLogger.Singleton.Log(ex.TypeName, ex.Reason, location, (flag || strictMode) ? DeserializationTypeLogger.BlockStatus.TrulyBlocked : DeserializationTypeLogger.BlockStatus.WouldBeBlocked); if (flag) { throw; } } } private static HashSet<string> BuildDisallowedGenerics() { return new HashSet<string> { typeof(SortedSet<>).GetGenericTypeDefinition().FullName }; } private static HashSet<string> BuildDisallowedTypesForDeserialization() { return new HashSet<string> { "Microsoft.Data.Schema.SchemaModel.ModelStore", "Microsoft.FailoverClusters.NotificationViewer.ConfigStore", "Microsoft.IdentityModel.Claims.WindowsClaimsIdentity", "Microsoft.Management.UI.Internal.FilterRuleExtensions", "Microsoft.Management.UI.FilterRuleExtensions", "Microsoft.Reporting.RdlCompile.ReadStateFile", "Microsoft.TeamFoundation.VersionControl.Client.PolicyEnvelope", "Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource", "Microsoft.VisualStudio.Editors.PropPageDesigner.PropertyPageSerializationService+PropertyPageSerializationStore", "Microsoft.VisualStudio.EnterpriseTools.Shell.ModelingPackage", "Microsoft.VisualStudio.Modeling.Diagnostics.XmlSerialization", "Microsoft.VisualStudio.Publish.BaseProvider.Util", "Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties", "Microsoft.VisualStudio.Web.WebForms.ControlDesignerStateCache", "Microsoft.Web.Design.Remote.ProxyObject", "System.Activities.Presentation.WorkflowDesigner", "System.AddIn.Hosting.AddInStore", "System.AddIn.Hosting.Utils", "System.CodeDom.Compiler.TempFileCollection", "System.Collections.Hashtable", "System.ComponentModel.Design.DesigntimeLicenseContextSerializer", "System.Configuration.Install.AssemblyInstaller", "System.Configuration.SettingsPropertyValue", "System.Data.DataSet", "System.Data.DataViewManager", "System.Data.Design.MethodSignatureGenerator", "System.Data.Design.TypedDataSetGenerator", "System.Data.Design.TypedDataSetSchemaImporterExtension", "System.Data.SerializationFormat", "System.DelegateSerializationHolder", "System.Drawing.Design.ToolboxItemContainer", "System.Drawing.Design.ToolboxItemContainer+ToolboxItemSerializer", "System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler", "System.IdentityModel.Tokens.SessionSecurityToken", "System.IdentityModel.Tokens.SessionSecurityTokenHandler", "System.IO.FileSystemInfo", "System.Management.Automation.PSObject", "System.Management.IWbemClassObjectFreeThreaded", "System.Messaging.BinaryMessageFormatter", "System.Resources.ResourceReader", "System.Resources.ResXResourceSet", "System.Runtime.Remoting.Channels.BinaryClientFormatterSink", "System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider", "System.Runtime.Remoting.Channels.BinaryServerFormatterSink", "System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider", "System.Runtime.Remoting.Channels.CrossAppDomainSerializer", "System.Runtime.Remoting.Channels.SoapClientFormatterSink", "System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider", "System.Runtime.Remoting.Channels.SoapServerFormatterSink", "System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider", "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter", "System.Runtime.Serialization.Formatters.Soap.SoapFormatter", "System.Runtime.Serialization.NetDataContractSerializer", "System.Security.Claims.ClaimsIdentity", "System.Security.Claims.ClaimsPrincipal", "System.Security.Principal.WindowsIdentity", "System.Security.Principal.WindowsPrincipal", "System.Security.SecurityException", "System.Web.Security.RolePrincipal", "System.Web.Script.Serialization.JavaScriptSerializer", "System.Web.Script.Serialization.SimpleTypeResolver", "System.Web.UI.LosFormatter", "System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem", "System.Web.UI.ObjectStateFormatter", "System.Windows.Data.ObjectDataProvider", "System.Windows.Forms.AxHost+State", "System.Windows.ResourceDictionary", "System.Workflow.ComponentModel.Activity", "System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector", "System.Xml.XmlDataDocument", "System.Xml.XmlDocument" }; } private static HashSet<string> BuildGlobalDisallowedGenericsForDeserialization() { return new HashSet<string>(); } }
Interesting to note that this doesn’t seem to contain the entries for System.Runtime.Remoting.ObjectRef
which was a new gadget chain just added with https://github.com/pwntester/ysoserial.net/pull/115 that relies on a rouge .NET remoting server like https://github.com/codewhitesec/RogueRemotingServer. There is a writeup on this at https://codewhitesec.blogspot.com/2022/01/dotnet-remoting-revisited.html that explains more but this would allow RCE via a serialized payload attached to the rouge .NET remoting server.
Anyway so from earlier we know that the strict mode is determined via the line bool strictModeStatus = Microsoft.Exchange.Data.Serialization.Serialization.GetStrictModeStatus(DeserializeLocation.ExchangeCertificateRpc);
so this provides our other bypass.
Lets check if the result of this is False
or not:
So from here we can likely supply a System.Runtime.Remoting.ObjectRef
, take advantage of the lack of strict checking on this, and get the whole exploit to work. The problem now is finding the whole chain to reach this vulnerable call and then trigger the deserialization.
January 2022 Patch Analysis
- No adjustments to the
ChainedSerializationBinder
deny list at all.
Here is the Jan 2022 version of the deny list:
private static HashSet<string> BuildDisallowedTypesForDeserialization() { return new HashSet<string> { "Microsoft.Data.Schema.SchemaModel.ModelStore", "Microsoft.FailoverClusters.NotificationViewer.ConfigStore", "Microsoft.IdentityModel.Claims.WindowsClaimsIdentity", "Microsoft.Management.UI.Internal.FilterRuleExtensions", "Microsoft.Management.UI.FilterRuleExtensions", "Microsoft.Reporting.RdlCompile.ReadStateFile", "Microsoft.TeamFoundation.VersionControl.Client.PolicyEnvelope", "Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource", "Microsoft.VisualStudio.Editors.PropPageDesigner.PropertyPageSerializationService+PropertyPageSerializationStore", "Microsoft.VisualStudio.EnterpriseTools.Shell.ModelingPackage", "Microsoft.VisualStudio.Modeling.Diagnostics.XmlSerialization", "Microsoft.VisualStudio.Publish.BaseProvider.Util", "Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties", "Microsoft.VisualStudio.Web.WebForms.ControlDesignerStateCache", "Microsoft.Web.Design.Remote.ProxyObject", "System.Activities.Presentation.WorkflowDesigner", "System.AddIn.Hosting.AddInStore", "System.AddIn.Hosting.Utils", "System.CodeDom.Compiler.TempFileCollection", "System.Collections.Hashtable", "System.ComponentModel.Design.DesigntimeLicenseContextSerializer", "System.Configuration.Install.AssemblyInstaller", "System.Configuration.SettingsPropertyValue", "System.Data.DataSet", "System.Data.DataViewManager", "System.Data.Design.MethodSignatureGenerator", "System.Data.Design.TypedDataSetGenerator", "System.Data.Design.TypedDataSetSchemaImporterExtension", "System.Data.SerializationFormat", "System.DelegateSerializationHolder", "System.Drawing.Design.ToolboxItemContainer", "System.Drawing.Design.ToolboxItemContainer+ToolboxItemSerializer", "System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler", "System.IdentityModel.Tokens.SessionSecurityToken", "System.IdentityModel.Tokens.SessionSecurityTokenHandler", "System.IO.FileSystemInfo", "System.Management.Automation.PSObject", "System.Management.IWbemClassObjectFreeThreaded", "System.Messaging.BinaryMessageFormatter", "System.Resources.ResourceReader", "System.Resources.ResXResourceSet", "System.Runtime.Remoting.Channels.BinaryClientFormatterSink", "System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider", "System.Runtime.Remoting.Channels.BinaryServerFormatterSink", "System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider", "System.Runtime.Remoting.Channels.CrossAppDomainSerializer", "System.Runtime.Remoting.Channels.SoapClientFormatterSink", "System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider", "System.Runtime.Remoting.Channels.SoapServerFormatterSink", "System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider", "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter", "System.Runtime.Serialization.Formatters.Soap.SoapFormatter", "System.Runtime.Serialization.NetDataContractSerializer", "System.Security.Claims.ClaimsIdentity", "System.Security.Claims.ClaimsPrincipal", "System.Security.Principal.WindowsIdentity", "System.Security.Principal.WindowsPrincipal", "System.Security.SecurityException", "System.Web.Security.RolePrincipal", "System.Web.Script.Serialization.JavaScriptSerializer", "System.Web.Script.Serialization.SimpleTypeResolver", "System.Web.UI.LosFormatter", "System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem", "System.Web.UI.ObjectStateFormatter", "System.Windows.Data.ObjectDataProvider", "System.Windows.Forms.AxHost+State", "System.Windows.ResourceDictionary", "System.Workflow.ComponentModel.Activity", "System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector", "System.Xml.XmlDataDocument", "System.Xml.XmlDocument" }; }
Looking at this in [[Meld]] shows that the deny list for ChainedSerializationBinder
did not change between November 2021 and January 2022. So we could use System.Runtime.Remoting.ObjRef
to bypass this deny list, potentially also allowing RCE on the latest version.
- Removed
Microsoft.Exchange.DxStore.Common.DxBinarySerializationUtil
which seemed to have some options for doing unsafe deserialization.
using System; using System.IO; using FUSE.Weld.Base; using Microsoft.Exchange.Diagnostics; using Microsoft.Exchange.DxStore.Server; namespace Microsoft.Exchange.DxStore.Common; public static class DxBinarySerializationUtil { private static readonly string[] allowedTypes = new string[101] { typeof(ExceptionUri).FullName, typeof(Ranges).FullName, typeof(Range).FullName, typeof(Target).FullName, typeof(CommonSettings).FullName, typeof(DataStoreStats).FullName, typeof(DxStoreAccessClientException).FullName, typeof(DxStoreAccessClientTransientException).FullName, typeof(DxStoreAccessReply).FullName, typeof(DxStoreAccessReply.CheckKey).FullName, typeof(DxStoreAccessReply.DeleteKey).FullName, typeof(DxStoreAccessReply.DeleteProperty).FullName, typeof(DxStoreAccessReply.ExecuteBatch).FullName, typeof(DxStoreAccessReply.GetAllProperties).FullName, typeof(DxStoreAccessReply.GetProperty).FullName, typeof(DxStoreAccessReply.GetPropertyNames).FullName, typeof(DxStoreAccessReply.GetSubkeyNames).FullName, typeof(DxStoreAccessReply.SetProperty).FullName, typeof(DxStoreAccessRequest).FullName, typeof(DxStoreAccessRequest.CheckKey).FullName, typeof(DxStoreAccessRequest.DeleteKey).FullName, typeof(DxStoreAccessRequest.DeleteProperty).FullName, typeof(DxStoreAccessRequest.ExecuteBatch).FullName, typeof(DxStoreAccessRequest.GetAllProperties).FullName, typeof(DxStoreAccessRequest.GetProperty).FullName, typeof(DxStoreAccessRequest.GetPropertyNames).FullName, typeof(DxStoreAccessRequest.GetSubkeyNames).FullName, typeof(DxStoreAccessRequest.SetProperty).FullName, typeof(DxStoreAccessServerTransientException).FullName, typeof(DxStoreBatchCommand).FullName, typeof(DxStoreBatchCommand.CreateKey).FullName, typeof(DxStoreBatchCommand.DeleteKey).FullName, typeof(DxStoreBatchCommand.DeleteProperty).FullName, typeof(DxStoreBatchCommand.SetProperty).FullName, typeof(DxStoreBindingNotSupportedException).FullName, typeof(DxStoreClientException).FullName, typeof(DxStoreClientTransientException).FullName, typeof(DxStoreCommand).FullName, typeof(DxStoreCommand.ApplySnapshot).FullName, typeof(DxStoreCommand.CreateKey).FullName, typeof(DxStoreCommand.DeleteKey).FullName, typeof(DxStoreCommand.DeleteProperty).FullName, typeof(DxStoreCommand.DummyCommand).FullName, typeof(DxStoreCommand.ExecuteBatch).FullName, typeof(DxStoreCommand.PromoteToLeader).FullName, typeof(DxStoreCommand.SetProperty).FullName, typeof(DxStoreCommand.UpdateMembership).FullName, typeof(DxStoreCommand.VerifyStoreIntegrity).FullName, typeof(DxStoreCommand.VerifyStoreIntegrity2).FullName, typeof(DxStoreCommandConstraintFailedException).FullName, typeof(DxStoreInstanceClientException).FullName, typeof(DxStoreInstanceClientTransientException).FullName, typeof(DxStoreInstanceComponentNotInitializedException).FullName, typeof(DxStoreInstanceKeyNotFoundException).FullName, typeof(DxStoreInstanceNotReadyException).FullName, typeof(DxStoreInstanceServerException).FullName, typeof(DxStoreInstanceServerTransientException).FullName, typeof(DxStoreInstanceStaleStoreException).FullName, typeof(DxStoreManagerClientException).FullName, typeof(DxStoreManagerClientTransientException).FullName, typeof(DxStoreManagerGroupNotFoundException).FullName, typeof(DxStoreManagerServerException).FullName, typeof(DxStoreManagerServerTransientException).FullName, typeof(DxStoreReplyBase).FullName, typeof(DxStoreRequestBase).FullName, typeof(DxStoreSerializeException).FullName, typeof(DxStoreServerException).FullName, typeof(DxStoreServerFault).FullName, typeof(DxStoreServerTransientException).FullName, typeof(HttpReply).FullName, typeof(HttpReply.DxStoreReply).FullName, typeof(HttpReply.ExceptionReply).FullName, typeof(HttpReply.GetInstanceStatusReply).FullName, typeof(HttpRequest).FullName, typeof(HttpRequest.DxStoreRequest).FullName, typeof(HttpRequest.GetStatusRequest).FullName, typeof(HttpRequest.GetStatusRequest.Reply).FullName, typeof(HttpRequest.PaxosMessage).FullName, typeof(InstanceGroupConfig).FullName, typeof(InstanceGroupMemberConfig).FullName, typeof(InstanceGroupSettings).FullName, typeof(InstanceManagerConfig).FullName, typeof(InstanceSnapshotInfo).FullName, typeof(InstanceStatusInfo).FullName, typeof(LocDescriptionAttribute).FullName, typeof(PaxosBasicInfo).FullName, typeof(PaxosBasicInfo.GossipDictionary).FullName, typeof(ProcessBasicInfo).FullName, typeof(PropertyNameInfo).FullName, typeof(PropertyValue).FullName, typeof(ReadOptions).FullName, typeof(ReadResult).FullName, typeof(WcfTimeout).FullName, typeof(WriteOptions).FullName, typeof(WriteResult).FullName, typeof(WriteResult.ResponseInfo).FullName, typeof(GroupStatusInfo).FullName, typeof(GroupStatusInfo.NodeInstancePair).FullName, typeof(InstanceMigrationInfo).FullName, typeof(KeyContainer).FullName, typeof(DateTimeOffset).FullName }; private static readonly string[] allowedGenerics = new string[6] { "System.Collections.Generic.ObjectEqualityComparer`1", "System.Collections.Generic.EnumEqualityComparer`1", "System.Collections.Generic.EqualityComparer`1", "System.Collections.Generic.GenericEqualityComparer`1", "System.Collections.Generic.KeyValuePair`2", "System.Collections.Generic.List`1" }; public static void Serialize(MemoryStream ms, object obj) { ExchangeBinaryFormatterFactory.CreateSerializeOnlyFormatter().Serialize(ms, obj); } public static object DeserializeUnsafe(Stream s) { return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.HttpBinarySerialize).Deserialize(s); } public static object Deserialize(Stream s) { return DeserializeSafe(s); } public static object DeserializeSafe(Stream s) { return ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SwordFish_AirSync, strictMode: false, allowedTypes, allowedGenerics).Deserialize(s); } }
- Added in
Microsoft.Exchange.DxStore.Common.IDxStoreDynamicConfig.cs
which has the following code:
namespace Microsoft.Exchange.DxStore.Common; public interface IDxStoreDynamicConfig { bool IsRemovePublicKeyToken { get; } bool IsSerializerIncompatibleInitRemoved { get; } bool EnableResolverTypeCheck { get; } bool EnableResolverTypeCheckException { get; } }
Exploit Chain
Lets start at the deserialization chain and work backwards.
Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.DeserializeObject Microsoft.Exchange.Management.SystemConfigurationTasks.ExchangeCertificateRpc.ExchangeCertificateRpc(ExchangeCertificateRpcVersion version, byte[] inputBlob, byte[] outputBlob) Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServerHelper.GetCertificate(int version, byte[] inputBlob) Microsoft.Exchange.Servicelets.ExchangeCertificate.ExchangeCertificateServer.GetCertificate(int version, byte[] inputBlob)
We can then use the Get-ExchangeCertificate
commandlet from https://docs.microsoft.com/en-us/powershell/module/exchange/get-exchangecertificate?view=exchange-ps and set a breakpoint inside Microsoft.Exchange.ExchangeCertificateServicelet.dll
specifically within the Microsoft.Exchange.Servicelets.ExchangeCertificate.GetCertificate
handler.
Unfortunately it seems like the current way things work we are sending a ExchangeCertificateRpcVersion rpcVersion
with a version of Version2
.
Exploited process is Microsoft.Exchange.ServiceHost.exe
which runs as NT AUTHORITY\SYSTEM
.
Technical Analysis
Looks like this was a heap buffer overflow in WebRTC which could allow for a drive by attack that would grant attackers RCE on a target system. No news as to whether or not this was used with a sandbox escape though, It was reported by Jan Vojtesek from the Avast Threat Intelligence team on 2022-07-01 according to https://chromereleases.googleblog.com/2022/07/stable-channel-update-for-desktop.html, yet interestingly https://chromereleases.googleblog.com/2022/07/chrome-for-android-update.html also note it affects Chrome for Android.
There is a real world exploit for this out in the wild but given the generally tight lipped news around this and that it was found from a threat intelligence team, I would imagine this may have been used in more targeted attacks, but still widely enough that a threat intelligence team picked up on it. Bit hard to tell though since I hadn’t heard about the Avast Threat Intelligence team prior to this; I imagine its possible one of their customers was targeted selectively and then they found out and notified Google.
With heap overflow bugs I generally err on the side of “well these things are harder to exploit” however with browsers you typically have access to a much wider arsenal to use for crafting the heap into a state that is desirable for exploitation purposes, so the risk is a bit higher here. That being said exploitation of such bugs tends to be a little more complex in most cases, particularly given recent mitigations. I’d still recommend patching this one if you can, but if not then you should try to disable WebRTC on your browsers until you can patch given in the wild exploitation.
Technical Analysis
This is a bypass for CVE-2022-21919 which is in turn a bypass for CVE-2021-34484. As noted at https://twitter.com/billdemirkapi/status/1508527492285575172, CVE-2022-21919 was already being exploited in the wild by using the binary from https://github.com/klinix5/ProfSvcLPE/blob/main/DoubleJunctionEoP/Release/UserProfileSvcEoP.exe.
The vulnerability, near as I can tell, occurs due to the CreateDirectoryJunction()
function inside profext.dll
not appropriately validating things before creating a directory junction between two directories. This can allow an attacker to create a directory junction between a directory they have access to and another directory that they should not have access to, thereby granting them the ability to plant files in sensitive locations and or read sensitive files.
The exploit code for this, which was originally at https://github.com/klinix5/SuperProfile but which got taken down, is now available at https://github.com/rmusser01/SuperProfile and its associated forks. I have taken this code and updated it and touched it up a bit into a Metasploit exploit module that is now available at https://github.com/rapid7/metasploit-framework/pull/16382.
This exploit code utilizes this vulnerability to plant a malicious comctl32.dll
binary in a location that the Narrator.exe
program will try to load the DLL from when it starts. By utilizing the ShellExecute
command with the runas
option, we can force a UAC prompt to come up that will run the consent.exe
program to run. If the PromptOnSecureDesktop
setting is set to 1
which is the default, this will result in consent.exe
running as SYSTEM
on the secure desktop, and a new narrator.exe
instance will also spawn as SYSTEM
on the secure desktop, which will then load the malicious comctl32.dll
DLL and allow us to execute our code as SYSTEM
.
Note that if PromptOnSecureDesktop
is set to 0 under the key HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
, then this LPE will not be possible as the UAC prompt will spawn as the current user vs as SYSTEM
on the restricted desktop, and therefore we will not achieve privilege elevation, so this is a workaround for the vulnerability whilst it is not patched.
It should be noted that as this stands the current exploit requires valid credentials for another user on the system who is a non-admin user and who has permissions to log into the target computer. They must also have a profile under C:\Users
for the exploit to function in its current state. There has been some rumors that it might be possible to do this without a secondary login, however nothing concrete has been found so far, so we are considering this a prerequisite for exploitation for the time being.
We, aka Rapid7, have reported this vulnerability to Microsoft and have given KLINIX5, who originally found this vulnerability and wrote the original exploit code, full credit for the discovery, however Microsoft have only given us this CVE number and have not provided a timeline on when they expect a fix for this vulnerability at this time. It is therefore recommended to use the mitigation above until an appropriate fix is developed.
Technical Analysis
Appears there may have been some confusion here. As noted at https://twitter.com/wdormann/status/1508555477491269638 and at https://twitter.com/BillDemirkapi/status/1508527487655067660/photo/1, the attackers tried to download UserProfileSvcEoP.exe
from https://github.com/klinix5/ProfSvcLPE/blob/main/DoubleJunctionEoP/Release/UserProfileSvcEoP.exe. If you look at https://github.com/klinix5/ProfSvcLPE/blob/main/write-up.docx you can see this is actually a patch bypass for CVE-2021-34484, and was later fixed by CVE-2022-21919.
Ironically enough this later got another patch bypass in the form of CVE-2022-26904 which at the time of writing is still unpatched.
All of these vulnerabilities exploit a logic flaw whereby the User Profile Service had a CreateDirectoryJunction()
function that did not appropriately validate its input to ensure it wasn’t using symbolic links along any point of the path prior to creating a directory junction between two directories. This could be abused by attackers manipulating paths along the file path to gain code execution as the SYSTEM user by planting a DLL in a sensitive location which would then be loaded by a privileged process.
Technical Analysis
CVE-2020-11899 (one of the Ripple20 bugs) has now been reported as exploited in the wild as per https://www.cisa.gov/known-exploited-vulnerabilities-catalog, No evidence that other bugs have been exploited though as of the time of writing.
Any reason for this high rating? Usually exploitation of these types of bugs require some sort of social engineering to convince the user to open the document which means that the exploitability will be lowered somewhat. This is because a bug that is noninteractive and can be exploited even against a user actively looking out for suspicious activity will be more readily exploitable vs something like this that could be caught and prevented by a vigilant user not opening suspicious attachments.