H3C G3/G5 HDM Firmware Verification Key Replacement

|
Updated
Vendors H3C
Products
H3C HDM Family-A G3, H3C HDM Family-A G5
  • H3C HDM Family-A G3
  • H3C HDM Family-A G5
Related

Executive summary #

H3C HDM Family-A verifies the RSA signature of a firmware image against a public key it reads from the mutable file /conf/public.pem. That file is writable by an authenticated Redfish client through the UpdateService.UploadSignKey action, which stores the uploaded body verbatim after validating only that the multipart filename ends in .pem. An administrator, or any account with firmware-update privilege, can therefore replace the trust anchor with their own key, sign a malicious image with the matching private key, and have it pass verification. Family-A has no hardware root-of-trust (no PFR) to catch the substituted key.

This issue is an instance of CWE-667 (Improper Locking), has an estimated CVSSv3.1 score of CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N (3.1, Low), and current SSVC vectors of "Exploitation: PoC" and "Technical Impact: Total."

Technical details #

The Family-A firmware verification routine is VerifyImageSign in usr/local/lib/libflash.so.2.23.0. It selects the public-key file with a two-step access() probe and uses whichever exists, checking /conf/public.pem first and only falling back to the image-embedded key at /tmp/public.pem:

<i>/* libflash.so.2.23.0 VerifyImageSign (site 0x1c088) */</i>
iVar5 = access("/conf/public.pem", 0);          <i>/* ptr 0x1c2a4 -> 0x20ab0 */</i>
pcVar6 = "/conf/public.pem";
if ((iVar5 == 0) ||
    (iVar5 = access("/tmp/public.pem", 0), pcVar6 = "/tmp/public.pem", iVar5 == 0)) {
    iVar7 = VerifySignatureWithDigest(pcVar6, digest, ...);   <i>/* PEM_read_RSA_PUBKEY + EVP_PKEY_verify */</i>
}

The file /conf/public.pem is written by the authenticated Redfish action UpdateService.UploadSignKey, implemented in usr/local/redfish/redfish/upload-file.lua (lines 48-62) and routed from extensions/routes/update_service_routes.lua:

elseif action == "UpdateService.UploadSignKey" then
    if req_args.pem_file ~= nil then
        ...
        local parsed_body = req_args.pem_file[1][1]
        local fname = req_args.pem_file[1]["content-disposition"]["filename"]
        if not string.match(fname, '(.-%.pem)$') then          <i>-- only a ".pem" filename-suffix check</i>
            ... InvalidSignKeyFormat
        else
            local f = io.open(tmp_name .. "public.pem", "w")
            f:write(parsed_body)                                <i>-- attacker body, unvalidated</i>
            ...
            os.execute("cp ".. tmp_name .. "public.pem " .. "/conf/")   <i>-- -> /conf/public.pem</i>

The only validation is the filename suffix; the PEM body is written verbatim and copied into the persistent /conf JFFS2 partition, so the substitution survives the subsequent update. There is no check that the uploaded key is an authorized vendor key.

Upload a replacement public key through the OEM action. The shipped handler returns HTTP 204 and writes the supplied body to /conf/public.pem:

curl -sk -X POST \
    -H "X-Auth-Token: $TOKEN" \
    -F '[email protected];filename=x.pem' \
    https://example.lan/redfish/v1/UpdateService/Actions/Oem/UpdateService.UploadSignKey
HTTP/1.1 204 No Content

Sign an HDM image with the matching private key and submit it through the normal update flow. VerifyImageSign selects /conf/public.pem before the image-embedded /tmp/public.pem, so the attacker signature passes the same verification path used for vendor images.

On HDM2, FLASHER_GetRSAPubKey reads /conf/flasher/RSA1024_public.pem and /conf/flasher/RSA4096_public.pem, but the UploadSignKey action does not ship. No equivalent network write path is established for HDM2.

Attacker value #

Anyone with the firmare-update privilege (typically, administrator) can flash a persistent firmware implant or downgrade to a known-vulnerable version. The installed image becomes the running BMC firmware and controls the platform-management functions exposed to the host. This vulnerability is primarily useful to an attacker seeking persistent access after obtaining firmware-update privileges through other means.

Credit #

This issue was discovered by HD Moore with the help of AI tools. Disclosure was coordinated by Tod Beardsley.

Timeline #

  • 2026-07-18: Initial findings discovered and documented
  • 2026-07-21: CVE-2026-65197 reserved
  • 2026-08-05: Disclosed to CISA (VU#415695)
  • 2026-08-28: CISA opened case VU#759570
  • 2026-10-13: Public disclosure

Written by todb

Tod Beardsley is VP of Security Research at runZero, where he "kicks assets and fakes frames." Prior to 2025, he was the Section Chief for the Vulnerability Response section for CSD/VM/VRC at CISA, the Cybersecurity and Infrastructure Security Agency, part of the US government, and a seasonal Travis County Election Judge in Texas. He's also a founder and CNA point of contact for AHA!. Tod spends much of his time involved in vulnerability research and coordinated vulnerability disclosure (CVD). He has over 30 years of hands-on security experience, stretching from in-band telephony switching to modern ICS/OT implementations. He has held IT ops, security, software engineering, and management positions in large organizations such as the US Government, Rapid7, 3Com, Dell, and Westinghouse, as both an offensive and defensive practitioner. Tod is a CVE Board member, has authored several research papers, and is an internationally-tolerated horror fiction expert.

More about todb
Subscribe Now

Get the latest news and expert insights delivered in your inbox.

Welcome to the club! Your subscription to our newsletter is successful.