◆吳紅
Oj庫(kù)反序列化攻擊面分析
◆吳紅
(國(guó)網(wǎng)眉山供電公司 四川 620010)
Oj是一個(gè)快速的JSON解析Ruby庫(kù),由于它極其高效且易用,根據(jù)Github統(tǒng)計(jì),有上萬(wàn)項(xiàng)目以其作為依賴(lài)項(xiàng)。Oj庫(kù)默認(rèn)使用一個(gè)類(lèi)型系統(tǒng)來(lái)解析Ruby對(duì)象,這允許應(yīng)用代碼在反序列化JSON字符串時(shí)恢復(fù)原始的對(duì)象,但這種能力可能導(dǎo)致安全漏洞,如遠(yuǎn)程代碼執(zhí)行或拒絕服務(wù)。本文將分析Oj庫(kù)反序列化時(shí)可能造成的漏洞,并給出消除此類(lèi)漏洞的建議。
Oj;反序列化漏洞;類(lèi)型系統(tǒng)
Oj(https://github.com/ohler55/oj)是一個(gè)快速的JSON解析及對(duì)象序列化器。它可以序列化絕大多數(shù)的Ruby對(duì)象,在反序列化時(shí)則解析JSON字符串中約定的格式來(lái)恢復(fù)對(duì)象信息。不安全的反序列化作為OWASP TOP10中的一種通用漏洞類(lèi)型[1],在各類(lèi)編程語(yǔ)言中廣泛存在,近年來(lái)在Java應(yīng)用中尤其嚴(yán)重,一些知名Java組件,如FastJSON[2]、XStream等頻頻爆出反序列化漏洞,這類(lèi)漏洞特點(diǎn)是利用簡(jiǎn)單、危害巨大,通常能造成遠(yuǎn)程代碼執(zhí)行。由此看來(lái),Oj提供的反序列化對(duì)象的能力同樣是一種攻擊面,值得深入研究。
Oj提供兩個(gè)核心API用戶(hù)序列化與反序列化過(guò)程:
(1)Oj.load(json,options={}){ ... }
(2)Oj.dump(obj,options={})
Oj.load方法用于反序列化一個(gè)字符串,返回類(lèi)型取決于具體配置。可以全局配置Oj選項(xiàng),存儲(chǔ)在Oj.default_options變量中。我們僅關(guān)注反序列化時(shí)可能造成安全風(fēng)險(xiǎn)的一些配置:
(1)auto_define
(2)mode
(3)create_additions與create_id
auto_define選項(xiàng)指定反序列化時(shí)類(lèi)名不存在時(shí)是否進(jìn)行自動(dòng)定義;mode選項(xiàng)指定Oj模式;create_additions與create_id選項(xiàng)為一組,指定特定情況下反序列化對(duì)象的鍵名,之后會(huì)詳細(xì)說(shuō)明。
Oj.dump方法用于將一個(gè)Ruby對(duì)象序列化為JSON字符串。
Oj當(dāng)前支持6種模式[3]:strict、null、compat、rails、object、custom,我們對(duì)object模式感興趣,這也是默認(rèn)的模式,因?yàn)樵撃J皆试S將Ruby對(duì)象進(jìn)行序列化與反序列化。當(dāng)使用object模式時(shí),Oj遵循一組特定的編碼來(lái)確定JSON字符串中哪些部分需要重建為對(duì)象或類(lèi)。具體規(guī)則如下:
(1)原生JSON類(lèi)型,true,false,nil,String,Hash,Array,Numbers正常編碼。
(2)Symbol類(lèi)型編碼為以“:”開(kāi)頭的字符串。
(3)以“^”開(kāi)頭的鍵名表示這是一個(gè)特殊的鍵。
(4)以':','^i'或'^r開(kāi)頭的字符串,將這三種前綴編碼為unicode字符串形式。
(5)一個(gè)"^c "的JSON對(duì)象鍵表示該值應(yīng)該被轉(zhuǎn)換為一個(gè)Ruby類(lèi)。
(6)一個(gè)"^t"的JSON對(duì)象鍵表示該值應(yīng)該被轉(zhuǎn)換為Ruby Time。
(7)一個(gè)"^o"的JSON對(duì)象鍵表示該值應(yīng)該被轉(zhuǎn)換為Ruby對(duì)象。JSON對(duì)象中的第一個(gè)條目必須是一個(gè)帶有"^o"鍵的類(lèi)。在這之后,每個(gè)條目都被視為Object的一個(gè)變量,其中的鍵是沒(méi)有前面的'@'的變量名。
(8)一個(gè)"^u"JSON對(duì)象的鍵表示該值應(yīng)該被轉(zhuǎn)換為Ruby Struct。JSON對(duì)象中的第一個(gè)條目必須是一個(gè)帶有"^u"鍵的類(lèi)。之后,每個(gè)條目在結(jié)構(gòu)中被賦予一個(gè)數(shù)字位置,并被用作JSON對(duì)象的鍵。
(9)當(dāng)對(duì)一個(gè)對(duì)象進(jìn)行編碼時(shí),如果變量名稱(chēng)不是以'@'字符開(kāi)始,那么名稱(chēng)前面會(huì)有一個(gè)'~'字符。
(10)如果一個(gè)Hash條目有一個(gè)不是字符串或符號(hào)的鍵,那么該條目將被編碼為"^#n"形式的鍵,其中n是一個(gè)十六進(jìn)制數(shù)字。值是一個(gè)數(shù)組,其中第一個(gè)元素是Hash中的鍵,第二個(gè)元素是值。
(11)在一個(gè)對(duì)象或數(shù)組中的"^i"JSON條目是被編碼的Ruby對(duì)象的ID。當(dāng)circular標(biāo)志被設(shè)置時(shí)被使用。它可以出現(xiàn)在一個(gè)JSON對(duì)象或JSON數(shù)組中。在一個(gè)對(duì)象中,"^i"鍵有一個(gè)相應(yīng)的引用。在一個(gè)數(shù)組中,該序列將包括一個(gè)嵌入的引用號(hào)。
(12)一個(gè)對(duì)象中的"^r"JSON條目是對(duì)已經(jīng)出現(xiàn)在JSON字符串中的一個(gè)對(duì)象或數(shù)組的引用。它必須與之前的"^i"引用號(hào)相匹配。
(13)如果一個(gè)數(shù)組元素是一個(gè)字符串,并且以"^i"開(kāi)頭,那么第一個(gè)字符'^'被編碼為一個(gè)十六進(jìn)制字符序列。
下面展示一些實(shí)例。定義類(lèi)A如下:
class A
def initialize(name)
@count = 1
@name = name
end
end
使用Oj序列化A的實(shí)例:Oj.dump(A.new(“John”)),結(jié)果如下:
{"^o":"A","count":1,"name":"John"}
對(duì)于這一結(jié)果,“^o”鍵的值為A,表示類(lèi)A的實(shí)例,后續(xù)的count及name鍵代表其實(shí)例變量名,值為實(shí)例變量的值。使用Oj.load方法反序列化:
Oj.load %Q({"^o":"A","count":1,"name":"John"})
獲得A類(lèi)的新實(shí)例,對(duì)應(yīng)的實(shí)例變量已被設(shè)置。
在第1小節(jié)中提到auto_define配置,該配置設(shè)置為true時(shí),Oj解析到未定義的類(lèi)時(shí),將自動(dòng)定義該類(lèi)。例如,執(zhí)行下列代碼后,對(duì)象命名空間中將存在類(lèi)NotDefined:
Oj.load %Q({"^c":"NotDefined"}),auto_define:true
我們可以模擬攻擊者大量請(qǐng)求反序列化不存在的類(lèi):
10000000000.times { Oj.load %Q({"^c":"A#{SecureRandom.hex(4)}"}),auto_define:true }
將觀察到Ruby進(jìn)程內(nèi)存占用顯著增長(zhǎng)。
盡管Oj默認(rèn)允許反序列化幾乎任意類(lèi)型,但它與FastJSON等庫(kù)的顯著區(qū)別在于反序列化過(guò)程中對(duì)象的方法不會(huì)被調(diào)用,例如實(shí)例化方法initialize及其他鉤子方法,這在一定程度上提供了安全性,至少不太可能在反序列化過(guò)程中被利用。
由于Oj代碼庫(kù)比較龐大,為了研究反序列化時(shí)的行為,我們可以先從結(jié)果入手。考慮下列代碼,嘗試反序列化一個(gè)Proc對(duì)象的實(shí)例:
Oj.load %Q({"^o":"Proc"})
這將導(dǎo)致拋出TypeError:allocator undefined for Proc。這與調(diào)用allocate方法[4]的行為一致。類(lèi)或模塊的allocate方法將分配一個(gè)對(duì)象實(shí)例但不調(diào)用initialize方法,這很好地避免了initialize方法在反序列化時(shí)被濫用,而限制在于并非所有的類(lèi)都支持allocate。默認(rèn)情況下Ruby提供allocate方法實(shí)現(xiàn),對(duì)于C擴(kuò)展可以使用rb_define_alloc_func函數(shù)注冊(cè)自己的allocate方法,或使用rb_undef_alloc_func禁用allocate方法。
盡管調(diào)用allocate可以避免調(diào)用初始化方法,但這種行為可能導(dǎo)致其他問(wèn)題。對(duì)于完全由運(yùn)行時(shí)托管的對(duì)象,僅調(diào)用allocate雖可能產(chǎn)生未定義的行為,例如由于某些變量未初始化而導(dǎo)致拋出異常,但這種異常可以被捕獲并處理,并不會(huì)導(dǎo)致進(jìn)程崩潰;然而如果某一對(duì)象的數(shù)據(jù)操作交由原生代碼處理,例如直接訪問(wèn)未初始化地址,則可能導(dǎo)致內(nèi)存損壞。一個(gè)例子是Ruby3.0.0中Ractor類(lèi)存在的Bug。Ractor是Ruby3中新增的Actor模型的實(shí)現(xiàn),它幾乎完全使用C代碼實(shí)現(xiàn)。在3.0.0版本中Ractor允許調(diào)用allocate方法并返回一個(gè)實(shí)例,但這將導(dǎo)致一些必要的初始化操作被跳過(guò),并在某一時(shí)刻產(chǎn)生對(duì)進(jìn)程地址的非法讀寫(xiě),最終使程序崩潰。
雖然這并不是Oj的責(zé)任,但如果Oj反序列化一個(gè)Ractor對(duì)象,則將導(dǎo)致返回一個(gè)僅調(diào)用allocate方法的對(duì)象,調(diào)用它的多數(shù)方法都將導(dǎo)致進(jìn)程崩潰進(jìn)而拒絕服務(wù),漏洞概念證明如下:
Oj.load(%Q({"^o":"Ractor"})).whatever
當(dāng)反序列化一個(gè)對(duì)象時(shí),對(duì)象的實(shí)例變量將設(shè)置為攻擊者可控制的值。獲取到返回的對(duì)象后,應(yīng)用代碼可能調(diào)用該對(duì)象上的任意方法,此時(shí)會(huì)產(chǎn)生類(lèi)型混淆,例如應(yīng)用代碼期望獲得一個(gè)Hash或Array對(duì)象,但實(shí)際上是一個(gè)String對(duì)象。一個(gè)String對(duì)象也許沒(méi)有什么危險(xiǎn),但攻擊者可以構(gòu)造一條Gadget鏈,通過(guò)操作對(duì)象實(shí)例變量的值,從應(yīng)用代碼的一次非預(yù)期調(diào)用開(kāi)始執(zhí)行到進(jìn)行危險(xiǎn)操作的方法。一個(gè)著名的例子是ActiveSupport中的DeprecatedInstanceVariableProxy[5]類(lèi),該類(lèi)代碼大體如下:
class DeprecatedInstanceVariableProxy < DeprecationProxy
def initialize(instance, method, ...)
@instance = instance
@method = method
end
private
def target
@instance.__send__(@method)
end
end
該類(lèi)繼承DeprecationProxy類(lèi),DeprecationProxy定義了method_missing鉤子方法,該方法在調(diào)用對(duì)象上未定義的方法時(shí)被調(diào)用:
def method_missing(...)
...
target.__send__(...)
end
對(duì)于這個(gè)Gadget,@instance與@method變量可控的情況下,在返回對(duì)象上調(diào)用任意不存在的方法(很容易滿足條件,因?yàn)檫@個(gè)類(lèi)本身就沒(méi)有實(shí)現(xiàn)太多方法),將導(dǎo)致父類(lèi)DeprecationProxy上的method_missing方法被調(diào)用,而該方法實(shí)現(xiàn)上又調(diào)用target方法,此時(shí)@instance對(duì)象的@method方法被執(zhí)行。@instance對(duì)象可以使用ERB對(duì)象,@method則為result方法,ERB對(duì)象設(shè)置其src實(shí)例變量為需要執(zhí)行的代碼,result方法被調(diào)用時(shí)將會(huì)執(zhí)行,從而實(shí)現(xiàn)遠(yuǎn)程代碼執(zhí)行。
經(jīng)過(guò)分析,產(chǎn)生反序列化漏洞的根源在于使用不安全輸入創(chuàng)建任意對(duì)象。Oj本身在其文檔中已指明這一點(diǎn),并提供了非常簡(jiǎn)單的解決方案:不使用object模式。如果使用compat模式,且開(kāi)啟了create_additions選項(xiàng),則還需要審計(jì)所有類(lèi)中的json_create方法是否存在代碼執(zhí)行的可能。
本文介紹了Oj庫(kù)反序列化攻擊面中的不同漏洞類(lèi)型,并給出防御解決方案。隨著近年攻防演練持續(xù)升溫,反序列化漏洞也越來(lái)越受關(guān)注,利用簡(jiǎn)單、危害巨大的特點(diǎn)使其在未來(lái)將被更多安全研究者進(jìn)一步研究、挖掘、利用。
[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.
網(wǎng)絡(luò)安全技術(shù)與應(yīng)用2022年3期