From: disclosure via Fulldisclosure <fulldisclosure () seclists org>
Date: Tue, 20 Oct 2026 02:31:17 +0000
0day Rubbish Research Team is publicly disclosing a vulnerability in the
ProSoft RLX-IFH24E, version v2.2.44, the industrial wireless radio from
ProSoft Technology, Inc. (United States). 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 vendor is identified precisely here, by its legal name, its published
headquarters address and its support domain, because a different and
unrelated company trades under a similar name in another country, and this
research concerns the United States industrial radio vendor only. ProSoft
Technology's own legal pages give the entity as ProSoft Technology, Inc.,
9201 Camino Media, Suite 200, Bakersfield, CA 93311, USA, and its support
pages note that support addresses moved from the vendor domain to
belden.com, the domain of its parent company.
THE EVIDENTIAL BOUNDARY, BEFORE ANYTHING ELSE
This disclosure has an unusual evidentiary shape and it is stated up front
so that no reader over-reads it. No HTTP request was ever issued at this
product. No physical appliance was used at any point. The chain is therefore
not presented as an exploit run against a live device, and it is not one.
The finding has two halves. The installer half was measured: the firmware's
own /sbin/upgrade script and the other scripts in /sbin were executed
byte-for-byte unmodified inside an isolated chroot sandbox built on a copy
of the upgrade package's own root filesystem, with marker files and a
recorded executing uid as the evidence. The upload half was inferred: it
rests entirely on disassembly of the shipped ARM big-endian CGI and of the
shared platform UI library, and nothing in it was observed in motion. The
line between those two halves runs down the middle of the chain and is drawn
explicitly wherever it appears in the published analysis.
TYPE AND ROOT CAUSE
Code installed and executed without an integrity or authenticity check.
CWE-494 is the primary classification, with CWE-345 and CWE-347 co-equal
because the only check anywhere in the path is a non-cryptographic string
equality and no signature is verified at any point. CWE-434, CWE-73, CWE-250
and CWE-269 are contributing and are reported separately without raising any
metric. This is explicitly not CWE-78: no command string is injected at
either hop, so a command-injection remediation, filename input filtering or
shell escaping would not close the chain. 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 involved at all.
The system upgrade package RLX-IFH24E_v2244.pkg contains four artefacts
that together describe the whole path: the upload handler
usr/lib/cgi-bin/tool_reprogramming.cgi, the installer /sbin/upgrade that it
invokes as a root process, the factory account database /etc/passwd, and
the default configuration store /etc/db1.
The upload hop, recovered by disassembly. HandleSubmit at offset 0x8d1c
takes the file1 part of a multipart/form-data POST to
/cgi-bin/tool_reprogramming.cgi and performs this sequence, with the format
string sitting at rodata offset 0x2b1c:
cgiFormFileName("file1") -> the client's original filename
cgiFormTempFileName -> path of the temporary body file
sprintf(buf, "%s /upgrade/upgrade/%s", tempfile, origfilename)
SubProgram_Start("/bin/mv", buf) ; must succeed first
SubProgram_Start("/sbin/upgrade", origfilename)
The original client filename is used unmodified twice: once to build the
destination of the move, and once as the installer's only argument. That
makes the file extension, and therefore the branch the installer takes,
entirely the requester's choice. The handler applies no extension
whitelist, no digest comparison and no path canonicalisation.
The installer hop, executed and observed. /sbin/upgrade is a short POSIX
shell script that runs with the privileges of the CGI that invoked it,
which are root, and dispatches purely on ${FILE##*.}:
case "$EXT" in
passwd) cp /upgrade/upgrade/$FILE /etc/passwd
cp /upgrade/upgrade/$FILE /etc/passwd_original ;;
factory) cp /upgrade/upgrade/$FILE /etc/db1 ;;
pkg) gunzip -t /upgrade/upgrade/$FILE || exit 1
tar -xf /upgrade/upgrade/$FILE -C <staging>
[ "$DEVICE_PRODUCT_NAME" = "$PKG_LOCK_NAME" ] || refused
echo "Match image."
./pre
tar -xf extract/upgradepkg.tar -C /
./post
echo "Upgrade Successful"
reboot ;;
esac
The .pkg branch is the code-execution primitive. ./pre runs before anything
else is applied, so requester-authored code executes against the pristine
filesystem and can read the old state; tar -xf extract/upgradepkg.tar -C /
applies an inner archive whose entry names are constrained in no way, which
is an arbitrary rootfs write; ./post runs after the write, so the same
requester also sees the new state; and the branch ends in an unconditional
reboot, which makes the change persistent and destroys the runtime
evidence. The .passwd branch is a second, independent route to the same
privilege ceiling and it needs no execution at all: it copies the upload
over both the live account database and the backup copy, so the device's own
recovery path restores the modified database rather than the shipped one.
The .factory branch is the same primitive aimed at the configuration store.
The only validation the installer performs before executing package content
is a string equality, and both of its operands are available to whoever
supplies the package. The expected value DEVICE_PRODUCT_NAME is read from
the appliance's own platform identity file /etc/MHX_CONFIG, where this
firmware records PRODUCT_NAME=IPx21PROSOFT, a compile-time constant
recoverable by reading the shipped firmware once. The asserted value
PKG_LOCK_NAME is read from a file called lockfile inside the package being
installed. A package whose identity is validated against a value carried
inside that same package is not a source of assurance about that package.
The gunzip -t test beside it is a gzip stream-integrity check and proves
only that the file is well-formed. The check that exists was never meant to
answer the authenticity question, and strengthening it would change nothing
while the requester supplies both sides; what is missing from the path
between an HTTP request and root execution is any authenticity step at all.
WHAT WAS MEASURED
A crafted .pkg carrying the correct platform string was placed in the
staging directory, reproducing by hand the state the handler's /bin/mv step
would have produced, and the unmodified installer was invoked on it. It
printed "Match image." and then "Upgrade Successful" and exited 0. Four
independent effects were observed on the sandbox filesystem: the pre and
post scripts left markers proving both executed; the inner archive left a
marker at the root of the filesystem, proving the unconstrained extraction;
and a marker file written by the payload at runtime contained
PRE-RAN-AS-UID=0. That last value is the load-bearing line, because the
payload wrote it during execution 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 for this finding is uid=0(root).
A .passwd run was performed the same way. Both /etc/passwd and
/etc/passwd_original were replaced by the uploaded text: the digest
recorded for the pre-seeded database before the run no longer matched
afterwards, and the marker line was present in both copies. The marker line
grants a uid-0 account with /bin/sh, which on this product is the
difference between the factory admin shell /bin/txtUI and a general root
shell available at the next interactive login.
A negative control establishes that none of that 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. Three conclusions
follow: the markers are produced by the install path; the observed success
was conditional on that one string comparison, so the check is bypassable by
supplying the documented string rather than being irrelevant; and the
positive and negative runs are mutually exclusive, so the positive result
cannot be explained by a sandbox in which everything appears to succeed.
The run count is stated exactly because it is small. One positive .pkg run,
one positive .passwd run and one negative control run, in one sandbox on one
host, with each effect observed once. The runs were not repeated across
multiple sandbox builds and were never performed on hardware.
Four substitutions were made in the harness and none in the code under test.
The firmware is ARM big-endian and the sandbox host is not, so an x86
busybox build supplied /bin/sh, tar, gunzip, mv and cp; 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 so that a before and after comparison was
possible. Two lines appearing in the transcript, "logger: not found" and
"mhx: not found", are absent helpers in the sandbox PATH and are not product
behaviour; they are reproduced rather than suppressed, because concealing a
negative-looking line would misrepresent the run.
WHAT WAS NOT MEASURED
No physical appliance was used. No HTTP request was issued to the product
and the uploader hop was never executed; the staging file was placed by
hand. The web server was not executed and could not have been, because
/bin/boa lives in the device base flash and is not contained in the system
upgrade package, and it is boa that decides whether a client is allowed to
fork a CGI at all. Its access control could therefore be neither proved nor
disproved from the artefacts available. The CGI binaries and the firmware's
own ARM helper utilities were not executed for the architecture reason. The
reboot closing the .pkg branch did not happen, since the sandbox is a
directory tree with no init; the path leading to it was observed up to that
point. No privilege escalation is claimed, the chain starting from the
administrative identity the product's own account database already grants
uid 0. There is no claimed reach into the radio baseband image
RLX-IFH24E_v5089.mhx, which was identified but not exercised, and no
demonstrated impact on any attached control system, PLC or historian.
AUTHENTICATION
The finding is published as requiring administrative privilege. The package
documents its own account model:
admin: <DES-crypt hash>:0:0:admin:/:/bin/txtUI
upgrade: <same DES-crypt hash>:500:500:ftpupgrade:/upgrade/upgrade:/bin/sh
The admin account is uid 0, so the management stack is not
privilege-separated and an administrative login already holds the privilege
the sink needs. Its shell is /bin/txtUI, a restricted interactive interface
rather than a general shell, which is a user-interface constraint and not a
security boundary once arbitrary code execution is available. A second
account exists with uid 500, a working /bin/sh, the same password hash, and
a home directory that is precisely the upload staging path. The interface
also carries a password-change page and a configurable RADIUS mode. The
factory credential itself is not reproduced here; the published advisory
deliberately withholds that hash.
What the package establishes about the handler layer is narrow and is stated
narrowly: none of the 57 CGIs contains authentication logic of its own. No
CGI imports crypt, none parses or sets a session cookie, and there is no
login CGI in the directory; the menu pages link directly to the CGIs. So
whatever access control exists on this surface lives in the web server's
request handling or not at all. That is a weaker statement than the reading
it is sometimes used to support, and no unauthenticated path is claimed.
The firmware's own default configuration record inside the package enables
the web interface on both transports, UI_Access_HTTP=B and HTTPS=B, while
carrying non-default values for the Telnet and SSH entries. That record is
a shipped artefact, which is why it is used as evidence of a default, and
the vector's Network classification comes from the transport the shipped
artefact establishes rather than from an observed reach.
SEVERITY
The published score is 7.2 High on the vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H. From first principles:
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. Scope is Unchanged because the vulnerable component and the impacted
component are the same appliance operating system; the CGI already runs as
root, the installer already runs as root, and the rootfs is extracted by
that same root process, so no authority boundary is crossed. PR:H because
installing firmware is an administrative function of the product and no
lower-privileged or unauthenticated principal was demonstrated reaching the
handler. AC:L because the chain is deterministic, with no race, no timing
window, no memory-layout dependency and no on-path position, and the one
piece of environment knowledge it needs is a platform string published
inside the shipped firmware.
Three readings are published as rejected. 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 the uid-500 upgrade account 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
single cause of refusal. Two of those rejected readings are higher than the
number published, and the advisory points that out itself rather than
leaving a reader to find it.
Two further readings are published as 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 it
was NOT demonstrated. It applies only if the management listener does not
gate the handler at all, which is a structural inference rather than a
measurement, because the binary that would answer the question is not in
the package that was analysed. 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 honest headline is
the conservative one: an administrative session can install unsigned code
that the device then executes as root, and the installation and execution
half of that was proved against unmodified firmware code in a sandbox
rather than against a live appliance.
IMPACT
Root command execution on the appliance through a single
multipart/form-data POST followed by the product's own install sequence,
with attacker-authored code running both before and after the rootfs write.
Full replacement of the account database, including userids, password
hashes and login shells, with both the live and the backup copy overwritten
so the recovery path restores the modified content. Rewrite of the device
configuration through /etc/db1, which holds the management interface's own
transport flags, accounts and radio behaviour, meaning whoever writes it can
widen or narrow the exposure of the surface they arrived through and can
survive a later reconfiguration by the operator. Persistence that is native
to the product rather than improvised, since a package install rewrites the
rootfs and the installer reboots afterwards, so whatever was installed is
what comes back up. Availability control at will, including by means of the
reboot the installer always performs 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 response is the vendor's own success page,
and a legitimate upgrade prints exactly the same two lines, so the usable
indicators are post-hoc and filesystem-level, namely an installed package
whose LOCK_NAME and VERSION pair matches no release the operator obtained, a
file left under /upgrade/upgrade/, and a uid-0 account whose shell is a
general shell the vendor did not ship.
SCOPE
Only the system upgrade package RLX-IFH24E_v2244.pkg was analysed. No other
build of the RLX-IFH24E, no other version of the OEM IPx21 platform and no
other product of either vendor is claimed as affected, and no version range
is asserted. 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; that is stated as a likelihood and
not scored, because it was not tested. The vendor's own public support site
lists the downloadable files for this model, including its firmware, inside
a Legacy Products directory described as its directory of products being
phased out, which is recorded here because it bears on what operators can
expect and because it did not bear on whether the defect is real.
ADJACENT OBSERVATIONS, NOT CLAIMED AND NOT SCORED
Three structural observations appear in the analysis and were not exercised.
The runtime startup configuration 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
has a first-argument dispatch that includes sending AT commands to the radio
module; no AT command was sent and the CGI was never executed. The platform
ships SNMP community strings public and private, which belong to the
configuration plane and are not part of this chain.
PROOF OF CONCEPT
The published script is Python 3 using only the standard library, with no
third-party dependency and no hardcoded target. It builds the crafted
package, 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 to /cgi-bin/tool_reprogramming.cgi. Host, port, scheme, credentials,
command, lock name, product name, field name, marker token and output path
are all command-line arguments defaulting to loopback-safe values; the
network send is a timeout-bounded request and the build half has no network
dependency at all. A control mode builds the structurally identical package
with a wrong product string, so an operator can confirm on hardware they own
and are authorised to test that the success path really is gated by that
string and nothing else. A separate mode builds the account-database variant
as a local file without sending it. The default payload is inert: it creates
one marker file and writes the executing uid into a second, with no
deletion, no encryption, no modification of any other file and no attempt to
survive a reboot. The script's own help text warns that the network half of
this chain was never exercised against an appliance, so running it against
hardware the operator does not own or hold written authorisation to test
will reboot and reimage that device.
REMEDIATION SUMMARY
Require a signature over the package verified against vendor public keys
held in the base flash, where an upgrade cannot replace them, and fail
closed before gunzip, tar, pre or the -C / extraction is reached. Move the
product-string comparison out of the trust path and document it as the
compatibility check it is. Confine package-supplied code by running pre and
post inside a chroot or mount namespace over the staging tree only, or
replace them with a fixed vendor script set chosen by a signed manifest.
Apply the inner archive through a path allowlist that rejects traversal
components, absolute names, symlink entries and device nodes, and remove the
-C / argument. Take the account database and the configuration store off the
HTTP upload surface entirely, or gate them on physical presence with
unconditional logging and structural validation. Separate privileges between
the web interface and the installer so that a bad package is confined to the
staging directory. Validate extensions at the handler, reject filenames
containing path separators, and log the digest of every accepted upload to
storage that survives the reboot. Ship regression tests asserting both a
distinct refusal and the absence of every marker the positive case produces,
for an absent signature, an invalid signature, a key not present in base
flash, traversal and absolute entries in the inner archive, a .passwd upload
and a .factory upload; the control case described above already provides a
working harness for all of them. Until a fix exists, operators should keep
the management listener on a dedicated management network behind an
authenticating proxy and off general user networks, confirm the factory
password has been changed on both accounts and use the RADIUS mode the
product offers, treat every firmware upgrade as a supply-chain event with
digests recorded before installation, place /etc/passwd,
/etc/passwd_original and /etc/db1 under file-integrity monitoring with an
alert on any uid-0 account whose shell is not the restricted interface,
snapshot configuration before and after each upgrade, and re-image from
independently verified media while rotating every credential the device
holds if compromise is suspected.
Full technical analysis and the reproducible proof of concept, published
unconditionally:
https://0day-rubbish.com/blog/prosoft-rlx-ifh24e-unsigned-upgrade-package-root-rce
Project archive (ongoing disclosure series):
https://github.com/Exploit-Garbage/0day-Rubbish
ProSoft Technology, Inc. was notified through the mailbox its own website
publishes for reporting a potential vulnerability, under a Security
Assurance heading on its Quality and Environment page, with a copy to the
North America technical-support address published on its Regional Technical
Support page. The vendor publishes no RFC 9116 security.txt on its web host
and has no security, product-security, vulnerability-disclosure or
responsible-disclosure page at any path tried; its legal pages contain no
vulnerability-reporting language. No report was filed with any third party
and no national coordination body was involved. This disclosure is
unconditional: the research is already public, no permission was asked for,
no embargo was requested, nothing is withheld pending a reply and nothing is
asked in return. Remediation questions are answered at no cost.
--
0day Rubbish Research Team
disclosure () 0day-rubbish com
https://0day-rubbish.com
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/
Current thread:
- [0day-rubbish] ProSoft Technology RLX-IFH24E v2.2.44 unsigned firmware upgrade package to root command execution and full account-database overwrite (7.2) disclosure via Fulldisclosure (Oct 20)