Created
July 24, 2021 18:11
-
-
Save charlie89/fd758e6e29b87e3c148a1fc2ac5d75be to your computer and use it in GitHub Desktop.
FRITZ!Box Reboot (works on FRITZ!OS 7.24 and above)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # vim: expandtab sw=4 ts=4 | |
| """ | |
| FRITZ!OS WebGUI Login | |
| Get a sid (session ID) via PBKDF2 based challenge response algorithm. | |
| Fallback to MD5 if FRITZ!OS has no PBKDF2 support. | |
| AVM 2020-09-25 | |
| Code from https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_deutsch_2021-05-03.pdf | |
| added a function to reboot the router | |
| """ | |
| import sys | |
| import hashlib | |
| import json | |
| import time | |
| import requests | |
| import urllib.request | |
| import urllib.parse | |
| import xml.etree.ElementTree as ET | |
| LOGIN_SID_ROUTE = "/login_sid.lua?version=2" | |
| class LoginState: | |
| def __init__(self, challenge: str, blocktime: int): | |
| self.challenge = challenge | |
| self.blocktime = blocktime | |
| self.is_pbkdf2 = challenge.startswith("2$") | |
| def get_sid(box_url: str, username: str, password: str) -> str: | |
| """ Get a sid by solving the PBKDF2 (or MD5) challenge-response | |
| process. """ | |
| try: | |
| state = get_login_state(box_url) | |
| except Exception as ex: | |
| raise Exception("failed to get challenge") from ex | |
| if state.is_pbkdf2: | |
| print("PBKDF2 supported") | |
| challenge_response = calculate_pbkdf2_response(state.challenge, password) | |
| else: | |
| print("Falling back to MD5") | |
| challenge_response = calculate_md5_response(state.challenge, | |
| password) | |
| if state.blocktime > 0: | |
| print(f"Waiting for {state.blocktime} seconds...") | |
| time.sleep(state.blocktime) | |
| try: | |
| sid = send_response(box_url, username, challenge_response) | |
| except Exception as ex: | |
| raise Exception("failed to login") from ex | |
| if sid == "0000000000000000": | |
| raise Exception("wrong username or password") | |
| return sid | |
| def get_login_state(box_url: str) -> LoginState: | |
| """ Get login state from FRITZ!Box using login_sid.lua?version=2 """ | |
| url = box_url + LOGIN_SID_ROUTE | |
| http_response = urllib.request.urlopen(url) | |
| xml = ET.fromstring(http_response.read()) | |
| # print(f"xml: {xml}") | |
| challenge = xml.find("Challenge").text | |
| blocktime = int(xml.find("BlockTime").text) | |
| # could get automatic username with xml.find("Users")[0].text | |
| return LoginState(challenge, blocktime) | |
| def calculate_pbkdf2_response(challenge: str, password: str) -> str: | |
| """ Calculate the response for a given challenge via PBKDF2 """ | |
| challenge_parts = challenge.split("$") | |
| # Extract all necessary values encoded into the challenge | |
| iter1 = int(challenge_parts[1]) | |
| salt1 = bytes.fromhex(challenge_parts[2]) | |
| iter2 = int(challenge_parts[3]) | |
| salt2 = bytes.fromhex(challenge_parts[4]) | |
| # Hash twice, once with static salt... | |
| hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1) | |
| # Once with dynamic salt. | |
| hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2) | |
| return f"{challenge_parts[4]}${hash2.hex()}" | |
| def calculate_md5_response(challenge: str, password: str) -> str: | |
| """ Calculate the response for a challenge using legacy MD5 """ | |
| response = challenge + "-" + password | |
| # the legacy response needs utf_16_le encoding | |
| response = response.encode("utf_16_le") | |
| md5_sum = hashlib.md5() | |
| md5_sum.update(response) | |
| response = challenge + "-" + md5_sum.hexdigest() | |
| return response | |
| def send_response(box_url: str, username: str, challenge_response: str) -> str: | |
| """ Send the response and return the parsed sid. raises an Exception on error """ | |
| # Build response params | |
| post_data_dict = {"username": username, "response": challenge_response} | |
| post_data = urllib.parse.urlencode(post_data_dict).encode() | |
| headers = {"Content-Type": "application/x-www-form-urlencoded"} | |
| url = box_url + LOGIN_SID_ROUTE | |
| # Send response | |
| http_request = urllib.request.Request(url, post_data, headers) | |
| http_response = urllib.request.urlopen(http_request) | |
| # Parse SID from resulting XML. | |
| xml = ET.fromstring(http_response.read()) | |
| return xml.find("SID").text | |
| def reboot_router(url, sid): | |
| """ | |
| Reboot the router. | |
| This needs multiple post requests, simply posting to /reboot.lua doesn't work. | |
| So this simply executes the same requests as the browser would do. | |
| """ | |
| headers = { | |
| "DNT": "1", | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| "sec-gpc": "1", | |
| } | |
| data = { | |
| 'xhr': 1, | |
| 'sid': sid, | |
| 'lang': 'de', | |
| 'page': 'reboot', | |
| 'xhrId': 'all', | |
| } | |
| post_data = urllib.parse.urlencode(data).encode() | |
| r = requests.post(url + '/data.lua', headers=headers, data=post_data) | |
| data = { | |
| 'xhr': 1, | |
| 'sid': sid, | |
| 'reboot': 1, | |
| 'lang': 'de', | |
| 'page': 'reboot', | |
| } | |
| post_data = urllib.parse.urlencode(data).encode() | |
| r = requests.post(url + '/data.lua', headers=headers, data=post_data) | |
| data = { | |
| 'xhr': 1, | |
| 'sid': sid, | |
| 'lang': 'de', | |
| 'page': 'rootReboot', | |
| } | |
| post_data = urllib.parse.urlencode(data).encode() | |
| r = requests.post(url + '/data.lua', headers=headers, data=post_data) | |
| data = { | |
| 'ajax': 1, | |
| 'sid': sid, | |
| 'no_sidrenew': 1, | |
| 'xhr': 1, | |
| 'useajax': 1, | |
| } | |
| post_data = urllib.parse.urlencode(data).encode() | |
| r = requests.post(url + '/reboot.lua', headers=headers, data=post_data) | |
| if r.status_code == 200: | |
| j = json.loads(r.text) | |
| if j['reboot_state'] == '0': | |
| print('Reboot successful') | |
| return | |
| raise Exception('Reboot failed') | |
| def main(): | |
| url = 'http://fritz.box' | |
| username = 'fritzXXXX' # since FRITZ!OS 7.24 there's always a username | |
| password = 'yourpassword' | |
| try: | |
| sid = get_sid(url, username, password) | |
| reboot_router(url, sid) | |
| except Exception as e: | |
| print('Error: {}'.format(e)) | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This reboots the FRITZ!Box the same way as rebooting via the web-browser does (over http, no TR-064 or UPnP).
It should work with FRITZ!OS 07.24 and above, just edit url, username and password near the end of the file.
The code for authentication is used from https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_deutsch_2021-05-03.pdf which uses the modern PBKDF2 encryption for the password and MD5 as fallback. To this i added a function which calls the same http post requests as the webbrowser would do when using System>Sicherung>Neustart (probably System>Backup>Restart with englich webinterface).