rubygems.org远程命令执行漏洞分析
Posted 聚锋实验室
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了rubygems.org远程命令执行漏洞分析相关的知识,希望对你有一定的参考价值。
通过rubygems.org上的反序列化漏洞执行远程代码, 是 Ruby 社区的一个非常受欢迎的托管服务。不过目前该漏洞的补丁已经发布,请点此(https://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2017-0903)升级到最新的版本。这个漏洞已被官方命名为CVE-2017-0903,关于该漏洞的详细官方介绍请点此。(http://blog.rubygems.org/2017/10/09/unsafe-object-deserialization-vulnerability.html)
如果你曾经编写过ruby应用程序,那么很可能你已经和rubygems.org进行过交互了。甚至你可能已经把该网站设置为信任,以允许它在你的计算机上运行任意程序,例如,当用gem命令安装rails时,gem程序就会从rubygems.org获取rails gem及其所有依赖项,并将所有内容安装到相应的位置,这样任何拥有账户的人都可以在后台发布gem命令了。
Rubygems.org本身就是一个rails应用程序,它有着清楚地信息披露条例。
远程命令执行漏洞分析
Ruby gems实际上只是tar文件,所以运行tar -xvf foo.gem通常会留给你三个文件:
metadata.gz data.tar.gz checksums.yaml.gz
这三个文件都是以.gz结尾,属于被压缩的文件。 metadata.gz包含一个YAML文件,包含有关gem的信息,如名称,作者,版本等。 data.tar.gz包含了另一个tar文件,该文件包含所有源代码。 checksums.yaml.gz包含一个YAML文件,其中包含gem命令的一些哈希加密。
不过我发现,解析不信任的YAML是危险的。原来,我一直认为它是一种类似JSON的良性交换格式,但事实上,YAML允许任意对象的编码,就像利用Python pickle可以实现任意代码执行。
当你将gem上传到rubygems.org时,应用程序将调用Gem::Package.new(body).spec。该方法所用的rubygems gem使用了不安全的YAML.load调用来加载gem中的YAML文件。
不过,rubygems.org的作者是知道该方法的安全隐患的,在2013年以前,开发者利用给内置对象扩展方法(Monkey Patching)修补了YAML和gem解析库,仅允许对白名单里的对象进行反序列化。到2015年则完全采用了Psych.safe_load。
不幸的是,monkey-patching的修复还是遗留了一些漏洞,因为它只修补了Gem::Specification#from_yaml方法。如果我来看看在调用到#spec时所发生的一些情况,我就会明白#verify的调用,下面是调用中的一些关键部分:
# ... @gem.with_read_io do |io| Gem::Package::TarReader.new io do |reader| read_checksums reader verify_files reader end end verify_checksums @digests, @checksums # ...
然后,在#read_checksums中会发生以下进程:
# ... Gem.load_yaml @checksums = gem.seek 'checksums.yaml.gz' do |entry| Zlib::GzipReader.wrap entry do |gz_io| YAML.load gz_io.read # oops end end # ...
现在,我就可以用我控制的输入调用YAML.load。最初,我试图在YAML.load调用时运行漏洞利用代码。但事实比我想得更复杂,虽然我可以反序列化任意对象,但其实对这些对象进行调用的方法却非常有限。是我可以对这些对象做出的唯一实际方法是非常有限的。 这时,就要在python上使用yaml解析库库,这可以让我多一些调用方法的选择,比如#[]=, #init_with,和#marshal_load(请注意不是Marshal.load)。但是对于大多数对象来说,这些方法并不会给攻击带来什么灵活性,因为他们通常的做法只是初始化几个变量并返回。在一些标准的rails库中存在一些危险的#[]=方法(如过去一样),但目前,我还没有找到一个能够攻击的对象。
于是,我又重新检查了rubygems.org应用程序,对其中的@checksums变量的作用进行重新评估,发现可以将其设置为任何类实例变量,在#verify_checksums中的情况如下:
# ... checksums.sort.each do |algorithm, gem_digests| gem_digests.sort.each do |file_name, gem_hexdigest| computed_digest = digests[algorithm][file_name] # ...
如果我可以构建一个调用#sort的对象,那就可以实施一些攻击,触发漏洞。这样,我就有了以下的POC。实际得到评估的有效载荷包含在底层64位编码的DEFLATE压缩的编组部分,在本例中,它只是负责运行echo "oops"。
SHA1: !ruby/object:Gem::Package::TarReader io: !ruby/object:Gem::Package::TarReader::Entry closed: false header: 'foo' read: 0 io: !ruby/object:ActiveSupport::Cache::MemoryStore options: {} monitor: !ruby/object:ActiveSupport::Cache::Strategy::LocalCache::LocalStore registry: {} key_access: {} data: '3': !ruby/object:ActiveSupport::Cache::Entry compressed: true value: !binary ' eJx1jrsKAjEQRbeQNT4QwQ9Q8hlTRXGL7UTFemMysIGYCZNZ0b/XYsHK8nIO nDtRBGbvJDzxMuRMLABHzIzOSqD0G+jbVMQmhzfLwd4jnphebwUrE0ZAoJrz YQpLE0PCRKGCmSnsWr3p0PW000S56G5eQ91cv9oDpScPC8YyRIG18WOMmGD7 /1X1AV+XPlQ='
可以看出,从最后一步才开始逆向进行#sort调用。
在底部,我有一个ActiveSupport::Cache::Entry对象。这个对象的重要之处在于,当#value方法被调用并且@compressed为true时,它将在攻击者提供的DEFLATE压缩的数据上调用Marshal.load。解组的对象的构造方式是这样的,只要调用其上的任何方法就可以执行攻击者的代码。该方法是我以前写的,工作原理请点击这里(https://github.com/charliesome/charlie.bz/blob/master/posts/rails-3.2.10-remote-code-execution.md)。不幸的是,我不能在实现代码执行时,只用YAML来反序列化这个对象,因为它几乎对所有的方法都进行了undef,包括允许我设置实例变量的方法。因此,在使用时,要对Marshal.load进行加载才可以。
我会利用ActiveSupport::Cache::MemoryStore对象在@data哈希中解组我的恶意对象。它的父类ActiveSupport::Cache::Store定义了一个在MemoryStore中调用#read_entry的#read方法,#read_entry基本上只是抓取@data中的条目并将其返回。
由于MemoryStore以反序列化后的数组或者序列化后的字节缓存(ByteBuffer)形式将代码块存储到内存中,所以对MemoryStore#read的调用来自对Gem::Package::TarReader::Entry#read的调用,而Gem::Package::TarReader::Entry#read本身是由Gem::Package::TarReader#each调用的。读取返回后,对返回的值调用#size,由于我的恶意解组对象未定义,所以这会导致我的有效载荷开始执行。
最后,因为Gem::Package::TarReader对可枚举性(enumerable)进行了指定,所以调用其#sort方法将会调用其#each方法,这样整个攻击链就被启动了。
总结
在本文中,我介绍了 YAML的强大功能,有时它也可以在表现力较弱但很安全的交换格式(如JSON)中使用。也许在将来,YAML.load可以被修改为将类的白名单作为可选参数,使复杂对象的反序列化成为选择性行为。其实,目前的YAML.load实际上应该被命名为类似YAML.unsafe_load这样的名称,这样用户就知道他们何时用YAML.safe_load了。
文章转自嘶吼
以上是关于rubygems.org远程命令执行漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章