简介SharePoint 是一个用于共享和管理内容、知识和应用程序以支持团队合作、快速查找信息以及在整个组织内无缝协作的平台。超过 200,000 个组织和 1.9 亿人将 SharePoint 用于 Intranet…
0x00 简介
SharePoint 是一个用于共享和管理内容、知识和应用程序以支持团队合作、快速查找信息以及在整个组织内无缝协作的平台。超过 200,000 个组织和 1.9 亿人将 SharePoint 用于 Intranet、团队网站和内容管理。上面的数字足以看出,这始终是安全研究人员寻找漏洞的一大目标。
使用 SharePoint,用户可以创建与任何其他网站一样工作的 Intranet(或 Intranet 系统)。除了组织的大站点外,sharepoint 还可以为每个组和内部部门划分小的子站点。此外,这是一个很棒的内容共享管理平台,具有可定制的列表。Sharepoint 上内置了某些类型的列表,例如图像列表、文档列表、表单……除了内置列表之外,用户还可以安装新列表并根据需要自定义该列表的属性。用于自定义 Sharepoint 的强大工具集是 Sharepoint Designer 和 InfoPath Designer。
0x01 CVE-2022-22005
Microsoft 的 2022 年 2 月补丁修复了代码 CVE-2022-22005 的漏洞。此漏洞允许攻击者远程执行代码,在 CVSSv3 计算器上得分为 8.8。受影响的版本如下所列
- Microsoft SharePoint Server 订阅版
- 微软 SharePoint 服务器 2019
- Microsoft SharePoint 企业服务器 2013 服务包 1
- 微软 SharePoint 企业服务器 2016
下面的分析是在 Microsoft SharePoint Enterprise Server 2016 上进行的。
0x02 补丁分析
安装 Sharepoint 2016 的 1 月和 2022 年 2 月补丁,收集 Sharepoint dll 文件并反编译成源。然后添加一些过滤后步骤以删除不必要的元素(评论,...)。最后比较这两个补丁,找出开发者在哪里打补丁的代码。反序列化补丁位置位于 Microsoft.Office.Server.Internal.Charting.UI.WebControls.ChartPreviewImage.loadChartImage()
该补丁使用绑定器来限制允许反序列化的类型,这是微软过去针对此类错误所做的。关于反序列化漏洞,您可以在此处了解更多信息。
0x03跟踪代码
稍微了解一下图表,这是一个 webpartpage - Sharepoint 上的一个页面组件。所以可以理解为,为了让数据去反序列化位置,必须有一个拥有创建页面权限的用户账号。结合调试和创建使用图表的页面,当图表加载数据时,代码被命中。观察导致漏洞的函数,位于缓冲区变量中的反序列化数据是通过 FetchBinaryData(sessionKey) 函数设置的。
// Microsoft.Office.Server.Internal.Charting.UI.WebControls.ChartPreviewImage.loadChartImage()
private ChartImageSessionBlock loadChartImage()
{
byte[] buffer = CustomSessionState.FetchBinaryData(this.sessionKey);
ChartImageSessionBlock result = null;
using (
MemoryStream memoryStream = new MemoryStream(buffer)
)
{
IFormatter formatter = new BinaryFormatter();
result = (ChartImageSessionBlock)formatter.Deserialize(memoryStream);
}
return result;
}
该代码与 Sharepoint 中的会话状态有关。这是一种将对象状态存储在 Sharepoint 中的机制,该状态可以是文件、图像……或者特别是在这种情况下是序列化的 ChartImageSessionBlock 对象。这些状态将作为二进制数据存储在数据库中并映射到会话密钥。所以要利用这个漏洞我们需要控制数据库中的二进制数据,然后通过loadChartImage函数反序列化任意对象。通过在调试过程中使用 Burp Suite 工具,我们可以获得触发漏洞的请求。
GET /_layouts/15/Chart/WebUI/Controls/ChartPreviewImage.aspx?sk=5264ebfb259840faa703bdbc976e069b_74929f85360d499d9f1d4f337bf49300&hash=2551012 HTTP/1.1
Host: sharepoint2016:33257
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Referer: http://sharepoint2016:33257/SitePages/testpage.aspx
Cookie: stsSyncAppName=Client; stsSyncIconPath=; WSS_FullScreenMode=false
Connection: Keep-Alive
这里的变量sk是传递给 FetchBinaryData 函数的 sessionKey,它的格式为 guid1_guid2,其中 guid1 是数据库的 id,guid2 是 ChartImageSessionBlock 的 id。为了利用该漏洞,我们将强制 guid2 使用包含任意二进制数据的另一个会话状态的 id。接下来要做的是弄清楚如何将任意二进制数据放入数据库中的会话状态表中。
ZDI 网站上发布了一篇关于先前漏洞的文章,其代码为 CVE-2021-27076,与会话状态相关,使用信息路径表单上的附件机制。当开始在信息路径列表中创建新项目时,该项目将使用itemId的会话密钥进行注册。接下来,当将文件附加到这个新项目时,该文件将作为二进制数据保存在数据库中,键为attachmentId。
附件文件中有任意二进制数据,attachmentId是我们触发漏洞所需要的。问题是在 infolist 中创建新项目时,只会返回itemId作为响应。通过搭建实验室,发现attachmentId的值在 item 的 binarydata 中,所以我们需要想办法通过itemId来获取\**attachmentId**。ZDI 文章还展示了如何解决这个问题,即将**itemId**重播到 FormServerAttachments.aspx,它将获取 item 的 binarydata 并将其作为文件返回。
这里有两个方向可以找到对FormServerAttachments.aspx的正确请求,一个是尝试功能,另一个是阅读代码并自己制作请求。第一个选项会更好,因为它可以节省时间,而且我们也会得到一个正确格式的请求。如果无法确定功能,则必须按照选项2读取代码。因为二进制文件作为文件返回,所以 FileDownload 函数引起了我的注意。
// Microsoft.Office.InfoPath.Server.Controls.FormServerAttachments.FileDownload(HttpContext)
private static bool FileDownload(HttpContext context)
{
string text = context.Request.QueryString["fid"];
string text2 = context.Request.QueryString["sid"];
string value = context.Request.QueryString["key"];
string strA = context.Request.QueryString["dl"];
int num = 0;
string empty = string.Empty;
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(value) || (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0 && string.Compare(strA, "ip", StringComparison.OrdinalIgnoreCase) != 0))
{
ULS.SendTraceTag(1831874679U, ULSCat.msoulscat_formservices_runtime, ULSTraceLevel.Medium, "Invalid request incorrect or missing query strings: {0}", new object[]
{
context.Request.Url.ToString()
});
return false;
}
using (new GlobalStorageContext(text))
{
try
{
SPSite spsite = SiteAndWebCache.Fetch().EnsureRequestSite();
Solution solutionById = SolutionCache.GetSolutionById(spsite, new SolutionIdentity(text2));
if (Canary.VerifyCanaryFromCookie(context, spsite, solutionById))
{
context.Response.Clear();
context.Response.Cache.SetExpires(DateTime.Now.AddDays(2.0));
using (BinaryWriter binaryWriter = new BinaryWriter(context.Response.OutputStream))
{
Base64DataStorage.Base64DataItem item = null;
StreamUtils.DeserializeObjectsFromString(value, delegate (EnhancedBinaryReader binaryReader)
{
item = new Base64DataStorage.Base64DataItem(binaryReader);
DocumentChildState.StateInfo stateInfo = new DocumentChildState.StateInfo();
((IBinaryDeserializable)stateInfo).Deserialize(binaryReader);
StateKey stateKey = StateKey.ParseKey(stateInfo.SerializedKey);
item.EnsureData(stateKey);
});
byte[] dataAsBytes = item.GetDataAsBytes();
using (Stream stream = new MemoryStream(dataAsBytes, false))
{
if (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0)
{
context.Response.AppendHeader("Content-Disposition", "attachment;filename=\"image\"");
context.Response.AppendHeader("X-Download-Options", "noopen");
context.Response.ContentType = ImageUtils.GetContentType(dataAsBytes);
return InlinePicture.ReadInfoFromStream(binaryWriter, stream);
}
context.Response.ContentType = "application/octet-stream";
if (FileAttachment.ReadInfoFromStream(binaryWriter, out num, out empty, stream))
{
FilePathUtils.AddFileDownloadHttpHeader(context, empty);
return true;
}
return false;
}
}
}
ULS.SendTraceTag(1831874680U, ULSCat.msoulscat_formservices_runtime, ULSTraceLevel.Verbose, "Can't verify canary from cookie for FileDownload");
return false;
}
catch (InfoPathException)
{
ULS.SendTraceTag(1831874681U, ULSCat.msoulscat_formservices_runtime, ULSTraceLevel.Medium, "InfoPathException occurred downloading fileattachment or inline picture");
}
}
return false;
}
幸运的是,请求所需的变量非常明显**——fid**、sid、key、dl。让我们深入了解一下组件,以下代码是错误返回条件
// fid -> text, sid -> text2, key -> value, dl -> strA
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(value) || (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0 && string.Compare(strA, "ip", StringComparison.OrdinalIgnoreCase) != 0))
所以所需的参数必须是非空的,其中dl必须是字符串 'fa' 或 'ip'。
// fid -> text, sid -> text2, key -> value, dl -> strA
SPSite spsite = SiteAndWebCache.Fetch().EnsureRequestSite();
Solution solutionById = SolutionCache.GetSolutionById(spsite, new SolutionIdentity(text2));
if (Canary.VerifyCanaryFromCookie(context, spsite, solutionById))
{
...
}
此代码从sid获取 solutionId并使用 cookie 中的 infopath canary 对其进行身份验证,例如像这样的 cookie:
_InfoPath_CanaryValueAGQX2G3RUCCXQRUNZHR3UB7IIEMSOL2MNFZXI4ZPORSXG5C7NFXGM327NRUXG5BPJF2GK3JPORSW24DMMF2GKLTYONXCWMKZLBZTE4TDI5WXC4ZSIIZGIUTINE4EI6DBGFWVKNKDLFZGSTJYLFNHE33VMJ5EGSLEOM=KBxeU4WXMZ3Yg8v0ZPZfAWcpoiLL/R3sfejthMFTfL1x9GqMoiIOMSS9XrT0gguJmdn0Yj2qw0gqlDJXT7X49A==|637806206864107501
此 cookie 有一个格式为“_InfoPath_CanaryValue”+ 后缀的键。后缀是要查找的**sid 。**接下来是从会话密钥获取二进制数据的代码。
// fid -> text, sid -> text2, key -> value, dl -> strA
Base64DataStorage.Base64DataItem item = null;
StreamUtils.DeserializeObjectsFromString(value, delegate (EnhancedBinaryReader binaryReader)
{
item = new Base64DataStorage.Base64DataItem(binaryReader);
DocumentChildState.StateInfo stateInfo = new DocumentChildState.StateInfo();
((IBinaryDeserializable)stateInfo).Deserialize(binaryReader);
StateKey stateKey = StateKey.ParseKey(stateInfo.SerializedKey);
item.EnsureData(stateKey);
});
byte[] dataAsBytes = item.GetDataAsBytes();
会话状态key会从key变量中取回,我们看DeserializeObjectsFromString函数的细节
// fid -> text, sid -> text2, key -> value, dl -> strA
internal static void DeserializeObjectsFromString(string value, Action<EnhancedBinaryReader> readerMethod)
{
using (Base64Stream base64Stream = new Base64Stream(value))
{
using (EnhancedBinaryReader enhancedBinaryReader = new EnhancedBinaryReader(base64Stream))
{
readerMethod(enhancedBinaryReader);
}
}
}
所以key需要是 base64 格式,参见 Base64DataStorage.Base64DataItem(binaryReader) 函数。
// Microsoft.Office.InfoPath.Server.SolutionLifetime.Base64DataStorage.Base64DataItem.Base64DataItem(EnhancedBinaryReader)
internal Base64DataItem(EnhancedBinaryReader reader)
{
this._state = (Base64ItemState)reader.ReadCompressedInt();
this._sessionDataType = (Base64DataStorage.Base64DataItem.DataTypeInSessionState)reader.ReadCompressedInt();
this._itemId = new Base64SerializationId(reader);
}
所以key结构中的前 3 个位置将是
- base64ItemState (int)
- dataTypeInSessionState (int)
- base64SerializationId(引导字符串)
让我们看看函数 DocumentChildState.StateInfo.Deserialize(binaryReader)
// DocumentChildState.StateInfo.Deserialize(binaryReader)
void IBinaryDeserializable.Deserialize(EnhancedBinaryReader reader)
{
this._serializedKey = reader.ReadString();
this._size = reader.ReadCompressedInt();
this._version = reader.ReadCompressedInt();
}
所以key结构中接下来的 3 个位置将是
- 序列化键(字符串)
- 大小(整数)
- 版本(整数)
接下来让我们考虑哪些组件需要正确的值。获取会话状态密钥的部分如下
StateKey stateKey = StateKey.ParseKey(stateInfo.SerializedKey);
item.EnsureData(stateKey);
所以serializedKey有guid1_guid2的形式,其中guid1是数据库id,guid2是我们输入的itemId,接下来看item.EnsureData(stateKey)函数
// Microsoft.Office.InfoPath.Server.SolutionLifetime.Base64DataStorage.Base64DataItem.EnsureData(StateKey)
internal void EnsureData(StateKey stateKey)
{
if (this.State == Base64ItemState.DelayLoad)
{
byte[] sessionData = StateManager.GetManager(HttpContext.Current).PeekState(stateKey); // get binary data from stateKey
this.SetSessionData(sessionData);
return;
}
if (this.State == Base64ItemState.Removed)
{
throw new InfoPathLocalizedException(InfoPathResourceManager.Ids.ServerGenericError, new string[0]);
}
}
必须满足第一个条件才能从数据库中获取binarydata,所以base64ItemState必须有枚举Base64ItemState.DelayLoad的值,见Base64ItemState里面
internal enum Base64ItemState
{
NoChange,
Updated,
Removed,
New,
DelayLoad // 4
}
从那里 base64ItemState 必须是 4。接下来看看 dataTypeInSessionState 的枚举值
private enum DataTypeInSessionState
{
Unknown,
Utf8String,
ByteArray // 2
}
我们需要的数据是以二进制数据形式存储在会话状态表中的,所以dataTypeInSessionState的值必须是2。综上,key的结构如下。
获取二进制数据后,代码将其作为文件返回
using (Stream stream = new MemoryStream(dataAsBytes, false))
{
if (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0)
{
context.Response.AppendHeader("Content-Disposition", "attachment;filename=\"image\"");
context.Response.AppendHeader("X-Download-Options", "noopen");
context.Response.ContentType = ImageUtils.GetContentType(dataAsBytes);
return InlinePicture.ReadInfoFromStream(binaryWriter, stream);
}
context.Response.ContentType = "application/octet-stream";
if (FileAttachment.ReadInfoFromStream(binaryWriter, out num, out empty, stream))
{
FilePathUtils.AddFileDownloadHttpHeader(context, empty);
return true;
}
return false;
}
所以dl必须有值'ip'。发送到 FormServerAttachments.aspx 的变量具有以下形式
0x04 利用步骤
经过详细分析,利用步骤总结如下:
- 在站点上创建一个信息路径列表。
- 打开表单以在列表中创建一个新项目,保存响应中的itemId。
- 附件文件包含该项目的有效负载,但不要按保存,以便可以将会话状态保存在数据库中。
- 将第2步获取的itemId信息放入发送到FormServerAttachments.aspx的请求中,保存响应中的attachmentId信息。
- 在请求中包含attachmentId以在 ChartPreviewImage 中触发反序列化。
0x05 条件
默认情况下,普通帐户有权创建子站点,并且该帐户将拥有新站点的完全权限。因此,我们只需要一个具有默认权限的帐户即可利用该漏洞。
0x06 概念证明
0x07 参考
本文转载自公众号 甲方安全建设
- 本文作者: 带头大哥
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1384
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!