Initial commit
This commit is contained in:
97
README.md
Normal file
97
README.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# C-Data ONU tools
|
||||||
|
Tools for managing settings of the C-Data FD511G-X xPON ONU (and possibly other models), which are not available from the web interface:
|
||||||
|
enabling/disabling shell access, getting SSH/Telnet password.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
The ONU web interface (usually available at http://192.168.101.1) allows you to download a configuration backup as a file. This is a regular XML file that is encrypted using XOR encryption with a key of `0x26`.
|
||||||
|
The tools presented here are designed to load, read, and modify this configuration file.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
- `enable_shell_access.py` helps to enable/disable SSH/Telnet root shell access from LAN/WAN and change username & password.
|
||||||
|
Usage:
|
||||||
|
```
|
||||||
|
$ python enable_shell_access.py --help
|
||||||
|
usage: enable_shell_access.py [-h] [-a AUTHORIZATION] [-d] [-i {LAN,WAN}] [-p {SSH,Telnet}] [--change-username CHANGE_USERNAME] [--change-password CHANGE_PASSWORD] [host]
|
||||||
|
|
||||||
|
Program to enable or disable Telnet or SSH access to the C-Data FD511G-X ONU
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
host ONU host (IP address or domain). Default: 192.168.101.1.
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-a, --authorization AUTHORIZATION
|
||||||
|
Web interface authorization credentials, separated by colon. Default: adminisp:adminisp.
|
||||||
|
-d, --disable Disable access. If this argument is not passed, it enables access.
|
||||||
|
-i, --interface {LAN,WAN}
|
||||||
|
Interface for enabling access: LAN or WAN. Default: LAN.
|
||||||
|
-p, --protocol {SSH,Telnet}
|
||||||
|
Protocol for enabling access: SSH or Telnet. Default: SSH.
|
||||||
|
--change-username CHANGE_USERNAME
|
||||||
|
Set new username
|
||||||
|
--change-password CHANGE_PASSWORD
|
||||||
|
Set new password
|
||||||
|
```
|
||||||
|
Usage examples:
|
||||||
|
- enable LAN SSH access:
|
||||||
|
```
|
||||||
|
$ python enable_shell_access.py 192.168.101.1
|
||||||
|
Logging in to http://192.168.101.1 with adminisp:adminisp...
|
||||||
|
Current SSH credentials:
|
||||||
|
username: manu
|
||||||
|
password: c8a9b413
|
||||||
|
|
||||||
|
SSH access from LAN enabled
|
||||||
|
Uploading new configuration to the ONU...
|
||||||
|
Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : Y
|
||||||
|
ONU reboot error. The device is probably already rebooting.
|
||||||
|
```
|
||||||
|
- disable LAN Telnet access:
|
||||||
|
```
|
||||||
|
$ python enable_shell_access.py 192.168.101.1 -p Telnet -d
|
||||||
|
Logging in to http://192.168.101.1 with adminisp:adminisp...
|
||||||
|
Current Telnet credentials:
|
||||||
|
username: manu
|
||||||
|
password: c8a9b413
|
||||||
|
|
||||||
|
Telnet access from LAN disabled
|
||||||
|
Uploading new configuration to the ONU...
|
||||||
|
Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : Y
|
||||||
|
Device is rebooting. Please wait.
|
||||||
|
```
|
||||||
|
- login with custom web interface credentials:
|
||||||
|
```
|
||||||
|
$ python enable_shell_access.py 192.168.101.1 -a adminisp:admin1234
|
||||||
|
Logging in to http://192.168.101.1 with adminisp:admin1234...
|
||||||
|
Current SSH credentials:
|
||||||
|
username: manu
|
||||||
|
password: c8a9b413
|
||||||
|
|
||||||
|
SSH access from LAN enabled
|
||||||
|
Uploading new configuration to the ONU...
|
||||||
|
Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : Y
|
||||||
|
ONU reboot error. The device is probably already rebooting.
|
||||||
|
```
|
||||||
|
|
||||||
|
- `extract_credentials.py` extracts credentials from the binary config file, including SSH and Telnet username & password, and also saves decrypted version of the config file.
|
||||||
|
Usage:
|
||||||
|
```
|
||||||
|
$ python extract_credentials.py ~/config.bin
|
||||||
|
File decrypted and saved to "/home/user/config.xml"
|
||||||
|
|
||||||
|
Web interface credentials:
|
||||||
|
root-level account:
|
||||||
|
username: adminisp
|
||||||
|
password: adminisp
|
||||||
|
|
||||||
|
user-level account:
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
|
|
||||||
|
Shell credentials (Telnet and SSH):
|
||||||
|
username: manu
|
||||||
|
password: c8a9b413
|
||||||
|
```
|
173
enable_shell_access.py
Normal file
173
enable_shell_access.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from typing import Literal, Optional, Union
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_ENC_KEY = 0x26
|
||||||
|
|
||||||
|
|
||||||
|
def xor_xcrypt(data: bytes, key: int, result_as_string: bool = False) -> Union[bytes, str]:
|
||||||
|
if result_as_string:
|
||||||
|
return ''.join(chr(c ^ key) for c in data)
|
||||||
|
else:
|
||||||
|
return bytes(c ^ key for c in data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_shell_access_creds(config: ET.Element, protocol: Literal['SSH', 'Telnet'] = 'SSH') -> str:
|
||||||
|
remote_management = config.find(f'.//RemoteManagement/{protocol.upper()}')
|
||||||
|
username = remote_management.find('TelnetUserName' if protocol == 'Telnet' else 'UserName').text
|
||||||
|
password = remote_management.find('TelnetPassword' if protocol == 'Telnet' else 'Password').text
|
||||||
|
return (f'username: {username}\n'
|
||||||
|
f'password: {password}\n')
|
||||||
|
|
||||||
|
|
||||||
|
def set_shell_access_creds(config: ET.Element, protocol: Literal['SSH', 'Telnet'] = 'SSH',
|
||||||
|
username: Optional[str] = None, password: Optional[str] = None) -> bool:
|
||||||
|
remote_management = config.find(f'.//RemoteManagement/{protocol.upper()}')
|
||||||
|
if remote_management is None:
|
||||||
|
return False
|
||||||
|
if username:
|
||||||
|
remote_management.find('TelnetUserName' if protocol == 'Telnet' else 'UserName').text = username
|
||||||
|
if password:
|
||||||
|
remote_management.find('TelnetPassword' if protocol == 'Telnet' else 'Password').text = password
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_access_enabled(config: ET.Element,
|
||||||
|
interface: Literal['LAN', 'WAN'] = 'LAN', protocol: Literal['SSH', 'Telnet'] = 'SSH') -> bool:
|
||||||
|
ssh_policy = config.find(f".//ServiceControl/*[ServiceList='{protocol.upper()}'][Interface='{interface}']/Policy")
|
||||||
|
return ssh_policy.text == "Permit"
|
||||||
|
|
||||||
|
|
||||||
|
def enable_access(config: ET.Element, interface: Literal['LAN', 'WAN'] = 'LAN',
|
||||||
|
protocol: Literal['SSH', 'Telnet'] = 'SSH', disable: bool = False) -> bool:
|
||||||
|
service_policy = config.find(
|
||||||
|
f".//ServiceControl/*[ServiceList='{protocol.upper()}'][Interface='{interface}']/Policy")
|
||||||
|
remote_management = config.find(f'.//RemoteManagement/{protocol.upper()}/{protocol}Enable')
|
||||||
|
if (service_policy is not None) and (remote_management is not None):
|
||||||
|
service_policy.text = "Discard" if disable else "Permit"
|
||||||
|
if not disable:
|
||||||
|
remote_management.text = '1'
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_arg_parser():
|
||||||
|
arg_parser = argparse.ArgumentParser(
|
||||||
|
description='Program to enable or disable Telnet or SSH access to the C-Data FD511G-X ONU')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'host',
|
||||||
|
nargs='?',
|
||||||
|
default='192.168.101.1',
|
||||||
|
help='ONU host (IP address or domain). Default: %(default)s.')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-a', '--authorization',
|
||||||
|
default='adminisp:adminisp',
|
||||||
|
help='Web interface authorization credentials, separated by colon. Default: %(default)s.')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-d', '--disable',
|
||||||
|
action='store_true',
|
||||||
|
help='Disable access. If this argument is not passed, it enables access.')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-i', '--interface',
|
||||||
|
choices=('LAN', 'WAN'),
|
||||||
|
default='LAN',
|
||||||
|
type=str,
|
||||||
|
help='Interface for enabling access: LAN or WAN. Default: %(default)s.')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-p', '--protocol',
|
||||||
|
choices=('SSH', 'Telnet'),
|
||||||
|
default='SSH',
|
||||||
|
type=str,
|
||||||
|
help='Protocol for enabling access: SSH or Telnet. Default: %(default)s.')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'--change-username',
|
||||||
|
type=str,
|
||||||
|
help='Set new username')
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'--change-password',
|
||||||
|
type=str,
|
||||||
|
help='Set new password')
|
||||||
|
return arg_parser
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
arguments = create_arg_parser().parse_args()
|
||||||
|
host = arguments.host
|
||||||
|
username, password = arguments.authorization.split(':', maxsplit=1)
|
||||||
|
disable = arguments.disable
|
||||||
|
interface = arguments.interface
|
||||||
|
protocol = arguments.protocol
|
||||||
|
change_username = arguments.change_username
|
||||||
|
change_password = arguments.change_password
|
||||||
|
|
||||||
|
http = httpx.Client(base_url=f'http://{host}')
|
||||||
|
hashed_password = hashlib.md5(password.encode('utf-8')).hexdigest()
|
||||||
|
print(f'Logging in to http://{host} with {username}:{password}...', file=sys.stderr)
|
||||||
|
resp = http.post(
|
||||||
|
'/post.json',
|
||||||
|
json={'module': 'login', 'username': username, 'encryPassword': hashed_password}
|
||||||
|
)
|
||||||
|
if resp.json()['code'] != 0:
|
||||||
|
sys.exit(f'Authorization error:\n{resp.text}')
|
||||||
|
|
||||||
|
resp = http.post('/boaform/formSaveConfig', data={'save_cs': 'backup_config'})
|
||||||
|
resp.raise_for_status()
|
||||||
|
encrypted_config = resp.content
|
||||||
|
decrypted_config = xor_xcrypt(encrypted_config, CONFIG_ENC_KEY, result_as_string=True).replace(',', '\n')
|
||||||
|
|
||||||
|
config = ET.fromstring(decrypted_config)
|
||||||
|
config_changed = False
|
||||||
|
|
||||||
|
print(f'Current {protocol} credentials:\n', get_shell_access_creds(config, protocol), sep='')
|
||||||
|
if change_username or change_password:
|
||||||
|
if set_shell_access_creds(config, protocol, change_username, change_password):
|
||||||
|
print(f'{protocol} changed to:\n', get_shell_access_creds(config, protocol), sep='')
|
||||||
|
config_changed = True
|
||||||
|
else:
|
||||||
|
print(f'Failed to change {protocol} credentials', file=sys.stderr)
|
||||||
|
|
||||||
|
enabled = is_access_enabled(config, interface, protocol)
|
||||||
|
if disable == enabled:
|
||||||
|
if enable_access(config, interface, protocol, disable=disable):
|
||||||
|
print(f'{protocol} access from {interface}', 'disabled' if disable else 'enabled')
|
||||||
|
config_changed = True
|
||||||
|
else:
|
||||||
|
print(f'Failed to {"disable" if disable else "enable"} {protocol}. Check configuration', file=sys.stderr)
|
||||||
|
else:
|
||||||
|
_tip = [f'{protocol} access from {interface} is already {"enabled" if enabled else "disabled"}.']
|
||||||
|
if enabled:
|
||||||
|
_tip.append(f"To disable, run \"{' '.join(sys.argv)} --disable\"")
|
||||||
|
print(' '.join(_tip))
|
||||||
|
|
||||||
|
if config_changed:
|
||||||
|
modified_config = ET.tostring(config, encoding='utf-8', xml_declaration=True, short_empty_elements=False)
|
||||||
|
encrypted_modified_config = xor_xcrypt(modified_config.replace(b'\n', b','), CONFIG_ENC_KEY)
|
||||||
|
|
||||||
|
print('Uploading new configuration to the ONU...', file=sys.stderr)
|
||||||
|
resp = http.post('/boaform/formSaveConfig', files={
|
||||||
|
'binary': ('config.bin', io.BytesIO(encrypted_modified_config), 'application/octet-stream')})
|
||||||
|
resp.raise_for_status()
|
||||||
|
if resp.json()['code'] != 0:
|
||||||
|
sys.exit(f'Error uploading config:\n{resp.text}')
|
||||||
|
resp = http.get('/get.json', params={'module': 'restore_info'})
|
||||||
|
resp.raise_for_status()
|
||||||
|
if resp.json()['code'] != 0:
|
||||||
|
sys.exit(f'Error applying config:\n{resp.text}')
|
||||||
|
|
||||||
|
if input('Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : ').lower() != 'n':
|
||||||
|
try:
|
||||||
|
resp = http.post('/post.json', json={'module': 'dev_config', 'reboot': 1})
|
||||||
|
except (httpx.ConnectError, httpx.ReadError):
|
||||||
|
print('ONU reboot error. The device is probably already rebooting.')
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
resp.raise_for_status()
|
||||||
|
print('Device is rebooting. Please wait.', file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print('Configuration has not been changed.')
|
60
extract_credentials.py
Normal file
60
extract_credentials.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
key = 0x26
|
||||||
|
|
||||||
|
|
||||||
|
def extract_web_interface_creds(tree: ET) -> str:
|
||||||
|
x_gc_login = tree.find('.//X_GC_LOGIIN')
|
||||||
|
result = []
|
||||||
|
for account in x_gc_login:
|
||||||
|
level = account.find('UserLevel').text
|
||||||
|
username = account.find('Username').text
|
||||||
|
password = account.find('Passwd').text
|
||||||
|
result.append(f'{level}-level account:\nusername: {username}\npassword: {password}')
|
||||||
|
return '\n\n'.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_shell_access_creds(tree: ET) -> str:
|
||||||
|
telnet = tree.find('.//RemoteManagement/TELNET')
|
||||||
|
ssh = tree.find('.//RemoteManagement/SSH')
|
||||||
|
telnet_username = telnet.find('TelnetUserName').text
|
||||||
|
telnet_password = telnet.find('TelnetPassword').text
|
||||||
|
ssh_username = ssh.find('UserName').text
|
||||||
|
ssh_password = ssh.find('Password').text
|
||||||
|
if (telnet_username == ssh_username) and (telnet_password == ssh_password):
|
||||||
|
return f'Shell credentials (Telnet and SSH):\nusername: {ssh_username}\npassword: {ssh_password}'
|
||||||
|
else:
|
||||||
|
return f'''Shell credentials:
|
||||||
|
Telnet:
|
||||||
|
username: {telnet_username}
|
||||||
|
password: {telnet_password}
|
||||||
|
|
||||||
|
SSH:
|
||||||
|
username: {ssh_username}
|
||||||
|
password: {ssh_password}'''
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
config_filename = Path(sys.argv[1])
|
||||||
|
except IndexError:
|
||||||
|
print(f'Usage: {sys.argv[0]} <path to config file backup>', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(config_filename, 'rb') as infile:
|
||||||
|
test_chunk = infile.read(7)
|
||||||
|
infile.seek(0)
|
||||||
|
if test_chunk.startswith(b'<?xml'):
|
||||||
|
print(f'It looks like file "{config_filename}" has already been decrypted.')
|
||||||
|
decrypted_contents = infile.read()
|
||||||
|
else:
|
||||||
|
decrypted_contents = ''.join(chr(c ^ key) for c in infile.read()).replace(',', '\n')
|
||||||
|
decrypted_filename = config_filename.resolve().with_suffix('.xml')
|
||||||
|
with open(decrypted_filename, 'wt') as outfile:
|
||||||
|
outfile.write(decrypted_contents)
|
||||||
|
print(f'File decrypted and saved to "{decrypted_filename}"')
|
||||||
|
|
||||||
|
config_tree = ET.fromstring(decrypted_contents)
|
||||||
|
print(f'\nWeb interface credentials:\n{extract_web_interface_creds(config_tree)}\n\n')
|
||||||
|
print(extract_shell_access_creds(config_tree))
|
BIN
img/C-Data FD511G-X front side.webp
Normal file
BIN
img/C-Data FD511G-X front side.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
httpx
|
Reference in New Issue
Block a user