Skip to content

Instantly share code, notes, and snippets.

@charlie89
Created July 24, 2021 18:11
Show Gist options
  • Select an option

  • Save charlie89/fd758e6e29b87e3c148a1fc2ac5d75be to your computer and use it in GitHub Desktop.

Select an option

Save charlie89/fd758e6e29b87e3c148a1fc2ac5d75be to your computer and use it in GitHub Desktop.
FRITZ!Box Reboot (works on FRITZ!OS 7.24 and above)
#!/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()
@charlie89
Copy link
Author

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment