作者:Ivan1ee@360云影实验室
0X00 前言
Newtonsoft.Json,这是一个开源的Json.Net库,官方地址:https://www.newtonsoft.com/json ,一个读写Json效率非常高的.Net库,在做开发的时候,很多数据交换都是以json格式传输的。而使用Json的时候,开发者很多时候会涉及到几个序列化对象的使用:DataContractJsonSerializer,JavaScriptSerializer 和 Json.NET即Newtonsoft.Json。大多数人都会选择性能以及通用性较好Json.NET,这个虽不是微软的类库,但却是一个开源的世界级的Json操作类库,从下面的性能对比就可以看到它的性能优点。
用它可轻松实现.Net中所有类型(对象,基本数据类型等)同Json之间的转换,在带来便捷的同时也隐藏了很大的安全隐患,在某些场景下开发者使用DeserializeObject方法序列化不安全的数据,就会造成反序列化漏洞从而实现远程RCE攻击,本文笔者从原理和代码审计的视角做了相关介绍和复现。
0X01 Json.Net序列化
在Newtonsoft.Json中使用JSONSerializer可以非常方便的实现.NET对象与Json之间的转化,JSONSerializer把.NET对象的属性名转化为Json数据中的Key,把对象的属性值转化为Json数据中的Value,如下Demo,定义TestClass对象
并有三个成员,Classname在序列化的过程中被忽略(JsonIgnore),此外实现了一个静态方法ClassMethod启动进程。 序列化过程通过创建对象实例分别给成员赋值,
用JsonConvert.SerializeObject得到序列化后的字符串
Json字符串中并没有包含方法ClassMethod,因为它是静态方法,不参与实例化的过程,自然在testClass这个对象中不存在。这就是一个最简单的序列化Demo。为了尽量保证序列化过程不抛出异常,笔者引入 SerializeObject方法的第二个参数并实例化创建JsonSerializerSettings,下面列出属性
修改代码添加 TypeNameAssemblyFormatHandling.Full、TypeNameHandling.ALL
将代码改成这样后得到的testString变量值才是笔者想要的,打印的数据中带有完整的程序集名等信息。
0x02 Json.Net反序列化
2.1、反序列化用法
反序列过程就是将Json字符串转换为对象,通过创建一个新对象的方式调用JsonConvert.DeserializeObject方法实现的,传入两个参数,第一个参数需要被序列化的字符串、第二个参数设置序列化配置选项来指定JsonSerializer按照指定的类型名称处理,其中TypeNameHandling可选择的成员分为五种
默认情况下设置为TypeNameHandling.None,表示Json.NET在反序列化期间不读取或写入类型名称。具体代码可参考以下
2.2、攻击向量—ObjectDataProvider
漏洞的触发点也是在于TypeNameHandling这个枚举值,如果开发者设置为非空值、也就是对象(Objects) 、数组(Arrays) 、自动识别 (Auto) 、所有值(ALL) 的时候都会造成反序列化漏洞,为此官方文档里也标注了警告,当您的应用程序从外部源反序列化JSON时应谨慎使用TypeNameHandling。
笔者继续选择ObjectDataProvider类方便调用任意被引用类中的方法,具体有关此类的用法可以看一下《.NET高级代码审计(第一课)XmlSerializer反序列化漏洞》,首先来序列化TestClass
指定TypeNameHandling.All、TypeNameAssemblyFormatHandling.Full后得到序列化后的Json字符串
{"$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35","ObjectInstance":{"$type":"WpfApp1.TestClass, WpfApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","Name":null,"Age":0},"MethodName":"ClassMethod","MethodParameters":{"$type":"MS.Internal.Data.ParameterCollection, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35","$values":["calc.exe"]},"IsAsynchronous":false,"IsInitialLoadEnabled":true,"Data":null,"Error":null}
如何构造System.Diagnostics.Process序列化的Json字符串呢?笔者需要做的工作替换掉ObjectInstance的$type、MethodName的值以及MethodParameters的$type值,删除一些不需要的Member、最终得到的反序列话Json字符串如下
{
'$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35',
'MethodName':'Start',
'MethodParameters':{
'$type':'System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
'$values':['cmd','/c calc']
},
'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}
再经过JsonConvert.DeserializeObject反序列化(注意一点指定TypeNameHandling的值一定不能是None),成功弹出计算器。
2.3、攻击向量—WindowsIdentity
WindowsIdentity类位于System.Security.Principal命名空间下。顾名思义,用于表示基于Windows认证的身份,认证是安全体系的第一道屏障肩负着守护着整个应用或者服务的第一道大门,此类定义了Windows身份一系列属性
对于用于表示认证类型的AuthenticationType属性来说,在工作组模式下返回NTLM。对于域模式,如果操作系统是Vista或者以后的版本,该属性返回Negotiate,表示采用SPNEGO认证协议。而对于之前的Windows版本,则该属性值为Kerberos。Groups属性返回WindowsIdentity对应的Windows帐号所在的用户组(User Group),而IsGuest则用于判断Windows帐号是否存在于Guest用户组中。IsSystem属性则表示Windows帐号是否是一个系统帐号。对于匿名登录,IIS实际上会采用一个预先指定的Windows帐号进行登录。而在这里,IsAnonymous属性就表示该WindowsIdentity对应的Windows帐号是否是匿名帐号。
2.3.1、ISerializable
跟踪定义得知继承于ClaimsIdentity类,并且实现了ISerializable接口
查看定义得知,只有一个方法GetObjectData
在.NET运行时序列化的过程中CLR提供了控制序列化数据的特性,如:OnSerializing、OnSerialized、NonSerialized等。为了对序列化数据进行完全控制,就需要实现Serialization.ISeralizable接口,这个接口只有一个方法,即 GetObjectData,第一个参数SerializationInfo包含了要为对象序列化的值的合集,传递两个参数给它:Type和IFormatterConverter,其中Type参数表示要序列化的对象全名(包括了程序集名、版本、公钥等),这点对于构造恶意的反序列化字符串至关重要
另一方面GetObjectData又调用SerializationInfo 类提供的AddValue多个重载方法来指定序列化的信息,AddValue添加的是一组<key,value> ;GetObjectData负责添加好所有必要的序列化信息。
2.3.2、ClaimsIdentity
ClaimsIdentity(声称标识)位于System.Security.Claims命名空间下,首先看下类的定义
其实就是一个个包含了claims构成的单元体,举个栗子:驾照中的“身份证号码:000000”是一个claim、持证人的“姓名: Ivan1ee”是另一个claim、这一组键值对构成了一个Identity,具有这些claims的Identity就是ClaimsIdentity,通常用在登录Cookie验证,如下代码
一般使用的场景我想已经说明白了,现在来看下类的成员有哪些,能赋值的又有哪些?
参考官方文档可以看到 Lable、BootstrapContext、Actor三个属性具备了set
查阅文档可知,这几个属性的原始成员分别为actor、bootstrapContext、lable如下
ClaimsIdentity类初始化方法有两个重载,并且通过前文介绍的SerializationInfo来传入数据,最后用Deserialize反序列化数据。
追溯的过程有点像框架类的代码审计,跟踪到Deserialize方法体内,查找BootstrapContextKey才知道原来它还需要被外层base64解码后带入反序列化
2.3.3、打造Poc
回过头来想一下,如果使用GetObjectData类中的AddValue方法添加“key : System.Security.ClaimsIdentity.bootstrapContext“、”value : base64编码后的payload“,最后实现System.Security.Principal.WindowsIdentity.ISerializable接口就能攻击成功。首先定义WindowsIdentityTest类
笔者用ysoserial生成反序列化Base64 Payload赋值给BootstrapContextKey,实现代码如下
到这步生成变量obj1的值就是一段poc,但还需改造一下,将$type值改为System.Security.Principal.WindowsIdentity完全限定名
最后改进后交给反序列化代码执行,抛出异常之前触发计算器,效果如下图
0x03 代码审计视角
从代码审计的角度其实很容易找到漏洞的污染点,通过前面几个小节的知识能发现需要满足一个关键条件非TypeNameHandling.None的枚举值都可以被反序列化,例如以下Json类
都设置成TypeNameHandling.All,攻击者只需要控制传入参数 _in便可轻松实现反序列化漏洞攻击。Github上很多的json类存在漏洞,例如下图
代码中改用了Auto这个值,只要不是None值在条件许可的情况下都可以触发漏洞,笔者相信肯定还有更多的漏洞污染点,需要大家在代码审计的过程中一起去发掘。
0x04 案例复盘
最后再通过下面案例来复盘整个过程,全程展示在VS里调试里通过反序列化漏洞弹出计算器。
- 输入http://localhost:5651/Default Post加载value值
- 通过JsonConvert.DeserializeObject 反序列化 ,并弹出计算器
最后附上动图
0x05 总结
Newtonsoft.Json库在实际开发中使用率还是很高的,攻击场景也较丰富,作为漏洞挖掘者可以多多关注这个点,攻击向量建议选择ObjectDataProvider,只因生成的Poc体积相对较小。最后.NET反序列化系列课程笔者会同步到 https://github.com/Ivan1ee/ 、https://ivan1ee.gitbook.io/ ,后续笔者将陆续推出高质量的.NET反序列化漏洞文章,请大伙持续关注。