Tuesday, November 27, 2012

Raven Airlink default password scanner

This is a short post that I have been wanting to do for a long time but I haven't have time. I know, excuses.

In some of the pentests that I'd performed over utility companies I have identified that as a consistent problem the use of small modems that will allow remote connectivity (think SCADA). The problem begins with the configuration of the modem itself since a lot of the times the modem is provided by the ISP (e.g. Verizon or ATT) and sometimes not really managed by the company that is using it, most of the times for lack of knowledge of plant engineers. In most of the cases these devices come with a default administrator password that can be used to configure the device remotely. This is a screenshot of the metasploit module that I developed to test for this misconfiguration and how to use it:



Just copy the code at the end of this post and paste it into a file in the modules/auxiliary/scanner/http/ folder and load the msfconsole. I already submitted this module to the metasploit dev team however I never know if they'll publish my stuff or not. In this one I believe they might not do it specially because I used the digest function inside my module. Don't get me wrong, I completely understand why they have high standards for the code they publish to the framework and they have all my respect for that but some of these modules I develop them on my free time and I will not waste more time on them after they work for me.

I hope this helps someone.

gr33tz to etlow

The code:




##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##

require 'msf/core'
require 'digest'

class Metasploit3 < Msf::Auxiliary

    include Msf::Exploit::Remote::HttpClient
    include Msf::Auxiliary::Report
    include Msf::Auxiliary::Scanner

    def initialize
        super(
            'Name'           => 'Raven GPRS default password',
            'Version'        => '$Revision: 14789 $',
            'Description'    => 'This module simply attempts to login to a Raven modem using the default user:pass.',
            'References'     =>
                [                      
                    [ 'CVE', '1999-0502'], # Weak password
                    ['Vendor URL', 'http://www.sierrawireless.com/en/productsandservices/AirLink/Gateways.aspx']
                ],
            'Author'         => [ '@c4an', 'David Llorens <[at]c4an>' ],
            'License'        => MSF_LICENSE
        )

        register_options(
            [
                Opt::RPORT(8088),              
                OptString.new('URI', [true, "URI for modem login. Default is /msci", "/msci"]),
                OptString.new('PASSWORD', [true, "Password for the modem. Default is 12345", "12345"]),
                OptInt.new('SLEEP', [true, "Seconds to delay the MD5 HTTP auth after the identification of the modem. This is required to have accurate results", 6]),
            ], self.class)
       
       
        register_advanced_options(
            [
                OptString.new('USER', [ false, "Default is user", 'user']),              
            ], self.class)
    end
       

    def run_host(ip)
        modem = false
        user = datastore['USER']
        pass = datastore['PASSWORD']
        begin          
            res,c = send_digest_request({
                'uri'     => "#{datastore['URI']}",
                'method'  => 'GET',
                #'DigestAuthIIS' => false,
                'DigestAuthUser' => user,
                'DigestAuthPassword' => pass,              
                }, 45)
            unless (res.kind_of? Rex::Proto::Http::Response)
                vprint_error("http://#{rhost}:#{rport}#{datastore['URI']} not responding")
                return :abort
            end
           
            if res.code != 401                  
                print_good("http://#{rhost}:#{rport}#{datastore['URI']} [Raven  GPRS modem] successful login '#{user}' : '#{pass}'")
                report_auth_info(
                        :host => rhost,
                        :port => rport,
                        :sname => (ssl ? "https" : "http"),
                        :user => user,
                        :pass => pass,
                        :proof => "WEBAPP=\"AT&T GPRS Raven Modem default password\"",
                        :source_type => "user_supplied",
                        :duplicate_ok => true,
                        :active => true
                )
              else
                vprint_error("http://#{rhost}:#{rport}#{datastore['URI']} ] [Raven GPRS Modem] failed to login as '#{user}':'#{pass}'")          
            end                              
        rescue ::Rex::ConnectionError => e
                vprint_error("http://#{rhost}:#{rport}#{datastore['URI']} - #{e}")
            return
        end
    end
  
    def send_digest_request(opts={}, timeout=20)
        # Code taken from http client Module in the framework developed by HD Moore
        # The reason for this is because I want to check if the device is actually a Raven device
        # before sending the credenctials and this was the only way to do it and using the digest function created in the framework
        @nonce_count = 0

        return [nil,nil] if not (datastore['DigestAuthUser'] or opts['DigestAuthUser'])
        to = opts['timeout'] || timeout

        digest_user = datastore['DigestAuthUser'] || opts['DigestAuthUser'] || ""
        digest_password = datastore['DigestAuthPassword'] || opts['DigestAuthPassword'] || ""

        method = opts['method']
        path = opts['uri']
        iis = true
        if (opts['DigestAuthIIS'] == false or datastore['DigestAuthIIS'] == false)
            iis = false
        end

        begin
        @nonce_count += 1

        resp = nil
       
        if not resp
            # Get authentication-challenge from server, and read out parameters required
            c = connect(opts)
            r = c.request_cgi(opts.merge({
                    'uri' => path,
                    'method' => method }))
            resp = c.send_recv(r, to)
            unless resp.kind_of? Rex::Proto::Http::Response
                return [nil,nil]
            end
            return [nil,nil] if resp.code == 404
            if resp.code != 401
                return resp
            end
            return [nil,nil] unless resp.headers['WWW-Authenticate']
           
        end
       
        # Don't anchor this regex to the beginning of string because header
        # folding makes it appear later when the server presents multiple
        # WWW-Authentication options (such as is the case with IIS configured
        # for Digest or NTLM).
        a = resp['www-authenticate'].match(/Digest (.*)/)[1]      
        parameters = {}
       
        a.split(/,[[:space:]]*/).each do |p|
            k, v = p.split("=", 2)
            parameters[k] = v.gsub('"', '')
        end
       
        modem = true if(resp.headers['WWW-Authenticate'].to_s.scan(/Airlink.com/i).size >= 1)
       
        if modem
            print_good("#{rhost}:#{rport} seems to be an Raven device!")
           
            # GPRS modems are incredibly slow to reply back after the first HTTP request is made.
            # The sleep is a patch to have accurate results before sending MD5 and make sure that GPRS replies back
            # Remove it at your own risk
            sleep(datastore['SLEEP'])
           
        else
            print("http://#{rhost}:#{rport}#{datastore['URI']} - Not an Raven GPRS modem")
            return [nil,nil]
        end
       
        qop = parameters['qop']

        if parameters['algorithm'] =~ /(.*?)(-sess)?$/
            algorithm = case $1
            when 'MD5' then Digest::MD5
            when 'SHA1' then Digest::SHA1
            when 'SHA2' then Digest::SHA2
            when 'SHA256' then Digest::SHA256
            when 'SHA384' then Digest::SHA384
            when 'SHA512' then Digest::SHA512
            when 'RMD160' then Digest::RMD160
            else raise Error, "unknown algorithm \"#{$1}\""
            end
            algstr = parameters["algorithm"]
            sess = $2
        else
            algorithm = Digest::MD5
            algstr = "MD5"
            sess = false
        end
       

        a1 = if sess then
            [
                algorithm.hexdigest("#{digest_user}:#{parameters['realm']}:#{digest_password}"),
                parameters['nonce'],
                @cnonce
            ].join ':'
        else
            "#{digest_user}:#{parameters['realm']}:#{digest_password}"
        end

        ha1 = algorithm.hexdigest(a1)
        ha2 = algorithm.hexdigest("#{method}:#{path}")

        request_digest = [ha1, parameters['nonce']]
        request_digest.push(('%08x' % @nonce_count), @cnonce, qop) if qop
        request_digest << ha2
        request_digest = request_digest.join ':'

        # Same order as IE7
        auth = [
            "Digest username=\"#{digest_user}\"",
            "realm=\"#{parameters['realm']}\"",
            "nonce=\"#{parameters['nonce']}\"",
            "uri=\"#{path}\"",
            "cnonce=\"#{@cnonce}\"",
            "nc=#{'%08x' % @nonce_count}",
            "algorithm=#{algstr}",
            "response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
            # The spec says the qop value shouldn't be enclosed in quotes, but
            # some versions of IIS require it and Apache accepts it.  Chrome
            # and Firefox both send it without quotes but IE does it this way.
            # Use the non-compliant-but-everybody-does-it to be as compatible
            # as possible by default.  The user can override if they don't likedatastore['PASSWORD']
            # it.
            if qop.nil? then
            elsif iis then
                "qop=\"#{qop}\""
            else
                "qop=#{qop}"
            end,
            if parameters.key? 'opaque' then
                "opaque=\"#{parameters['opaque']}\""
            end
        ].compact

        headers ={ 'Authorization' => auth.join(', ') }
        headers.merge!(opts['headers']) if opts['headers']

       
        # Send main request with authentication
        r = c.request_cgi(opts.merge({
            'uri' => path,
            'method' => method,
            'headers' => headers }))
        resp = c.send_recv(r, to)
        unless resp.kind_of? Rex::Proto::Http::Response
            return [nil,nil]
        end
       
        return [resp,c]

        rescue ::Errno::EPIPE, ::Timeout::Error
            vprint_error("Connection timed out")
        end
    end

end