Title: ProSoft RLX-IFH24E v2.2.44 - Unsigned Firmware Upgrade Package Executed as Root Author: 0day Rubbish Research Team Contact: disclosure@0day-rubbish.com Type: remote Platform: Hardware Advisory: https://0day-rubbish.com/blog/prosoft-rlx-ifh24e-unsigned-upgrade-package-root-rce CVE: pending Description: ProSoft RLX-IFH24E, version v2.2.44, the industrial wireless radio from ProSoft Technology, Inc. (United States) whose research designation is RLX-IH 24E, installs and executes code submitted over its management interface without performing any signature, hash or CRC check on it. The appliance is a closed-source embedded device and an OEM build of the MicroHard Systems IPx21 platform on the MHX-910 board design: an Intel IXP425 XScale processor, ARM big-endian, Linux 2.4.24-uc0, with static management pages under /usr/web and 57 ARM big-endian CGI programs under /usr/lib/cgi-bin/ served by boa over HTTP and boa_ssl over HTTPS. The classification is CWE-494, download of code without integrity check, with CWE-345 and CWE-347 co-equal, because the only authenticity check in the path is a non-cryptographic string equality and no signature is verified anywhere. CWE-434, CWE-73, CWE-250 and CWE-269 contribute. It is explicitly not CWE-78 and a command-injection remediation would not fix it: the shared platform helper SubProgram_Start at offset 0xbd5c splits its argument string on spaces with strtok into at most eight tokens and then calls vfork and execvp directly, so no shell is ever invoked and shell metacharacters are inert here. The upload hop is in usr/lib/cgi-bin/tool_reprogramming.cgi, routine HandleSubmit at offset 0x8d1c, with its formatting string at rodata offset 0x2b1c. On the file1 part of a multipart/form-data POST to /cgi-bin/tool_reprogramming.cgi it calls cgiFormFileName and cgiFormTempFileName, builds the move argument with sprintf(buf, "%s /upgrade/upgrade/%s", tempfile, origfilename), runs SubProgram_Start("/bin/mv", buf), which must succeed, and then runs SubProgram_Start("/sbin/upgrade", origfilename). The client-supplied original filename is therefore used unmodified twice, once as the destination of the move and once as the installer's only argument, which makes the file extension and so the installer branch entirely the requester's choice. The handler applies no extension whitelist, no digest comparison and no path canonicalisation. The install hop is /sbin/upgrade, a short POSIX shell script shipped in the same package, running with the CGI's own privileges, which are root. It dispatches on ${FILE##*.} and its three consequential branches are: ".pkg", which tests gzip stream integrity with gunzip -t, unpacks with tar -xf into a staging tree, compares the appliance's PRODUCT_NAME read from /etc/MHX_CONFIG against a LOCK_NAME read from a lockfile inside the package itself, prints "Match image." on equality, executes ./pre, applies the inner archive with tar -xf extract/upgradepkg.tar -C /, executes ./post, prints "Upgrade Successful" and then reboots unconditionally; ".passwd", which copies the upload over both /etc/passwd and /etc/passwd_original; and ".factory", which copies the upload over /etc/db1, the configuration store holding the management interface's own transport flags, accounts and radio behaviour. The equality test is the only gate before code execution and both of its operands are available to the requester: the expected value is the compile-time platform constant IPx21PROSOFT recoverable by reading the shipped firmware once, and the asserted value is carried inside the package being installed. A package whose identity is validated against data carried inside that same package cannot be a source of assurance about it. The defect is the absence of an authenticity step, not a weak one, and strengthening the compatibility check would change nothing while both operands come from the same side. Impact: root command execution on the appliance from a single multipart/form-data POST followed by the product's own documented install sequence, with requester-authored shell code running both before and after an unconstrained archive extraction into the root of the filesystem. Attacker code reading the pre-execution state, an arbitrary rootfs write through the inner archive whose entry names are constrained in no way, and attacker code running post-write are all present in one request. The .passwd branch reaches the same privilege ceiling without executing anything, replacing both the live account database and its backup copy so that the device's own recovery path restores the modified database rather than the shipped one; since the factory admin entry is uid 0 with the restricted shell /bin/txtUI, an added uid-0 entry with /bin/sh converts an administratively restricted login into a general root shell at the next interactive login. The .factory branch lets a requester rewrite the device's security configuration, widening or narrowing the exposure of the surface they arrived through and surviving a later reconfiguration by the operator. Persistence is native to the product rather than improvised, because the install rewrites the rootfs and the installer always reboots on success. Availability is controlled at will, both by that reboot and by installing content that stops the device serving traffic, which for a radio carrying traffic between a process network and its field instruments is also a position on the path that traffic takes. Detection is weak by construction: every step is documented product behaviour, the request is an ordinary firmware upload, the response is the vendor's own success page, and a legitimate upgrade prints exactly the same "Match image." and "Upgrade Successful", so the usable indicators are post-hoc and filesystem-level, namely an installed package whose LOCK_NAME and VERSION pair corresponds to no release the operator obtained, an unexpected file under /upgrade/upgrade/, and a uid-0 account whose shell is not the vendor's restricted interface. Verification boundary, stated exactly because it is narrower than the headline suggests, and because the classification above depends on it. The installer half of the chain was measured and the upload half was not. No HTTP request was ever issued at this product and no physical appliance was used at any point, so nothing here is an exploit run against a live device and it is not presented as one. What was executed is the firmware's own /sbin/upgrade, byte-for-byte unmodified, inside an isolated chroot sandbox built on a copy of the upgrade package's own root filesystem. A crafted .pkg printed "Match image." then "Upgrade Successful" and exited 0; the pre script left a marker proving it executed and wrote PRE-RAN-AS-UID=0 into a second marker, the post script left its own marker, and the inner archive applied an entry to the root of the filesystem. The uid marker is the load-bearing evidence: the payload wrote that value at runtime rather than having it read from a configuration file, so it is the executing identity and not an inference about it. The recorded landing identity is uid=0(root). A .passwd run replaced both /etc/passwd and /etc/passwd_original with the uploaded text, proven by a pre-run digest of the pre-seeded database that no longer matched afterwards and by a marker line present in both copies. A negative control was run to establish that none of this came from the harness: a structurally identical package, same outer tar layout, same metadata files, same inner archive, same marker-writing scripts, with exactly one deliberate difference, LOCK_NAME=WRONGPRODUCT, was refused with "This unit doesn't support the image." and produced no markers anywhere. That control also isolates the product-string comparison as the single cause of refusal and makes the positive and negative runs mutually exclusive. Four substitutions existed in the harness and none in the code under test: an x86 busybox build supplied /bin/sh, tar, gunzip, mv and cp because the firmware's own copies are ARM big-endian and the host is not; a stub replaced the platform mhx utility; /etc/MHX_CONFIG was populated with the product identity the firmware itself ships; and /etc/passwd was pre-seeded with three factory-style entries for comparison. Two transcript lines, "logger: not found" and "mhx: not found", are absent helpers in the sandbox PATH and are not product behaviour. The run count is one positive .pkg run, one positive .passwd run and one negative control, in one sandbox on one host, each effect observed once, never repeated across sandbox builds and never performed on hardware. Not measured, and not claimed: the web server and its access control, because /bin/boa lives in the device base flash and is not contained in the system upgrade package, so the access control gating this CGI could be neither proved nor disproved from the artefacts available; the CGI binaries and the firmware's ARM helpers, for the architecture reason; the closing reboot, since the sandbox is a directory tree with no init, though the path up to it was observed; any privilege escalation, the chain starting from the administrative identity the product's own account database already grants uid 0; any reach into the radio baseband image RLX-IFH24E_v5089.mhx, identified but not exercised; and any impact on an attached control system, PLC or historian. Consequently the Type field above reflects the documented delivery vector, a network POST to a CGI on the management listener, and the Platform field reflects a firmware and hardware target; the part that was actually executed was the vendor's own installer acting on a supplied file, and a reader should weight the two halves of this chain differently. That is what the published advisory is written to allow. CVSS: 7.2 High (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H) is the single score published for this finding, computed from first principles rather than inherited: ISS = 1 - (0.44 x 0.44 x 0.44) = 0.914816; Impact at Scope Unchanged = 6.42 x 0.914816 = 5.873119; Exploitability = 8.22 x 0.85 x 0.77 x 0.27 x 0.85 = 1.234708; Base = Roundup(7.107826) = 7.2. AV:N because the entry point is a network service and the firmware's own default configuration record inside the shipped package enables the web interface on both transports, UI_Access_HTTP=B and HTTPS=B, with non-default values for the Telnet and SSH entries; that shipped record is the evidence for a default, and the Network classification comes from the transport the artefact establishes rather than from a reach we observed. AC:L because the chain is deterministic, with no race, no timing window, no memory-layout dependency and no on-path position, and the single piece of environment knowledge it needs is a platform string published inside the shipped firmware. PR:H because installing firmware is an administrative function, the handler is a maintenance CGI linked directly from the interface's own menu pages, and the only account model the package documents for that interface is a uid-0 admin entry with a password-change page and a configurable RADIUS mode; no lower-privileged principal was demonstrated reaching the handler. UI:N because once a session holds that privilege the move, the install, both script executions and the reboot all follow from one request with no operator action and no confirmation step in the recovered handler path. Scope Unchanged because the vulnerable component and the impacted component are the same appliance operating system, the CGI already running as root, the installer already running as root and the rootfs extracted by that same root process, so no authority boundary is crossed. Three readings are published as rejected with their arithmetic: low privilege (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) computes to 8.8 and is available only by assuming that the shipped uid-500 upgrade account, whose home directory is the upload staging path and which shares the admin password hash, can reach the maintenance handler, which was not tested; Scope Changed (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H) computes to 9.1 and is rejected because an unproven boundary crossing may not raise a published score; high attack complexity (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H) computes to 6.6 and is rejected because the control run isolates a wrong product string as the sole cause of refusal. Two of those rejected readings score higher than the number published, and the advisory says so itself. Two further readings are labelled conditionals and neither is the headline. An unauthenticated HTTP layer (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) would compute to 9.8 and was NOT demonstrated; it is a structural inference that applies only if the management listener does not gate the handler, and the binary that would answer that question is not in the package analysed. The supporting observation about the package is narrower and is weaker than that reading: none of the 57 shipped CGIs imports crypt, none parses or sets a session cookie and no login CGI exists in the directory, so the handler layer adds no authentication of its own, which says nothing about what the web server does before it forks a CGI. A factory credential still in place on a deployed unit would remove the need to discover any secret, but it was not demonstrated either, no authentication was performed against any appliance and no first-login behaviour was tested; under CVSS the metric prices the privilege level of the account used rather than whether its secret was public, so that reading leaves the vector unchanged at 7.2 and is published as a deployment amplifier for operators to assess. The factory password hash is deliberately not reproduced in this submission or in the advisory. No version-range claim is made: only the system upgrade package RLX-IFH24E_v2244.pkg was analysed, no other build of this model, no other version of the OEM IPx21 platform and no other product of either company is claimed as affected, although the upgrade path is a shared platform component rather than a per-product script, so a wider population of devices built on the same OEM platform plausibly shares the root cause, and that is stated as a likelihood rather than scored. Adjacent structural observations recorded in the analysis and deliberately excluded from this finding are a startup configuration that symlink-exposes /etc/MHX_CONFIG, /var/log/messages and /etc/system.debug into the web document root as configuration, syslog and debug views; a legacy debug CGI named request.cgi whose first-argument dispatch includes sending AT commands to the radio module, which was never executed and to which no AT command was sent; and SNMP community strings public and private shipped on the platform, which belong to the configuration plane. Public records were searched by product designation, by the RLX-IH name, by the OEM platform name and by the vendor name with no result describing this appliance and this root cause; the one record associated with the vendor's RadioLinx engineering-software line concerns a pseudo-random number generator in that software family, a different product and a different defect class, and records naming the platform manufacturer's own product families concern different platforms and different chains. Proof of concept, published unconditionally. The shipped script is Python 3 using only the standard library, with no third-party dependency and no hardcoded target: the target address, port, scheme, credentials, command, lock name, product name, uploaded filename, marker token and output path are all command-line arguments and default to inert values, the default target being loopback and the default payload command being true. It builds a gzip-compressed tar containing the file and lockfile metadata the installer reads, the pre and post scripts at mode 0755 and extract/upgradepkg.tar, and can either write that package to disk for inspection or carry it in a multipart/form-data POST under the part name file1. Its modes and the flags that matter: Build the package locally only, performing no network access at all: python3 prosoft_rlx_ifh24e_unsigned_upgrade_package_root_rce.py --build-only --out demonstration.pkg Refuse a filename that would break the hand-off. The tool rejects any name containing a space or a path separator before it builds anything, because SubProgram_Start splits its argument string on spaces before execvp, so an extra token would change what /bin/mv is asked to do: --filename "payload.pkg" accepted --filename "pay load.pkg" rejected by the tool itself Build the negative control, for an operator who owns the hardware to confirm that the success path really is gated by the product string and by nothing else: python3 prosoft_rlx_ifh24e_unsigned_upgrade_package_root_rce.py --control --build-only --out control.pkg Build the account-database variant as a local file, which is never sent unless --send is also given: python3 prosoft_rlx_ifh24e_unsigned_upgrade_package_root_rce.py --passwd-mode --passwd-entry "marker:*:0:0:INERT-DEMONSTRATION-ENTRY:/root:/bin/sh" Deliver the built package to a management listener the operator owns and is authorised in writing to test, over HTTP or HTTPS, with optional HTTP Basic credentials: python3 prosoft_rlx_ifh24e_unsigned_upgrade_package_root_rce.py --target
--port 80 --user