Summary
On a FRITZ!Box 7590 running FRITZ!OS 154.08.25, get_internet_config() never finds the
ISP credentials — and instead fills serial_username / serial_password with the literal
placeholder ppp / ppp taken from an unrelated block. The output therefore looks like it
contains a credential pair, so there is no signal that the real one was missed. A silently
wrong value is considerably worse than a missing field, which is why I'm reporting it rather
than just patching locally.
Environment
- FRITZ!Box 7590, FRITZ!OS 154.08.25 (export labelled
154.08.25i)
- VDSL with a manually configured provider (
active_provider = "other")
- Verified against a real password-protected export: 149 encrypted values decrypted with
0 MD5 integrity failures, so decryption itself works perfectly. This is purely a
key-lookup problem.
What happens
get_internet_config() looks for the ISP credentials under three key names:
pppoe_user = self._get_cfg_encrypted(ar7, "pppoeuser") or self._get_cfg_value(ar7, "pppoeuser")
pppoe_pass = self._get_cfg_encrypted(ar7, "pppoepwd") or self._get_cfg_encrypted(ar7, "ppppasswd")
None of pppoeuser, pppoepwd or ppppasswd exists anywhere in this export, so neither
pppoe_username nor pppoe_password is ever added to the result.
Execution then falls through to the serial lookup:
serial_user = self._get_cfg_encrypted(ar7, "username") or self._get_cfg_value(ar7, "username")
serial_pass = self._get_cfg_encrypted(ar7, "passwd")
Because _get_cfg_encrypted() runs re.search() across the entire ar7.cfg text with no
block scoping, this returns the first username / passwd in the file. Here that is
ar7cfg > serialcfg, which decrypts to the literal string ppp for both:
"internet": {
"mode": "dsldmode_router",
"ipv4_mode": "ipv4_normal",
"serial_username": "ppp",
"serial_password": "ppp",
"wan_interfaces": [ ... ]
}
Someone recovering a forgotten ISP password will reasonably read that as the answer.
Where the credentials actually live
ar7cfg > targets > local
username = "$$$$..."; <- ISP login
passwd = "$$$$..."; <- ISP password
Both are $$$$-encrypted and decrypt correctly with the existing decrypt_value().
Why this slipped through testing
The committed sample output FRITZBox_decoded_anonymized.json shows exactly the same
symptom:
"serial_username": "ppp",
"serial_password": "ppp",
with no pppoe_* keys present. That sample also contains
"provider": "tr069_unitymedia_hessen" and WAN interface nsgmii1 — i.e. a cable box
provisioned over TR-069, which has no PPPoE credentials to find in the first place. So the
PPPoE branch was never exercised by the available fixture, and the ppp/ppp output looked
like a plausible result rather than a red flag.
Suggested fix
Scope the lookup to the enclosing block instead of searching the whole file:
- Extract the
targets block, then each named sub-block inside it (local, …).
- Read
username / passwd from that sub-block and report them as
pppoe_username / pppoe_password (or a provider-neutral isp_*).
- Keep the existing
pppoeuser / pppoepwd / ppppasswd names as an additional lookup,
for firmware that does use them.
- Either drop the
serial_* fallback or reject obvious placeholders — a three-character
value of ppp is never a real credential. Emitting nothing is much safer than emitting
something wrong.
More broadly: _get_cfg_value() / _get_cfg_encrypted() taking the first global match is
fragile for any key that repeats. This one export has 31 username and 12 passwd
keys spread across serialcfg, targets > local, ddns > accounts, webui, TR_064,
emailnotify and 20 entries under apps > apps — so a global first-match is effectively a
lottery, and which value wins depends on file ordering.
I worked around it locally with a small brace-depth walker that yields
(line_number, line, nesting_path) for each section and dumps every $$$$ value annotated
with its block path; that surfaces the credentials unambiguously and also makes it obvious
which of the 31 username keys is which. Happy to open a PR with a block-scoped
get_internet_config() if that would be useful.
Aside
Genuinely nice tool to audit before trusting it with a file full of secrets: single file, one
dependency, no network access of any kind, and the key-derivation chain
(MD5(password) + 16 zero bytes → AES-256-CBC on the header Password= field → first 16
bytes + 16 zero bytes as the session key) matches the community's reverse-engineering of the
AVM scheme exactly. That auditability is the right property for this kind of utility, and it
is why I was comfortable running it at all.
Unrelated nit: README.md links to a LICENSE file for the MIT terms, but no LICENSE
file is present in the repository.
Summary
On a FRITZ!Box 7590 running FRITZ!OS 154.08.25,
get_internet_config()never finds theISP credentials — and instead fills
serial_username/serial_passwordwith the literalplaceholder
ppp/ppptaken from an unrelated block. The output therefore looks like itcontains a credential pair, so there is no signal that the real one was missed. A silently
wrong value is considerably worse than a missing field, which is why I'm reporting it rather
than just patching locally.
Environment
154.08.25i)active_provider = "other")0 MD5 integrity failures, so decryption itself works perfectly. This is purely a
key-lookup problem.
What happens
get_internet_config()looks for the ISP credentials under three key names:None of
pppoeuser,pppoepwdorppppasswdexists anywhere in this export, so neitherpppoe_usernamenorpppoe_passwordis ever added to the result.Execution then falls through to the serial lookup:
Because
_get_cfg_encrypted()runsre.search()across the entirear7.cfgtext with noblock scoping, this returns the first
username/passwdin the file. Here that isar7cfg > serialcfg, which decrypts to the literal stringpppfor both:Someone recovering a forgotten ISP password will reasonably read that as the answer.
Where the credentials actually live
Both are
$$$$-encrypted and decrypt correctly with the existingdecrypt_value().Why this slipped through testing
The committed sample output
FRITZBox_decoded_anonymized.jsonshows exactly the samesymptom:
with no
pppoe_*keys present. That sample also contains"provider": "tr069_unitymedia_hessen"and WAN interfacensgmii1— i.e. a cable boxprovisioned over TR-069, which has no PPPoE credentials to find in the first place. So the
PPPoE branch was never exercised by the available fixture, and the
ppp/pppoutput lookedlike a plausible result rather than a red flag.
Suggested fix
Scope the lookup to the enclosing block instead of searching the whole file:
targetsblock, then each named sub-block inside it (local, …).username/passwdfrom that sub-block and report them aspppoe_username/pppoe_password(or a provider-neutralisp_*).pppoeuser/pppoepwd/ppppasswdnames as an additional lookup,for firmware that does use them.
serial_*fallback or reject obvious placeholders — a three-charactervalue of
pppis never a real credential. Emitting nothing is much safer than emittingsomething wrong.
More broadly:
_get_cfg_value()/_get_cfg_encrypted()taking the first global match isfragile for any key that repeats. This one export has 31
usernameand 12passwdkeys spread across
serialcfg,targets > local,ddns > accounts,webui,TR_064,emailnotifyand 20 entries underapps > apps— so a global first-match is effectively alottery, and which value wins depends on file ordering.
I worked around it locally with a small brace-depth walker that yields
(line_number, line, nesting_path)for each section and dumps every$$$$value annotatedwith its block path; that surfaces the credentials unambiguously and also makes it obvious
which of the 31
usernamekeys is which. Happy to open a PR with a block-scopedget_internet_config()if that would be useful.Aside
Genuinely nice tool to audit before trusting it with a file full of secrets: single file, one
dependency, no network access of any kind, and the key-derivation chain
(
MD5(password)+ 16 zero bytes → AES-256-CBC on the headerPassword=field → first 16bytes + 16 zero bytes as the session key) matches the community's reverse-engineering of the
AVM scheme exactly. That auditability is the right property for this kind of utility, and it
is why I was comfortable running it at all.
Unrelated nit:
README.mdlinks to aLICENSEfile for the MIT terms, but noLICENSEfile is present in the repository.