Update find_ysoserial_offsets

* Apply rubocop suggestions for style
* Support patching an existing JSON file
* Use an OptionParser
This commit is contained in:
Spencer McIntyre 2021-09-10 18:19:36 -04:00
parent 6b90582864
commit 521975976b
1 changed files with 88 additions and 77 deletions

View File

@ -4,73 +4,82 @@ require 'diff-lcs'
require 'json'
require 'base64'
require 'open3'
require 'optparse'
YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'
YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'.freeze
PAYLOAD_TEST_MIN_LENGTH = 0x0101
PAYLOAD_TEST_MAX_LENGTH = 0x0102
YSOSERIAL_MODIFIED_TYPES = %w{ bash cmd powershell }
YSOSERIAL_UNMODIFIED_TYPE = 'none'
YSOSERIAL_ALL_TYPES = [YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES
YSOSERIAL_MODIFIED_TYPES = %w[bash cmd powershell].freeze
YSOSERIAL_UNMODIFIED_TYPE = 'none'.freeze
YSOSERIAL_ALL_TYPES = ([YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES).freeze
# 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 ' -p [payloads] Specified ysoserial payload (payloads1,payloads2,...)'
puts ' -a Generate all types of payloads'
puts
abort
end
@debug = false
@generate_all = false
@payload_type = nil
@ysoserial_modified = false
@ysoserial_payloads = []
@json_document = {}
OptionParser.new do |opts|
opts.banner = "Usage #{File.basename($PROGRAM_NAME)} [options]"
@generate_all = ARGV.include?('-a')
@debug = ARGV.include?('-d')
@ysoserial_modified = ARGV.include?('-m')
if @ysoserial_modified
@payload_type = ARGV[ARGV.find_index('-m')+1]
unless YSOSERIAL_MODIFIED_TYPES.include?(@payload_type)
STDERR.puts 'ERROR: Invalid payload type specified'
opts.on('-a', '--all', 'Generate all types of payloads') do
@generate_all = true
end
opts.on('-d', '--debug', 'Debug mode (output offset information only)') do
@debug = true
end
opts.on('-h', '--help', 'Help') do
puts opts
abort
end
end
if (index = ARGV.index('-p'))
@ysoserial_payloads = ARGV[index+1].split(',')
end
opts.on('-m', '--modified [TYPE]', String, 'Use \'ysoserial-modified\' with the specified payload type') do |modified_type|
@ysoserial_modified = true
@payload_type = modified_type
end
opts.on('-p', '--payload [PAYLOAD]', String, 'Specified ysoserial payload') do |payload|
@ysoserial_payloads << payload
end
opts.on('-j', '--json [PATH]', String, 'Update an existing JSON document') do |json_path|
@json_document = JSON.parse(File.read(json_path))
end
end.parse!
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
search_string = '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, @payload_type, searchString)
stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string)
else
stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-original.jar', payload_name, searchString)
stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-original.jar', payload_name, search_string)
end
payload = stdout
payload.force_encoding('binary')
if @debug && payload.length == 0 && stderr.length > 0
if @debug && payload.empty? && !stderr.empty?
# Pipe errors out to the console
STDERR.puts stderr.split("\n").each {|i| i.prepend(" ")}
warn(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"
# 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"
# 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"
# STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported"
return nil
else
#STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}"
# 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:]]{14}/, 'ysoserial/Pwner00000000000000')
payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{13,14}/, 'ysoserial/Pwner00000000000000')
return payload
end
end
@ -81,6 +90,7 @@ def generate_payload_array(payload_name)
(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
@ -89,10 +99,8 @@ end
def length_offset?(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
if next_byte && current_byte.position == next_byte.position && current_byte.action == '-' && (next_byte.element.ord - current_byte.element.ord == 1)
return true
end
false
@ -107,9 +115,10 @@ def buffer_offset?(current_byte, next_byte)
false
end
def diff(a, b)
return nil if a.nil? or b.nil?
diffs = Diff::LCS.diff(a, b)
def diff(blob_a, blob_b)
return nil if blob_a.nil? || blob_b.nil?
diffs = Diff::LCS.diff(blob_a, blob_b)
diffs.flatten(1)
end
@ -127,24 +136,25 @@ def get_payload_list
payload_list = []
payloads.each do |line|
# Skip the header rows
next unless line.start_with? " "
next unless line.start_with? ' '
payload_list.push(line.match(/^ +([^ ]+)/)[1])
end
payload_list - ['JRMPClient', 'JRMPListener']
end
#YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE)
# YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE)
def generated_ysoserial_payloads
results = {}
@payload_list.each do |payload|
STDERR.puts "Generating payloads for #{payload}..."
warn "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"}
warn " ERROR: Errored while generating '#{payload}' and it will not be supported"
results[payload] = { status: 'unsupported' }
next
end
@ -156,19 +166,19 @@ def generated_ysoserial_payloads
# 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])
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
diffs.length.times do |j|
current_byte = diffs[j]
next_byte = diffs[j+1]
prev_byte = diffs[j-1]
next_byte = diffs[j + 1]
prev_byte = diffs[j - 1]
if j > 0
if j > 0 && (prev_byte.position == current_byte.position)
# Skip this if we compared these two bytes on the previous iteration
next if prev_byte.position == current_byte.position
next
end
# Compare this byte and the following byte to identify length and buffer offsets
@ -179,28 +189,28 @@ def generated_ysoserial_payloads
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)}"
warn " 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}"
warn " BUFFER OFFSET #{buffer_offset}"
end
STDERR.puts " PAYLOAD LENGTH: #{empty_payload.length}"
warn " PAYLOAD LENGTH: #{empty_payload.length}"
end
payload_bytes = Base64.strict_encode64(empty_payload)
if buffer_offsets.length > 0
if buffer_offsets.empty?
# TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this.
results[payload] = {
'status': 'dynamic',
'lengthOffset': length_offsets.uniq,
'bufferOffset': buffer_offsets.uniq,
'bytes': payload_bytes
status: 'static',
bytes: payload_bytes
}
else
#TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this.
results[payload] = {
'status': 'static',
'bytes': payload_bytes
status: 'dynamic',
lengthOffset: length_offsets.uniq,
bufferOffset: buffer_offsets.uniq,
bytes: payload_bytes
}
end
end
@ -208,36 +218,37 @@ def generated_ysoserial_payloads
end
@payload_list = get_payload_list
if @ysoserial_payloads
unless @ysoserial_payloads.empty?
unknown_list = @ysoserial_payloads - @payload_list
if unknown_list.empty?
@payload_list = @ysoserial_payloads
else
STDERR.puts "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}"
warn "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}"
abort
end
end
results = {}
if @generate_all
YSOSERIAL_ALL_TYPES.each do |type|
STDERR.puts "Generating payload type for #{type}..."
warn "Generating payload type for #{type}..."
@ysoserial_modified = (type != YSOSERIAL_UNMODIFIED_TYPE)
@payload_type = type
results[type] = generated_ysoserial_payloads
STDERR.puts
@json_document[type] ||= {}
@json_document[type].merge!(generated_ysoserial_payloads)
$stderr.puts
end
else
@payload_type ||= YSOSERIAL_UNMODIFIED_TYPE
results[@payload_type] = generated_ysoserial_payloads
@json_document[@payload_type] ||= {}
@json_document[@payload_type].merge!(generated_ysoserial_payloads)
end
payload_count = {}
payload_count['skipped'] = 0
payload_count['static'] = 0
payload_count['static'] = 0
payload_count['dynamic'] = 0
results.each_value do |vs|
@json_document.each_value do |vs|
vs.each_value do |v|
case v[:status]
when 'unsupported'
@ -251,7 +262,7 @@ results.each_value do |vs|
end
unless @debug
puts JSON.pretty_generate(results)
puts JSON.pretty_generate(@json_document)
end
STDERR.puts "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads."
warn "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads."