This commit is contained in:
mgeeky 2021-10-29 13:08:19 +02:00
parent 158f3b0410
commit d5dd4db5ee
1 changed files with 97 additions and 17 deletions

View File

@ -104,6 +104,7 @@ import textwrap
import socket import socket
import time import time
import base64 import base64
from html import escape
from dateutil import parser from dateutil import parser
from email import header as emailheader from email import header as emailheader
@ -139,6 +140,7 @@ options = {
'verbose': False, 'verbose': False,
'nocolor' : False, 'nocolor' : False,
'log' : sys.stderr, 'log' : sys.stderr,
'format' : 'text',
} }
class Logger: class Logger:
@ -172,6 +174,35 @@ class Logger:
def with_color(c, s): def with_color(c, s):
return "\x1b[%dm%s\x1b[0m" % (c, s) return "\x1b[%dm%s\x1b[0m" % (c, s)
@staticmethod
def replaceColorToHtml(s):
out = ''
i = 0
while i < len(s):
if s[i] == '\x1b' and s[i+1] == '[' and s[i+2] != '0':
c = int(s[i+2:i+4])
pos = 0
while pos < len(s):
if s[pos] == '\x1b' and s[pos+1] == '[' and s[pos+2] == '0' and s[pos+3] == 'm':
break
pos += 1
txt = s[i+5:pos]
i = i + 5 + pos + 4
for k, v in Logger.colors_map.items():
if v == c:
out += f'<p style="color:{k}">{escape(txt)}</p>'
break
continue
out += s[i]
i += 1
return out
def colored(self, txt, col): def colored(self, txt, col):
if self.options['nocolor']: if self.options['nocolor']:
return txt return txt
@ -257,6 +288,9 @@ class Logger:
def dbg(self, txt, **kwargs): def dbg(self, txt, **kwargs):
if self.options['debug']: if self.options['debug']:
if self.options['format'] == 'html':
txt = f'<!-- {txt} -->'
kwargs['nocolor'] = self.options['nocolor'] kwargs['nocolor'] = self.options['nocolor']
Logger.out(txt, self.options['log'], 'debug', **kwargs) Logger.out(txt, self.options['log'], 'debug', **kwargs)
@ -637,7 +671,7 @@ class SMTPHeadersAnalysis:
'19618925003' : 'Mail body contained suspicious words (like Viagra).', '19618925003' : 'Mail body contained suspicious words (like Viagra).',
# triggered on mail with empty body and subject "Click here" # triggered on mail with empty body and subject "Click here"
'28233001' : 'Subject line contained suspicious words luring action (like "Click here"). ', '28233001' : 'Subject line contained suspicious words luring action (ex. "Click here"). ',
# triggered on a mail with test subject and 1500 words of http://nietzsche-ipsum.com/ # triggered on a mail with test subject and 1500 words of http://nietzsche-ipsum.com/
'30864003' : 'Mail body contained a lot of text (more than 10.000 characters).', '30864003' : 'Mail body contained a lot of text (more than 10.000 characters).',
@ -656,11 +690,11 @@ class SMTPHeadersAnalysis:
'166002' : 'HTML mail body contained URL <a> link.', '166002' : 'HTML mail body contained URL <a> link.',
# Message contained <a href="https://something.com/file.html?parameter=value" - GET parameter with value. # Message contained <a href="https://something.com/file.html?parameter=value" - GET parameter with value.
'21615005' : 'Mail body contained <a> tag with URL containing GET parameter: href="https://foo.bar/file?aaa=bbb"', '21615005' : 'Mail body contained <a> tag with URL containing GET parameter: ex. href="https://foo.bar/file?aaa=bbb"',
# Message contained <a href="https://something.com/file.html?parameter=https://another.com/website" # Message contained <a href="https://something.com/file.html?parameter=https://another.com/website"
# - GET parameter with value, being a URL to another website # - GET parameter with value, being a URL to another website
'45080400002' : 'Mail body contained <a> tag with URL containing GET parameter with value of another URL: href="https://foo.bar/file?aaa=https://baz.xyz/"', '45080400002' : 'Mail body contained <a> tag with URL containing GET parameter with value of another URL: ex. href="https://foo.bar/file?aaa=https://baz.xyz/"',
# Message contained <a> with href pointing to a file with dangerous extension, such as file.exe # Message contained <a> with href pointing to a file with dangerous extension, such as file.exe
'460985005' : 'Mail body contained HTML <a> tag with href URL pointing to a file with dangerous extension (such as .exe)', '460985005' : 'Mail body contained HTML <a> tag with href URL pointing to a file with dangerous extension (such as .exe)',
@ -673,7 +707,7 @@ class SMTPHeadersAnalysis:
# Message1 - FirstHop Gmail SMTP Received with ESMTPS. # Message1 - FirstHop Gmail SMTP Received with ESMTPS.
# Message2 - FirstHop Gmail SMTP-Relay Received with ESMTPSA. # Message2 - FirstHop Gmail SMTP-Relay Received with ESMTPSA.
# #
'121216002' : 'First Hop MTA SMTP Server used as a SMTP Relay. It used to originate e-mails, but here it acted as a Relay. Or it\'s due to use of "with ESMTPSA" instead of ESMTPS', '121216002' : 'First Hop MTA SMTP Server used as a SMTP Relay. It\'s known to originate e-mails, but here it acted as a Relay. Or maybe due to use of "with ESMTPSA" instead of ESMTPS?',
} }
@ -1336,6 +1370,13 @@ Results will be unsound. Make sure you have pasted your headers with correct spa
if options['debug']: if options['debug']:
raise raise
idsOfDecodeAll = [int(x[0]) for x in testsDecodeAll]
for a in self.testsToRun:
if a in idsOfDecodeAll:
self.decode_all = True
break
if self.decode_all: if self.decode_all:
for testId, testName, testFunc in testsDecodeAll: for testId, testName, testFunc in testsDecodeAll:
try: try:
@ -3657,7 +3698,7 @@ Src: https://www.cisco.com/c/en/us/td/docs/security/esa/esa11-1/user_guide/b_ESA
self.received_path = path self.received_path = path
if '1' not in self.testsToRun: if 1 not in self.testsToRun:
return [] return []
return { return {
@ -4058,6 +4099,8 @@ Src: https://www.cisco.com/c/en/us/td/docs/security/esa/esa11-1/user_guide/b_ESA
else: else:
tmp += '\n\n\t- Use --decode-all to print its hexdump.' tmp += '\n\n\t- Use --decode-all to print its hexdump.'
result += tmp
return { return {
'header' : header, 'header' : header,
'value': '...', 'value': '...',
@ -4512,7 +4555,7 @@ def opts(argv):
opt = o.add_argument_group('Options') opt = o.add_argument_group('Options')
opt.add_argument('-o', '--outfile', default='', type=str, help = 'Output file with report') opt.add_argument('-o', '--outfile', default='', type=str, help = 'Output file with report')
opt.add_argument('-f', '--format', choices=['json', 'text'], default='text', help='Analysis report format. JSON, text. Default: text') opt.add_argument('-f', '--format', choices=['json', 'text', 'html'], default='text', help='Analysis report format. JSON, text. Default: text')
opt.add_argument('-N', '--nocolor', default=False, action='store_true', help='Dont use colors in text output.') opt.add_argument('-N', '--nocolor', default=False, action='store_true', help='Dont use colors in text output.')
opt.add_argument('-v', '--verbose', default=False, action='store_true', help='Verbose mode.') opt.add_argument('-v', '--verbose', default=False, action='store_true', help='Verbose mode.')
opt.add_argument('-d', '--debug', default=False, action='store_true', help='Debug mode.') opt.add_argument('-d', '--debug', default=False, action='store_true', help='Debug mode.')
@ -4526,7 +4569,7 @@ def opts(argv):
args = o.parse_args() args = o.parse_args()
if len(args.outfile) > 0 or args.format == 'json': if len(args.outfile) > 0 and (args.format == 'json' or args.format == 'text'):
args.nocolor = True args.nocolor = True
options.update(vars(args)) options.update(vars(args))
@ -4537,7 +4580,7 @@ def opts(argv):
def printOutput(out): def printOutput(out):
output = '' output = ''
if options['format'] == 'text': if options['format'] == 'text' or options['format'] == 'html':
width = 100 width = 100
num = 0 num = 0
@ -4596,6 +4639,27 @@ def printOutput(out):
{analysis} {analysis}
''' '''
if options['format'] == 'html':
output2 = f'''<html>
<head>
<title>decode-spam-headers</title>
</head>
<body>
{output}
</body>
</html>'''
output = output2.replace('\n', '<br/>').replace('\t', '&nbsp;' * 4).replace(' ', '&nbsp;')
output2 = output
for m in re.finditer(r'(<[^>]+>)', output, re.I):
a = m.group(1)
b = a.replace('&nbsp;', ' ')
output2 = output2.replace(a, b)
return Logger.replaceColorToHtml(output2)
#return output
elif options['format'] == 'json': elif options['format'] == 'json':
output = json.dumps(out) output = json.dumps(out)
@ -4604,7 +4668,7 @@ def printOutput(out):
def main(argv): def main(argv):
args = opts(argv) args = opts(argv)
if not args: if not args:
return Falsex return False
if args.list: if args.list:
print('[.] Available tests:\n') print('[.] Available tests:\n')
@ -4627,16 +4691,25 @@ def main(argv):
logger.info('Analysing: ' + args.infile) logger.info('Analysing: ' + args.infile)
an0 = SMTPHeadersAnalysis(logger)
(a, b, c) = an0.getAllTests()
maxTest = 0
for i in a+b+c:
test = int(i[0])
if test > maxTest:
maxTest = test
text = '' text = ''
with open(args.infile) as f: with open(args.infile) as f:
text = f.read() text = f.read()
try: try:
include_tests = [] include_tests = set()
exclude_tests = [] exclude_tests = set()
if len(args.include_tests) > 0: include_tests = [int(x) for x in args.include_tests.split(',')] if len(args.include_tests) > 0: include_tests = set([int(x) for x in args.include_tests.replace(' ', '').split(',')])
if len(args.exclude_tests) > 0: exclude_tests = [int(x) for x in args.exclude_tests.split(',')] if len(args.exclude_tests) > 0: exclude_tests = set([int(x) for x in args.exclude_tests.replace(' ', '').split(',')])
if len(include_tests) > 0 and len(exclude_tests) > 0: if len(include_tests) > 0 and len(exclude_tests) > 0:
logger.fatal('--include-tests and --exclude-tests options are mutually exclusive!') logger.fatal('--include-tests and --exclude-tests options are mutually exclusive!')
@ -4644,8 +4717,9 @@ def main(argv):
raise raise
logger.fatal('Tests to be included/excluded need to be numbers! Ex. --include-tests 1,5,7') logger.fatal('Tests to be included/excluded need to be numbers! Ex. --include-tests 1,5,7')
testsToRun = set() _testsToRun = set()
for i in range(1000):
for i in range(maxTest + 5):
if len(include_tests) > 0: if len(include_tests) > 0:
if i not in include_tests: if i not in include_tests:
continue continue
@ -4654,7 +4728,9 @@ def main(argv):
if i in exclude_tests: if i in exclude_tests:
continue continue
testsToRun.add(i) _testsToRun.add(i)
testsToRun = sorted(_testsToRun)
an = SMTPHeadersAnalysis(logger, args.resolve, args.decode_all, testsToRun) an = SMTPHeadersAnalysis(logger, args.resolve, args.decode_all, testsToRun)
out = an.parse(text) out = an.parse(text)
@ -4663,6 +4739,10 @@ def main(argv):
if len(args.outfile) > 0: if len(args.outfile) > 0:
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
output2 = output
if args.format != 'html':
output2 = ansi_escape.sub('', output) output2 = ansi_escape.sub('', output)
with open(args.outfile, 'w') as f: with open(args.outfile, 'w') as f: