Skip to content

Instantly share code, notes, and snippets.

@tiberiucorbu
Last active December 28, 2023 10:46
Show Gist options
  • Save tiberiucorbu/a51c81b82b5196ac002c52ac6f39987f to your computer and use it in GitHub Desktop.
Save tiberiucorbu/a51c81b82b5196ac002c52ac6f39987f to your computer and use it in GitHub Desktop.
Retrive active devices on the network form the vodafone router : CGA4233DE

Retrive active devices on the network form the vodafone kabelbox (CGA4233DE)

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.

Security Chanlenges

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

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 :

Creating a session

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.

Getting the encryption salts

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!

Logging in

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

Getting the list of active devices

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;
    }

Terminating the session

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)

Some end words

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

#!/usr/bin/python3
import calendar
import hashlib
import sys
import time
from requests import Session
router_address = 'http://kabelbox.local' # change acordingly
username = 'admin'
password = 'some password'
headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-CSRF-TOKEN': '', 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'http://192.168.0.1/'}
session = Session()
salt_response = session.post(router_address+'/api/v1/session/login', headers=headers, data={'username': username, 'password': 'seeksalthash'}).json()
# print(saltResponse)
if (salt_response['error'] != 'ok'):
print('{}');
sys.exit(0)
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]
# print(b);
response = session.post(router_address+'/api/v1/session/login', headers=headers,
data={'username': 'admin', 'password': b})
# print(response.json());
response = session.get(router_address+'/api/v1/sta_lan_status', headers=headers)
print(response.json())
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)
@guerda
Copy link

guerda commented Jan 24, 2023

I fiddled around and your code is great, @tiberiuscorbu.
What is missing for you @TwizzyDizzy with the recent version of Vodafone station is a call to /api/v1/session/menu. This needs to be performed before AssociatedDevices5 can be called.

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