◆吴红
Oj库反序列化攻击面分析
◆吴红
(国网眉山供电公司 四川 620010)
Oj是一个快速的JSON解析Ruby库,由于它极其高效且易用,根据Github统计,有上万项目以其作为依赖项。Oj库默认使用一个类型系统来解析Ruby对象,这允许应用代码在反序列化JSON字符串时恢复原始的对象,但这种能力可能导致安全漏洞,如远程代码执行或拒绝服务。本文将分析Oj库反序列化时可能造成的漏洞,并给出消除此类漏洞的建议。
Oj;反序列化漏洞;类型系统
Oj(https://github.com/ohler55/oj)是一个快速的JSON解析及对象序列化器。它可以序列化绝大多数的Ruby对象,在反序列化时则解析JSON字符串中约定的格式来恢复对象信息。不安全的反序列化作为OWASP TOP10中的一种通用漏洞类型[1],在各类编程语言中广泛存在,近年来在Java应用中尤其严重,一些知名Java组件,如FastJSON[2]、XStream等频频爆出反序列化漏洞,这类漏洞特点是利用简单、危害巨大,通常能造成远程代码执行。由此看来,Oj提供的反序列化对象的能力同样是一种攻击面,值得深入研究。
Oj提供两个核心API用户序列化与反序列化过程:
(1)Oj.load(json,options={}){ ... }
(2)Oj.dump(obj,options={})
Oj.load方法用于反序列化一个字符串,返回类型取决于具体配置。可以全局配置Oj选项,存储在Oj.default_options变量中。我们仅关注反序列化时可能造成安全风险的一些配置:
(1)auto_define
(2)mode
(3)create_additions与create_id
auto_define选项指定反序列化时类名不存在时是否进行自动定义;mode选项指定Oj模式;create_additions与create_id选项为一组,指定特定情况下反序列化对象的键名,之后会详细说明。
Oj.dump方法用于将一个Ruby对象序列化为JSON字符串。
Oj当前支持6种模式[3]:strict、null、compat、rails、object、custom,我们对object模式感兴趣,这也是默认的模式,因为该模式允许将Ruby对象进行序列化与反序列化。当使用object模式时,Oj遵循一组特定的编码来确定JSON字符串中哪些部分需要重建为对象或类。具体规则如下:
(1)原生JSON类型,true,false,nil,String,Hash,Array,Numbers正常编码。
(2)Symbol类型编码为以“:”开头的字符串。
(3)以“^”开头的键名表示这是一个特殊的键。
(4)以':','^i'或'^r开头的字符串,将这三种前缀编码为unicode字符串形式。
(5)一个"^c "的JSON对象键表示该值应该被转换为一个Ruby类。
(6)一个"^t"的JSON对象键表示该值应该被转换为Ruby Time。
(7)一个"^o"的JSON对象键表示该值应该被转换为Ruby对象。JSON对象中的第一个条目必须是一个带有"^o"键的类。在这之后,每个条目都被视为Object的一个变量,其中的键是没有前面的'@'的变量名。
(8)一个"^u"JSON对象的键表示该值应该被转换为Ruby Struct。JSON对象中的第一个条目必须是一个带有"^u"键的类。之后,每个条目在结构中被赋予一个数字位置,并被用作JSON对象的键。
(9)当对一个对象进行编码时,如果变量名称不是以'@'字符开始,那么名称前面会有一个'~'字符。
(10)如果一个Hash条目有一个不是字符串或符号的键,那么该条目将被编码为"^#n"形式的键,其中n是一个十六进制数字。值是一个数组,其中第一个元素是Hash中的键,第二个元素是值。
(11)在一个对象或数组中的"^i"JSON条目是被编码的Ruby对象的ID。当circular标志被设置时被使用。它可以出现在一个JSON对象或JSON数组中。在一个对象中,"^i"键有一个相应的引用。在一个数组中,该序列将包括一个嵌入的引用号。
(12)一个对象中的"^r"JSON条目是对已经出现在JSON字符串中的一个对象或数组的引用。它必须与之前的"^i"引用号相匹配。
(13)如果一个数组元素是一个字符串,并且以"^i"开头,那么第一个字符'^'被编码为一个十六进制字符序列。
下面展示一些实例。定义类A如下:
class A
def initialize(name)
@count = 1
@name = name
end
end
使用Oj序列化A的实例:Oj.dump(A.new(“John”)),结果如下:
{"^o":"A","count":1,"name":"John"}
对于这一结果,“^o”键的值为A,表示类A的实例,后续的count及name键代表其实例变量名,值为实例变量的值。使用Oj.load方法反序列化:
Oj.load %Q({"^o":"A","count":1,"name":"John"})
获得A类的新实例,对应的实例变量已被设置。
在第1小节中提到auto_define配置,该配置设置为true时,Oj解析到未定义的类时,将自动定义该类。例如,执行下列代码后,对象命名空间中将存在类NotDefined:
Oj.load %Q({"^c":"NotDefined"}),auto_define:true
我们可以模拟攻击者大量请求反序列化不存在的类:
10000000000.times { Oj.load %Q({"^c":"A#{SecureRandom.hex(4)}"}),auto_define:true }
将观察到Ruby进程内存占用显著增长。
尽管Oj默认允许反序列化几乎任意类型,但它与FastJSON等库的显著区别在于反序列化过程中对象的方法不会被调用,例如实例化方法initialize及其他钩子方法,这在一定程度上提供了安全性,至少不太可能在反序列化过程中被利用。
由于Oj代码库比较庞大,为了研究反序列化时的行为,我们可以先从结果入手。考虑下列代码,尝试反序列化一个Proc对象的实例:
Oj.load %Q({"^o":"Proc"})
这将导致抛出TypeError:allocator undefined for Proc。这与调用allocate方法[4]的行为一致。类或模块的allocate方法将分配一个对象实例但不调用initialize方法,这很好地避免了initialize方法在反序列化时被滥用,而限制在于并非所有的类都支持allocate。默认情况下Ruby提供allocate方法实现,对于C扩展可以使用rb_define_alloc_func函数注册自己的allocate方法,或使用rb_undef_alloc_func禁用allocate方法。
尽管调用allocate可以避免调用初始化方法,但这种行为可能导致其他问题。对于完全由运行时托管的对象,仅调用allocate虽可能产生未定义的行为,例如由于某些变量未初始化而导致抛出异常,但这种异常可以被捕获并处理,并不会导致进程崩溃;然而如果某一对象的数据操作交由原生代码处理,例如直接访问未初始化地址,则可能导致内存损坏。一个例子是Ruby3.0.0中Ractor类存在的Bug。Ractor是Ruby3中新增的Actor模型的实现,它几乎完全使用C代码实现。在3.0.0版本中Ractor允许调用allocate方法并返回一个实例,但这将导致一些必要的初始化操作被跳过,并在某一时刻产生对进程地址的非法读写,最终使程序崩溃。
虽然这并不是Oj的责任,但如果Oj反序列化一个Ractor对象,则将导致返回一个仅调用allocate方法的对象,调用它的多数方法都将导致进程崩溃进而拒绝服务,漏洞概念证明如下:
Oj.load(%Q({"^o":"Ractor"})).whatever
当反序列化一个对象时,对象的实例变量将设置为攻击者可控制的值。获取到返回的对象后,应用代码可能调用该对象上的任意方法,此时会产生类型混淆,例如应用代码期望获得一个Hash或Array对象,但实际上是一个String对象。一个String对象也许没有什么危险,但攻击者可以构造一条Gadget链,通过操作对象实例变量的值,从应用代码的一次非预期调用开始执行到进行危险操作的方法。一个著名的例子是ActiveSupport中的DeprecatedInstanceVariableProxy[5]类,该类代码大体如下:
class DeprecatedInstanceVariableProxy < DeprecationProxy
def initialize(instance, method, ...)
@instance = instance
@method = method
end
private
def target
@instance.__send__(@method)
end
end
该类继承DeprecationProxy类,DeprecationProxy定义了method_missing钩子方法,该方法在调用对象上未定义的方法时被调用:
def method_missing(...)
...
target.__send__(...)
end
对于这个Gadget,@instance与@method变量可控的情况下,在返回对象上调用任意不存在的方法(很容易满足条件,因为这个类本身就没有实现太多方法),将导致父类DeprecationProxy上的method_missing方法被调用,而该方法实现上又调用target方法,此时@instance对象的@method方法被执行。@instance对象可以使用ERB对象,@method则为result方法,ERB对象设置其src实例变量为需要执行的代码,result方法被调用时将会执行,从而实现远程代码执行。
经过分析,产生反序列化漏洞的根源在于使用不安全输入创建任意对象。Oj本身在其文档中已指明这一点,并提供了非常简单的解决方案:不使用object模式。如果使用compat模式,且开启了create_additions选项,则还需要审计所有类中的json_create方法是否存在代码执行的可能。
本文介绍了Oj库反序列化攻击面中的不同漏洞类型,并给出防御解决方案。随着近年攻防演练持续升温,反序列化漏洞也越来越受关注,利用简单、危害巨大的特点使其在未来将被更多安全研究者进一步研究、挖掘、利用。
[1]A8:2017-Insecure Deserialization[EB/OL].https://owasp.org/www-project-top-ten/2017/A8_2017-Insecure_Deserialization,2017-12.
[2]Fastjson 反序列化漏洞史[EB/OL].https://paper. seebug. org/1192/,2020-05-08.
[3]Oj Modes[EB/OL].https://github.com/ohler55/oj/blob/ develop/pages/Modes.md,2021-08-03.
[4]method-i-allocate[EB/OL].https://ruby-doc.org/core-2.5.0/Class.html#method-i-allocate,2020-05.
[5]DeprecatedInstanceVariableProxy[EB/OL].https://github.com/rails/rails/blob/18707ab17fa492eb25ad2e8f9818a320dc20b823/activesupport/lib/active_support/deprecation/proxy_wrappers.rb#L88,2021-07-30.