From 1c5a1e5c8afb84717bb044c2a186306c9c929477 Mon Sep 17 00:00:00 2001 From: Artur Augustyniak Date: Wed, 31 Jan 2018 21:46:46 +0100 Subject: [PATCH] modus refactor --- .gitignore | 2 + modus_operandi/.gitignore | 6 ++ modus_operandi/CHANGELOG | 107 ++++++++++++++++++++ modus_operandi/modus.py | 65 ++++++++++++ modus_operandi/modus/__init__.py | 0 modus_operandi/modus/analyzer.py | 76 ++++++++++++++ modus_operandi/modus/cli.py | 107 ++++++++++++++++++++ modus_operandi/modus/ctx.py | 98 ++++++++++++++++++ modus_operandi/modus/walker.py | 34 +++++++ modus_operandi/modus/worker.py | 107 ++++++++++++++++++++ modus_operandi/plugins/basic_code_review.py | 82 +++++++++++++++ modus_operandi/plugins/dlink.py | 30 ++++++ modus_operandi/plugins/find_todo.py | 30 ++++++ modus_operandi/plugins/genix.py | 34 +++++++ modus_operandi/plugins/horde.py | 35 +++++++ modus_operandi/plugins/piwigo.py | 57 +++++++++++ modus_operandi/plugins/wp.py | 88 ++++++++++++++++ 17 files changed, 958 insertions(+) create mode 100644 .gitignore create mode 100644 modus_operandi/.gitignore create mode 100644 modus_operandi/CHANGELOG create mode 100755 modus_operandi/modus.py create mode 100644 modus_operandi/modus/__init__.py create mode 100755 modus_operandi/modus/analyzer.py create mode 100644 modus_operandi/modus/cli.py create mode 100644 modus_operandi/modus/ctx.py create mode 100644 modus_operandi/modus/walker.py create mode 100644 modus_operandi/modus/worker.py create mode 100644 modus_operandi/plugins/basic_code_review.py create mode 100644 modus_operandi/plugins/dlink.py create mode 100644 modus_operandi/plugins/find_todo.py create mode 100644 modus_operandi/plugins/genix.py create mode 100644 modus_operandi/plugins/horde.py create mode 100644 modus_operandi/plugins/piwigo.py create mode 100644 modus_operandi/plugins/wp.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99ec428 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.idea diff --git a/modus_operandi/.gitignore b/modus_operandi/.gitignore new file mode 100644 index 0000000..9c63e84 --- /dev/null +++ b/modus_operandi/.gitignore @@ -0,0 +1,6 @@ +*.pyc +.idea +modus/bin/ +modus/include/ +modus/lib/ +modus/lib64 diff --git a/modus_operandi/CHANGELOG b/modus_operandi/CHANGELOG new file mode 100644 index 0000000..ea26a4d --- /dev/null +++ b/modus_operandi/CHANGELOG @@ -0,0 +1,107 @@ +# -------------------------------------------------------------------------------------- +# modus.py - 'modus operandi' project +# -------------------------------------------------------------------------------------- +# based mostly on bugs described at code610.blogspot.com +# -------------------------------------------------------------------------------------- +# +# last updates: (v0.8) +# +# 31.01.2018 / 21:18 -- complete refactor, multiprocessing and thread pools +# due to use of python-magic an libmagic this version won't work under windows +# -- support for dynamic plugin load +# -- support select plugins dir +# -- csv output +# +# last updates: (v0.7) +# +# 10.01.2018 / 12:36 -- added: piwigo.check_xss, piwigo.check_sqli_pwg_realesc +# 10.01.2018 / 12:20 -- added: dlink.check_xss module +# +# 10.01.2018 / 11:12 -- added module: +# -- basic_traversal (checking.py) +# -- basic_unserialize (checking.py) +# -- check_fileinc (wordpress.py) +# -- check_rce (wordpress.py) +# +# 10.01.2018 / 11:07 -- added modules: +# -- horde -- find few types of XSS (based on Horde 5.2.2x) +# -- wordpress -- find few XSS and SQLi bugs (based on Wordpress 4.x) +# -- genix -- find few basic XSS (based on GenixCMS 1.1.5) +# -- basic -- few sample test for XSS and SQLi +# -- +# +# 09.01.2018 / 10:00 -- rewrited again to v0.7 +# +# last updates: (v0.5) +# +# 03.01.2018 / 10:59 -- modified wordpress-declar modules... +# 02.01.2018 / 18:45 -- modified: wordpress_sqli_get* modules; breaktime;] +# 02.01.2018 / 16:42 -- added: wordpress_sqli_get_row_declar_where +# 02.01.2018 / 16:25 -- added: wordpress_sqli_get_results_declar_2ndParam +# 02.01.2018 / 16:11 -- modified: added missing HTTP methods to parse +# -- wordpress_sqli_declar renamed to +# wordpress_sqli_get_results_declar. splited again. +# 02.01.2018 / 14:20 -- modified wordpress_sqli_declar to properly +# spot sqli bug as well as verify buggy declaration in the +# source. TODO: prepare more checks; split checks to f()'s +# +# 02.01.2018 / 08:58 -- added: basic_traversal() +# 01.01.2018 / 23:51 -- notes:TODO:wordpres_* to modify... +# 01.01.2018 / 17:57 -- added: +# -- wordpress_fileinc | A +# -- wordpress_rce() | A +# -- detailed time (.now()) +# +# 01.01.2018 / 15:43 -- added/modified: +# -- dlink_xss - quick test to grab some XSS (based on dir300) | A +# -- piwigo_xss - find XSS bugs in Piwigo code (based on 2.9.2) | A +# -- piwigo_sqli - to check for sqli bugs (based on 2.9.2) | A +# -- genix_sqli - check for sqli bugs in genix (1.1.5) | A +# -- genix_xss - find XSS bugs (based on genix1.1.5) | A +# -- horde_xss_setget -- XSS when set/get is not sanitized | A +# -- horde_xss_setdef - based on CVE-2017-1690[6-8] | A +# -- wordpress_sqli - to find more WPplugin bugs... | A +# -- wordpress_sqli_declar - to find WP plugin bugs (4.x) | A +# -- basic_xss - to find XSS bugs (methods) | M +# -- basic_fileinc_declar - include bugs with declaration(s) | M + +# 01.01.2018 / 15:23 -- some small changes in function below; still TODO +# 01.01.2018 / 11:40 -- few modification of base_fileinc_declar; TODO +# 01.01.2018 / 11:06 -- continue... +# 29.12.2017 / 12:55 -- rewrited: basic_xss(_declar), basic_fileinc +# 29.12.2017 / 09:03 -- new idea for modus skeleton (v0.5) +# 28.12.2017 / 10:40 -- edited wordpress modules +# 28.12.2017 / 07:54 -- added: basic path/dir traversal check +# 27.12.2017 / 13:32 -- added: basic file include check +# 27.12.2017 / 13:13 -- added: basic unserialize check +# 26.12.2017 / 20:35 -- retesting zabbix (3.4.4) module +# 24.12.2017 / 08:47 -- added: wordpress_sqli_declar draft +# 24.12.2017 / 01:02 -- added: wordpress_sqli module draft +# 24.12.2017 / 00:40 -- few modification of basix_xss*-module(s) +# 23.12.2017 / 17:31 -- added: basic_sqli, basic_xss updated +# 23.12.2017 / 13:01 -- added: horde_xss_setget, basic_xss +# basic_xss_declar +# 23.12.2017 / 12:01 -- first stages ready (v0.4) +# 23.12.2017 / 11:06 -- rewriting the whole code again to v0.4 +# 22.12.2017 / 10:22 -- preparing tests for new zabbix(3.4.4) +# 21.12.2017 / 17:08 -- checking dlink's bug modified +# 20.12.2017 / 15:31 -- few updates to verify bugs in dlink's +# 12.12.2017 / 09:29 -- recheck to verify horde bugs; +# / 12:04 -- 2 new sqli bugs in Horde5.2.x found +# / 17:54 -- reediting xss_piwigo +# 11.12.2017 / 19:04 -- 'the easiest solution is in the Code' - enlil +# 10.12.2017 / 12:08 -- updated sqli_piwigo module +# 10.12.2017 / 07:46 -- updating new modules +# 09.12.2017 / 11:42 -- modified to v0.3 +# 08.12.2017 / 15:00 -- added: more test to sqli @piwigo +# -- added: tests for xss @piwigo +# 06.12.2017 / 18:59 -- testing horde with current modules +# 05.12.2017 / 00:08 -- rewriting the whole code to v0.2 +# 30.11.2017 / 23:57 -- see: readme.txt +# 26.11.2017 / 20:11 -- rewrited few lines... +# + +# more: +# code610.blogspot.com +# + diff --git a/modus_operandi/modus.py b/modus_operandi/modus.py new file mode 100755 index 0000000..344143e --- /dev/null +++ b/modus_operandi/modus.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +modus.py - v0.8 + +small script I called modus.py from 'modus operandi'. +maybe you will find it useful. +for some reason, try to keep it private for now. +thanks. + +more: code610.blogspot.com + +last update(s): please see CHANGELOG file. +have fun +""" +import sys +from os.path import dirname +from os.path import abspath +import signal +from modus.cli import parse_params +from modus.ctx import ECTX +from modus.walker import SourceWalker +from modus.worker import run_worker_processes +from modus.analyzer import register_plugins + + +def manager_signal_handler(): + signal.signal(signal.SIGINT, signal.SIG_IGN) + + +def setup_analyzers(plugins_dir): + return [A() for A in register_plugins(plugins_dir)] + + +def main(): + ctx = ECTX() + parse_params(ctx) + + source_walker = SourceWalker(ctx) + analyzers = setup_analyzers(ctx[ECTX.plugins_repo]) + + try: + + run_worker_processes( + ctx, + source_walker, + analyzers + ) + + finally: + ctx.work_queue.close() + ctx.work_queue.cancel_join_thread() + + +if __name__ == "__main__": + sys.path.append( + dirname(dirname(abspath(__file__))) + ) + try: + main() + except KeyboardInterrupt: + sys.stderr.write("Terminated\n") + finally: + sys.stdout.flush() + sys.stderr.flush() diff --git a/modus_operandi/modus/__init__.py b/modus_operandi/modus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modus_operandi/modus/analyzer.py b/modus_operandi/modus/analyzer.py new file mode 100755 index 0000000..b2b9405 --- /dev/null +++ b/modus_operandi/modus/analyzer.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from abc import ABCMeta, abstractmethod +import os +import imp +import re +from os import path + + +class PluginRegistry(ABCMeta): + plugins = [] + + def __init__(cls, name, bases, attrs): + if name != 'Analyzer': + PluginRegistry.plugins.append(cls) + + +class Analyzer(object): + __metaclass__ = PluginRegistry + MAX_DISPLAY_LINE_LEN = 120 + + def __init__(self): + self.description = self.search_description() + + def fit_line(self, line): + if Analyzer.MAX_DISPLAY_LINE_LEN < len(line): + out_str = line[:Analyzer.MAX_DISPLAY_LINE_LEN] + "...".lstrip() + else: + out_str = line[:-1].lstrip() + return out_str.replace('\n', ' ').replace('\r', '') + + def scan(self, file_info): + file_path = file_info[0] + mime_type = file_info[1] + results = [] + with open(file_path, 'rb') as f: + lines = f.readlines() + for tag, matcher in self.description.iteritems(): + regex = re.compile(matcher[0]) + for line_num, line in enumerate(lines): + if re.search(regex, line): + results.append( + (tag, + matcher[0], + mime_type, + path.realpath(file_path), + line_num + 1, + self.fit_line(line), + matcher[1] + ) + ) + if results: + return results + + @abstractmethod + def search_description(self): + pass + + @abstractmethod + def target_mime_wildcards(self): + pass + + +def register_plugins(plugins_dir): + for filename in os.listdir(plugins_dir): + modname, ext = os.path.splitext(filename) + if ext == '.py': + f, path, descr = imp.find_module(modname, [plugins_dir]) + if f: + _ = imp.load_module(modname, f, path, descr) + return PluginRegistry.plugins + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/modus/cli.py b/modus_operandi/modus/cli.py new file mode 100644 index 0000000..fd35053 --- /dev/null +++ b/modus_operandi/modus/cli.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import argparse + +from ctx import ECTX +from ctx import get_default_plugins_path +from analyzer import register_plugins +import sys + +LINE_WITH = 80 + + +class ListModules(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + if namespace.repo: + plugins_dir = namespace.repo + else: + plugins_dir = get_default_plugins_path() + plugins = register_plugins(plugins_dir) + for plugin_class in plugins: + print(plugin_class.__name__) + print(plugin_class.__doc__) + print(" Mime types:") + for m in plugin_class().target_mime_wildcards(): + print("\t%s" % m) + print("\n Search description:") + regex_dict = plugin_class().search_description() + for k, v in regex_dict.iteritems(): + print(" TAG: %s" % k) + print("\tRegex: %s" % v[0]) + print("\tComment: %s" % v[1]) + print("-" * LINE_WITH) + sys.exit(0) + + +def stb(v): + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + + +def parse_params(ctx): + banner = '\t[--] modus v0.7 [--]\n' + + parser = argparse.ArgumentParser(description=banner) + parser.add_argument( + ECTX.start_path, + metavar="PATH", + type=str, + help='Source files directory.' + ) + parser.add_argument( + '-%s' % ECTX.wildcard[0], + '--%s' % ECTX.wildcard, + metavar="REGEXP", + type=str, + help='File name wildcard. Default value is "%s".' % ctx['filter'] + ) + parser.add_argument( + '-%s' % ECTX.workers_num[0], + '--%s' % ECTX.workers_num, + metavar="NUMBER", + type=int, + help='Worker processes. Default value is (%d - %d). ' + 'Max possible value is (%d * %d)' % (ctx['cpu'], ctx['mps'], ctx['cpu'], ctx['mpl']) + ) + parser.add_argument( + '-%s' % ECTX.parallelism_level[0], + '--%s' % ECTX.parallelism_level, + metavar="NUMBER", + type=int, + help='Internal worker parallelism. Default value is %d. ' + 'Value in inclusive range <%d-%d>.' % (ctx['threads'], ctx['lwp'], ctx['hwp']) + ) + parser.add_argument( + '-%s' % ECTX.plugins_repo[0], + '--%s' % ECTX.plugins_repo, + metavar="PATH", + type=str, + help='Custom plugins dir. Default value is %s. ' % (ctx['repo']) + ) + parser.add_argument( + '-%s' % ECTX.strict_mime_check[0], + '--%s' % ECTX.strict_mime_check, + metavar="BOOL", + type=stb, + help='Strict mime match. WARNING! Disabling this will run all enabled analyzers ' + 'against all types of files ignoring potential ' + 'extension-content inconsistency Default value is %s. ' % (ctx['smc']) + ) + parser.add_argument( + '-l', + '--list', + action=ListModules, + nargs=0, + help="List available checks." + ) + + ctx.update(parser.parse_args().__dict__) + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/modus/ctx.py b/modus_operandi/modus/ctx.py new file mode 100644 index 0000000..a2b3461 --- /dev/null +++ b/modus_operandi/modus/ctx.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import multiprocessing +from os import path + + +def get_default_plugins_path(): + return path.realpath(path.join( + path.dirname(path.realpath(__file__)), + "../plugins" + )) + + +class ECTX(object): + start_path = "dir" + wildcard = "filter" + workers_num = "proc" + parallelism_level = "threads" + cpu_count = "cpu" + plugins_repo = "repo" + min_processes_sub = "mps" + max_processes_mul = "mpl" + lowest_worker_parallelism = "lwp" + highest_worker_parallelism = "hwp" + strict_mime_check = "smc" + + def __init__(self): + + self.work_queue = multiprocessing.Queue() + + self.VALUES = { + ECTX.cpu_count: multiprocessing.cpu_count(), + ECTX.wildcard: '*', + ECTX.workers_num: multiprocessing.cpu_count() - 1, + ECTX.parallelism_level: 10, + ECTX.min_processes_sub: 1, + ECTX.max_processes_mul: 3, + ECTX.lowest_worker_parallelism: 1, + ECTX.highest_worker_parallelism: 100, + ECTX.plugins_repo: get_default_plugins_path(), + ECTX.strict_mime_check: True + + } + + def is_workers_num_valid(desired): + max_workers = self[ECTX.cpu_count] \ + * self[ECTX.max_processes_mul] + return 0 < desired <= max_workers + + def is_parallelism_level_valid(desired): + return self[ECTX.lowest_worker_parallelism] \ + <= desired <= \ + self[ECTX.highest_worker_parallelism] + + self.VALIDATION_RULES = { + ECTX.workers_num: [ + is_workers_num_valid, + "Worker processes number out of range." + ], + ECTX.parallelism_level: [ + is_parallelism_level_valid, + "Internal worker parallelism out of range." + ], + ECTX.start_path: [ + lambda p: path.isdir(p), + "Start path is not a directory." + ] + } + + def __getitem__(self, item): + return self.VALUES[item] + + def __validate(self, d): + filtered_dict = { + k: v for k, v in d.iteritems() if + k in self.VALIDATION_RULES.iterkeys() + } + for key, value in filtered_dict.iteritems(): + if not self.VALIDATION_RULES[key][0](value): + raise ValueError( + '[%s=%s] %s' % ( + key, + value, self.VALIDATION_RULES[key][1] + ) + ) + + def __dropna(self, d): + return dict((k, v) for k, v in d.iteritems() if v is not None) + + def update(self, d): + input_params = self.__dropna(d) + self.__validate(input_params) + self.VALUES.update(input_params) + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/modus/walker.py b/modus_operandi/modus/walker.py new file mode 100644 index 0000000..12d890d --- /dev/null +++ b/modus_operandi/modus/walker.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from fnmatch import fnmatch +from os import path +import magic +from modus.ctx import ECTX + + +class SourceWalker(object): + + def __init__(self, execution_context): + self.ctx = execution_context + self.m = magic.open(magic.MAGIC_MIME) + self.m.load() + + def handle_matched_filepath(self, pattern, directory, files): + + for filename in files: + if fnmatch(filename, pattern): + f = path.join(directory, filename) + if path.isfile(f) and not path.islink(f): + self.ctx.work_queue.put((f, self.m.file(f))) + + def run(self): + path.walk( + self.ctx[ECTX.start_path], + self.handle_matched_filepath, + self.ctx[ECTX.wildcard], + ) + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/modus/worker.py b/modus_operandi/modus/worker.py new file mode 100644 index 0000000..8028756 --- /dev/null +++ b/modus_operandi/modus/worker.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from concurrent.futures import ThreadPoolExecutor, as_completed +from multiprocessing import Process +from ctx import ECTX +import re +import os +from sys import stdout +import csv +from multiprocessing import Lock + +lock = Lock() +csv_writer = csv.writer(stdout, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) + + +class PoisonPill(StopIteration): + pass + + +def write_csv_line(data): + with lock: + csv_writer.writerow(data) + stdout.flush() + + +def prepare_mime_matcher(target_mime): + def mime_matcher(a): + for wc in a.target_mime_wildcards(): + pattern = re.compile(wc) + if re.search(pattern, target_mime): + return True + return False + + return mime_matcher + + +def __perform_scan_task(file_info, analyzers, strict=False): + if strict: + mime_filter = prepare_mime_matcher(file_info[1]) + analyzers = filter(mime_filter, analyzers) + for a in analyzers: + s_info = a.scan(file_info) + if s_info: + for row in s_info: + write_csv_line(row) + + +def __consume_from(queue): + while True: + val = queue.get() + if PoisonPill is not type(val): + yield val + else: + return + + +def __worker(threads_num, work_queue, analyzers, strict=True): + try: + pool = ThreadPoolExecutor(threads_num) + for file_info in __consume_from(work_queue): + pool.submit( + __perform_scan_task, + file_info, + analyzers, + strict + ) + except KeyboardInterrupt: + pass + finally: + pool.shutdown(wait=False) + + +def run_worker_processes(ctx, source_walker, analyzers): + processes = [] + processes.extend([Process( + target=__worker, + args=( + ctx[ECTX.parallelism_level], + ctx.work_queue, + analyzers, + ctx[ECTX.strict_mime_check] + )) + for _ in xrange(0, ctx[ECTX.workers_num]) + ]) + for p in processes: + p.start() + + write_csv_line([ + 'tag', + 'pattern', + "mime-type", + "file_path", + "line number", + "line_part", + "comment" + ]) + source_walker.run() + for _ in processes: + ctx.work_queue.put(PoisonPill()) + + for p in processes: + p.join() + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/basic_code_review.py b/modus_operandi/plugins/basic_code_review.py new file mode 100644 index 0000000..dd4173c --- /dev/null +++ b/modus_operandi/plugins/basic_code_review.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class CodeReview(Analyzer): + """ + This module was designed to spot the 'most basic' + bugs during the source code review. for now we have: + -- basic_xss + -- basic_unserialize + -- basic_path_traversal + + """ + + def __prepare_xss_definitions(self, methods): + xss_dict = {} + + functions = ['echo ', 'return ', 'print '] + for m in methods: + for f in functions: + tag = "XSS:%s-%s" % (f.rstrip(), m) + xss_dict[tag] = (f + "(.*?)\$_" + m + "\[(.*?)\]", "possible xss") + return xss_dict + + def __prepare_taversal_definitions(self, methods): + traversal_dict = {} + functions = [ + 'readfile', 'readfile ', + 'fopen', 'fopen ', + 'is_readable', 'is_readable ', + 'glob', 'glob ' + ] + for m in methods: + for f in functions: + tag = "PATH-TRAVERSAL:%s-%s" % (f.rstrip(), m) + traversal_dict[tag] = (f + "(.*?)\\$_" + m + "\['(.*?)'\]", "possible path traversal") + + return traversal_dict + + def __prepare_unserialize_definitions(self, methods): + unserialize_dict = {} + + functions = [ + 'unserialize', + 'unserialize ' + ] + for m in methods: + for f in functions: + tag = "SERIALIZATION:%s-%s" % (f.rstrip(), m) + unserialize_dict[tag] = (f + "(.*?)\\$_" + m + "\['(.*?)'\]", "possible umsafe unserialize") + + return unserialize_dict + + def target_mime_wildcards(self): + return ["text/x-php*"] + + def search_description(self): + methods = [ + 'GET', + 'POST', + 'REQUEST', + 'FILES', + 'COOKIE', + 'SERVER', + 'SESSION', + 'ENV', + 'COOKIE' + ] + desc_dict = self.__prepare_xss_definitions(methods) + desc_dict.update( + self.__prepare_taversal_definitions(methods) + ) + desc_dict.update( + self.__prepare_unserialize_definitions(methods) + ) + return desc_dict + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/dlink.py b/modus_operandi/plugins/dlink.py new file mode 100644 index 0000000..c7c5ab6 --- /dev/null +++ b/modus_operandi/plugins/dlink.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class BasicXssAnalyzer(Analyzer): + """ + this module was designed to spot the 'most basic' + XSS bugs during the source code review of Dlink router. + Based on version dir300. For now we have: + + redefined basic modules for v0.7: + -- check_xss -- + """ + + def target_mime_wildcards(self): + return ["text/x-php*"] + + def search_description(self): + return { + # TODO one below hangs on 192k php-font file + # "TAG:def": ("(.*?)echo \$_POST\[\"(.*?)\"", "it looks like we got a basic SQLi bug."), + "bug:XSS:Dlink[GET]": ("input (.*?)echo \$_GET\[\"(.*?)\"", "it looks like we got a basic XSS bug."), + "bug:XSS:Dlink[POST]": ("echo \$_POST\[\".*?\"", "it looks like we got a basic XSS bug."), + } + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/find_todo.py b/modus_operandi/plugins/find_todo.py new file mode 100644 index 0000000..6deb351 --- /dev/null +++ b/modus_operandi/plugins/find_todo.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class FileAnalyzer(Analyzer): + """ + Example modus analyzer + + User must implement two methods + + Note: + blah blah + + Attributes: + blah blah + """ + + def target_mime_wildcards(self): + return ["text*"] + + def search_description(self): + return { + "TAG:TODO": ("#.*?TODO.*?", "comment ie. CVE?"), + } + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/genix.py b/modus_operandi/plugins/genix.py new file mode 100644 index 0000000..cc47c4a --- /dev/null +++ b/modus_operandi/plugins/genix.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class Genix(Analyzer): + """ + this module was designed to spot the 'most basic' + bugs during the source code review of Genix CMS. + based on version 1.1.5 (described also at the blog). + for now we have: + -- check_xss -- check for functions with GET/POST, etc... + -- check_sqli -- to check some basic SQLi bugs found in Genix + """ + + def target_mime_wildcards(self): + return ["text/x-php*"] + + def search_description(self): + return { + "Genix:bug:SQLi": ( + "Db::result\(\"SELECT(.*?)\{\$(.*?)\}", + "possible SQLi; wrong declaration" + ), + "Genix:bug:XSS": ( + "\$(.*?) = Typo::cleanX\(\$_GET\['(.*?)'", + "it looks like we got a basic XSS bug for Genix CMS" + ), + } + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/horde.py b/modus_operandi/plugins/horde.py new file mode 100644 index 0000000..71e45fc --- /dev/null +++ b/modus_operandi/plugins/horde.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class Horde(Analyzer): + """ + this module was designed to spot the 'most basic' + bugs during the source code review of Horde 5.2.x. + based on version 5.2.21-22 (described also at the blog). + for now we have: + -- check_xss_setget -- check for functions with GET/POST, etc... + -- check_xss_setdef -- check for functinos declared as setDefault + """ + + def target_mime_wildcards(self): + return ["text/x-php*"] + + def search_description(self): + return { + "bug:XSS:Horde:setdef": ( + "-\>setDefault\(\$vars->get\('(.*?)'", + "possible XSS (setDefault) for Horde based on: CVE-2017-1690[6-8]" + ), + "bug:XSS:Horde:set-get": ( + "\$vars->set\('(.*?)',(.*?)->get\('(.*?)'", + "possible XSS (set/get) for Horde" + ), + + } + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/piwigo.py b/modus_operandi/plugins/piwigo.py new file mode 100644 index 0000000..791a3e4 --- /dev/null +++ b/modus_operandi/plugins/piwigo.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class Piwigo(Analyzer): + """ + this module was designed to spot the 'most basic' + bugs during the source code review of Piwigo 2.9.2. + For now we have: + -- check_xss -- testing for basic XSS + -- check_sqli_pwg_realesc -- checking for wrong sanitization + when pwg_db_real_escape_string is used in query + """ + + def target_mime_wildcards(self): + return ["text/x-php*"] + + def __prepare_check_sqli_pwg_realesc_definitions(self, methods): + traversal_dict = {} + for m in methods: + regexes = [ + "pwg_db_real_escape_string(.*?)\$_" + m + "\['(.*?)'", + "WHERE(.*?)" + m + "\['(.*?)'" + ] + for r in regexes: + tag = "bug:SQLI:Piwigo:%s-%s" % (r[:4], m) + traversal_dict[tag] = ( + r, "possible path traversal" + + ) + + return traversal_dict + + def search_description(self): + methods = [ + 'GET', + 'POST', + 'REQUEST', + 'FILES', + 'COOKIE', + 'SERVER', + 'SESSION', + 'ENV', + 'COOKIE' + ] + desc_dict = self.__prepare_check_sqli_pwg_realesc_definitions(methods) + desc_dict['bug:XSS:Piwigo'] = ( + "span class=(.*?){/if}>{\$(.*?)}", + "it looks like we got a basic XSS for Piwigo" + ) + return desc_dict + + +if __name__ == "__main__": + pass diff --git a/modus_operandi/plugins/wp.py b/modus_operandi/plugins/wp.py new file mode 100644 index 0000000..da103d4 --- /dev/null +++ b/modus_operandi/plugins/wp.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from modus.analyzer import Analyzer + + +class Wordrpess(Analyzer): + """ + this module was designed to spot the 'most basic' + bugs during the source code review of Wordpress 4.x. + for now we have: + -- check_xss_methods + -- check_sqli_declar + -- check_fileinc + """ + + def __prepare_rce_definitions(self, methods): + dict = {} + functions = [ + 'call_user_func', 'call_user_func ', + 'function_exists', 'function_exists ' + ] + for m in methods: + for f in functions: + tag = "bug:RCE:%s-%s" % (f.rstrip(), m) + dict[tag] = ( + f + "(.*?)\\$_" + m + "\['(.*?)'\]", + "possible path traversal", + 'it looks like we got a basic "Wordpress-related" RCE bug' + ) + + return dict + + def __prepare_file_inc_definitions(self, methods): + dict = {} + functions = [ + 'include', 'include ', + 'include_once', 'include_once ', + 'require', 'require ', + 'require_once ', 'require_once ' + ] + for m in methods: + for f in functions: + tag = "bug:INCLUDE:%s-%s" % (f.rstrip(), m) + dict[tag] = ( + f + "(.*?)\\$_" + m + "\['(.*?)'\]", + 'it looks like we got a basic "Wordpress-related" include-bug...' + ) + return dict + + def __prepare_sqli_definitions(self, methods): + dict = {} + for m in methods: + tag = "bug:SQLi:Wordpress:%s" % m + dict[tag] = ( + "\$wpdb->get_results\('SELECT(.*?)WHERE (.*?)=(.*?)\$_" + m + "\[(.*?)\]\);", + "possible umsafe unserialize" + ) + + return dict + + def target_mime_wildcards(self): + return ["text/x-php*"] + + def search_description(self): + methods = [ + 'GET', + 'POST', + 'REQUEST', + 'FILES', + 'COOKIE', + 'SERVER', + 'SESSION', + 'ENV', + 'COOKIE' + ] + desc_dict = self.__prepare_rce_definitions(methods) + desc_dict.update( + self.__prepare_file_inc_definitions(methods) + ) + desc_dict.update( + self.__prepare_sqli_definitions(methods) + ) + return desc_dict + + +if __name__ == "__main__": + pass