#!/usr/bin/env ruby # encoding: ASCII-8BIT # By Ramon de C Valle. This work is dedicated to the public domain. require 'openssl' require 'optparse' require 'socket' Version = [0, 0, 1] Release = nil def prf(secret, label, seed) if secret.empty? s1 = s2 = '' else length = ((secret.length * 1.0) / 2).ceil s1 = secret[0..(length - 1)] s2 = secret[(length - 1)..(secret.length - 1)] end hmac_md5 = OpenSSL::HMAC.digest(OpenSSL::Digest.new('md5'), s1, label + seed) hmac_md5 = OpenSSL::HMAC.digest(OpenSSL::Digest.new('md5'), s1, hmac_md5 + label + seed) hmac_sha1 = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), s2, label + seed) hmac_sha1 = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), s2, hmac_sha1 + label + seed) result = '' [hmac_md5.length, hmac_sha1.length].max.times { |i| result << [(hmac_md5.getbyte(i) || 0) ^ (hmac_sha1.getbyte(i) || 0)].pack('C') } result end def prf_sha256(secret, label, seed) hmac_sha256 = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, label + seed) OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, hmac_sha256 + label + seed) end class String def hexdump(stream=$stdout) 0.step(bytesize - 1, 16) do |i| stream.printf('%08x ', i) 0.upto(15) do |j| stream.printf(' ') if j == 8 if i + j >= bytesize stream.printf(' ') else stream.printf('%02x ', getbyte(i + j)) end end stream.printf(' ') 0.upto(15) do |j| if i + j >= bytesize stream.printf(' ') else if /[[:print:]]/ === getbyte(i + j).chr && /[^[:space:]]/ === getbyte(i + j).chr stream.printf('%c', getbyte(i + j)) else stream.printf('.') end end end stream.printf("\n") end end end options = {} OptionParser.new do |parser| parser.banner = "Usage: #{parser.program_name} [options] host" parser.separator('') parser.separator('Options:') parser.on('-H', '--local-host HOST', 'Local host') do |host| options[:local_host] = host end parser.on('-P', '--local-port PORT', 'Local port') do |port| options[:local_port] = port end parser.on('-d', '--debug', 'Debug mode') do options[:debug] = true end parser.on('-h', '--help', 'Show this message') do puts parser exit end parser.on('-o', '--output FILE', 'Output file') do |file| options[:file] = File.new(file, 'w+b') end parser.on('-p', '--port PORT', 'Port') do |port| options[:port] = port end parser.on('-v', '--verbose', 'Verbose mode') do options[:verbose] = true end parser.on('--version', 'Show version') do puts parser.ver exit end end.parse! local_host = options[:local_host] || '0.0.0.0' local_port = options[:local_port] || 443 debug = options[:debug] || false file = options[:file] || nil host = ARGV[0] or fail ArgumentError, 'no host given' port = options[:port] || 443 verbose = options[:verbose] || false proxy = TCPServer.new(local_host, local_port) puts 'Listening on %s:%d' % [proxy.addr[2], proxy.addr[1]] if debug || verbose loop do Thread.start(proxy.accept) do |client| puts 'Accepted connection from %s:%d' % [client.peeraddr[2], client.peeraddr[1]] if debug || verbose finished_sent = false handshake_messages = '' version = '' context = OpenSSL::SSL::SSLContext.new(:TLSv1) context.verify_mode = OpenSSL::SSL::VERIFY_NONE tcp_socket = TCPSocket.new(host, port) ssl_server = OpenSSL::SSL::SSLSocket.new(tcp_socket, context) ssl_server.connect puts 'Connected to %s:%d' % [ssl_server.peeraddr[2], ssl_server.peeraddr[1]] if debug || verbose server = TCPSocket.new(host, port) puts 'Connected to %s:%d' % [server.peeraddr[2], server.peeraddr[1]] if debug || verbose loop do readable, = IO.select([client, server]) readable.each do |r| if r == ssl_server # ssl_server is an SSL socket; read application data directly header = '' fragment = r.readpartial(4096) fragment.hexdump($stderr) if debug puts '%d bytes received' % [fragment.bytesize] if debug || verbose else header = r.read(5) raise EOFError if header.nil? header.hexdump($stderr) if debug puts '%d bytes received' % [header.bytesize] if debug || verbose fragment = r.read(header[3, 2].unpack('n')[0]) fragment.hexdump($stderr) if debug puts '%d bytes received' % [fragment.bytesize] if debug || verbose end if finished_sent if file # Save application data file.write(fragment) file.flush file.fsync end elsif fragment =~ /^\x0e\x00\x00\x00/ # server_hello_done # Drop the server hello done message and send the finished # message in plaintext. if header[2, 1] == "\x03" verify_data = prf_sha256('', 'server finished', OpenSSL::Digest::SHA256.digest(handshake_messages)) verify_data = verify_data[0, 12] else verify_data = prf('', 'server finished', OpenSSL::Digest::MD5.digest(handshake_messages) + OpenSSL::Digest::SHA1.digest(handshake_messages)) verify_data = verify_data[0, 12] end finished = "\x14#{[verify_data.length].pack('N')[1, 3]}#{verify_data}" record = header[0, 3] + [finished.length].pack('n') + finished count = client.write(record) client.flush record.hexdump($stderr) if debug puts '%d bytes sent' % [count] if debug || verbose finished_sent = true # Change to the SSL socket server.close server = ssl_server # Save version used in the handshake version = header[2, 1] next else # Save handshake messages handshake_messages << fragment end case r when client if finished_sent # server is an SSL socket count = server.write(fragment) server.flush fragment.hexdump($stderr) if debug puts '%d bytes sent' % [count] if debug || verbose else # server isn't an SSL socket record = header + fragment count = server.write(record) server.flush record.hexdump($stderr) if debug puts '%d bytes sent' % [count] if debug || verbose end when ssl_server # client isn't an SSL socket; add the record layer header with # the same version used in the handshake. header = "\x17\x03#{version}" + [fragment.length].pack('n') record = header + fragment count = client.write(record) client.flush record.hexdump($stderr) if debug puts '%d bytes sent' % [count] if debug || verbose when server record = header + fragment count = client.write(record) client.flush record.hexdump($stderr) if debug puts '%d bytes sent' % [count] if debug || verbose end end end client.close server.close end end proxy.close