User-level access was enough to recover passwords for higher-privilege local accounts and management services.
Super-Admin Password Leak Affecting Zyxel CPE/ONT/LTE Fleet
The issue was first reported against VMG3625-T50B firmware V5.50(ABTL.0)b2k and later expanded into a broader product-line issue: authenticated low-privilege sessions could reach backend DAL endpoints that returned administrator, supervisor, FTPS, and TR-069 secrets in cleartext. Later Zyxel advisories expanded the scope well beyond a single ISP-customized router image.
The firmware tree shows the exposure lived in shared libzcfg_fe_dal management code, not in a one-off UI template.
Several later fixes merely hid values in CLI or TR-98 presentation paths after the backend had already loaded the raw secrets.
Original finding
A user-privileged account could browse directly to:
/cgi-bin/DAL?oid=login_privilege/cgi-bin/DAL?oid=tr69
and obtain cleartext values for local accounts and TR-069 credentials. Disclosure material also shows a related
/getDefaultInformation response leaking default passwords for root, supervisor, admin, admin1, and ftps.
From one ISP image to a larger fleet
Zyxel first assigned the CVE for VMG3625-T50B, but its later public advisory broadened affected scope across multiple product families.
- DSL / Ethernet CPE: VMG3625-T50B, VMG3927-T50K, VMG8623-T50B, VMG8825-T50K, EMG3525-T50B, EMG5523-T50B, EMG5723-T50K, DX3301-T0, DX5401-B0, EX5401-B0, EX5501-B0
- Fiber ONT: AX7501-B series, EP240P, PMG5617GA, PMG5622GA, PMG5317-T20B, PMG5617-T20B2, PM7300-T0
- 4G / 5G CPE: LTE3301-PLUS, LTE5388 family, LTE7480 family, LTE7490-M804, NR5101, NR7101, NR7102
That matters because the exposed code belongs to a shared management framework, which matches the later vendor-side scope expansion.
Why the leak existed
1. Login privilege GET returned raw passwords
In the firmware source, zcfgFeDal_LoginPrivilege_Get iterates through
RDM_OID_ZY_LOG_CFG_GP_ACCOUNT and copies each account's Password field directly into the outgoing JSON array.
json_object_object_add(paramJobj, "Username",
JSON_OBJ_COPY(json_object_object_get(loginPrivilegeObj, "Username")));
json_object_object_add(paramJobj, "Password",
JSON_OBJ_COPY(json_object_object_get(loginPrivilegeObj, "Password")));
The DAL command registration then exposes login_privilege as edit|get with an empty privilege string, even though the inline comment still says root_only.
2. TR-069 GET copied the whole management object
The TR-069 DAL getter uses a generic copy path across the management parameter list. That includes Password and
ConnectionRequestPassword, which are copied out unless another layer explicitly strips them.
else {
json_object_object_add(pramJobj, paraName,
JSON_OBJ_COPY(json_object_object_get(mgmtJobj, paraName)));
}
The corresponding DAL registration exposes tr69 as get|edit.
3. Cosmetic hiding arrived after the backend exposure
Later patches show Zyxel masking values in display code by printing ******** for ACS and SIP passwords. That is a UI / CLI presentation fix, not a backend access-control fix.
printf("%-45s %s\n", "ACS Password", "********");
4. Subsequent metadata fixes confirm the sensitivity
Other later patches add PARAMETER_ATTR_PASSWORD to fields like Password,
ConnectionRequestPassword, DefaultPassword, and PasswordHash.
That change acknowledges these values should not have been returned verbatim in getter flows.
Disclosure path
CVE-2021-35036 for the VMG3625-T50B case.
The VMG8825 genpass clue
The repository also includes an adapted copy of the public Zyxel VMG8825-T50 keygen work by boginw, tailored for the VMG8825-B50B because the original version did not fit this router model directly. It demonstrates that password material was embedded in reusable vendor logic and could be regenerated from device identity inputs instead of being treated like narrowly scoped operator secrets.
run-qemu.bat launches the ARM guest, rootfs.ext2 carries the extracted filesystem, and the tracked
adapted-runtime/opt/genpass/genpass wrapper is the B50B-specific version included in the repository. That wrapper accepts serials whose fifth character is V, Y, or H
before calling into vendor password-generation code under /opt/zyxel.
- Launch the emulator from the repo with
.\zyxel-vmg8825-b50b-keygen-lab\run-qemu.bat. - At the guest login, authenticate with
root/root. - Run
genpass S182V12345678to derive the supervisor and admin password families from a sample modem serial.
# host
PS> .\zyxel-vmg8825-b50b-keygen-lab\run-qemu.bat
# inside the bundled emulator
root@VMG8825-B50B-emul:~# genpass S182V12345678
Old algorithm supervisor password.............. 789630c0
New algorithm supervisor password.............. dEfczwP8Sy
...
additional admin and Wi-Fi outputs continue below
That does not prove CVE-2021-35036 by itself, but it reinforces the design pattern behind the analysis: password material was treated as reusable product data that multiple components could fetch, transform, display, or restore. Once the DAL layer exposed those backing objects too broadly, the rest of the stack never had a chance.
How genpass works internally
1. The shell script is only a front-end
Reverse engineering starts with the extracted genpass shell wrapper, not the password algorithm itself. The script validates the serial format,
exports it as SERIAL, sets LD_LIBRARY_PATH to Zyxel's extracted libraries, preloads libhook.so, and then invokes
the real worker binary: /opt/genpass/getpassword.
That matters because the visible script does not derive passwords on its own. It acts as an execution harness that feeds controlled input into vendor code that was originally meant to run inside the router environment.
2. libhook.so replaces the router's serial source
The small preload library is the most revealing part of the setup. Static analysis of its exported symbols shows that it overrides
zyUtilIGetSerialNumber and zcfgBeCommonIsApplyRandomSupervisorPasswordNewAlgorithm.
In the first hook, the code calls getenv("SERIAL"). If the environment variable is present, it copies that value into the caller's buffer.
If not, it falls back to the original function via dlsym(RTLD_NEXT, "zyUtilIGetSerialNumber"). In the second hook, it simply returns
1, forcing the "new random supervisor password" path on during emulation.
3. getpassword is an orchestrator, not the algorithm
String and symbol analysis of getpassword shows that it dynamically resolves the real generators at runtime. Its string table contains
libzcfg_be.so, libzcfg_be_wind.so,
zcfgBeCommonGenKeyBySerialNumMethod2, zcfgBeCommonGenKeyBySerialNumMethod3,
zcfgBeCommonGenKeyBySerialNumConfigLength, zcfgBeCommonGenKeyBySerialNumConfigLengthOld,
and zcfgBeWlanGenDefaultKey.
That is a strong indicator that the binary does not embed one monolithic algorithm. Instead, it loads vendor library entry points, asks them for the required buffer sizes, invokes multiple generation routines, and prints the results under labels such as old/new supervisor, old/new admin, WIND-specific admin variants, and several Wi-Fi key families.
4. The emulator recreates enough of the firmware to make the vendor code run
The reason this works in QEMU is that the extracted filesystem still contains the same userland and shared objects the original firmware expected.
The wrapper points LD_LIBRARY_PATH at Zyxel's library directories, the preload hook substitutes missing hardware-derived state,
and the vendor password functions execute as if they were running on the device.
From a reverse-engineering perspective, that makes genpass a harness around reusable product logic rather than a standalone cracking tool.
The lab setup does not reimplement Zyxel's algorithms; it restores just enough runtime context to call them directly.
5. The supervisor routines are deterministic and portable
Disassembly of zcfgBeCommonGenKeyBySerialNumMethod2 and
zcfgBeCommonGenKeyBySerialNumMethod3 shows two exact supervisor paths. Method2 computes MD5(serial), renders each digest byte as
vendor-style hex where one-nibble bytes are duplicated instead of zero-padded, hashes that string again, and then samples every third character to build the
old supervisor password.
Method3 reuses that same double-hash stream, uppercases it, derives a 16-bit seed from bytes 1 and 2 of MD5(serial), increments the seed
until a ten-slot mod-3 schedule contains uppercase, lowercase, and digit classes, and then maps each sampled byte into safe alphabets with explicit substitutions
for I, O, l, o, 1, and 0. That is why the generator can be ported cleanly into browser
JavaScript: the firmware logic is deterministic and table driven, not server backed.
Supervisor generator for Method2 and Method3
This widget is an exact browser-side port of the two supervisor routines exposed by the bundled getpassword runtime.
It validates the B50B serial format accepted by this repository's wrapper, reproduces Zyxel's double-hash quirk, and replays the final Method3 slot mapping.
MD5(serial) in vendor hex
MD5(round1) in vendor hex
Method2 returns the first eight tapped characters. Method3 uppercases the ten-character tap before class mapping.
The scheduler advances the seed until the ten slots contain uppercase, lowercase, and digit classes at least once.
Ready. Generate a result or replay the slot-by-slot mapping.
Method2
Method3
This matches the call order inside getpassword: old supervisor is Method2, new supervisor is Method3.
Not just “passwords in config”
Reducing this case to “cleartext storage” understates the practical impact: secrets belonging to higher-privilege accounts and remote-management channels were retrievable from web-exposed DAL endpoints by a weaker account. In real deployments, that turns credential disclosure into a pivot for privilege escalation, service abuse, and post-auth compromise.
The later patch trail is also revealing. Instead of one clean architectural fix, the source history shows several incremental attempts: exposing getters, adding masking in display functions, and only later marking parameters as password-type data. That progression is exactly what makes this class of bug dangerous in shared OEM / ISP firmware stacks.