In order to automate the network activity like getting the list of connected devices, notify the presensce of a device, log and debug wifi issues, it would be nice to have the list provided by the router of the current status.
In this aricle I am going to explore this posibility, reverse engenier the web app from the router and try to authentificate and retrive the active data table from the router.
The first step is to analyze the web console into the network tab and login to see which requests are beeing made.
From the first glance the router hosts a PHP application and serves a couple of assest via a lighttpd
server, if we look at the about page (http://routerip/#/status/about
) the version of these software is revealed, this is a major security flaw as one can track the security issues and exploit the machine using the issues found for a specific version narowing down the guessing. Anyway, the WebUI is based on JQuery and JQuery UI. What I note is that the javascripts are obfuscaded and ran using eval()
. Unfortunately none of these measures stops an experienced programmer to debug and trace the login flow :)
By looking at the network tab for the login flow I have reversed engenierd the following diagram:
@startuml component
actor user
node Router Server
node WebUI
Router Sever -> WebUI -> create php session
user -> WebUI: enters username and password
WebUI -> Router Server : Get encryption stalts from the login endpoint
WebUI -> WebUI : Compute pbkdf2 of the password
WebUI -> Router Server : Post username and encrypted password to the login endpoint
@endum
When the webUI is opened the php app creates a session and tracks it with a cookie named by default : PHPSESSID
This session holds the authentification, once the login is succesfull all the preceding calls don't require an authentification token.
In the headers I've also noted that the app uses a crsf protection, however for the login flow the crsf is not required but the header has to be set.
So lets break it down in to smaller steps, for the sake of simplicity and because it has the batteries included, I've used python 3.8 to code the calls :
In python that is simple, just require the Session from the request package and initialize it
from requests import Session
session = Session()
Now every cookie that is going to be set by the server will be held in our session.
For the whole flow there are needed the following variables :
username = 'admin' # this seems to be constant as there is no way to change the username in the ui
password = 'the admin password' # used to generate the authentification token
router_address = 'http://kabelbox.local' # change with the gateway ip if the local dns is not working
Also for each request a couple of header variables are validated, I'm not completly sure if all of them has to be send but I had success with the following dictionary :
headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-CSRF-TOKEN': '', 'X-Requested-With': 'XMLHttpRequest', 'Referer': router_address}
If your router has a diffrent Ip, change tit.
the encription salts are provided by the same endpoint as the login but in the payload sets the password as seeksalthash
salt_response = session.post(router_address+'/api/v1/session/login', headers=headers, data={'username': username, 'password': 'seeksalthash'}).json()
if there is no other active session, the endpoint would return the following payload:
{
"error": "ok",
"salt": "some random string",
"saltwebui": "some other random string"
}
The salts are a bit wierd from the security point of view, the salt
field seems to be constant between requests and only the saltwebui
is newly generated on each request.
Boths salts are going to be used to hash/encrypt the password, and funny the security guy/girl that implemented all of this thougt that It would confuse someone to encrypt the password two times.
The hashing algoritm used on the client side is pbkdf2
as I've traced it with the debuger. The ui uses a javascript library called sjcl
https://bitwiseshiftleft.github.io/sjcl/doc/sjcl.misc.html, it encrypts the password and also cuts the first 32 chars of the resulted hash.
In python there is the same algorithm implementesd in the hashlib
package
import hashlib
a = hashlib.pbkdf2_hmac('sha256', bytes(password, 'utf-8'), bytes(salt_response['salt'], 'utf-8'), 1000).hex()[:32]
b = hashlib.pbkdf2_hmac('sha256', bytes(a, 'utf-8'), bytes(salt_response['saltwebui'], 'utf-8'), 1000).hex()[:32]
That was easy!
Once we have the token we can call the login endpoint again but now with the token as the password
response = session.post(router_address+'/api/v1/session/login', headers=headers, data={'username': username, 'password': b})
If the response contains {error: 'ok'}
it means it has been authentificated :)
After login the request are plain simple, to get the lan status just call the following endpoint:
response = session.get('http://192.168.0.1/api/v1/sta_lan_status', headers=headers)
print(response.json());
the response has the following format :
export interface DhcpipStaticTbl {
__id: string;
static_mac: string;
dhcpip_static: string;
}
export interface DhcpTbl {
__id: string;
dhcpmac: string;
wifi_type: string;
dhcphost: string;
dhcpstatus: string;
dhcpexpires: string;
dhcptype: string;
dhcpip_dhcp: string;
}
export interface Data {
interface: string;
DHCP_addrpool_start: string;
DHCP_addrpool_end: string;
RadioEnable2: string;
RadioEnable5: string;
dhcpip_staticTbl: DhcpipStaticTbl[];
dhcpTbl: DhcpTbl[];
IPAddressRT: string[];
DNSTblRT: string[];
IPAddressGW: string;
}
export interface RootObject {
error: string;
message: string;
data: Data;
}
To terminate the session the logout
endpoint is available, this one needs however the crsf token.
The sta_lan_status
endpoint doens't require or returns a token. For the token a request to any other endpoint has to be made, for some reason the endpoints are called with the timestamp of the client on the _
query param :
import time
now = calendar.timegm(time.gmtime())
response = session.get(router_address+'/api/v1/host/AssociatedDevices5?_=' + str(now), headers=headers)
headers['X-CSRF-TOKEN'] = response.json()['token']
response = session.post(router_address+'/api/v1/session/logout', headers=headers)
The security of this device is medium, the login
endpoint is not protected by crsf neither the sta_lan_status
, in some scenarios an attacker can exploit these via a browser extension or a crypeld browser and open ports to your local devices. Therefor is strongly recomended to change the default password
So there you have it, attached you can find the whole script
Seems like Vodafone changed something for the endpoints where you add the timestamp in firmware version 3. I always get "Unauthorized User" for the "AssociatedDevices5" endpoint (same result on calls). Also tried using a more precise timestamp, like the browser UI seems to be using, but same result. Maybe someone has another idea?