#!/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) #Display flow puts flow #Create smtpinfo object 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 # #################################################################################################