Background Link to heading
My ISP has a “modern” web app using Angular and REST backend. that said, their website sucks because their login form breaks my password manager. So, I have to login manually every time.
Obviously, they don’t have a documented API. so, i have to trace the xhr requests in the browser. This is a 3 hour journey to reverse engineer their login API including deepdive into their weird password encryption!
requests session Link to heading
I am using requests
session because I wasn’t sure what kind of cookies they are using and i wanted to focus on REST API.
import requests
s = requests.Session()
Login Endpoint Link to heading
Starting with the login page https://my.te.eg/#/home/signin
. I saw xhr post
request on https://api-my.te.eg/api/user/generatetoken?channelId=WEB_APP
. the request payload was
(data = {
"header": {
"msisdn": "<PHONE NUMBER>",
"locale": "En"
},
"body": {
"password": "<SOME HASH>"
}
})
okey, This is the login endpoint. But why is the password hashed?!
I guess they are not sending the password in plain text. which is an overkill considering it’s all on SSL. Anyway, I circle back to that.
So, I tried post
with that hash and phone number. but I got an authentication error
{
"header": {
"responseMessage": "Your Session has been expired, please sign in to continue",
}
so, I went back to the browser for a deeper look at the login request/response. this time, i noticed the request header has jwt
.
wat? This is the login request.why is there jwt?
I assumed that jwt was stored in local storage. and it was. It turns out that there was get request to another endpoint to generate a guest jwt which is needed for the login API. PARANOID much?
Anyway, quick get request to extract guest jwt.
# Get initial JWT Tocken
r = s.get(TOKEN_API)
jwt = r.json()["body"]["jwt"]
I tried to login again. This time i sent the jwt in the headers.
data = {
"header": {
"msisdn": "<PHONE NUMBER>",
"locale": "En"
},
"body": {
"password": "<SOME HASH>"
}
}
headers = {
"jwt": jwt
}
r = s.post(SIGN_API, json=data, headers=headers)
And It worked! Now i am logged in and I have a new auth jwt in the response.
Hitting the information endpoints Link to heading
Time to get information about my remaining quota this month which I always exceed :(
Beside the auth jwt, I know that requests needs customerId. so, I extracted that as well.
customerId = r.json()["header"]["customerId"]
jwt = r.json()["body"]["jwt"]
Set the new jwt and data json for freeunitusage
endpoint.
headers = {
"jwt": jwt
}
data = {
"header": {
"customerId": customerId,
"msisdn": <PHONE NUMBER>,
"locale": "En"
},
"body": {}
}
FREEUNITS_API = "https://api-my.te.eg/api/line/freeunitusage"
r = s.post(FREEUNITS_API, json=data, headers=headers)
print(r.json()["body"]["summarizedLineUsageList"][0]["freeAmount"])
The Password hash Link to heading
At this point, i have a working script but using the weird hash i got from the browser. How was that generated?
Initially i thought it’s some kind of hash. well I was wrong. hashid
failed to detect the hash type.
$ hashid HASH
Analyzing 'HASH'
[+] Unknown hash
Well, I was curios enough that i decided to trace frontend javascript to know what generated the hash. and i got lucky :)
Step1 Set a breakpoint on XHR/fetch requests Link to heading
I know that login form will generate the XHR request with hashed password. So, I set XHR breakpoint there.
Step2 Login Link to heading
now i have a breakpoint, i tried to login again in the browser
The browser stopped right before sending xhr. I went through the stack trace frame by frame until i found what i was looking for signIn
.
I guess this is login service called from angular login component.
going through signin
javacript function. Ugh!
I finally saw it. Surprise! It wasn’t a hash. it’s AES.
I looked into aesService
object and it has the key
and iv
for AES-128.
Disclaimer: I don’t know why they are encrypting password. I assume they have key on the backend to decrypt and hash. but is the key fixed? is it the same for everyone? I don’t know. but if it’s, what is the point!?
Encrypting the password Link to heading
Now, I can use any AES implementation to encrypt my password before sending login request.
I found an example of AES encryption at github. I modified it a little to use variable iv
.
key = b"16 byte KEY"
iv = b"16 byte IV"
password_enc = AESCipher(key, iv).encrypt(args.password)
putting it all together Link to heading
#! /usr/bin/python3
import logging
import sys
import json
import argparse
import pprint
import requests
from aes import AESCipher
import getpass
# API URI
TOKEN_API = "https://api-my.te.eg/api/user/generatetoken?channelId=WEB_APP"
SIGNIN_API = "https://api-my.te.eg/api/user/login?channelId=WEB_APP"
BALANCE_API = "https://api-my.te.eg/api/line/postpaid/balance"
FREEUNITS_API = "https://api-my.te.eg/api/line/freeunitusage"
# CLI parser
parser = argparse.ArgumentParser(description="WE command line")
parser.add_argument("msisdn")
args = parser.parse_args()
# Arguments
msisdn = args.msisdn
try:
password = getpass.getpass()
except Exception as error:
print('ERROR', error)
# Start requests session
s = requests.Session()
# Get initial JWT Tocken
r = s.get(TOKEN_API)
if not r:
print('Error: Guest Token!')
exit()
jwt = r.json()["body"]["jwt"]
# Login
# AES encryption kets extracted from browser
# Key = 0f0e0d0c0b0a09080706050403020100
# iv = 000102030405060708090a0b0c0d0e0f
key = (
b"\x0f\x0e\x0d\x0c\x0b\x0a\x09\x08\x07\x06\x05\x04\x03\x02\x01\x00")
iv = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
# AES implementation from
# https://gist.github.com/wowkin2/a2b234c87290f6959c815d3c21336278
password_enc = AESCipher(key, iv).encrypt(password)
data = {
"header": {
"msisdn": msisdn,
"locale": "En"
},
"body": {
"password": password_enc
}
}
headers = {
"jwt": jwt
}
r = s.post(SIGNIN_API, json=data, headers=headers)
if r.json()["header"]["customerId"] is None:
print('Error: Can\'t login! Check phone number or password')
exit()
customerId = r.json()["header"]["customerId"]
jwt = r.json()["body"]["jwt"]
# Hit the API
headers = {
"jwt": jwt
}
data = {
"header": {
"customerId": customerId,
"msisdn": msisdn,
"locale": "En"
},
"body": {}
}
r = s.post(BALANCE_API, json=data, headers=headers)
outstandingAmount, unbilledFees = [
r.json(
)["body"][k] for k in ('outstandingAmount', 'unbilledFees')]
print(f"outstanding Amount: {outstandingAmount} EGP")
print(f"unbilled Amount: {unbilledFees} EGP")
r = s.post(FREEUNITS_API, json=data, headers=headers)
initialTotalAmount, usedAmount, freeAmount = [
r.json(
)["body"]["summarizedLineUsageList"][0][k] for k in ('initialTotalAmount', 'usedAmount', 'freeAmount')]
print(f"Total Amount: {initialTotalAmount} Gb")
print(f"Used Amount: {usedAmount} Gb")
print(f"Free Amount: {freeAmount} Gb")