#!/usr/bin/ruby
## smtpdump v0.1 : Extract some SMTP informations from PCAP files
## Copyright (C) 2009 Franck GUENICHOT
## franck {dot} guenichot {at} orange {dot} fr
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 3
## of the License, or any later version.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see .
## Written for the Network Forensic Puzzle #2
## http://forensicscontest.com
require 'rubygems'
require 'pcaplet'
require 'base64'
require 'digest/md5'
require 'optparse'
require 'tmail'
$VERSION = "0.1"
class Flow
attr_reader :index,:src,:dst,:srcport,:dstport
attr_accessor :data
@@flow_index = 0
def initialize(src,dst,srcport,dstport)
@@flow_index += 1
@index = @@flow_index
@src = src
@dst = dst
@srcport = srcport
@dstport = dstport
@data = ""
end
def self.exist?(src,dst,srcport,dstport)
found = nil
ObjectSpace.each_object(Flow) { |f|
found = f if (f.src == src and f.dst == dst and f.srcport == srcport and f.dstport == dstport)
}
return true if found
return false
end
def self.find_by_index(index)
found = nil
ObjectSpace.each_object(Flow) { |f|
found = f if f.index == index
}
found
end
def self.find_flow(src,dst,srcport,dstport)
found = nil
ObjectSpace.each_object(Flow) { |f|
found = f if (f.src == src and f.dst == dst and f.srcport == srcport and f.dstport == dstport)
}
found
end
def to_s
"[" + @index.to_s + "] " + @src.to_s + ":" + @srcport.to_s + " => " + @dst.to_s + ":" + @dstport.to_s
end
end
class SmtpInfo
attr_reader :authinfo,:smtp_data
@@SMTP_AUTH_TYPES = ['LOGIN','PLAIN','CRAM-MD5']
def initialize(data)
@data = data
@authinfo = {}
@smtp_data = ""
@auth_seen = false
@username_seen = false
@data_seen = false
@eom_seen = false
parse
end
def parse
@data.lines.each_with_index { |line, i|
if @data_seen #We are reading data
if line.match(/^\.\s/)
@eom_seen = true # End Of Message seen
end
if @eom_seen
@data_seen = false
break
else
@smtp_data << line
end
elsif @auth_seen
@authinfo['username'] = Base64.decode64(line)
@username_seen = true
@auth_seen = false
elsif @username_seen
@authinfo['password'] = Base64.decode64(line)
@username_seen = false
end
request = line[0,4]
if request == "AUTH"
@authinfo['auth_type'] = line.split[1] if @@SMTP_AUTH_TYPES.include?(line.split[1].upcase)
if @authinfo['auth_type'].upcase == "LOGIN"
@auth_seen = true
end
elsif request == "DATA"
@data_seen = true
end
}
end
end
$options = {}
$email_count = 0
# USeful funcs.
def md5sum(file)
#Calculate md5sum of a file
digest = Digest::MD5.hexdigest(File.read(file))
digest
end
def smtpdump(flow)
puts flow
smtpinfo = SmtpInfo.new(flow.data)
if $options[:auth] then
unless smtpinfo.authinfo.empty?
puts " === Authentication infos ==="
puts " Found " + smtpinfo.authinfo['auth_type'] + " method"
puts " Username: " + smtpinfo.authinfo['username']
puts " Password: " + smtpinfo.authinfo['password']
puts " "
else
puts "No Authentication information found."
end
end
email = TMail::Mail.parse(smtpinfo.smtp_data) unless smtpinfo.smtp_data.empty?
if email
$email_count += 1
if $options[:save2file] then
fname = "outfile#{email_count.to_s}.eml"
puts " Saving raw email to file: #{fname}"
puts " "
File.open(fname,"w") {|f| f.write(message)}
end
if $options[:imfinfo] then
puts " === Email infos ==="
puts " "
puts " Mail From #{email['from'].to_s} to #{email['to'].to_s}"
puts " Subject: #{email.subject}"
puts " Content: #{email.body}" unless $options[:brief]
puts " "
end
if $options[:xtract] then
puts " === Attachments infos ==="
puts " "
email.parts.each {|part|
if email.attachment?(part)
puts " Type: #{part.content_type}"
filename = part.disposition_param('filename')
if filename then
puts " Saving file to disk: #{filename}"
puts ""
File.open(filename,'w') { |f| f.write(part.body)
}
if $options[:md5sum] then
md5 = md5sum(filename)
puts " File: #{filename} (MD5: 0x#{md5})"
puts " "
end
end
end
}
end
else
if $options[:save2file] or $options[:imfinfo] or $options[:xtract] then
puts "No IMF data found"
end
end
end
#################################################################################################
# handle command line args
opts = OptionParser.new do |opts|
opts.banner = "
smtpdump version #{$VERSION},
Copyright (C) 2009 Franck GUENICHOT
smtpdump comes with ABSOLUTELY NO WARRANTY;
This is free software, and you are welcome
to redistribute it under certain conditions.
(GPL v3)
Usage: smtpdump [$options] -r "
$options[:auth] = false
opts.on( '-A', '--auth', 'Display SMTP Auth informations (only LOGIN method)' ) do
$options[:auth] = true
end
$options[:imfinfo] = false
opts.on( '-e', '--info', 'Display Email informations' ) do
$options[:imfinfo] = true
end
$options[:brief] = false
opts.on( '-b', '--brief', 'Display minimum email informations' ) do
$options[:brief] = true
end
$options[:xtract] = false
opts.on( '-x', '--xtract', 'Extract email attachments' ) do
$options[:xtract] = true
end
flows = ObjectSpace.each_object(Flow).to_a
flows.sort! {|a,b| a.index <=> b.index}
$options[:md5sum] = false
opts.on( '-m', '--md5', 'Display extracted attachment MD5 Hash' ) do
$options[:md5sum] = true
end
$options[:save2file] = false
opts.on( '-s', '--save', 'Save raw email to file' ) do
$options[:save2file] = true
end
$options[:flow] = nil
opts.on( '-f ', '--flow-index', 'Filters only given index flow' ) do|index|
$options[:flow] = index
end
$options[:infile] = nil
opts.on( '-r', '--read ', 'Read the given pcap file [REQUIRED]' ) do|infile|
$options[:infile] = infile
end
opts.on( '-v', '--version', 'Display version information' ) do
puts $VERSION
exit
end
opts.on( '-h', '--help', 'Display this screen' ) do
puts opts
exit
end
end
#parse command line args
opts.parse!
# if no pcap file in input => display help and exit
if $options[:infile] == nil
puts opts
exit(0)
end
#File not found error.
unless File.exist?($options[:infile])
puts "File: #{$options[:infile]} does not exist."
exit(0)
end
# open the pcap file and add a filter for SMTP trafic on port 25/tcp or 587/tcp
smtpdump = Pcaplet.new('-n -r ' + $options[:infile].to_s)
smtp_filter = Pcap::Filter.new('tcp and (dst port 587 or 25)', smtpdump.capture)
smtpdump.add_filter(smtp_filter)
# Filtering PCAP and creating flow objects
smtpdump.each_packet { |pkt|
case pkt
when smtp_filter
unless aFlow = Flow.find_flow(pkt.ip_src,pkt.ip_dst,pkt.tcp_sport,pkt.tcp_dport)
aFlow = Flow.new(pkt.ip_src,pkt.ip_dst,pkt.tcp_sport,pkt.tcp_dport)
end
aFlow.data << pkt.tcp_data if pkt.tcp_data
end
}
flows = ObjectSpace.each_object(Flow).to_a
flows.sort! {|a,b| a.index <=> b.index}
if $options[:flow]
flow = flows.select {|f| f.index.to_s == $options[:flow]} # ugly but...
smtpdump(flow.first)
else
puts "=== SMTP flows ==="
flows.each { |flow|
smtpdump(flow)
}
end
#################################################################################################
# END of smtpdump #
#################################################################################################