mirror of
https://github.com/xvzf/zyxel-gpon-sfp.git
synced 2025-08-06 02:47:19 +02:00
feat: sharing initial discoveries
Signed-off-by: Matthias Riegler <matthias@xvzf.tech>
This commit is contained in:
commit
2779180504
4 changed files with 332 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
venv/
|
259
Readme.md
Normal file
259
Readme.md
Normal file
|
@ -0,0 +1,259 @@
|
|||
# "Hacking" the (Telekom) Zyxel GPON SFP module (PMG3000-D20B)
|
||||
> The SFP can be sourced very easily and is widely available in Germany.
|
||||
|
||||
## TLDR
|
||||
|
||||
Checkout the three options for configuring your SFP.
|
||||
|
||||
### 1. WEB UI
|
||||
1. Configure the ethernet interface the SFP is in with the IP `10.10.1.2/24`.
|
||||
2. Port-forward the SFPs web interface to your local machine via SSH: `ssh -L 127.0.0.1:8080:10.10.1.1:80 <user@router>`.
|
||||
3. Access the web-interface on `http://localhost:8080`, username `admin`, password `1234`.
|
||||
|
||||
### 2. CLI (on the SFP)
|
||||
> Note: The PLOAM ID has to be HEX encoded, in case yours is a 10-character string, you can transform it using `python3 -c 'print(hex("<enter PLOAM/SLID between the qotes>"))'`. Omit the `0x` prefix.
|
||||
|
||||
1. Configure the ethernet interface the SFP is in with the IP `10.10.1.2/24`.
|
||||
2. SSH into the module using `admin@10.10.1.1`, password `admin`.
|
||||
3. Login into the CLI with user `admin`, paddword `1234`.
|
||||
4. Change the _PLOAM/SLID/Installationskennung_ by entering following commands followed by a newline:
|
||||
- `hal`
|
||||
- `password <PLOAM/SLID>`
|
||||
|
||||
### 3. CLI (remote)
|
||||
> Note: requires Python >= 3.8
|
||||
|
||||
```
|
||||
NAME
|
||||
zyxel_gpon_sfp.py --sfp_addr=http://10.10.1.1
|
||||
|
||||
SYNOPSIS
|
||||
zyxel_gpon_sfp.py --sfp_addr=http://10.10.1.1 - COMMAND
|
||||
|
||||
COMMANDS
|
||||
COMMAND is one of the following:
|
||||
|
||||
info
|
||||
|
||||
set_slid
|
||||
|
||||
set_sn
|
||||
```
|
||||
|
||||
|
||||
## Motivation
|
||||
My ISPs ([Deutsche Telekom](https://www.telekom.de/)) FTTH offering uses on a GPON network and distributes ONUs with a 1G (or 2.5G Ethernet) for non-business customers.
|
||||
I intended to run the fiber directly into my Linux router (using one of the SFP+ ports).
|
||||
Looking at the business offerings building upon the same technology revealed SFPs distributed only business customers using the [_Digitalisierungsbox Premium 2_](https://www.telekom.de/hilfe/geraete-zubehoer/router/digitalisierungsbox/premium-2#e_745060).
|
||||
The mentioned SFP is made by Zyxel with the identifier `PMG3000-D20B` and sold as [_Digitalisierungsbox Glasfaser Modem_](https://geschaeftskunden.telekom.de/internet-dsl/produkt/digitalisierungsbox-glasfasermodem-kaufen) (Telekom only sells it to business customers but it is available online for ~40 Euros).
|
||||
|
||||
## Accessing the module
|
||||
|
||||
After _reverse engineering_ (this time it has been a `fzf` through all files, not analysing the binaries) the firmware of _Telekom Digitalisierungsbox 2_, I've identified the IP address of the module being `10.10.1.1/24` based on a SQL statement with a comment:
|
||||
```sql
|
||||
-- BS-6456: remove marker 'RESERVED' from static IP used to access the SFP module
|
||||
UPDATE Ip SET Name="" WHERE IpAddress="10.10.1.2" AND Interface="eth1" AND LogicalInterface="eth1";
|
||||
```
|
||||
|
||||
Digging a bit further in plaintext SQL statements reveals the credentials.
|
||||
```sql
|
||||
-- ...
|
||||
INSERT INTO SshConfiguration VALUES ( 1, 0, 5, 22, 'Access only for authorized persons!', 0, '' );
|
||||
INSERT INTO SshUser VALUES ( 1, 0, 'admin', 'admin', 0 );
|
||||
-- ...
|
||||
INSERT INTO GPONConfig VALUES ( 1, 1, '10.10.1.1', 'admin', '1234', '', '' );
|
||||
```
|
||||
|
||||
Well, let's give it a try. SSH access sounds like a charm and is confirmed by nmap:
|
||||
```bash
|
||||
xvzf@e300 ~ % nmap 10.10.1.1
|
||||
Starting Nmap 7.80 ( https://nmap.org ) at 2022-02-02 06:31 UTC
|
||||
Nmap scan report for 10.10.1.1
|
||||
Host is up (0.00079s latency).
|
||||
Not shown: 998 closed ports
|
||||
PORT STATE SERVICE
|
||||
22/tcp open ssh
|
||||
80/tcp open http
|
||||
MAC Address: <redacted> (Zyxel Communications)
|
||||
|
||||
Nmap done: 1 IP address (1 host up) scanned in 4.15 seconds
|
||||
```
|
||||
|
||||
Let's give it a try with `ssh admin@10.10.1.1`:
|
||||
```
|
||||
#######################################################
|
||||
# #
|
||||
# Please login to CLI mode. Then You can do commands. #
|
||||
# #
|
||||
#######################################################
|
||||
|
||||
Entering character mode
|
||||
Escape character is '^]'.
|
||||
|
||||
|
||||
Login: admin
|
||||
Password: <not echoed `1234`>
|
||||
ZYXEL#
|
||||
ZYXEL# <not echoed `?`>
|
||||
linuxshell Enter linux shell
|
||||
show show
|
||||
system
|
||||
manufactory
|
||||
config
|
||||
mib
|
||||
sf
|
||||
log
|
||||
timer
|
||||
bsp
|
||||
hal
|
||||
igmp
|
||||
omci
|
||||
ssp
|
||||
ZYXEL# show version
|
||||
Project Name: TW2362H-CDEL
|
||||
Client Product Name: GTO100I_SFP_ZYXEL
|
||||
Internal Product Name: GTO100I_SFP_ZYXEL
|
||||
Hardware Version: PMG3000-D20B
|
||||
Boot Version: V1.0.0
|
||||
Client Software Version: V1.0.0
|
||||
Internal Software Version: V1.0.0
|
||||
Build User: jiangyuanqi
|
||||
Build Time: 2021-05-08 11:28:36
|
||||
Build Method: export ONU=gto100i_sfp_zyxel && cd ../drv && make install && cd .. && make rootfs && make install
|
||||
GIT Info: TW2362H-CDEL_lantiq98035/customize/TW2362H-CDEL_lantiq98035_general_20150131:e057bd83
|
||||
ZYXEL#
|
||||
```
|
||||
|
||||
So, we can get a linux shell, nice. My SFP is running a (very old) release of [OpenWrt](https://openwrt.org):
|
||||
```bash
|
||||
ZYXEL# linuxshell
|
||||
BusyBox v1.19.4 (2014-06-30 12:00:02 CST) built-in shell (ash)
|
||||
Enter 'help' for a list of built-in commands.
|
||||
|
||||
_______ ________ __
|
||||
| |.-----.-----.-----.| | | |.----.| |_
|
||||
| - || _ | -__| || | | || _|| _|
|
||||
|_______|| __|_____|__|__||________||__| |____|
|
||||
|__| W I R E L E S S F R E E D O M
|
||||
-----------------------------------------------------
|
||||
ATTITUDE ADJUSTMENT (Attitude Adjustment, 12.09_ltq)
|
||||
-----------------------------------------------------
|
||||
* 1/4 oz Vodka Pour all ingredients into mixing
|
||||
* 1/4 oz Gin tin with ice, strain into glass.
|
||||
* 1/4 oz Amaretto
|
||||
* 1/4 oz Triple sec
|
||||
* 1/4 oz Peach schnapps
|
||||
* 1/4 oz Sour mix
|
||||
* 1 splash Cranberry juice
|
||||
-----------------------------------------------------
|
||||
admin@SFP:~# uname -a
|
||||
Linux SFP 3.10.12 #2 Wed Jul 12 12:01:33 CST 2017 mips GNU/Linux
|
||||
admin@SFP:~#
|
||||
```
|
||||
|
||||
## Changing GPON Serial Number / PLOAM Password
|
||||
|
||||
```
|
||||
ZYXEL# hal
|
||||
Hal#
|
||||
linuxshell Enter linux shell
|
||||
show show HAL configuration
|
||||
sn change ont parameters
|
||||
password change ont password
|
||||
set set ont parameters
|
||||
to1 change ont to1 interval
|
||||
to2 change ont to2 interval
|
||||
berinterval change BER interval
|
||||
sfthreshold change SF threshold
|
||||
sdthreshold change SD threshold
|
||||
tcont add tcont
|
||||
no delete HAL item
|
||||
gemport add HAL item
|
||||
reset Reset all pon configurations
|
||||
get get
|
||||
omci omci
|
||||
stream stream
|
||||
mvlanaction mvlanaction
|
||||
uni PPTP UNI configuration
|
||||
mtu MTU R/W
|
||||
multicast multicast configartion
|
||||
iphost iphost
|
||||
init init
|
||||
deny deny
|
||||
permit permit
|
||||
monitor monitor
|
||||
mac mac
|
||||
storm storm
|
||||
print print
|
||||
igmp igmp
|
||||
mcastfilt McastFilt
|
||||
Hal# sn
|
||||
<string> change ont serial number
|
||||
Hal# password
|
||||
<string> Formate:XXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
The password seems to consist of 10 bytes, entered hex encoded. This is likely the PLOAM password / SLID / _Installationskennung_ / whatever you'd like to call it.
|
||||
The `sn` seems to change the serial number of the ONU (ONT) itself, I did not test that so far.
|
||||
|
||||
I assumed the CLI is using the configuration interface of OpenWRT under the hood; turns out I was right:
|
||||
```
|
||||
uci show gpon
|
||||
gpon.ploam=gpon
|
||||
gpon.ploam.nPassword=0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20
|
||||
gpon.ploam.nT01=16000
|
||||
gpon.ploam.nT02=100
|
||||
gpon.ploam.nEmergencyStopState=0
|
||||
gpon.ploam.nRogueMsgIdUpstreamReset=255
|
||||
gpon.ploam.nRogueMsgRepeatUpstreamReset=3
|
||||
gpon.ploam.nRogueMsgIdDeviceReset=255
|
||||
gpon.ploam.nRogueMsgRepeatDeviceReset=3
|
||||
gpon.ploam.nRogueEnable=0
|
||||
gpon.gtc=gpon
|
||||
gpon.gtc.bDlosEnable=0
|
||||
gpon.gtc.bDlosInversion=0
|
||||
gpon.gtc.nDlosWindowSize=0
|
||||
gpon.gtc.nDlosTriggerThreshold=0
|
||||
gpon.gtc.ePower=0
|
||||
gpon.gtc.nLaserGap=0
|
||||
gpon.gtc.nLaserOffset=0
|
||||
gpon.gtc.nLaserEnEndExt=0
|
||||
gpon.gtc.nLaserEnStartExt=0
|
||||
gpon.gtc.nDyingGaspHyst=0
|
||||
gpon.gtc.nDyingGaspMsg=0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
gpon.gtc.nDyingGaspEnable=0
|
||||
gpon.ethernet=gpon
|
||||
gpon.ethernet.bUNI_PortEnable0=1
|
||||
gpon.ethernet.bUNI_PortEnable1=1
|
||||
gpon.ethernet.bUNI_PortEnable2=1
|
||||
gpon.ethernet.bUNI_PortEnable3=1
|
||||
gpon.gpe=gpon
|
||||
gpon.gpe.nPeNumber=6
|
||||
```
|
||||
|
||||
## Observing the GPON SN and Password in real time.
|
||||
|
||||
### Serial and Password
|
||||
The `onu` command helps debugging the system:
|
||||
- `onu gtcpg`: Retrieve password
|
||||
- `onu gtcsng`: Retrieve serial number
|
||||
|
||||
### Connection state
|
||||
**Connected** (`curr_state=5`)
|
||||
```bash
|
||||
admin@SFP:~# onu ploamsg
|
||||
errorcode=0 curr_state=5
|
||||
```
|
||||
|
||||
**Disconnected** (`curr_state=1`):
|
||||
```bash
|
||||
admin@SFP:~# onu ploamsg
|
||||
errorcode=0 curr_state=1 previous_state=0 elapsed_msec=16907701
|
||||
```
|
||||
|
||||
## HTTP API
|
||||
|
||||
Only after getting SSH access I discovered the SFP comes with a WebUI and a _sort of_ API. The CLI `zyxel_gpon_sfp.py` makes use of this API to remotely configure the PLOAM password and possibly SN (again, didn't check it).
|
||||
|
||||
## TODO
|
||||
- [ ] Prometheus exporter
|
||||
- [ ] Integrate into OpenWRT
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
demjson==1.6
|
||||
fire==0.4.0
|
||||
requests==2.27.1
|
69
zyxel_gpon_sfp.py
Normal file
69
zyxel_gpon_sfp.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import fire
|
||||
import requests
|
||||
import random
|
||||
import demjson
|
||||
from binascii import hexlify
|
||||
|
||||
|
||||
def is_hex(a):
|
||||
""" checks if a is a properly formated hex string """
|
||||
try:
|
||||
int(a, 16) # Just check if we can properly parse the hex
|
||||
return len(a) % 2 == 0 # We're dealing with strings hex encoded
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
class SFP:
|
||||
def __init__(self, sfp_addr, username="admin", password="1234"):
|
||||
self._sfp_addr = sfp_addr
|
||||
self._user = username
|
||||
self._pass = password
|
||||
|
||||
def _req(self, path, method="GET", headers={}):
|
||||
""" Helper to perform request on the SFPs HTTP API """
|
||||
full_path = f"{self._sfp_addr}{path}"
|
||||
auth = (self._user, self._pass)
|
||||
|
||||
if method == "GET":
|
||||
return requests.get(full_path, auth=auth, headers=headers)
|
||||
elif method == "POST":
|
||||
return requests.post(full_path, auth=auth, headers=headers)
|
||||
|
||||
def info(self):
|
||||
resp_sn = self._req(path="/cgi/get_sn")
|
||||
resp_gpon = self._req(path="/cgi/get_gpon_info")
|
||||
|
||||
# WARNING: This is a javascript object, not JSON...
|
||||
test = demjson.decode(resp_sn.text) | demjson.decode(resp_gpon.text)
|
||||
|
||||
return test
|
||||
|
||||
def set_slid(self, slid, string=False):
|
||||
# Transform to a valid parameter
|
||||
_slid = hexlify(slid).decode(
|
||||
"ascii").lower() if string else slid.lower()
|
||||
|
||||
if not is_hex(_slid):
|
||||
return f"[!] Invalid SLID `{_slid}` (HEX)"
|
||||
|
||||
print(f"[ ] Applying SLID `{_slid}` (HEX)")
|
||||
resp = self._req(
|
||||
path=f"/cgi/set_sn?mode=1&pass={_slid}", method="POST")
|
||||
if resp.status_code == 200 and resp.text == "1":
|
||||
return f"[+] Applied SLID `{_slid}`, a reboot of the SFP is required."
|
||||
|
||||
def set_sn(self, sn, string):
|
||||
return "Untested"
|
||||
# _sn = hexlify(sn) if string else sn
|
||||
# f not is_hex(_sn):
|
||||
# return f"[!] Invalid SN `{_sn}` (HEX)"
|
||||
|
||||
# print(f"[ ] Applying SN `{_sn}` (HEX)")
|
||||
# resp = self._req(path=f"/cgi/set_sn?mode=1?sn={sn}", method="POST")
|
||||
# if resp.status_code == 200 and resp.text == "1":
|
||||
# return f"[+] Applied SN `{_sn}`, a reboot of the SFP is required."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fire.Fire(SFP)
|
Loading…
Add table
Add a link
Reference in a new issue