Repository: IOActive/XDiFF
Branch: master
Commit: 552d3394e119
Files: 22
Total size: 157.0 KB
Directory structure:
gitextract_la3db652/
├── .travis.yml
├── README.md
├── classes/
│ ├── __init__.py
│ ├── compat.py
│ ├── db.py
│ ├── dbsqlite.py
│ ├── dump.py
│ ├── execute.py
│ ├── fuzzer.py
│ ├── monitor.py
│ ├── queue.py
│ ├── settings.py
│ └── webserver.py
├── docs/
│ ├── 1.-Install.md
│ ├── 2.-The-input.md
│ ├── 3.-The-software.md
│ ├── 4.-The-fuzzer.md
│ ├── 5.-The-output.md
│ └── Changelog.md
├── xdiff_analyze.py
├── xdiff_dbaction.py
└── xdiff_run.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .travis.yml
================================================
language: python
cache: pip
python:
- 2.7
- 3.6
#- nightly
#- pypy
#- pypy3
matrix:
allow_failures:
- python: nightly
- python: pypy
- python: pypy3
install:
#- pip install -r requirements.txt
- pip install flake8 # pytest # add another testing frameworks later
before_script:
# stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E901,E999,F821,F822,F823,F821 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
script:
- true # pytest --capture=sys # add other tests here
notifications:
on_success: change
on_failure: change # `always` will be the setting once code changes slow down
================================================
FILE: README.md
================================================
# What is XDiFF?
XDiFF is an Extended Differential Fuzzing Framework built for finding
vulnerabilities in software. It collects as much data as possible from
different executions an then tries to infer different potential vulnerabilities
based on the different outputs obtained.
The vulnerabilities can either be found in isolated pieces of software or by
comparing:
* Different inputs
* Different versions
* Different implementations
* Different operating systems' implementations
The fuzzer uses Python and runs on multiple OSs (Linux, Windows, OS X, and
Freebsd). Its main goal is to detect issues based on diffential fuzzing aided
with the extended capabilities to increase coverage. Still, it will found
common vulnerabilities based on hangs and crashes, allowing to attach a
memory debugger to the fuzzing sessions.
## Quick guide
Please follow the following steps:
1. [Install](https://github.com/IOActive/XDiFF/wiki/1.-Install) XDiFF
2. Define [the input](https://github.com/IOActive/XDiFF/wiki/2.-The-input)
3. Define [the software](https://github.com/IOActive/XDiFF/wiki/3.-The-software)
4. Run [the fuzzer](https://github.com/IOActive/XDiFF/wiki/4.-The-fuzzer)
5. Analyze [the output](https://github.com/IOActive/XDiFF/wiki/5.-The-output)
6. ...
7. Profit!
## Disclaimer
The tool and the fuzzing process can be susceptible to code execution.
Use it at your own risk always inside a VM.
## Authors
- Fernando Arnaboldi - _Initial work_
- [cclauss](https://github.com/cclauss)
For contributions, please propose a [Changelog](https://github.com/IOActive/XDiFF/wiki/Changelog) entry in the pull-request comments.
## Acknowledgments
Thanks Lucas Apa, Tao Sauvage, Scott Headington, Carlos Hollman, Cesar Cerrudo, Federico Muttis, Topo for their feedback and Arlekin for the logo.
## License
This project is licensed under the GNU general public license version 3.
## Logo

================================================
FILE: classes/__init__.py
================================================
================================================
FILE: classes/compat.py
================================================
from __future__ import print_function
from __future__ import absolute_import
# Python 2
try:
unicode
unicode = unicode
import cgi
def escape(value):
"""Use cgi.escape for Python 2"""
return cgi.escape(value)
# Python 3
except NameError:
import html
def unicode(value, errors=None): # Python 3
"""Just return the string an ignore the errors parameter"""
return str(value)
def escape(value):
"""Use html.escape for Python 3"""
return html.escape(value)
xrange = range
================================================
FILE: classes/db.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
import subprocess
class Db(object):
"""High level DB class: other databases could used this general set of queries"""
def __init__(self, settings):
self.db_connection = None
self.db_cursor = None
self.restrict_software = ""
self.settings = settings
def commit(self):
"""Save changes to the database"""
try:
self.db_connection.commit()
except Exception as e:
p = subprocess.Popen(["fuser", self.settings["db_file"]], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
self.settings['logger'].error("The database is locked by the following PIDs: %s", stdout)
def get_fuzz_testcase(self):
"""Get the fuzz testcases """
results = []
try:
self.db_cursor.execute("SELECT testcase FROM fuzz_testcase")
results = self.db_cursor.fetchall()
self.settings['logger'].debug("Testcases read: %s " % str(len(results)))
except Exception as e:
self.settings['logger'].critical("Exception when trying to retrieve information from fuzz_testcase: %s" % str(e))
if not results:
self.settings['logger'].warning("No testcases defined")
return results
def delete_unused_testcases(self):
"""Delete any unused testcases generated"""
self.db_cursor.execute("DELETE FROM fuzz_testcase WHERE id NOT IN (SELECT testcaseid FROM fuzz_testcase_result);")
self.commit()
def get_functions(self):
"""Get the name of the functions"""
results = []
try:
self.db_cursor.execute("SELECT function FROM function")
results = self.db_cursor.fetchall()
self.settings['logger'].debug("Functions read: %s " % str(len(results)))
except Exception as e:
self.settings['logger'].critical("Exception when trying to retrieve information from function: %s" % str(e))
if not results:
self.settings['logger'].warning("No functions defined")
return results
def get_values(self):
"""Get the fuzzing values"""
results = []
try:
self.db_cursor.execute("SELECT value FROM value")
results = self.db_cursor.fetchall()
self.settings['logger'].debug("Values read: %s " % str(len(results)))
except Exception as e:
self.settings['logger'].critical("Exception when trying to retrieve information from value: %s" % str(e))
return results
def list_software(self, active=None):
"""Get the list of [active] software used with testcases"""
results = []
if active is True:
active = "WHERE s.id IN (SELECT DISTINCT(r.softwareid) FROM fuzz_testcase_result AS r WHERE 1 = 1 " + self.restrict_software + ")"
else:
active = "WHERE 1 = 1 " + self.restrict_software.replace("r.softwareid", "s.id")
try:
self.db_cursor.execute("SELECT s.id, s.name, s.type, s.os FROM fuzz_software AS s " + active + " ORDER BY s.name ASC")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to list software: %s" % str(e))
return results
def set_software(self, softwareids):
"""Restrict the results to certain software ids"""
if softwareids:
self.restrict_software = " AND r.softwareid IN (" + ",".join(softwareids) + ") "
else:
self.restrict_software = ""
def get_software(self):
"""Get the current software ids restriction"""
return self.restrict_software
def get_software_type(self, category_type):
"""Get the software ids associated to a certain category type"""
results = []
try:
self.db_cursor.execute("SELECT s.id FROM fuzz_software AS s WHERE s.type = '" + category_type + "' " + self.restrict_software.replace("r.softwareid", "s.id") + " ORDER BY s.name")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to get software type: %s" % str(e))
return results
def list_results(self, lowerlimit=0, toplimit=-1):
"""Get a list of the fuzzed results"""
results = []
if toplimit is None:
toplimit = -1
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.stdout, r.stderr, c.name FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t, fuzz_constants AS c WHERE t.id >= " + str(lowerlimit) + " AND r.softwareid = s.id AND r.testcaseid = t.id AND c.type = 'kill_status' AND c.id = r.kill_status " + self.restrict_software + " ORDER BY r.testcaseid LIMIT " + str(int(toplimit)))
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to list results: %s" % str(e))
return results
def list_killed_results(self):
"""Get a list of the killed fuzzed results"""
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.stdout, r.stderr, c.name FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t, fuzz_constants AS c WHERE r.softwareid = s.id AND r.testcaseid = t.id AND c.type = 'kill_status' AND c.id = r.kill_status AND c.name != 'not killed' " + self.restrict_software + " ORDER BY r.testcaseid ")
return self.db_cursor.fetchall()
def count_results(self, lowerlimit=0, toplimit=-1):
"""Get a count of how many testcases where fuzzed"""
if toplimit is None:
toplimit = -1
self.db_cursor.execute("SELECT COUNT(r.testcaseid) FROM fuzz_testcase_result AS r WHERE 1=1 " + self.restrict_software + " ORDER BY r.testcaseid LIMIT " + str(int(toplimit)) + " OFFSET " + str(int(lowerlimit)))
return self.db_cursor.fetchone()[0]
def list_return_code_per_software(self):
"""Get the count of returncodes for each piece of software"""
results = []
try:
self.db_cursor.execute("SELECT s.name, s.type, s.os, r.returncode, COUNT(r.returncode) FROM fuzz_testcase_result AS r, fuzz_testcase AS t, fuzz_software AS s WHERE t.id = r.testcaseid and s.id = r.softwareid AND r.returncode != '' " + self.restrict_software + " GROUP BY r.returncode,s.name ORDER BY s.name, r.returncode;")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to list return code per software: %s" % str(e))
return results
def analyze_specific_return_code(self, returncodes):
"""Get the testcases that matches the return code"""
results = []
returncodes = " AND r.returncode IN (" + ",".join(returncodes) + ") "
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.returncode, r.stdout, r.stderr FROM fuzz_testcase_result AS r, fuzz_testcase AS t, fuzz_software AS s WHERE t.id = r.testcaseid and s.id = r.softwareid AND r.returncode != '' " + self.restrict_software + returncodes + " ORDER BY s.name, r.returncode")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze specific return code: %s" % str(e))
return results
def analyze_return_code_differences(self):
"""Find testcases where the return code was different depending on the input"""
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, r.returncode, r.stdout, r.stderr FROM fuzz_testcase AS t, fuzz_software AS s, fuzz_testcase_result AS r WHERE r.softwareid = s.id AND r.testcaseid = t.id AND r.returncode != '' " + self.restrict_software + " ORDER BY r.testcaseid")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the return code differences: %s" % str(e))
return results
def count_software(self):
"""Count how many different pieces of software have been tested"""
results = None
try:
self.db_cursor.execute("SELECT COUNT(DISTINCT(id)) FROM fuzz_testcase_result AS r, fuzz_software AS s WHERE r.softwareid = s.id")
results = self.db_cursor.fetchone()[0]
except Exception as e:
self.settings['logger'].critical("Exception when trying to count the amount of software: %s" % str(e))
return results
def count_testcases(self):
"""Count how many testcases are available"""
results = None
try:
self.db_cursor.execute("SELECT COUNT(testcase) FROM fuzz_testcase")
results = self.db_cursor.fetchone()[0]
except Exception as e:
self.settings['logger'].critical("Exception when trying to count the amount of test cases: %s" % str(e))
return results
def count_reference(self, reference):
"""Count how many testcases matching the reference are available"""
results = None
if reference:
try:
self.db_cursor.execute("SELECT COUNT(testcase) FROM fuzz_testcase WHERE testcase LIKE '%" + reference + "%'")
query = self.db_cursor.fetchone()
results = query[0]
except Exception as e:
self.settings['logger'].critical("Exception when trying to count how many testcases matching the reference are available: %s" % str(e))
return results
def analyze_canary_file(self):
"""Get all stdout/stderr references of canary files that were not originally used on the testcase"""
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.stdout, r.stderr FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t WHERE r.softwareid = s.id AND r.testcaseid = t.id AND t.testcase NOT LIKE '%canaryfile%' AND (r.stdout LIKE '%canaryfile%' OR r.stderr LIKE '%canaryfile%') " + self.restrict_software)
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the canary file: %s" % str(e))
return results
def analyze_top_elapsed(self, killed):
"""Find which software took more time (whether they were killed or not)"""
results = []
if killed is None:
killed = ""
elif killed is False:
killed = " AND c.name = 'not killed' "
elif killed is True:
killed = " AND c.name != 'not killed' "
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.elapsed FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t, fuzz_constants AS c WHERE r.softwareid = s.id AND r.testcaseid = t.id AND c.type = 'kill_status' AND c.id = r.kill_status " + killed + self.restrict_software + " ORDER BY r.elapsed DESC")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the top time elapsed: %s" % str(e))
return results
def analyze_killed_differences(self):
"""Find which testcases were required to be killed AND were also not killed (loop vs no loop for others)"""
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, c.name, r.stdout, r.stderr FROM fuzz_testcase AS t, fuzz_software AS s, fuzz_testcase_result AS r, fuzz_constants AS c WHERE r.softwareid = s.id AND r.testcaseid = t.id AND c.type = 'kill_status' AND r.kill_status = c.id " + self.restrict_software + " ORDER BY r.testcaseid")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze differences when killing software: %s" % str(e))
return results
def analyze_same_software(self):
"""Find testcases when the same software produces different results when using different inputs (ie, Node_CLI vs Node_File) """
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, r.stdout FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t WHERE r.softwareid = s.id AND r.testcaseid = t.id " + self.restrict_software + " ORDER BY r.testcaseid, s.name")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the same software: %s" % str(e))
return results
def analyze_stdout(self, lowerlimit, upperlimit):
"""Finds testcases that produce the same output"""
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, r.stdout, s.category, s.os,t.id FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t WHERE r.softwareid = s.id AND r.testcaseid = t.id AND r.stdout != '' AND r.testcaseid >= " + str(lowerlimit) + " AND r.testcaseid <= " + str(upperlimit) + self.restrict_software + " ORDER BY r.testcaseid")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the stdout: %s" % str(e))
return results
def analyze_same_stdout(self):
"""Used to analyze when different testcases are producing the same output"""
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.stdout FROM fuzz_testcase_result AS r, fuzz_testcase AS t, fuzz_software AS s WHERE r.softwareid = s.id AND r.testcaseid = t.id AND r.stdout in (SELECT DISTINCT(r2.stdout) FROM fuzz_testcase_result AS r2, fuzz_testcase AS t2 WHERE r2.testcaseid = t2.id AND r2.stdout != '' ) " + self.restrict_software + " ORDER BY r.stdout, s.name")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the same stdout: %s" % str(e))
return results
def analyze_string_disclosure(self, searchme, excludeme="", excludecli="", where=None):
"""Return stdout and stderr values containing a specific string"""
results = []
if excludeme != "":
excludeme = " AND r.stdout NOT LIKE '%" + excludeme + "%' AND r.stderr NOT LIKE '%" + excludeme + "%' "
if excludecli != "":
excludecli = " AND s.type = 'File' "
if where is None:
where = "r.stdout LIKE '%" + searchme + "%' OR r.stderr LIKE '%" + searchme + "%' ESCAPE '_'"
elif where is 'stdout':
where = "r.stdout LIKE '%" + searchme + "%' ESCAPE '_'"
elif where is 'stderr':
where = "r.stderr LIKE '%" + searchme + "%' ESCAPE '_'"
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.stdout, r.stderr, r.returncode FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t WHERE r.softwareid = s.id AND r.testcaseid = t.id AND (" + where + ")" + excludeme + excludecli + self.restrict_software)
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze the string disclosure: %s" % str(e))
return results
def analyze_remote_connection(self, searchme=""):
"""Get the remote connections established"""
results = []
try:
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.stdout, r.stderr, r.network FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t WHERE r.softwareid = s.id AND r.testcaseid = t.id AND r.network !='' AND (r.stdout LIKE '%" + searchme + "%' OR r.stderr LIKE '%" + searchme + "%' ESCAPE '_')" + self.restrict_software)
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze remote connections: %s" % str(e))
return results
def analyze_output_messages(self, messages):
"""Get the results that produced error messages"""
self.db_cursor.execute("SELECT substr(t.testcase, 1, " + str(self.settings['testcase_limit']) + "), s.name, s.type, s.os, r.returncode, r." + messages + " FROM fuzz_testcase_result AS r, fuzz_software AS s, fuzz_testcase AS t WHERE r.softwareid = s.id AND r.testcaseid = t.id AND r." + messages + " !='' " + self.restrict_software) # sqli ftw!
return self.db_cursor.fetchall()
def analyze_elapsed(self):
"""Analize the total time required for each piece of software"""
results = []
try:
self.db_cursor.execute("SELECT s.name, s.type, s.os, SUM(r.elapsed) FROM fuzz_testcase_result AS r, fuzz_software AS s WHERE r.softwareid = s.id GROUP BY r.softwareid")
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].critical("Exception when trying to analyze time elapsed: %s" % str(e))
return results
def get_rows(self, table):
"""Return all the rows from a certain given table"""
results = None
if table:
try:
self.db_cursor.execute("SELECT * FROM " + table)
results = self.db_cursor.fetchall()
except Exception as e:
self.settings['logger'].error("Exception when trying to return the rows from the table %s: %s" % (table, str(e)))
return results
================================================
FILE: classes/dbsqlite.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
from __future__ import absolute_import
import os
import sqlite3
import subprocess
import sys
import time
from . import db
class DbSqlite(db.Db):
"""Used in conjunction with the class Db, with supposedly specific SQLite content"""
def __init__(self, settings, db_file):
super(DbSqlite, self).__init__(settings)
self.settings['db_file'] = db_file
if not self.settings['db_file'] or not os.path.isfile(self.settings['db_file']):
self.settings['logger'].error("The database file '%s' does not exists", settings['db_file'])
else:
try:
self.db_connection = sqlite3.connect(self.settings['db_file'])
self.db_cursor = self.db_connection.cursor()
self.db_connection.execute("PRAGMA journal_mode = OFF")
self.db_connection.execute("PRAGMA synchronous = OFF")
self.db_connection.execute("PRAGMA temp_store = MEMORY")
self.db_connection.execute("PRAGMA count_changes = OFF")
# self.db_connection.text_factory = lambda x: x.decode("utf-8", "ignore") # python3
except Exception as e:
self.settings['logger'].error("Exception when initializing the database: %s" % str(e))
p = subprocess.Popen(["fuser", self.settings["db_file"]], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if stdout:
self.settings['logger'].error("The database is locked by the following PIDs: %s", stdout)
def optimize(self):
"""Optimize the DB before starting"""
self.db_cursor.execute('VACUUM')
self.commit()
def close(self):
"""Close the DB connection"""
if self.db_connection:
self.db_connection.close()
def create_table(self):
"""Create and define initial values for the tables"""
self.db_cursor.execute('CREATE TABLE IF NOT EXISTS fuzz_software (id INTEGER PRIMARY KEY, name TEXT, type TEXT, suffix TEXT, filename TEXT, execute TEXT, os TEXT, category TEXT, UNIQUE(name, type, os))')
self.db_cursor.execute('CREATE TABLE IF NOT EXISTS fuzz_testcase_result (softwareid INTEGER, testcaseid INTEGER, stdout TEXT, stderr TEXT, network TEXT, returncode TEXT, elapsed TEXT, kill_status TEXT, UNIQUE(softwareid, testcaseid))')
self.db_cursor.execute('CREATE TABLE IF NOT EXISTS fuzz_constants (id INTEGER PRIMARY KEY, type TEXT, name TEXT)')
self.db_cursor.execute('CREATE TABLE IF NOT EXISTS fuzz_testcase (id INTEGER PRIMARY KEY, testcase BLOB UNIQUE)')
self.db_cursor.execute('CREATE TABLE IF NOT EXISTS function (function BLOB UNIQUE)')
self.db_cursor.execute('CREATE TABLE IF NOT EXISTS value (value BLOB UNIQUE)')
self.db_cursor.execute("SELECT id FROM fuzz_constants WHERE type = 'kill_status'")
if self.db_cursor.fetchone() is None:
self.db_cursor.execute("INSERT INTO fuzz_constants (type, name) VALUES ('kill_status', 'not killed')")
self.db_cursor.execute("INSERT INTO fuzz_constants (type, name) VALUES ('kill_status', 'requested')")
self.db_cursor.execute("INSERT INTO fuzz_constants (type, name) VALUES ('kill_status', 'killed')")
self.db_cursor.execute("INSERT INTO fuzz_constants (type, name) VALUES ('kill_status', 'not found')")
self.commit()
def get_software_id(self, piece):
"""Return the software id using all the data associated to software: name, type, suffix, filename, execution and category"""
softwareid = None
if not isinstance(piece, dict) or "execute" not in piece or "category" not in piece or "name" not in piece or "type" not in piece or not isinstance(piece["type"], list) or "suffix" not in piece or not isinstance(piece["suffix"], list) or "filename" not in piece or not isinstance(piece["filename"], list):
self.settings["logger"].error("Piece parameter is incorrect")
elif self.db_cursor:
self.db_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='fuzz_software'")
if self.db_cursor.fetchone() is None:
self.settings['logger'].critical("Error: the fuzz_software table was not found. Where the testcases generated with xdiff_dbaction.py?")
else:
softwareid = self.save_software(piece)
return softwareid
def save_software(self, piece):
"""Insert a piece of software and return its id"""
softwareid = None
piece_suffix = ','.join(piece['suffix'])
piece_type = ','.join(piece['type'])
piece_filename = ','.join(piece['filename'])
self.db_cursor.execute("INSERT OR IGNORE INTO fuzz_software (name, type, suffix, filename, execute, os, category) VALUES (:name, :type, :suffix, :filename, :execute, :os, :category)", {"name": str(piece['name']), "type": piece_type, "suffix": piece_suffix, "filename": piece_filename, "execute": str(piece['execute']), "os": str(sys.platform), "category": str(piece['category'])})
self.commit()
self.db_cursor.execute("SELECT id FROM fuzz_software WHERE name=:name AND (type=:type or type is NULL) AND (suffix=:suffix or suffix is NULL) AND (filename=:filename or filename is NULL) AND (execute=:execute or execute is NULL) AND (category=:category or category is NULL)", {"name": str(piece['name']), "type": piece_type, "suffix": piece_suffix, "filename": piece_filename, "execute": str(piece['execute']), "category": str(piece['category'])})
softwareid = self.db_cursor.fetchone()
# UNIQUE Constraint: fuzz_software.name, fuzz_software.type, fuzz_software.os
if softwareid is None:
self.settings['logger'].critical("Error: there was no software found. Is there a unique name, type and os for the fuzzed software? Did you change the definition of the software in software.ini after an initial execution?")
else:
softwareid = softwareid[0]
return softwareid
def get_constant_value(self, constant_type, constant_name):
"""Return constant value for a certain constant type and name"""
self.db_cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'fuzz_constants'")
value = self.db_cursor.fetchone()
if value is None:
return None # table does not exists
self.db_cursor.execute("SELECT id FROM fuzz_constants WHERE type=:type AND name=:name", {"type": constant_type, "name": constant_name})
value = self.db_cursor.fetchone()
if value is not None:
value = value[0]
return value
def get_latest_id(self, software):
"""Return the latest testcase id stored in the database"""
latestid = None
if software:
ids = []
for piece in software:
if 'softwareid' in piece:
ids.append(str(piece['softwareid']))
else:
self.settings['logger'].error("get_latest_id() received an incorrect software parameter")
try:
self.db_cursor.execute("SELECT testcaseid FROM fuzz_testcase_result WHERE softwareid IN (" + ",".join(ids) + ") ORDER BY testcaseid DESC LIMIT 1") # lazy sqli everywhere ftw
result = self.db_cursor.fetchone()
if result is None:
latestid = 0
else:
latestid = result[0] + 1
except Exception as e:
self.settings['logger'].critical("Exception when trying to retrieve the latest id: %s " % str(e))
return latestid
def get_test(self, latest_id, limit):
"""compiles test cases for fuzzing"""
tests = []
if latest_id is not None and limit is not None:
try:
self.db_cursor.execute("SELECT id, testcase FROM fuzz_testcase WHERE id >= :latest_id LIMIT :limit", {"latest_id": str(latest_id), "limit": str(limit)})
tests = self.db_cursor.fetchall()
if not tests and 'generate_tests' in self.settings:
self.settings['queue'].generate_tests(latest_id, limit)
tests = self.get_test(latest_id, limit)
except Exception as e:
self.settings['logger'].critical("Exception when trying to retrieve information from fuzz_testcase: %s", e)
return tests
def set_results(self, results):
"""Save fuzzing results"""
current_amount = 0
size = 0
while True:
try:
self.db_cursor.execute("SELECT count(testcaseid) FROM fuzz_testcase_result")
result = self.db_cursor.fetchone()
original_amount = result[0]
size = os.stat(self.settings['db_file']).st_size
break
except:
pass
if isinstance(results, list):
while True:
# if you are having concurrency with the sqlite database, things may break apart
try:
self.db_cursor.executemany("INSERT OR IGNORE INTO fuzz_testcase_result ('softwareid', 'testcaseid', 'stdout', 'stderr', 'network', 'returncode', 'elapsed', 'kill_status') VALUES (:softwareid, :testcaseid, :stdout, :stderr, :network, :returncode, :elapsed, :kill_status)", results)
self.commit()
break
except sqlite3.OperationalError as e:
self.settings['logger'].warning("Exception when setting the results: %s" % str(e))
time.sleep(2)
self.db_cursor.execute("SELECT count(testcaseid) FROM fuzz_testcase_result")
result = self.db_cursor.fetchone()
current_amount = result[0]
size = os.stat(self.settings['db_file']).st_size - size
return (current_amount - original_amount, size) # return the amount of testcases saved and the size of them
def set_testcase(self, testcases):
"""save tests"""
self.db_cursor.executemany("INSERT OR IGNORE INTO fuzz_testcase ('testcase') VALUES (?)", testcases)
self.settings['logger'].debug("Testcases saved %s" % str(len(testcases)))
self.commit()
def set_values(self, values):
"""used by migrate.py to save the values"""
self.db_cursor.executemany("INSERT OR IGNORE INTO value ('value') VALUES (?)", values)
self.settings['logger'].debug("Values saved %s " % str(len(values)))
self.commit()
def set_functions(self, functions):
"""used by migrate.py to save the functions"""
self.db_cursor.executemany("INSERT OR IGNORE INTO function ('function') VALUES (?)", functions)
self.settings['logger'].debug("Functions saved %s " % str(len(functions)))
self.commit()
def get_columns(self, table):
"""Return a table's columns"""
try:
self.db_cursor.execute("SELECT * FROM " + table)
return list(map(lambda x: x[0], self.db_cursor.description))
except:
return None
def insert_row(self, table, column, row):
"""Insert a row into a table"""
while True:
# if you are having concurrency with the sqlite database, things may break apart
try:
self.db_cursor.execute("INSERT OR IGNORE INTO " + table + " (" + ",".join(column) + ") VALUES (" + ','.join('?' * len(column)) + ")", row)
self.commit()
break
except Exception as e:
self.settings['logger'].warning("Exception when trying to insert a row: %s " % str(e))
time.sleep(2)
self.commit()
================================================
FILE: classes/dump.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
import compat
class Dump(object):
"""Dump the results received in html, txt or csv"""
def __init__(self, settings):
self.settings = settings
self.toggle_table = True
def get_screen_size(self, columns):
"""Defines the size of the columns, based on the amount of columns"""
size = [None]
if isinstance(columns, list):
col0 = 20
col1 = 9
size = size * len(columns)
if len(columns) == 1:
size[0] = self.settings['output_width'] - 3
elif len(columns) == 2:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = self.settings['output_width'] - size[0] - 5
elif len(columns) == 3:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = col1 # fixed length, meant to be used with the software name
size[2] = self.settings['output_width'] - size[1] - size[0] - 7
elif len(columns) == 4:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = col1 # fixed length, meant to be used with the software name
size[2] = (self.settings['output_width'] - size[1] - size[0]) / 2 - 9
size[3] = self.settings['output_width'] - size[2] - size[1] - size[0] - 9
elif len(columns) == 5:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = col1 # fixed length, meant to be used with the software name
size[2] = (self.settings['output_width'] - size[1] - size[0]) / 3 - 3
size[3] = (self.settings['output_width'] - size[1] - size[0]) / 3 - 3
size[4] = self.settings['output_width'] - size[3] - size[2] - size[1] - size[0] - 11
elif len(columns) == 6:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = col1 # fixed length, meant to be used with the software name
size[2] = (self.settings['output_width'] - size[1] - size[0]) / 4 - 3
size[3] = (self.settings['output_width'] - size[1] - size[0]) / 4 - 3
size[4] = (self.settings['output_width'] - size[1] - size[0]) / 4 - 3
size[5] = self.settings['output_width'] - size[4] - size[3] - size[2] - size[1] - size[0] - 13
elif len(columns) == 7:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = col1 # fixed length, meant to be used with the software name
size[2] = (self.settings['output_width'] - size[1] - size[0]) / 5 - 3
size[3] = (self.settings['output_width'] - size[1] - size[0]) / 5 - 3
size[4] = (self.settings['output_width'] - size[1] - size[0]) / 5 - 3
size[5] = (self.settings['output_width'] - size[1] - size[0]) / 5 - 3
size[6] = self.settings['output_width'] - size[5] - size[4] - size[3] - size[2] - size[1] - size[0] - 15
elif len(columns) == 8:
size[0] = col0 # fixed length, meant to be used with the testcase
size[1] = col1 # fixed length, meant to be used with the software name
size[2] = (self.settings['output_width'] - size[1] - size[0]) / 6 - 3
size[3] = (self.settings['output_width'] - size[1] - size[0]) / 6 - 3
size[4] = (self.settings['output_width'] - size[1] - size[0]) / 6 - 3
size[5] = (self.settings['output_width'] - size[1] - size[0]) / 6 - 3
size[6] = (self.settings['output_width'] - size[1] - size[0]) / 6 - 3
size[7] = self.settings['output_width'] - size[6] - size[5] - size[4] - size[3] - size[2] - size[1] - size[0] - 17
else:
self.settings['logger'].error("Too many columns: ", len(columns))
else:
self.settings['logger'].error("Incorrect columns type received")
return size
def print_text_top_row(self, title, columns):
"""Print the first row of the table (and then print_text_row and print_text_bottom_row will be used)"""
output = None
if isinstance(title, str) and isinstance(columns, list):
size = self.get_screen_size(columns)
output = "-" * self.settings['output_width'] + "\n"
output += "| " + title + " " * (self.settings['output_width'] - len(title) - 4) + " |\n"
output += "-" * self.settings['output_width'] + "\n"
for colid in range(0, len(columns)):
output += "| {message:{fill}{align}{width}}".format(message=columns[colid][:size[colid]], fill=" ", align='<', width=size[colid])
output += "|\n"
output += "-" * self.settings['output_width'] + "\n"
return output
def print_text_row(self, columns, results):
"""Print a row of the table (previously print_text_top_row was used and finally print_text_bottom_row will be used used)"""
size = self.get_screen_size(columns)
output = ""
if isinstance(results, list):
for result in results:
if result:
for row in result:
for colid in range(0, len(row)):
message = ""
if type(row[colid]).__name__ == 'int':
message = str(row[colid])
elif type(row[colid]).__name__ == 'buffer':
message = "<binary>"
elif type(row[colid]).__name__ != 'NoneType':
message = row[colid].encode("utf-8")
try:
message = message.replace('\n', ' ') # Python 2
except:
message = message.decode().replace('\n', ' ') # Python 3
output += "| {message:{fill}{align}{width}}".format(message=message[:size[colid]], fill=" ", align='<', width=size[colid])
output += "|\n"
output += "-" * self.settings['output_width'] + "\n"
return output
def print_text_bottom_row(self):
"""Print the last bottom row of a txt output"""
return "\n"
def print_csv_top_row(self, columns):
"""Print the first row of the csv table (and then print_csv_row will be used)"""
output = ""
if isinstance(columns, list):
output = ",".join(columns) + "\n"
return output
def print_csv_row(self, results):
"""Print a row of the table (previously print_text_top_row was used and finally print_text_bottom_row will be used used)"""
output = ""
if isinstance(results, list):
for result in results:
for row in result:
for colid in range(0, len(row)):
if type(row[colid]).__name__ in ['int', 'NoneType']:
message = str(row[colid])
else:
message = (row[colid]).encode("utf-8")
if colid != 0:
output += ","
try:
output += message # Python 2
except:
output += message.decode() # Python 3
output += "\n"
return output
def print_xml_row(self, title, column, results):
"""Print a row of the table (previously print_text_top_row was used and finally print_text_bottom_row will be used used)"""
output = ""
if isinstance(title, str) and isinstance(column, list) and isinstance(results, list):
output = "\t<" + "".join(ch for ch in title if ch.isalnum()) + ">\n"
for result in results:
for row in result:
column_id = 0
for item in row:
output += "\t\t<" + str(compat.escape(column[column_id])) + ">" + str(compat.escape(item)) + "</" + str(compat.escape(column[column_id])) + ">\n"
column_id += 1
output += "\n"
output += "\t</" + "".join(ch for ch in title if ch.isalnum()) + ">\n"
return output
def print_html_top_row(self, title, columns):
"""Print the first row of the HTML table (and then print_html_row will be used)"""
output = ""
if isinstance(title, str) and isinstance(columns, list):
output = """<table>
<tr>
<th><a id='""" + "".join(ch for ch in title if ch.isalnum()) + """Link' onclick="toggleTable('""" + "".join(ch for ch in title if ch.isalnum()) + """Table');" href='#""" + "".join(ch for ch in title if ch.isalnum()) + """'>""" + str(compat.escape(title)) + """</a><a href="#"><div class="arrow-up">top </div></a></th>
</tr>
</table>
<table id='""" + "".join(ch for ch in title if ch.isalnum()) + """Table'>
<tr>"""
for column in columns:
output += "<th>" + str(compat.escape(column)) + "</th>"
output += "</tr>\n"
return output
def print_html_row(self, results):
"""Print a row of the table (previously print_html_top_row was used and finally print_html_bottom_row will be used used)"""
output = ""
if isinstance(results, list):
cont = 1
for result in results:
if cont % 2 == 0:
trclass = " class='gray'"
else:
trclass = ""
for row in result:
output += " <tr" + trclass + ">"
for item in row:
output += "<td><div style='white-space: pre-wrap;'>" + str(compat.escape(str(item)).encode('ascii', 'xmlcharrefreplace')) + "</div></td>"
output += "</tr>\n"
cont += 1
return output
def print_html_bottom_row(self, title):
"""Print the first row of the HTML table (and then print_html_row will be used)"""
output = "</table><br/>\n"
if isinstance(title, str) and title.find("Analyze") != -1 and self.toggle_table:
output += "<script>toggleTable('" + "".join(ch for ch in title if ch.isalnum()) + "Table');</script>\n"
return output
def set_toggle_table(self, toggle):
"""Set a boolean flag to activate/deactivate if a table will be shown in HTML"""
self.toggle_table = bool(toggle)
def pre_general(self, output):
"""Print any previous code or perform tasks required before printing any table"""
contents = ""
if output == "xml":
contents = "<fuzzer>\n"
elif output == "html":
contents = """<!DOCTYPE html>
<html lang="en">
<head>
<title>Fuzzer Results for """ + str(compat.escape(self.settings['db_file'])) + """</title>
<meta charset="UTF-8">
<style>
a {
transition: color .3s;
color: #265C83;
font-size: 16px;
}
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 1200px;
margin-left: auto;
margin-right: auto;
}
td {
border: 1px solid #dddddd;
text-align: left;
padding: 4px;
font-size: 12px;
}
th {
border: 1px solid #dddddd;
text-align: left;
padding: 4px;
font-size: 14px;
}
tr.gray {
background-color: #dddddd;
}
pre {
text-align: left;
padding: 0px;
font-size: 12px;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
.arrow-up {
float: right;
font-size: 8px;
margin-right: 20px;
}
</style>
<script>
function toggleTable(id) {
var elem = document.getElementById(id);
var hide = elem.style.display == "none";
if (hide) {
elem.style.display = "table";
}
else {
elem.style.display = "none";
}
}
</script>
</head>
<body><a id='#'></a>"""
if "output_file" in self.settings:
self.write_file(self.settings['output_file'], 'w+', contents)
else:
print(contents)
def post_general(self, output):
"""Print any post code required before wrapping up"""
contents = ""
if output == "xml":
contents = "</fuzzer>"
elif output == "html":
contents = " </body>\n</html>"
if "output_file" in self.settings:
self.write_file(self.settings['output_file'], 'a+', contents)
else:
print(contents)
def general(self, output, title, columns, rows):
"""Main function to dump stuff: from here, you can export in different formats (txt, csv, xml, html) to the screen or files"""
if not rows:
return
contents = ""
title = title + " (" + str(len(rows)) + " rows)"
if output is None:
return
elif output == "txt":
contents = self.print_text_top_row(title, columns)
contents += self.print_text_row(columns, rows)
contents += self.print_text_bottom_row()
elif output == "csv":
contents = self.print_csv_top_row(columns)
contents += self.print_csv_row(rows)
elif output == "xml":
contents += self.print_xml_row(title, columns, rows)
elif output == "html":
contents += self.print_html_top_row(title, columns)
contents += self.print_html_row(rows)
contents += self.print_html_bottom_row(title)
else:
self.settings['logger'].error("Incorrect output selected")
if output in ["txt", "csv", "xml", "html"] and contents:
if "output_file" in self.settings and self.settings['output_file'] is not None:
self.write_file(self.settings['output_file'], 'a+', contents)
else:
print(contents)
def write_file(self, output_file, mode, content):
"""Write the content into a file"""
if content:
try:
target = open(output_file, mode)
target.write(content)
target.close()
except:
self.settings['logger'].error("Could not write in file '%s'.", output_file)
================================================
FILE: classes/execute.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
import os
import signal
import subprocess
import threading
import time
import compat
class Execute(object):
"""Thread being executed by Fuzzer"""
def __init__(self, settings, piece, testcase):
self.kill_status = None
self.settings = settings
self.results = {}
self.t = threading.Thread(target=self.run_subprocess, args=(piece, testcase))
self.t.start()
self.deleteme = testcase['data']
def join(self):
"""Join the results to the thread"""
try:
self.t.join()
except:
pass
def get_output(self):
"""Delete the file as part of getting the output"""
if self.deleteme and os.path.isfile(self.deleteme[0]['datafile'][1]):
os.remove(self.deleteme[0]['datafile'][1])
return self.results
def kill_process(self, process):
"""After the defined timeout, try to kill the process"""
self.kill_status = self.settings['kill_status']['requested']
if process.poll() is None: # don't send the signal unless it seems it is necessary
try:
# Unix
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
# Windows/Unix
# process.kill()
self.kill_status = self.settings['kill_status']['killed']
except OSError: # ignore
self.kill_status = self.settings['kill_status']['not_killed']
self.settings['logger'].debug("Killed process status: %s" % str(self.kill_status))
def run_subprocess(self, piece, testcase):
"""Obtain the stdout and stderr when executing a piece of software using subprocess"""
self.settings['logger'].debug("Input received: " + str(testcase))
stdout = stderr = elapsed = returncode = ""
self.kill_status = self.settings['kill_status']['not_killed']
start_test = time.time()
if "execute" in piece:
try:
if 'stdin' in testcase:
# Unix
p = subprocess.Popen(testcase['execute'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
# Windows/Unix
# p = subprocess.Popen(testcase['execute'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
# Unix
p = subprocess.Popen(testcase['execute'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
# Windows/Unix
# p = subprocess.Popen(testcase['execute'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
t = threading.Timer(self.settings['timeout'], self.kill_process, [p])
t.start()
if 'stdin' in testcase:
stdout, stderr = p.communicate(input=testcase['stdin'])
else:
stdout, stderr = p.communicate()
t.cancel()
returncode = p.returncode
stdout = compat.unicode(stdout.strip(), errors='ignore')
stderr = compat.unicode(stderr.strip(), errors='ignore')
stdout, stderr = self.analyze_results(stdout, stderr)
except OSError:
stderr = "Exception: OSErrorException"
except KeyboardInterrupt:
stderr = "Exception: KeyboardInterruptException"
except Exception as e:
stderr = "Exception: " + str(e)
elapsed = str(round(time.time() - start_test, 4))
self.results = {"softwareid": piece['softwareid'], "testcaseid": testcase['testcaseid'], "stdout": stdout, "stderr": stderr, "network": None, "returncode": returncode, "elapsed": elapsed, "kill_status": self.kill_status}
self.settings['logger'].debug("Output produced: " + str(self.results))
def analyze_results(self, stdout, stderr):
"""Save full results for certain specific special strings"""
if 'soft_bypass' in self.settings:
full = False
if any([x in stdout for x in self.settings['soft_bypass']]):
full = True
elif any([x in stderr for x in self.settings['soft_bypass']]):
full = True
if not full:
stdout = stdout[:self.settings['soft_limit']]
stderr = stderr[:self.settings['soft_limit']]
if 'hard_limit' in self.settings:
stdout = stdout[:self.settings['hard_limit']]
stderr = stderr[:self.settings['hard_limit']]
if 'hard_limit_lines' in self.settings:
stdout = stdout.split("\n")[0]
return stdout, stderr
================================================
FILE: classes/fuzzer.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
import os
import random
import string
import subprocess
import sys
import tempfile
import compat
from distutils.spawn import find_executable
from .execute import Execute
class Fuzzer(object):
"""Executes fuzzing threads"""
def __init__(self, settings, ids):
self.settings = settings
self.ids = ids
def chdir_tmp(self):
"""Change to the temporary directory"""
status = False
try:
os.chdir(self.settings['tmp_dir']) # it is safer to operate somewhere else
status = True
except Exception as e:
self.settings['logger'].error("It wasn't possible to change to the ram disk directory (%s). Instructions to mount it: %s\nError: %s" % (self.settings['tmp_dir'], self.settings['tmp_dir_howto'], e))
return status
def fuzz(self, tests):
"""Executes something in all the different pieces of software"""
process = [] # info to be return and saved in the database
# go through each test
for test in tests:
for piece in self.settings['software']:
input = self.get_input(piece, test)
try:
process.append(Execute(self.settings, piece, input))
except Exception:
self.settings['logger'].critical("Error when trying to append a new process, try using less parallel threads. Just in case, check also if there are too many processes running in the background.")
sys.exit()
for x in range(0, len(process)):
process[x].join()
for x in range(0, len(process)):
process[x] = process[x].get_output()
# save the network results
if self.ids:
for x in range(0, len(self.ids)):
for z in range(0, len(process)):
if process[z]['testcaseid'] == self.ids[x][0] and process[z]['softwareid'] == self.ids[x][1]:
process[z]['network'] = self.ids[x][2]
if self.ids[x][3] != None:
process[z]['stdout'] = self.ids[x][3]
if self.ids[x][4] != None:
process[z]['elapsed'] = self.ids[x][4]
if self.ids[x][5] != None:
process[z]['stderr'] = self.ids[x][5]
break
self.ids = []
self.settings['logger'].debug("Process: %s" % str(process))
return process
def get_input(self, piece, test):
"""Based on how the type, suffix and fuzzdata that were defined in the piece of software,
create a valid input file, url or as part of the CLI for the test"""
input = {}
input['testcaseid'] = test[0]
input['execute'] = []
input['data'] = []
# default values
data = ""
typeid = 0
for arg in piece['execute']:
if arg.startswith("-fuzzdata="):
randomstring = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in range(10))
data = compat.unicode(arg[len("-fuzzdata="):])
data = data.replace("[[test]]", test[1])
data = data.replace("canaryhost", self.settings['canaryhost'])
data = data.replace("[[softwareid]]", str(piece['softwareid']))
data = data.replace("[[randomstring]]", randomstring)
data = data.replace("[[testcaseid]]", str(input['testcaseid']))
input_type = piece['type'][typeid].lower()
if input_type in ['file', 'url']:
if 'suffix' not in piece:
piece['suffix'] = []
for suffixid in xrange(0, len(piece['type'])):
piece['suffix'].append("")
if 'filename' in piece and piece['filename'][0]:
fileid = os.open(piece['filename'][typeid], os.O_RDWR|os.O_CREAT)
datafile = []
datafile.append(fileid)
datafile.append(piece['filename'][typeid])
else:
datafile = tempfile.mkstemp(suffix=piece['suffix'][typeid], prefix=self.settings['tmp_prefix'] + str(test[0]) + "_", dir=self.settings['tmp_dir'])
input['data'].append({"data": data, "datafile": datafile})
if input_type == "file":
input['execute'].append(datafile[1])
elif input_type == "url":
input['execute'].append("http://" + self.settings['canaryhost'] + "/" + os.path.basename(datafile[1]))
elif input_type == 'stdin':
input['stdin'] = data
else:
input['execute'].append(data) # cli
typeid += 1
else:
input['execute'].append(arg)
for id in xrange(0, len(input['data'])):
for id2 in xrange(0, len(input['data'])):
input['data'][id]['data'] = input['data'][id]['data'].replace("[[file" + str(id2) + "]]", os.path.basename(input['data'][id2]['datafile'][1]))
if 'canaryhost' in self.settings:
input['data'][id]['data'] = input['data'][id]['data'].replace("[[url" + str(id2) + "]]", "http://" + self.settings['canaryhost'] + "/" + os.path.basename(input['data'][id2]['datafile'][1]))
os.write(input['data'][id]['datafile'][0], input['data'][id]['data'].encode('utf8'))
os.close(input['data'][id]['datafile'][0])
return input
def generate_tests(self, latest_id, limit):
"""Generate random tests using functions as an input and values as random entry points"""
if 'generate_tests' not in self.settings:
self.settings["logger"].error("Generate test option not defined")
elif self.settings['generate_tests'] > 5 or self.settings['generate_tests'] < 0:
self.settings["logger"].error("Option for random tests not available")
elif not isinstance(latest_id, int):
self.settings["logger"].error("The latest id should be an int")
elif not isinstance(limit, int):
self.settings["logger"].error("The limit should be an int")
else:
values = self.settings['db'].get_values()
if not values:
self.settings["logger"].error("No values detected, you require at least 1 value in the table 'value'. For example: ./xdiff_dbaction.py -d %s -t value -i canaryfile", self.settings['db_file'])
else:
functions = self.settings['db'].get_functions()
if not functions:
self.settings["logger"].error("No functions detected, you require at least 1 value in the table 'function'. For example: ./xdiff_dbaction.py -d %s -t function -i [[test]]", self.settings['db_file'])
else:
self.settings['logger'].info("Testcases being generated")
count = 0
while count < (limit * self.settings['generate_multiplier']): # add more tests than necessary
for value in values:
stdout = [] # where the new random values will be stored
if self.settings['generate_tests'] in [0, 1, 2, 3]: # radamsa
if not find_executable("radamsa"):
self.settings["logger"].error("Radamsa not found within PATH")
sys.exit()
input_value = tempfile.mkstemp(suffix="File", prefix=self.settings['tmp_prefix'] + "mutate_", dir=self.settings['tmp_dir'])
if self.settings['generate_tests'] in [0, 2]: # add a newline to speed up the generation process
os.write(input_value[0], value[0] + "\n")
repeat = 1
input_count = limit
else:
os.write(input_value[0], value[0])
repeat = limit
input_count = 1
os.close(input_value[0])
for x in range(0, repeat):
stdout.append(self.execute_shell("radamsa -n " + str(input_count) + " " + input_value[1]))
os.unlink(input_value[1])
if self.settings['generate_tests'] in [0, 1, 4, 5]: # zzuf
if not find_executable("zzuf"):
self.settings["logger"].error("Zzuf not found within PATH")
sys.exit()
input_value = tempfile.mkstemp(suffix="File", prefix=self.settings['tmp_prefix'] + "mutate_", dir=self.settings['tmp_dir'])
if self.settings['generate_tests'] in [0, 4]: # add a newline to speed up the generation process
os.write(input_value[0], "\n".join([value[0]] * limit))
repeat = 1
else:
os.write(input_value[0], value[0])
repeat = limit
os.close(input_value[0])
for x in range(0, repeat):
stdout.append(self.execute_shell("zzuf -r" + str(random.uniform(0.01, 0.03)) + " -s" + str(latest_id + repeat + x) + " <" + input_value[1])) # zzuf -s 1<file, without a space before the '<' sign, causes a crash :)
os.unlink(input_value[1])
if self.settings['generate_tests'] in [0, 2, 4]:
stdout = '\n'.join(str(x) for x in stdout).split('\n')
for x in range(0, len(stdout)):
stdout[x] = compat.unicode(stdout[x], errors='ignore')
# uncommenting the next line may crash python depending on the values :P
# print "values:",stdout
count += self.settings['dbaction'].permute(functions, stdout)
self.settings['logger'].debug("Testcases generated: %s" % str(count))
def execute_shell(self, cmd):
"""Execute a fuzzer generator within a shell context"""
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = p.communicate()
return stdout
================================================
FILE: classes/monitor.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
import ctypes
import os.path
import shutil
import socket
import subprocess
import sys
try:
from urllib2 import urlopen # python 2
from urllib2 import HTTPError
from urllib2 import URLError
except ImportError:
from urllib.request import urlopen # python 3
from urllib.error import HTTPError
from urllib.error import URLError
class Monitor(object):
"""Checks that everything is looking good before the fuzzer stats, and while the fuzzer operates"""
def __init__(self, settings):
"""Execute all the checks within this class to verify that canarys have been properly set up in the testcases"""
self.settings = settings
def check_once(self):
"""Check only once"""
self.check_canary_references(self.settings['canaryfile'])
self.check_canary_references("canaryhost")
self.check_canary_web(self.settings['canaryhost'], self.settings['canaryfile'], self.settings['canaryfileremote'])
self.check_canary_command(self.settings['canaryexec'], self.settings['canaryexectoken'])
self.check_ulimit()
self.check()
return None
def check(self):
"""Check on each loop the canary file and the free space"""
self.remove_stuff()
status = self.check_canary_file(self.settings['tmp_dir'] + self.settings['canaryfile'], self.settings['canaryfiletoken'])
status += self.check_free_space()
return status
def remove_stuff(self):
"""Remove files that may affect the behaviour"""
# delete specific files
if sys.platform == "linux2":
try:
os.remove(os.getenv("HOME") + '.hhvm.hhbc') # hhvm may fill up the disk with temp stuff
except:
pass
# delete all tmp_dir files
for root, dirs, files in os.walk(self.settings['tmp_dir']):
for f in files:
try:
if os.path.isfile(os.path.join(root, f)):
os.unlink(os.path.join(root, f))
except:
pass
for d in dirs:
try:
if os.path.isdir(os.path.join(root, d)):
shutil.rmtree(os.path.join(root, d))
except:
pass
def check_canary_file(self, filename, token):
"""Check if the file exists and its contents are equal to the token"""
status = None
if not isinstance(filename, str):
self.settings['logger'].error("Filename is not a string")
elif not isinstance(token, str):
self.settings['logger'].error("Token is not a string")
else:
if os.path.isfile(filename):
try:
token_file = open(filename, 'r')
except:
self.settings['logger'].debug("CanaryFile could not be open, changing its permissions")
os.chmod(filename, 0o644)
token_file = open(filename, 'r')
tmptoken = token_file.read().strip()
token_file.close()
if tmptoken == token:
return 1
else:
self.settings['logger'].debug("CanaryFile token differs, creating a new one")
else:
self.settings['logger'].debug("CanaryFile %s not found, creating a new one", str(filename))
status = self.create_canary_file(filename, token)
return status
def create_canary_file(self, filename, token):
"""Create a text file with a certain token"""
status = None
if not isinstance(filename, str):
self.settings['logger'].error("Filename is not a string")
elif not isinstance(token, str):
self.settings['logger'].error("Token is not a string")
else:
canary_file = open(filename, 'w')
canary_file.write(token)
canary_file.close()
self.settings['logger'].debug("CanaryFile created")
status = True
return status
def check_canary_web(self, hostname, filename, token):
"""Check if the hostname exists, that is possible to retrieve the filename and the contents are equal to the token"""
status = None
if not isinstance(hostname, str):
self.settings['logger'].error("Hostname is not a string")
elif not isinstance(filename, str):
self.settings['logger'].error("Filename is not a string")
elif not isinstance(token, str):
self.settings['logger'].error("Token is not a string")
else:
url = "http://" + hostname + "/" + filename + "?monitor"
try:
response = urlopen("http://" + hostname + "/" + filename + "?monitor", timeout=5)
data = response.read().strip()
if data == token:
status = True
else:
self.settings['logger'].warning("CanaryWeb token mismatch: expected %s and received %s", token, data)
status = False
except socket.error:
self.settings['logger'].warning("CanaryWeb Hostname %s not found", str(hostname))
status = False
except HTTPError:
self.settings['logger'].warning("CanaryWeb Filename %s not found: %s", str(filename), url)
status = False
except URLError:
self.settings['logger'].warning("CanaryWeb may not work, network is unreachable")
status = False
return status
def check_canary_command(self, command, token):
"""Check that the command can be executed and returns the expected token"""
stdout = None
found = None
try:
stdout, stderr = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
except Exception as e:
self.settings['logger'].warning("CanaryCommand %s not found: %s", str(command), str(e))
if stdout:
found = True
if token not in stdout.strip():
self.settings['logger'].warning("CanaryCommand token (%s) differs: '%s'", token, str(stdout.strip()))
found = False
return found
def check_canary_references(self, reference):
"""Check if the reference is on any of the testcases of the database"""
found = 1
if self.settings['db'].count_reference(reference) == 0:
self.settings['logger'].warning("CanaryReferences were not found in the db for the string: %s", str(reference))
found = 0
return found
def check_free_space(self):
"""Check if the there are more than Xmb free"""
if sys.platform == "win32":
free_bytes = ctypes.c_ulong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p("."), None, None, ctypes.pointer(free_bytes))
free_mb = free_bytes.value / 1024 / 1024
else:
stat = os.statvfs('.')
free_mb = stat.f_bfree * stat.f_frsize / 1024 / 1024
if free_mb <= self.settings['lowerlimit']:
self.settings['logger'].critical("There is not enough space on the device. The current free disk space in gigabytes is: %s", str(stat.f_bfree * stat.f_frsize / 1024 / 1024))
sys.exit()
return 1
def check_ulimit(self):
"""Check that the command can be executed and returns the expected token"""
if sys.platform != "win32":
minimum = 1024
try:
stdout, stderr = subprocess.Popen(["ulimit", "-n"], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
except:
self.settings['logger'].debug("ulimit check did not work")
return 0
if int(stdout.strip()) < minimum:
self.settings['logger'].critical("ulimit is too low (%s), you must raise ulimit (`ulimit -n %s`)", str(stdout.strip()), str(minimum))
sys.exit()
return 1
================================================
FILE: classes/queue.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import
from .fuzzer import Fuzzer
from .webserver import WebServer
class Queue(Fuzzer, WebServer):
"""Used to share information between executions and the webserver"""
def __init__(self, settings):
self.ids = []
Fuzzer.__init__(self, settings, self.ids)
WebServer.__init__(self, settings)
================================================
FILE: classes/settings.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
from __future__ import absolute_import
import getpass
import logging
import os
import random
import sys
from xdiff_dbaction import Dbaction
from .queue import Queue
from .dbsqlite import DbSqlite
from .monitor import Monitor
def define_software(settings):
"""The software gets loaded in a dictionary"""
software = []
if "software" in settings and settings['software'] and "fuzz_category" in settings and settings['fuzz_category']:
Category = None
if os.path.isfile(settings['software']):
software_file = open(settings['software'], "r")
for line in software_file:
line = line.strip()
if line[:1] != "#": # parse lines that are not comments
if line[:1] == "[" and line[len(line) - 1:len(line)] == "]": # is this a category?
Category = line[1:len(line) - 1]
Type = None
Suffix = None
Filename = None
OS = []
if Category == settings['fuzz_category']:
if line[:2] == "OS" or line[:4] == "Type" or line[:6] == "Suffix" or line[:8] == "Filename":
exec(line)
if OS is not None and sys.platform not in OS:
OS = None
else:
if line.find('=') != -1 and OS is not None:
if Type is None:
Type = ["CLI"]
if Suffix is None:
Suffix = [""]
if Filename is None:
Filename = [""]
item = {}
item['category'] = Category
item['type'] = Type
item['suffix'] = Suffix
item['filename'] = Filename
item['name'] = line[:line.find('=')].strip()
if 'valgrind' in settings and settings['valgrind']:
item['execute'] = eval('["valgrind", "-q", ' + line[line.find('=') + 1:].strip()[1:])
else:
item['execute'] = eval(line[line.find('=') + 1:].strip())
item['softwareid'] = settings['db'].get_software_id(item)
if item['softwareid']:
settings['logger'].debug("Software found: %s", str(item))
software.append(item)
software_file.close()
else:
settings['logger'].error("The settings file %s does not exist", os.path.abspath(settings['software']))
return software
def set_logger(settings):
"""Insantiate the logging functionality"""
logging.basicConfig(filename='fuzz.log', level=logging.INFO, format='%(asctime)s %(levelname)s %(module)s: %(message)s', datefmt='%Y-%m-%d %H.%M.%S')
console = logging.StreamHandler()
console.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(module)s: %(message)s'))
logger = logging.getLogger('fuzzer')
logger.addHandler(console)
if 'loglevel' in settings and settings['loglevel'] == 'debug':
logger.setLevel(logging.DEBUG)
elif 'loglevel' in settings and settings['loglevel'] == 'critical':
logger.setLevel(logging.CRITICAL)
return logger
def load_settings(settings):
"""Define global settings"""
settings['logger'] = set_logger(settings)
# Run
settings['version'] = "1.2.0 (HITB Edition)"
settings['soft_limit'] = 250 # maximum limit for the output of stdout & stderr
settings['soft_bypass'] = ["canarytoken", getpass.getuser(), "root", "/usr", "/bin", "PATH", "== "] # exceptions for the soft_limit setting
settings['hard_limit'] = 1024 # maximum hard limit, regardless of the soft_limit & soft_bypass
# settings['hard_limit_lines'] = 1 # maximum line limit in the output
settings['tmp_prefix'] = "chkF_" # prefix for temporary files created
if sys.platform in ["darwin"]:
settings['tmp_dir'] = "/Volumes/ramdisk/"
settings['tmp_dir_howto'] = "diskutil erasevolume HFS+ 'ramdisk' `hdiutil attach -nomount ram://838860`"
elif sys.platform == "win32":
settings['tmp_dir'] = "X:\\"
settings['tmp_dir_howto'] = "imdisk -a -s 512M -m X: -p \"/fs:ntfs /q/y\"; notepad \"C:\Windows\System32\canaryfile.bat\": @echo off; echo canarytokencommand"
elif sys.platform == "linux2" or sys.platform == "freebsd11":
settings['tmp_dir'] = "/mnt/ramdisk/"
settings['tmp_dir_howto'] = "mkdir /mnt/ramdisk; mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk; echo \"tmpfs /mnt/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=512M 0 0\" >> /etc/fstab"
settings['webserver_port'] = random.randrange(10000, 65535) # dynamic web server port: crashes in the same port may interfere
# settings['webserver_port'] = 8000 # sometimes you just need a fixed value
if "db_file" not in settings:
settings["db_file"] = None
settings['db'] = DbSqlite(settings, settings['db_file'])
if settings['db'].db_connection:
settings['kill_status'] = {"not_killed": settings['db'].get_constant_value("kill_status", "not killed"), "requested": settings['db'].get_constant_value("kill_status", "requested"), "killed": settings['db'].get_constant_value("kill_status", "killed"), "not_found": settings['db'].get_constant_value("kill_status", "not found")}
if "db_tests" not in settings:
settings['db_tests'] = 100 # save the results in the database every X tests
if "software" not in settings:
settings['software'] = os.path.abspath("software.ini") # software definitions
if "timeout" not in settings:
settings['timeout'] = 10 # default timeout for threads in seconds
settings['software'] = define_software(settings) # load the software and find potential inconsistencies
settings['queue'] = Queue(settings) # prepare the fuzzer and the webserver to interact
settings['monitor'] = Monitor(settings) # instantiate the monitor object
settings['dbaction'] = Dbaction(settings) # instantiate the dbaction object
# Fuzzer
if "generate_multiplier" not in settings:
settings['generate_multiplier'] = 100 # multiply the testcase limit by this number to generate new test cases
# Monitor
settings['lowerlimit'] = 200 # minimum free space in megabytes
settings['canaryfile'] = "canaryfile"
settings['canaryfiletoken'] = "canarytokenfilelocal" # contents of settings['canaryfile']
settings['canaryexec'] = "canaryfile"
settings['canaryexectoken'] = "canarytokencommand" # contents of settings['canaryexec']
settings['canaryhost'] = "127.0.0.1:" + str(settings['webserver_port'])
settings['canaryfileremote'] = "canarytokenfileremote"
# Analyze
settings['output_width'] = 130
settings['testcase_limit'] = 200 # a low number will help with RAM comsumption when performing queries against big databases
if "output_type" not in settings:
settings["output_type"] = "html" # default output type
settings["print_risk"] = False # print the risk?
if "minimum_risk" not in settings:
settings["minimum_risk"] = 0 # defaul minimum risk
settings["max_results"] = 999999999 # ridiculous high number to get all the occurrences of a function
if settings['db_file']:
settings['output_file'] = settings['db_file'] + "." + settings['output_type']
settings['error_disclosure'] = ["Exception", "stack trace", "core dump", "egmentation fault", "Traceback"]
settings['soft_bypass'].extend(settings['error_disclosure'])
return settings
================================================
FILE: classes/webserver.py
================================================
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
import threading
import os.path
import compat
try: # Python 2
from SimpleHTTPServer import SimpleHTTPRequestHandler
import BaseHTTPServer
import urlparse
except ImportError: # Python 3
from http.server import SimpleHTTPRequestHandler
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
class BaseHandler(SimpleHTTPRequestHandler):
"""Changes a few things from SimpleHTTPServer to handle requests"""
my_class = None # type:BaseHandler
def log_message(self, format, *args):
"""Avoid SimpleHTTPServer logs"""
pass
def do_GET(self):
"""Handle GET requests to parse parameters and save the responses to the corresponding ids"""
# self.my_class.settings['logger'].debug("URL: %s Query: %s", str(url), str(query))
data = compat.unicode("GET " + str(self.path) + "\n" + str(self.headers), errors='ignore')
self.do_REQUEST(data)
def do_POST(self):
"""Handle GET requests to parse parameters and save the responses to the corresponding ids"""
# self.my_class.settings['logger'].debug("URL: %s Query: %s", str(url), str(query))
data = compat.unicode("POST " + str(self.path) + "\n" + str(self.headers), errors='ignore')
self.do_REQUEST(data)
def do_REQUEST(self, data):
"""Handle GET and POST requests to parse parameters and save the responses to the corresponding ids"""
url = urlparse.urlparse(self.path)
query = url.query.split('&')
self.my_class.settings['logger'].debug("%s", data)
if len(query) > 1:
# with tag0 we can identify the testcaseid
tag0 = query[0].split("=")
# with tag1 we can identify the softwareid
tag1 = query[1].split("=")
if tag0[0] == "tag0" and tag1[0] == "tag1":
testcaseid = None
softwareid = None
try:
testcaseid = int(tag0[1])
except Exception as e:
self.my_class.settings['logger'].warning("Tag0 received, but is not a number: %s",e)
try:
softwareid = int(tag1[1])
except Exception as e:
self.my_class.settings['logger'].warning("Tag1 received, but is not a number: %s",e)
# if we found a testcaseid and a software id, we can correlate it to the results
if testcaseid and softwareid:
# we don't want dupes, check if the request hasn't been issued before
flag = False
for x in range(0, len(self.my_class.ids)):
if self.my_class.ids[x][0] == testcaseid and self.my_class.ids[x][1] == softwareid and self.my_class.ids[x][2] == data:
flag = True
break
if not flag:
# can we extract the stdout and elapsed data from the url?
stdout = None
elapsed = None
stderr = None
for parameter in query:
parameter = parameter.split('=')
if len(parameter) == 2:
if parameter[0] == 'stdout':
stdout = parameter[1]
elif parameter[0] == 'elapsed':
elapsed = parameter[1]
elif parameter[0] == 'stderr':
stderr = parameter[1]
self.my_class.ids.append([testcaseid, softwareid, data, stdout, elapsed, stderr])
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
getfile = url[2][1:].split('?')[0]
if url.path == "/canaryfile":
self.wfile.write(self.my_class.settings['canaryfileremote'])
elif os.path.isfile(getfile):
content = open(getfile, "r")
self.wfile.write(content.read())
class WebServer(object):
"""Used to parse HTTP connections"""
def __init__(self, settings):
self.settings = settings
self.server = None
def start_web_server(self):
"""Web server: load simplehttpserver as a thread and continue execution"""
BaseHandler.my_class = self
self.server = BaseHTTPServer.HTTPServer(("127.0.0.1", self.settings['webserver_port']), BaseHandler)
thread = threading.Thread(target=self.server.serve_forever)
thread.daemon = True
self.settings['logger'].debug("Loading web server using port %s" % str(self.settings['webserver_port']))
try:
thread.start()
except KeyboardInterrupt:
self.stop_web_server()
def stop_web_server(self):
"""Web server shutdown when closing the fuzzer"""
if self.server:
self.settings['logger'].debug("Shutting down Web Server...")
self.server.shutdown()
================================================
FILE: docs/1.-Install.md
================================================
Follwing are the instructions on how to execute XDiFF in:
* [Linux](#Linux)
* [OSX](#OSX)
* [Freebsd](#Freebsd)
* [Windows](#Windows)
---
## <a name="Linux"></a>Linux (Ubuntu/Debian)
1. Install some utilities as root:
```
apt update; apt -y install python2.7 gcc make git sqlite3 wget
```
2. Download the latest copy of XDiFF:
```
git clone https://github.com/IOActive/XDiFF.git; cd XDiFF
```
3. Install some input fuzzers (minimum 1gb of RAM required) as root:
```
git clone https://github.com/aoh/radamsa.git; cd radamsa; make OFLAGS=-O1; make install; cd ..; rm -r radamsa/
wget https://github.com/samhocevar/zzuf/releases/download/v0.15/zzuf-0.15.tar.bz2; tar -xf zzuf-0.15.tar.bz2; cd zzuf-0.15/; ./configure; make; make install; cd ..; rm -r zzuf-0.15.tar.bz2 zzuf-0.15/
```
4. Create a ramdisk where files will be created as root:
```
mkdir /mnt/ramdisk; mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk; echo "tmpfs /mnt/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=512M 0 0" >> /etc/fstab
```
5. Point the host *canaryhost* to *localhost* as root:
```
echo "127.0.0.1 canaryhost"|tee -a /etc/hosts
```
6. Create the *canarycommand*:
```
echo '#!/bin/sh'>/usr/local/bin/canaryfile.bat; echo 'echo canarytokencommand'>>/usr/local/bin/canaryfile.bat; chmod +x /usr/local/bin/canaryfile.bat; cp /usr/local/bin/canaryfile.bat /usr/local/bin/canaryfile
```
---
## <a name="OSX"></a>OSX
1. Install some utilities. The following utilies are installed using brew, if you don't have it you can install it by executing ```/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"```:
```
brew install git wget
```
2. Download the latest copy of XDiFF:
```
git clone https://github.com/IOActive/XDiFF.git; cd XDiFF
```
3. Install some input fuzzers (minimum 1gb of RAM required):
```
git clone https://github.com/aoh/radamsa.git; cd radamsa; make OFLAGS=-O1; sudo cp bin/radamsa /usr/local/bin/; cd ..
wget https://github.com/samhocevar/zzuf/releases/download/v0.15/zzuf-0.15.tar.bz2; tar -xf zzuf-0.15.tar.bz2; cd zzuf-0.15/; ./configure; make; make install; cd ..; rm -r zzuf-0.15.tar.bz2 zzuf-0.15/
```
4. Create a ramdisk where files will be created:
```
diskutil erasevolume HFS+ 'ramdisk' `hdiutil attach -nomount ram://838860`
```
5. Point the host *canaryhost* to *localhost*:
```
echo "127.0.0.1 canaryhost"|sudo tee -a /etc/hosts
```
6. Create the *canarycommand*:
```
echo '#!/bin/sh'>/usr/local/bin/canaryfile.bat; echo 'echo canarytokencommand'>>/usr/local/bin/canaryfile.bat; chmod +x /usr/local/bin/canaryfile.bat; cp /usr/local/bin/canaryfile.bat /usr/local/bin/canaryfile
```
7. Raise the ulimit
```
ulimit -n 1024
```
---
## <a name="Freebsd"></a>Freebsd
1. Install some utilities:
```
pkg install git wget py27-sqlite3
```
2. Download the latest copy of XDiFF:
```
git clone https://github.com/IOActive/XDiFF.git; cd XDiFF
```
3. Install some input fuzzers (minimum 1gb of RAM required):
```
git clone https://github.com/aoh/radamsa.git; cd radamsa; make OFLAGS=-O1; sudo make install; cd ..; rm -r radamsa/
```
Pending: Zzuf compile options
4. Create a ramdisk where files will be created:
```
sudo mkdir /mnt/ramdisk; sudo mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk; sudo echo "tmpfs /mnt/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=512M 0 0" >> /etc/fstab
```
5. Point the host *canaryhost* to *localhost*:
```
echo "127.0.0.1 canaryhost" | sudo tee -a /etc/hosts
```
6. Create the *canarycommand*:
```
echo '#\!/bin/sh' > /usr/local/bin/canaryfile.bat ; echo 'echo canarytokencommand' >> /usr/local/bin/canaryfile.bat ; chmod +x /usr/local/bin/canaryfile.bat ; cp /usr/local/bin/canaryfile.bat /usr/local/bin/canaryfile
```
---
## <a name="Windows"></a>Windows
1. Download and install some utilities:
```
Python 2.7: https://www.python.org/ftp/python/2.7.14/python-2.7.14.amd64.msi
IMDisk: https://sourceforge.net/projects/imdisk-toolkit/files/latest/download
```
2. Download the latest copy of XDiFF:
```
https://github.com/IOActive/XDiFF/archive/master.zip
```
3. Download some input fuzzers. For Radamsa, download and put within your PATH the .dll and the .exe:
```
https://github.com/vah13/radamsa/releases
```
4. Create a ramdisk where files will be created:
```
imdisk -a -s 512M -m X: -p \"/fs:ntfs /q/y\"
```
Then, format the ram disk once the Windows pop up appears
5. Point the host *canaryhost* to *localhost*. Right click on startup -> Command Prompt (Admin):
```
echo 127.0.0.1 canaryhost >> C:\Windows\System32\drivers\etc\hosts
```
6. Create the *canarycommand*. Right click on startup -> Command Prompt (Admin):
```
echo @echo off > C:\Windows\System32\canaryfile.bat & echo.echo canarytokencommand >> C:\Windows\System32\canaryfile.bat
```
---
# What's next?
You want to define [the input](https://github.com/IOActive/XDiFF/wiki/2.-The-input)
================================================
FILE: docs/2.-The-input.md
================================================
# Why do I want to use a database?
A database allows you to compare the results of how the software was executed when using different inputs, versions, implementations or operating systems. All the test cases to be evaluated are contained in one place and any issues found across multiple scenarios will be detected. Not only there is value on exploiting the vulnerabilities with the higher risk, but also the ones that affect multiple pieces of software at the same time. The performance and capabilities of SQLite for the fuzzer were proven to be better than MySQL and Redis.
## How's the database structure?
The initial analysis of the database was constructed around how to fuzz programming languages. They allow you to create any piece of software, so they will have access to all the functionalities. With this in mind, this is the basic look of a plain SQLite database used by XDiFF:
<pre>
# sqlite3 dbs/plain.sqlite
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> .tables
<b>function</b> <b>fuzz_software</b> <b>fuzz_testcase_result</b>
<b>fuzz_constants</b> <b>fuzz_testcase</b> <b>value</b>
</pre>
There are two tables where you may want to manually insert or edit some values:
* **value**: contains the items that will replace the ```[[test]]``` values in *function*. If you don't have a 'function', you can use the values in here with input fuzzers.
* **function**: contains what you want to fuzz. There is a special keyword ```[[test]]``` that gets replaced by the values contained in **value**. For example, if you would like to fuzz the print() function, you would normally want to have in here ```print([[test]])```.
The tables that start with 'fuzz_' are generated by XDiFF:
* **fuzz_testcase**: contains the combination of *function* and *value*
* **fuzz_software**: contains the software defined in *software.ini*
* **fuzz_testcase_result**: contains the result of executing the software defined in *fuzz_software* with the input defined in *fuzz_testcase*
* **fuzz_constants**: contains internal constant values used by the fuzzer
## Grab a sample database
Let's grab a copy of the plain.sqlite database:
```
cp dbs/plain.sqlite shells.sqlite
```
## Insert testcases
Data can be inserted in the database using a ***sqlite3*** parser or using the ***xdiff_dbaction.py*** script. In case your test case/s are in a file, you may want to insert it directly into the database like this for example:
<pre>
echo "insert into value values (readfile('<b>sample_file</b>'))"|sqlite3 <b>shells.sqlite</b>
</pre>
## Insert combinations of functions/values
If you have a certain function (or portion of code) that you want to fuzz with certain values, you can insert first the functions into the database:
<pre>
./xdiff_dbaction.py -d <b>shells.sqlite</b> -t function -i "<b>foo([[test]])</b>"
</pre>
Insert the values that you want to use to fuzz the piece of code within the function table:
<pre>
./xdiff_dbaction.py -d <b>shells.sqlite</b> -t value -i "<b>bar</b>"
</pre>
Then you can generate the permutations:
<pre>
./xdiff_dbaction.py -d <b>shells.sqlite</b> -g 1
2017-11-20 22:06:24,901 INFO dbaction: Values: 1 - Functions: 1
2017-11-20 22:06:24,901 INFO dbaction: <b>Testcases generated: 1</b>
2017-11-20 22:06:24,902 INFO dbaction: Time required: 0.0 seconds
</pre>
You can later confirm how the information everything looks like:
<pre>
./xdiff_dbaction.py -d <b>shells.sqlite</b> -t <b>fuzz_testcase</b> -p
----------------------------------------------------------------------------------------------------------
| fuzz_testcase (1 rows) |
----------------------------------------------------------------------------------------------------------
| id | testcase |
----------------------------------------------------------------------------------------------------------
| 1 | <b>foo(bar)</b> |
----------------------------------------------------------------------------------------------------------
</pre>
## Extending the detection
Part of the install process required to create a command named ```canaryfile``` (and ```canaryfile.bat```). When this file gets executed, it produces a specific output that can be later analyzed. Basically, you want the string ```canaryfile``` as part of your values.
Moreover, if the software may open network connections, you also want to define the ```canaryhost``` as part of the potential values to be used. The connections will be detected locally and be included as part of the output to be analyzed.
# What's next?
You want to define [the software](https://github.com/IOActive/XDiFF/wiki/3.-The-software)
================================================
FILE: docs/3.-The-software.md
================================================
In here you will find information about how to define pieces of software in the file *software.ini*.
This defines pieces of data in three columns:
1. The first column defines the software category between brackets. Lets suppose that you want to fuzz command shells, so we can name the software category ***shells***.
```javascript
[shells]
```
2. The second column has four predefined possibilities:
2.1. **Type**: how the information is going to be read by the programs. By default if you don't specify anything is going to be ```CLI```, which means that the input to be fuzzed is grabbed from the command line. Another possibility is ```File```, which means that the contents of what's going to be fuzzed will be written into a file first. Moreover, whenever you're fuzzing files, you may want to specify what is the suffix of that file (please see below in 2.3). Finally, one last possibility for the input is ```Stdin```, as you would use it when piping information to another program.
2.2. **OS**: it could either be ***darwin***, ***linux2***, ***freebsd11*** or ***win32***
2.3. **Suffix**: the suffix used for files when the input type is set to ```File```. We can easily fuzz command shells without files and suffixes, but to illustrate the point let's use them:
<pre>
<b>Type</b> = ["File"]
<b>OS</b> = ["darwin", "linux2", "freebsd11"]
<b>Suffix</b> = [".sh"]</pre>
2.4. **Filename**: if the software to be fuzzed reads information from a certain static filename, you can define it in here. Don't forget to run the fuzzer with only 1 thread when using this.
3. The third column defines the pieces of software to be fuzzed. If you want to fuzz mp3 files using mpg321 and mpg123, you can do it like this:
<pre>
Bash = ["bash", "-c", "<b>-fuzzdata=echo $(([[test]]))</b>"]
Ksh = ["ksh", "-c", "<b>-fuzzdata=echo $(([[test]]))</b>"]</pre>
First we set the name of the software to be fuzzed (***bash***, ***dash***, or ***ksh***). Then, we defined in an array the command and options to be executed. There is a special option named *-fuzzdata=* that indicates the fuzzer that the next piece of information is where we will be placed our fuzzed test case. The *[[test]]* will be replaced by a temporary file name containing a weird mp3 to fuzz the software on this example.
### Putting all the pieces together
This is how you could define the software category ***shells*** to be fuzzed using the ***CLI***:
```
# Sample fuzzing of shells
[shells]
OS = ["darwin", "linux2", "freebsd11"]
Bash = ["bash", "-c", "-fuzzdata=echo $(([[test]]))"]
Ksh = ["ksh", "-c", "-fuzzdata=echo $(([[test]]))"]
```
---
# What's next?
You want to [run the fuzzer](https://github.com/IOActive/XDiFF/wiki/4.-The-fuzzer)
================================================
FILE: docs/4.-The-fuzzer.md
================================================
## Fuzzing
The most basic execution requires defining which category and which database will be used:
```
./xdiff_run.py -c shells -d shells.sqlite
```
The output should look like this:

It includes a lot of debugging information, and the most important parts are marked. At the top is the execution, and at the bottom is the beginning of the execution along with the rate (you want this number to be as high as possible).
## Fuzzing using the input fuzzers
If you want to generate new test cases based on the currently defined test cases, you can use the input fuzzers that were installed as part of the install process.
```
./xdiff_run.py -c shells -d shells.sqlite -z 0
```
Now the output should indicate now and then when new inputs are being generated

## Additional fuzzing options:
There are three additional important optional settings to be mentioned:
- [*-D*]: Print debugging information
- [*-t 100*]: The amount of threads to be executed in parallel.
- [*-T 10*]: The timeout per thread
- [*-v*]: Use valgrind to execute the software to be fuzzed.
The combination of threads and the timeout is something to be defined per category. Fuzzing a shell requires no time, while compiling and fuzzing a java program takes much more time. Pay attention at the output produced to see if the software is being properly executed (or is getting mostly killed because the timeout is too low).
---
# What's next?
You want to analyze [the output](https://github.com/IOActive/XDiFF/wiki/5.-The-output)
================================================
FILE: docs/5.-The-output.md
================================================
## Analyzing the output
The most basic form of analyzing the output is running:
```
./xdiff_analyze.py -d shells.sqlite
```
A normal analysis output looks like this:

### HTML
The previous execution creates by default an HTML file named ```shells.sqlite.html``` that for this session looks like this on a web browser:

### Text
Another possibility is to output the analysis as text when using the ```-t txt``` option:

## The analytic functions
There are multiple analytic functions that can expose information from the database. The default function that gets executed is ```report```, which include 15 functions. Following is the whole list of function, and the ones in bold are already included as part of the ```report```:
- **```analyze_canary_file```**: Find canary filenames in the stdout or stderr, even though canary files were not part of the payload
- **```analyze_canary_token_code```**: Find canary tokens of code executed in the stdout or in the stderr
- **```analyze_canary_token_command```**: Find canary tokens of commands in the stdout or stderr
- **```analyze_canary_token_file```**: Find canary tokens of files in the stdout or in the stderr
- ```analyze_elapsed```: Analize which was the total time required for each piece of software
- ```analyze_error_disclosure```: Analyze errors disclosed in the output taken from settings['error_disclosure']
- ```analyze_file_disclosure_without_path```: Find the tmp_prefix in the stdout or stderr without the full path
- ```analyze_file_disclosure```: Find the tmp_prefix in the stdout or in the stderr
- ```analyze_killed_differences```: Find when one piece of software was killed AND another one was not killed for the same input
- **```analyze_output_messages```**: Analize which were the different output messages for each piece of software
- ```analyze_path_disclosure_without_file```: Find the tmp_dir in the stdout or stderr, even though the testcase did not have a temporary file
- ```analyze_path_disclosure_stdout```: Find the tmp_dir in the stdout
- ```analyze_path_disclosure_stderr```: Find the tmp_dir in the stderr
- **```analyze_remote_connection```**: Find remote connections made
- **```analyze_return_code_differences```**: Find when different return codes are received for the same input
- **```analyze_return_code_same_software_differences```**: Find when different return codes are received for the same software using different input forms
- **```analyze_return_code```**: Get the different return codes for each piece of software
- ```analyze_same_software```: Find when the same software produces different results when using different inputs
- ```analyze_same_stdout```: Finds different testcases that produce the same standard output
- ```analyze_specific_return_code```: Find specific return codes
- **```analyze_stdout```**: Find when different pieces of software produces different results
- ```analyze_top_elapsed_killed```: Find which killed tests cases required more time
- ```analyze_top_elapsed_not_killed```: Find which not killed tests cases required more time
- **```analyze_username_disclosure```**: Find when a specific username is disclosed in the stdout or in the stderr
- **```analyze_valgrind```**: Find Valgrind references in case it was used
- ```list_killed_results```: Print the killed fuzzing results
- **```list_results```**: Print the fuzzing results: valuable to see how the software worked with the testcases defined, without using any constrains
- **```list_software```**: Print the list of [active] software used with testcases from the database
- ```list_summary```: Print an quantitative information summary using all the analytic functions from this class
### Working with the analytic functions
Depending on what type of software you're fuzzing, it may be convenient to enable or disable certain functions. The best way is to modify the ```xdiff_analyze.py``` script to expose the information that we need.
For other scenarios, you may just want to expose the output of a single function. Let's suppose that you only care about the analytic function ```analyze_return_code``` to see how code behaves:
<pre>
./xdiff_analyze.py -d shells.sqlite -m <b>analyze_return_code</b> -o txt
</pre>
The previous command produces the following output:
```
----------------------------------------------------------------------------------------
| Analyze Different Return Codes per Software - analyze_return_code (5 rows) |
----------------------------------------------------------------------------------------
| Software | Type | OS | Return Code | Amount |
----------------------------------------------------------------------------------------
| Bash | CLI | darwin | 1 | 499 |
----------------------------------------------------------------------------------------
| Bash | CLI | darwin | 2 | 76 |
----------------------------------------------------------------------------------------
| Ksh | CLI | darwin | 0 | 73 |
----------------------------------------------------------------------------------------
| Ksh | CLI | darwin | 1 | 495 |
----------------------------------------------------------------------------------------
| Ksh | CLI | darwin | 3 | 7 |
----------------------------------------------------------------------------------------
```
================================================
FILE: docs/Changelog.md
================================================
# Changelog
Changes are listed in time order: newer changes are at the top, older changes are at the bottom.
## Version: [1.2.0](https://github.com/IOActive/XDiFF/releases/tag/1.2)
- Changed main function names in the root directory
- Improved code, documentation, and (most of) the code is now tested. Tons of bugfixes.
- Added new analysis for error disclosure (analyze_error_disclosure) and path disclosure (analyze_path_disclosure_stderr)
- Added new compatibility class (classes.compat) to support Python 3
- Added risk value to the different analytic functions. Print functions based on their rating: ./xdiff_analyze.py -d db.sqlite -r 0/1/2/3
- Improved analysis of network connections to test browsers connections
- software.ini: added support to test non random filenames. Set on the second column: Filename = /etc/myfixedfilename
- Added -d for debug output
- Added new parameters in the settings.py class
#### Contributors:
- farnaboldi
## Version: [1.1.1](https://github.com/IOActive/XDiFF/releases/tag/1.1.1) (beta)
- Added support for Python 3 [[2]](https://github.com/IOActive/XDiFF/pull/2)
#### Contributors:
- cclauss
## Version: [1.1.0](https://github.com/IOActive/XDiFF/releases/tag/1.1.0)
- First public release for Blackhat Europe 2017
#### Contributors:
- farnaboldi
================================================
FILE: xdiff_analyze.py
================================================
#!/usr/bin/env python
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
from __future__ import print_function
import datetime
import getopt
import getpass
import inspect
import os
# import profile # uncomment here for benchmarking and at the bottom
import re
import sys
import time
import classes.settings
from classes.dump import Dump
try:
reload # Python 2
except NameError: # Python 3
from importlib import reload
class Analyze(object):
"""Analyzes the fuzzing information for abnormal behaviors"""
def __init__(self, settings):
reload(sys)
try:
sys.setdefaultencoding('utf8')
except:
pass # Python3
self.settings = settings
self.settings['tmp_dir'] = "ramdisk" # by using this, it will work on multiple directories (ie, /Volumes/ramdisk, /mnt/ramdisk, etc)
self.dump = Dump(self.settings)
self.count_results = None
def check_minimum_risk(self, function_risk, title):
"""Check if the function has the minum risk required"""
check = False
if self.settings['print_risk']:
print("Function: %s, Risk: %s, Title: %s" % (inspect.stack()[1][3], function_risk, title[:title.find(" - ")]))
elif function_risk >= self.settings['minimum_risk']:
check = True
return check
def dump_results(self, method, toplimit, extra):
"""Prints the output of an internal method"""
success = False
method_to_call = None
if self.settings['output_type'] not in ["txt", "csv", "xml", "html"]:
self.settings['logger'].error("Incorrect output type selected. Valid outputs: txt, csv, xml, html.")
else:
if method not in ['dump_results']:
try:
method_to_call = getattr(self, method)
except Exception as e:
self.settings['logger'].error("Error when executing the method %s: %s", method, e)
if method_to_call:
if method != "report":
self.settings["minimum_risk"] = 0 # set the minimum risk to 0
self.dump.set_toggle_table(False)
start_time = time.time()
self.settings['logger'].info("Dumping: database %s - method %s - output %s" % (self.settings['db_file'], method, self.settings['output_type']))
self.dump.pre_general(self.settings['output_type'])
if extra:
try:
method_to_call(self.settings['output_type'], toplimit, extra)
success = True
except Exception as e:
self.settings['logger'].error("Error executing the method '%s' with parameter '%s': %s", method, extra, e)
else:
try:
method_to_call(self.settings['output_type'], toplimit)
success = True
except Exception as e:
self.settings['logger'].error("Error executing the method '%s': %s", method, e)
if success:
self.dump.post_general(self.settings['output_type'])
size = ""
if 'output_file' in self.settings and os.path.isfile(self.settings['output_file']):
size = ", output file: " + self.settings['output_file'] + " (" + str(int(os.stat(self.settings['output_file']).st_size / 1024)) + " kb)"
elif 'output_file' in self.settings:
size = ". No information to be written into the output file."
finish_time = time.time() - start_time
self.settings['logger'].info("Time elapsed %s seconds%s" % (str(int(finish_time)), size))
return success
def report(self, output, toplimit):
"""Print several functions in the form of a report (useful for HTML)"""
# self.settings['db'].set_software(["9", "10"])
# self.list_summary(output, toplimit) # informational
self.list_software(output, self.settings["max_results"])
self.analyze_elapsed(output, toplimit) # informational
self.list_results(output, toplimit)
self.analyze_top_elapsed_killed(output, toplimit) # informational
self.analyze_top_elapsed_not_killed(output, toplimit) # informational
self.analyze_valgrind(output, toplimit)
self.analyze_username_disclosure(output, toplimit, username="root")
if getpass.getuser() != "root": # do not repeat the information if the root user was the one already used for the execution
self.analyze_username_disclosure(output, toplimit, username=getpass.getuser())
self.analyze_canary_token_file(output, toplimit)
self.analyze_canary_token_code(output, toplimit)
self.analyze_remote_connection(output, toplimit)
self.analyze_canary_token_command(output, toplimit)
self.analyze_canary_file(output, toplimit)
self.analyze_killed_differences(output, toplimit) # informational
self.analyze_return_code(output, toplimit)
self.analyze_specific_return_code(output, toplimit)
self.analyze_return_code_differences(output, toplimit)
self.analyze_return_code_same_software_differences(output, toplimit)
self.analyze_output_messages(output, toplimit, 'stderr')
self.analyze_output_messages(output, toplimit, 'stdout')
self.analyze_error_disclosure(output, toplimit)
self.analyze_same_software(output, toplimit) # low_risk
self.analyze_stdout(output, toplimit)
self.analyze_same_stdout(output, toplimit) # low_risk
self.analyze_file_disclosure(output, toplimit) # low_risk
self.analyze_file_disclosure_without_path(output, toplimit) # low_risk
self.analyze_path_disclosure_stdout(output, toplimit) # low_risk
self.analyze_path_disclosure_stderr(output, toplimit) # low_risk
self.analyze_path_disclosure_without_file(output, toplimit) # low_risk
def list_summary(self, output, toplimit):
"""Print an quantitative information summary using all the analytic functions from this class"""
title = "Summary for " + self.settings['db_file']
columns = ["Information", "Amount"]
function_risk = 0
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = len(self.list_software(None, self.settings["max_results"]))
rows.append([["Pieces of Software", str(results)]])
if self.count_results is None:
self.count_results = self.settings['db'].count_results(0, None)
rows.append([["Amount of Testcases", str(self.count_results)]])
rows.append([["Output Top Limit", str(toplimit)]])
results = len(self.analyze_valgrind(None, self.settings["max_results"]))
rows.append([["Valgrind References Found", str(results)]])
results = len(self.analyze_username_disclosure(None, self.settings["max_results"], "root"))
rows.append([["Username 'root' Disclosure", str(results)]])
results = len(self.analyze_username_disclosure(None, self.settings["max_results"], getpass.getuser()))
rows.append([["Username '" + getpass.getuser() + "' Disclosure", str(results)]])
results = len(self.analyze_canary_token_file(None, self.settings["max_results"]))
rows.append([["Canary Token File Found", str(results)]])
results = len(self.analyze_canary_token_code(None, self.settings["max_results"]))
rows.append([["Canary Token Code Found", str(results)]])
results = len(self.analyze_canary_token_command(None, self.settings["max_results"]))
rows.append([["Canary Token Command Found", str(results)]])
results = len(self.analyze_canary_file(None, self.settings["max_results"]))
rows.append([["Canary File Found", str(results)]])
results = len(self.analyze_top_elapsed_killed(None, self.settings["max_results"]))
rows.append([["Testcases Killed", str(results)]])
results = len(self.analyze_top_elapsed_not_killed(None, self.settings["max_results"]))
rows.append([["Testcases not Killed", str(results)]])
results = len(self.analyze_killed_differences(None, self.settings["max_results"]))
rows.append([["Software Killed and Not Killed", str(results)]])
results = len(self.analyze_return_code(None, self.settings["max_results"]))
rows.append([["Return Code", str(results)]])
results = len(self.analyze_return_code_differences(None, self.settings["max_results"]))
rows.append([["Return Code Differences", str(results)]])
results = len(self.analyze_return_code_same_software_differences(None, self.settings["max_results"]))
rows.append([["Return Code Same Software Differences", str(results)]])
results = len(self.analyze_same_software(None, self.settings["max_results"]))
rows.append([["Same Software having a Different Output", str(results)]])
results = len(self.analyze_stdout(None, self.settings["max_results"]))
rows.append([["Stdout for Different Results", str(results)]])
results = len(self.analyze_output_messages(None, self.settings["max_results"], 'stderr'))
rows.append([["Different Stderr Messages", str(results)]])
results = len(self.analyze_output_messages(None, self.settings["max_results"], 'stdout'))
rows.append([["Different Stdout Messages", str(results)]])
results = len(self.analyze_error_disclosure(None, self.settings["max_results"]))
rows.append([["Analyze Error Messages for exceptions", str(results)]])
results = len(self.analyze_same_stdout(None, self.settings["max_results"]))
rows.append([["Testcases that Produce the Same Stdout", str(results)]])
results = len(self.analyze_file_disclosure(None, self.settings["max_results"]))
rows.append([["Temp File Disclosure", str(results)]])
results = len(self.analyze_file_disclosure_without_path(None, self.settings["max_results"]))
rows.append([["Temp File Disclosure (without path)", str(results)]])
results = len(self.analyze_path_disclosure_stdout(None, self.settings["max_results"]))
rows.append([["Path Disclosure Stdout", str(results)]])
results = len(self.analyze_path_disclosure_stderr(None, self.settings["max_results"]))
rows.append([["Path Disclosure Stderr", str(results)]])
results = len(self.analyze_path_disclosure_without_file(None, self.settings["max_results"]))
rows.append([["Path Disclosure (without temp file)", str(results)]])
results = len(self.analyze_remote_connection(None, self.settings["max_results"]))
rows.append([["Remote Connections", str(results)]])
results = self.analyze_elapsed(None, self.settings["max_results"])
results = datetime.timedelta(seconds=round(results, 0))
rows.append([["Total Time Elapsed", str(results)]])
self.dump.general(output, title, columns, rows)
def list_software(self, output, toplimit):
"""Print the list of [active] software used with testcases from the database"""
title = "List Software Tested - list_software "
columns = ["ID", "Software", "Type", "OS"]
function_risk = 0
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].list_software()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([result])
self.dump.general(output, title, columns, rows)
return rows
def list_results(self, output, toplimit):
"""Print the fuzzing results: valuable to see how the software worked with the testcases defined, without using any constrains"""
lowerlimit = 0
title = "Analyze the Testcase Results from " + str(int(lowerlimit)) + " to " + str(lowerlimit + toplimit) + " - list_results"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr", "Kill"]
function_risk = 0
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
testcase = None
tmpoutput = []
results = self.settings['db'].list_results(lowerlimit, toplimit * len(self.list_software(None, self.settings["max_results"])))
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase is None:
testcase = result[0]
if testcase != result[0]:
testcase = result[0]
rows.append(tmpoutput)
tmpoutput = []
tmpoutput.append((result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5], result[6]))
if len(rows) < toplimit and tmpoutput:
rows.append(tmpoutput)
self.dump.general(output, title, columns, rows)
return rows
def analyze_valgrind(self, output, toplimit):
"""Find Valgrind references in case it was used"""
title = "Analyze Valgrind Output - analyze_valgrind"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr", "Return Code"]
function_risk = 2
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure("== ",)
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if result[5][:10].count('=') == 4: # Valgrind outputs can be detected because they have 4 equal signs in the first 10 characters
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5], result[6])])
self.dump.general(output, title, columns, rows)
return rows
def list_killed_results(self, output, toplimit):
"""Print the killed fuzzing results"""
title = "Analyze the Killed Testcase Results - list_killed_results"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr", "Kill"]
function_risk = 2
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
testcase = None
tmpoutput = []
results = self.settings['db'].list_killed_results()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase is None:
testcase = result[0]
if testcase != result[0]:
testcase = result[0]
rows.append(tmpoutput)
tmpoutput = []
tmpoutput.append((result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4][:500], result[5][:500], result[6]))
if len(rows) < toplimit and tmpoutput:
rows.append(tmpoutput)
self.dump.general(output, title, columns, rows)
return rows
def analyze_return_code(self, output, toplimit):
"""Get the different return codes for each piece of software"""
title = "Analyze Different Return Codes per Software - analyze_return_code"
columns = ["Software", "Type", "OS", "Return Code", "Amount"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].list_return_code_per_software()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0], result[1], result[2], result[3], result[4])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_specific_return_code(self, output, toplimit):
"""Find specific return codes"""
returncodes = ["-6", "-9", "-11", "-15"]
title = "Analyze Specific Return Codes: " + ",".join(returncodes) + " - analyze_specific_return_code"
columns = ["Testcase", "Software", "Type", "OS", "Returncode", "Stdout", "Stderr"]
function_risk = 2
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_specific_return_code(returncodes)
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5], result[6])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_return_code_same_software_differences(self, output, toplimit):
"""Find when different return codes are received for the same software using different input forms"""
title = "Analyze Return Code Same Software Differences - analyze_return_code_same_software_differences"
columns = ["Testcase", "Software", "Type", "Return Code", "Stdout", "Stderr"]
function_risk = 2
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
# First check if there is more than one type of input per software, and save the IDs
software_ids = []
software_name = ""
results = self.settings['db'].list_software()
for result in results:
if software_name == result[1]:
software_ids.append(str(result[0]))
else:
software_name = result[1]
rows = []
if software_ids:
original_ids = self.settings['db'].get_software()
self.settings['db'].set_software(software_ids) # restrict the ids
software = ""
software_returncode = ""
testcase = ""
outputtmp = []
results = self.settings['db'].analyze_return_code_differences()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase == result[0] and software == result[1] and software_returncode != result[3]:
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5]])
else:
if len(outputtmp) > 1:
rows.append(outputtmp)
outputtmp = []
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5]])
testcase = result[0]
software = result[1]
software_returncode = result[3]
self.settings['db'].set_software(original_ids)
self.dump.general(output, title, columns, rows)
return rows
def analyze_return_code_differences(self, output, toplimit):
"""Find when different return codes are received for the same input"""
title = "Analyze Return Code Differences - analyze_return_code_differences"
columns = ["Testcase", "Software", "Type", "Return Code", "Stdout", "Stderr"]
function_risk = 2
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
software_returncode = ""
testcase = ""
outputtmp = []
results = self.settings['db'].analyze_return_code_differences()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase == result[0] and software_returncode != result[3]:
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5]])
else:
if len(outputtmp) > 1:
rows.append(outputtmp)
outputtmp = []
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5]])
testcase = result[0]
software_returncode = result[3]
self.dump.general(output, title, columns, rows)
return rows
def analyze_username_disclosure(self, output, toplimit, username=None):
"""Find when a specific username is disclosed in the stdout or in the stderr"""
title = "Analyze Username Disclosure: " + username + " - analyze_username_disclosure"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if username is None:
print("Error: extra parameter username has not been defined")
help()
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure(username, excludeme=self.settings['tmp_prefix'])
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_error_disclosure(self, output, toplimit):
"""Find canary filenames in the stdout or stderr, even though canary files were not part of the payload"""
title = "Analyze Presence of Exceptions - analyze_error_disclosure"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
for error in self.settings['error_disclosure']:
results = self.settings['db'].analyze_string_disclosure(error)
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if result[0].find('canaryfile') == -1:
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_canary_file(self, output, toplimit):
"""Find canary filenames in the stdout or stderr, even though canary files were not part of the payload"""
title = "Analyze Presence of Canary Files - analyze_canary_file"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 3
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_canary_file()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if result[0].find('canaryfile') == -1:
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_canary_token_file(self, output, toplimit):
"""Find canary tokens of files in the stdout or in the stderr"""
title = "Analyze Presence of Canary Tokens File Local - analyze_canary_token_file"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 3
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure("canarytokenfile")
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_canary_token_code(self, output, toplimit):
"""Find canary tokens of code executed in the stdout or in the stderr"""
title = "Analyze Presence of Canary Tokens Code - analyze_canary_token_code"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 3
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure("canarytokencode")
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_canary_token_command(self, output, toplimit):
"""Find canary tokens of commands in the stdout or stderr"""
title = "Analyze Presence of Canary Tokens Command - analyze_canary_token_command"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 3
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure("canarytokencommand")
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_remote_connection(self, output, toplimit):
"""Find remote connections made"""
title = "Analyze Remote Connections - analyze_remote_connection"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr", "Network"]
function_risk = 3
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
testcase = ""
outputtmp = []
rows = []
results = self.settings['db'].analyze_remote_connection()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase != result[0] and outputtmp:
testcase = result[0]
rows.append(outputtmp)
outputtmp = []
outputtmp.append((result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5], result[6]))
if outputtmp:
rows.append(outputtmp)
self.dump.general(output, title, columns, rows)
return rows
def analyze_top_elapsed_killed(self, output, toplimit):
"""Find which killed tests cases required more time"""
title = "Analyze Top Time Elapsed (and eventually killed) - analyze_top_elapsed_killed"
columns = ["Testcase", "Software", "Type", "OS", "Elapsed"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_top_elapsed(True)
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_top_elapsed_not_killed(self, output, toplimit):
"""Find which not killed tests cases required more time"""
title = "Analyze Top Time Elapsed (but not killed) - analyze_top_elapsed_not_killed"
columns = ["Testcase", "Software", "Type", "OS", "Elapsed"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_top_elapsed(False)
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_killed_differences(self, output, toplimit):
"""Find when one piece of software was killed AND another one was not killed for the same input"""
title = "Analyze Killed Software vs Not Killed Software - analyze_killed_differences"
columns = ["Testcase", "Software", "Type", "OS", "Kill", "Stdout", "Stderr"]
function_risk = 2
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
testcase = kill_status = None
outputtmp = []
try:
results = self.settings['db'].analyze_killed_differences()
except:
print("Error when requesting the killed differences")
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase is None or testcase != result[0]:
testcase = result[0]
kill_status = result[4]
if testcase == result[0] and kill_status != result[4]:
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5], result[6]])
else:
if len(outputtmp) > 1:
rows.append(outputtmp)
outputtmp = []
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5], result[6]])
testcase = result[0]
kill_status = result[4]
self.dump.general(output, title, columns, rows)
return rows
def analyze_same_software(self, output, toplimit):
"""Find when the same software produces different results when using different inputs (ie, Node CLI vs Node File Input)"""
title = "Analyze Same Software having a Different Output - analyze_same_software"
columns = ["Testcase", "Software", "Type", "Stdout"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
# First check if there is more than one type of input per software, and save the IDs
software_ids = []
software_name = ""
results = self.settings['db'].list_software()
for result in results:
if software_name == result[1]:
software_ids.append(str(result[0]))
else:
software_name = result[1]
rows = []
if software_ids:
original_ids = self.settings['db'].get_software()
self.settings['db'].set_software(software_ids) # restrict the ids
software = ""
software_stdout = ""
testcase = ""
outputtmp = []
results = self.settings['db'].analyze_same_software()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase == result[0] and software == result[1] and software_stdout != result[3]:
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3]])
else:
if len(outputtmp) > 1:
rows.append(outputtmp)
outputtmp = []
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3]])
testcase = result[0]
software = result[1]
software_stdout = result[3]
if len(outputtmp) > 1:
rows.append(outputtmp)
self.dump.general(output, title, columns, rows)
self.settings['db'].set_software(original_ids)
return rows
def analyze_stdout(self, output, toplimit):
"""Find when different pieces of software produces different results (basic differential testing)"""
title = "Analyze Stdout for Different Results (Basic Differential Testing) - analyze_stdout"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "ID"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
testcase = ""
stdout = ""
tobeprinted = False
outputtmp = []
rows = []
lowerlimit = 0
upperlimit = 100000
while True:
results = self.settings['db'].analyze_stdout(lowerlimit, upperlimit)
if not results:
break
lowerlimit += 100000
upperlimit += 100000
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase != result[0]:
testcase = result[0]
stdout = result[3]
if outputtmp and tobeprinted:
rows.append(outputtmp)
tobeprinted = False
outputtmp = []
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[5], result[3], result[6]])
if stdout != result[3]:
tobeprinted = True
if outputtmp and tobeprinted and len(rows) < toplimit:
rows.append(outputtmp)
self.dump.general(output, title, columns, rows)
return rows
def analyze_same_stdout(self, output, toplimit):
"""Finds different testcases that produce the same standard output, but ignore the testcases where ALL the pieces of software match"""
title = "Analyze Testcases that Produce the Same Stdout - analyze_same_stdout"
columns = ["Testcase", "Software", "Type", "OS", "Stdout"]
function_risk = 0
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
testcase = ""
outputtmp = []
rows = []
countsoftware = self.settings['db'].count_software()
results = self.settings['db'].analyze_same_stdout()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if testcase != result[4]:
if outputtmp and len(outputtmp) != countsoftware:
rows.append(outputtmp)
outputtmp = []
testcase = result[4]
if not results or results[len(results) - 1][0] != result[0] or results[len(outputtmp) - 1][1] != result[1]:
outputtmp.append([result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4]])
#if outputtmp and len(outputtmp) != countsoftware and len(rows) < toplimit:
# rows.append(outputtmp)
self.dump.general(output, title, columns, rows)
return rows
def analyze_file_disclosure(self, output, toplimit):
"""Find the tmp_prefix in the stdout or in the stderr"""
title = "Analyze Temp File Disclosure (" + self.settings['tmp_prefix'] + ") - analyze_file_disclosure"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure(self.settings['tmp_prefix'])
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_file_disclosure_without_path(self, output, toplimit):
"""Find the tmp_prefix in the stdout or stderr without the full path"""
title = "Analyze Temp File Disclosure (" + self.settings['tmp_prefix'] + ") Without Path (" + self.settings['tmp_dir'] + ") - analyze_file_disclosure_without_path"
columns = ["Test", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure(self.settings['tmp_prefix'])
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if result[3].find(self.settings['tmp_dir']) == -1 and result[4].find(self.settings['tmp_dir']) == -1:
rows.append([(result[0], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_path_disclosure_stdout(self, output, toplimit):
"""Find the tmp_dir in the stdout or stderr"""
title = "Analyze Path Disclosure Stdout (" + self.settings['tmp_dir'] + ") - analyze_path_disclosure_stdout"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure(self.settings['tmp_dir'], where='stdout')
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_path_disclosure_stderr(self, output, toplimit):
"""Find the tmp_dir in the stdout or stderr"""
title = "Analyze Path Disclosure Stderr (" + self.settings['tmp_dir'] + ") - analyze_path_disclosure_stderr"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_string_disclosure(self.settings['tmp_dir'], where='stderr')
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
return rows
def analyze_path_disclosure_without_file(self, output, toplimit):
"""Find the tmp_dir in the stdout or stderr, even though the testcase did not have a temporary file"""
title = "Analyze Path Disclosure (" + self.settings['tmp_dir'] + ") Without Temp File (" + self.settings['tmp_prefix'] + ") - analyze_path_disclosure_without_file"
columns = ["Testcase", "Software", "Type", "OS", "Stdout", "Stderr"]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
software_ids = []
results = self.settings['db'].get_software_type('CLI')
for result in results:
software_ids.append(str(result[0]))
rows = []
if software_ids:
original_ids = self.settings['db'].get_software()
self.settings['db'].set_software(software_ids) # restrict the ids
results = self.settings['db'].analyze_string_disclosure(self.settings['tmp_dir'])
self.settings['db'].set_software(original_ids) # set the ids to the original value
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
if result[3].find(self.settings['tmp_prefix']) == -1 and result[4].find(self.settings['tmp_prefix']) == -1:
rows.append([(result[0][:self.settings['testcase_limit']], result[1], result[2], result[3], result[4], result[5])])
self.dump.general(output, title, columns, rows)
self.settings['db'].set_software(original_ids)
return rows
def analyze_output_messages(self, output, toplimit, messages='stderr'):
"""Analize which were the different output messages for each piece of software"""
title = "Analyze Different " + messages[0].upper() + messages[1:] + " Output Messages - analyze_output_messages"
columns = ["Software", "Type", "OS", "Return Code", messages[0].upper() + messages[1:]]
function_risk = 1
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
rows = []
results = self.settings['db'].analyze_output_messages(messages)
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
output_parsed = result[5]
if len(result[0]) > 5:
output_parsed = output_parsed.replace(result[0], "TESTCASE") # if possible, remove the testcase from output
output_parsed = output_parsed.replace(str(result[0].encode("utf-8")), "TESTCASE") # if possible, remove the testcase from output
if output_parsed.find(self.settings['tmp_prefix']) != -1:
regex = re.compile('[\S]*' + self.settings['tmp_prefix'] + '[\S]*')
regex_iter = re.finditer(regex, output_parsed)
for match in regex_iter:
output_parsed = output_parsed.replace(match.group(0), "TMPFILE")
test = [result[1], result[2], result[3], result[4], output_parsed]
flag = False
for row in rows:
if [test] == row:
flag = True
break
if not flag:
rows.append([test])
rows = sorted(rows)
self.dump.general(output, title, columns, rows)
return rows
def analyze_elapsed(self, output, toplimit):
"""Analize which was the total time required for each piece of software"""
title = "Analyze Elapsed Time - analyze_elapsed"
columns = ["Software", "Type", "OS", "Elapsed", "Average per Testcase"]
function_risk = 0
if not self.check_minimum_risk(function_risk, title):
return False
if output:
self.settings['logger'].info(title)
total = 0
rows = []
if self.count_results is None:
self.count_results = self.settings['db'].count_results(0, None)
results = self.settings['db'].analyze_elapsed()
for result in results:
if toplimit is not None and len(rows) >= toplimit:
break
rows.append([[result[0], result[1], result[2], str(datetime.timedelta(seconds=int(result[3]))), str(round(result[3] / self.count_results, 5))]])
total += result[3]
self.dump.general(output, title, columns, rows)
return total
def help(err=""):
"""Print a help screen and exit"""
if err:
print("Error: %s\n" % err)
print("Syntax: ")
print(os.path.basename(__file__) + " -d db.sqlite Choose the database")
print("\t\t [-D] Debug information")
print("\t\t [-m methodName] Method: report (default), analyze_stdout, analyze_specific_return_code, etc")
print("\t\t [-e extra_parameter] Extra parameter used when specifying a for certain methodName (ie, analyze_username_disclosure)")
print("\t\t [-o html] Output: html (default), txt or csv.")
print("\t\t [-l 20] Top limit results (default: 20)")
print("\t\t [-r 3] Minimum risk (0:informational, 1:low, 2:medium, 3:high (default)")
sys.exit()
def main():
"""Analyze potential vulnerabilities on a database fuzzing session"""
try:
opts, args = getopt.getopt(sys.argv[1:], "hd:De:m:o:pl:r:", ["help", "database=", "extra=", "method=", "output=", "limit=", "risk="])
except getopt.GetoptError as err:
help(err)
settings = {}
method = "report" # default method name
toplimit = 20 # default top limit
extra = None
for o, a in opts:
if o in ("-d", "--database"):
if os.path.isfile(a):
settings['db_file'] = a
else:
help("Database should be a valid file.")
elif o in ("-D"):
settings['loglevel'] = 'debug'
elif o in ("-e", "--extra"):
extra = a
elif o in ("-h", "--help"):
help()
elif o in ("-l", "--limit"):
try:
toplimit = int(a)
except ValueError:
help("Top limit should be an integer.")
elif o in ("-m", "--method"):
method = a
elif o in ("-o", "--output"):
settings["output_type"] = a
elif o in ("-p"):
settings["print_risk"] = True
elif o in ("-r", "--risk"):
try:
settings["minimum_risk"] = int(a)
except ValueError:
help("Risk should be an integer.")
if 'db_file' not in settings:
help("The database was not specified.")
elif 'db_file' not in settings and 'print_risk' not in settings:
help("The database was not specified and the only functionality without a database -p was not selected. ")
settings = classes.settings.load_settings(settings)
if settings['db'].db_connection:
analyze = Analyze(settings)
analyze.dump_results(method, toplimit, extra)
if __name__ == "__main__":
main()
# profile.run('analyze.dump_results(method, toplimit)')
================================================
FILE: xdiff_dbaction.py
================================================
#!/usr/bin/env python
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
import getopt
import itertools
import os.path
import sys
import time
import classes.settings
import classes.compat
from classes.dump import Dump
from classes.dbsqlite import DbSqlite
class Dbaction(object):
"""Do stuff with the fuzzer's databases: copy databases, print tables, insert stuff and generate testcases"""
def __init__(self, settings):
self.settings = settings
if 'max_permutation' not in self.settings:
self.settings['max_permutation'] = 5
if 'generate_type' not in self.settings:
self.settings['generate_type'] = 2
def print_table(self, fromdb, table, output_type):
"""Print all the conents of a table"""
if table is None:
self.settings['logger'].error("You must select a table.")
else:
self.settings['output_file'] = None
self.settings['db'] = DbSqlite(self.settings, fromdb)
columns = self.settings['db'].get_columns(table)
rows = self.settings['db'].get_rows(table)
if columns:
dump = Dump(self.settings)
dump.general(output_type, table, columns, [rows])
else:
self.print_valid_tables(table)
def insert_table(self, fromdb, table, separator, insert):
"""Insert a row into a table"""
if table is None:
self.settings['logger'].error("You must select a table.")
else:
if not insert:
self.settings['logger'].error("There are no values to be inserted")
else:
self.settings['db'] = DbSqlite(self.settings, fromdb)
columns = self.settings['db'].get_columns(table)
if columns:
# If the user supplied one value less than the one required and the first column is called id, just ignore that column..
if len(columns) == (len(insert.split(separator)) + 1) and columns[0] == 'id':
del columns[0]
if len(columns) != len(insert.split(separator)):
print("The table '" + table + "' has " + str(len(columns)) + " columns: " + str(columns) + ". However, you want to insert " + str(len(insert.split(separator))) + " value/s: " + str(insert.split(separator)) + ". It doesn't work like that.")
else:
self.settings['db'].insert_row(table, columns, insert.split(separator))
else:
self.print_valid_tables(table)
def print_valid_tables(self, table=None):
"""Provide information on what are the valid tables"""
if table:
self.settings['logger'].error("Error: table '%s' not found" % table)
else:
if self.output_type:
print("Valid table names:")
print("- fuzz_testcase: contains the inputs to be sent to the software. You can define an input in 'function' and potential values in 'value' and generate the combinations on this table.")
print("- function: contains what you want to fuzz. The special keyword [[test]] gets replaced by the values contained in the table 'value'. Ie, if you want to fuzz the 'print()'' function, you want to write in here 'print([[test]])'.")
print("- value: contains the items that will replace the [[test]] values in the 'function' table")
print("")
print("Valid tables generated by XDiFF:")
print("- fuzz_software: contains the software defined in software.ini")
print("- fuzz_testcase_result: contains the result of executing the software defined in 'fuzz_software' with the input defined in 'fuzz_testcase'")
print("- fuzz_constants: contains internal constant values used by the fuzzer")
def permute(self, functions, values):
"""Perform a permutation between the two lists received (functions & values)"""
total = 0
if not functions:
self.settings['logger'].error("There are no functions to permute")
elif not values:
self.settings['logger'].error("There are no values to permute")
else:
# Prioritize the lower count injections
for count in range(0, self.settings['max_permutation'] + 1):
# Give a heads up of how many testcases will be generated
subtotal = 0
countfunctions = functions
for function in countfunctions:
if isinstance(function, tuple):
if len(function) == 1:
function = function[0] # when it is generated by random testcases (classes/fuzzer.py)
elif len(function) == 2:
function = function[1] # when it is read from the database
if function is not None and count == function.count("[[test]]"):
subtotal += 1
self.settings['logger'].debug("Testcases generation: %s entry points, %s testcases to be generated." % (str(count), str(subtotal)))
# Generate the testcases
for function in functions:
if len(function) == 1:
function = function[0] # when it is generated by random testcases (classes/fuzzer.py)
elif len(function) == 2:
function = function[1] # when it is read from the database
if function is not None and count == function.count("[[test]]"):
testcases, total = self.permute_values(values, function, total)
self.settings['db'].set_testcase(testcases)
return total
def permute_values(self, values, function, total):
"""Perform a permutation between the values and the functions received based on the generate_type received"""
testcases = []
function_tuple = function
# There are no values, only functions:
if not values:
testcases.append((classes.compat.unicode(function_tuple),))
else:
if self.settings['generate_type'] == 1:
# Permute
for valuetuple in itertools.product(values, repeat=function_tuple.count("[[test]]")):
total += 1
for value in valuetuple:
# unicode values are tuples
if isinstance(valuetuple, tuple):
value = value[0]
value = value.replace('[[id]]', str(total))
function_tuple = function_tuple.replace("[[test]]", value, 1)
testcases.append((classes.compat.unicode(function_tuple),))
function_tuple = function # reset to the original value
elif self.settings['generate_type'] == 2:
# Do not permute, just replace
for value in values:
if isinstance(value, tuple):
value = value[0]
total += 1
value = value.replace('[[id]]', str(total))
function_tuple = function_tuple.replace('[[test]]', value)
testcases.append((classes.compat.unicode(function_tuple),))
function_tuple = function # reset to the original value
elif self.settings['generate_type'] == 3:
# Do not permute, replace but also include testcases with less parameters
if (function.count("[[test]]")) > 1:
for tests in range(1, function.count("[[test]]") + 1):
for value in values:
if isinstance(value, tuple):
value = value[0]
total += 1
value = value.replace('[[id]]', str(total))
function_tuple = function_tuple.replace('[[test]]', value)
testcases.append((classes.compat.unicode(function_tuple),))
function_tuple = function # reset to the original value
function_tuple = function = function.replace(',[[test]]', '', 1)
else:
print("Error: the permutation type does not exist")
sys.exit()
return testcases, total
def generate(self, fromdb):
"""Generate the testcases with a permutation of values and functions"""
start_time = time.time()
self.settings['db'] = DbSqlite(self.settings, fromdb)
if self.settings['db'].db_connection:
self.settings['db'].create_table()
values = self.settings['db'].get_values()
functions = self.settings['db'].get_functions()
self.settings['logger'].info("Values: %s - Functions: %s" % (str(len(values)), str(len(functions))))
total = self.permute(functions, values)
self.settings['db'].commit()
finish_time = time.time() - start_time
self.settings['logger'].info("Testcases generated: %s" % str(total))
self.settings['logger'].info("Time required: %s seconds" % str(round(finish_time, 2)))
def migrate(self, fromdb, todb):
"""Migrates tables from one database ('dbfrom') to another database ('dbto')"""
start_time = time.time()
self.settings['dbfrom'] = DbSqlite(self.settings, fromdb)
self.settings['dbto'] = DbSqlite(self.settings, todb)
if self.settings['dbfrom'].db_connection and self.settings['dbto'].db_connection:
self.settings['dbto'].create_table()
values = self.settings['dbfrom'].get_values()
self.settings['dbto'].set_values(values)
functions = self.settings['dbfrom'].get_functions()
self.settings['dbto'].set_functions(functions)
self.settings['dbto'].commit()
finish_time = time.time() - start_time
self.settings['logger'].info("Finished, time elapsed %s seconds" % str(finish_time)[:5])
def help(err=None):
"""Print a help screen and exit"""
if err:
print("Error: %s\n" % str(err))
print("Syntax: ")
print(os.path.basename(__file__) + " -d db.sqlite -D fuzz.db Migrate values and functions to another database")
print("\t\t -d fuzz.db -g 1 [-m 5] Generate testcases permuting values and functions (set to maximum 5 input test cases)")
print("\t\t -d fuzz.db -g 2 [-m 5] Generate testcases replacing values in functions (set to max..)")
print("\t\t -d fuzz.db -g 3 [-m 5] Generate testcases replacing values in functions including testcases with less parameters (set to max..)")
print("\t\t -d fuzz.db -t table -p Print a database table: fuzz_software, fuzz_testcase, value, function)")
print("\t\t -d fuzz.db -t table [-s,] -i \"foo\" Insert foo into table (optional field separator -s uses a comma)")
sys.exit()
def main():
"""Perform multiple database actions"""
try:
opts, args = getopt.getopt(sys.argv[1:], "hd:D:g:i:m:ps:t:", ["help", "database=", "Database=", "generate=", "insert=", "maximum=", "print", "separator=", "table="])
except getopt.GetoptError as err:
help(err)
settings = {}
settings['output_type'] = 'txt'
fromdb = None
todb = None
table = None
action = None
separator = ","
for o, a in opts:
if o in ("-h", "--help"):
help()
elif o in ("-d", "--database"):
fromdb = a
if os.path.isfile(fromdb):
settings['db_file'] = fromdb
else:
help("The database selected '%s' is not a valid file." % a)
elif o in ("-D", "--Database"):
todb = a
action = "migrate"
break
elif o in ("-g", "--generate"):
action = "generate"
try:
settings['generate_type'] = int(a)
except:
help("The generate parameter should be a number")
elif o in ("-i", "--insert"):
action = "insert"
insert = classes.compat.unicode(str(a), errors='ignore')
elif o in ("-m", "--maximum"):
try:
settings['max_permutation'] = int(a)
except ValueError:
help("The max permutation parameter should be a number")
elif o in ("-p", "--print"):
action = "print"
elif o in ("-s", "--separator"):
separator = a
elif o in ("-t", "--table"):
table = a
if not fromdb:
help("The database was not specified.")
settings = classes.settings.load_settings(settings)
dbaction = Dbaction(settings)
if action == "migrate":
dbaction.migrate(fromdb, todb)
elif action == "generate":
if todb is not None:
fromdb = todb
dbaction.generate(fromdb)
elif action == "print":
dbaction.print_table(fromdb, table, settings['output_type'])
elif action == "insert":
dbaction.insert_table(fromdb, table, separator, insert)
else:
help("You must select an action: migrate, generate, print or insert.")
if __name__ == "__main__":
main()
================================================
FILE: xdiff_run.py
================================================
#!/usr/bin/env python
#
# Copyright (C) 2018 Fernando Arnaboldi
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
import getopt
import os
import signal
import sys
import time
import classes.settings
def dfuzz(settings):
"""Fuzz something based on he settings received"""
if 'fuzz_category' not in settings:
help("The category was not specified.")
settings = classes.settings.load_settings(settings) # load the fuzzer settings
if not settings:
return False
if not settings['software']:
help("There is no software associated to the category selected")
if not settings['queue'].chdir_tmp():
return False
banner = "Starting Fuzzer v%s" % str(settings['version'])
settings['logger'].info(len(banner) * "-")
settings['logger'].info(banner)
settings['logger'].info(len(banner) * "-")
for key in sorted(settings.iterkeys()):
settings['logger'].debug("Setting %s: %s" % (key, str(settings[key])))
settings['queue'].start_web_server() # load the webserver
settings['monitor'].check_once() # check before start if the canaries are in place
settings['db'].optimize()
total_testcases = settings['db'].count_testcases()
current_test = settings['db'].get_latest_id(settings['software'])
settings['logger'].info("Setting testcases: %s/%s" % (str(current_test), str(total_testcases)))
elapsed_time = 0
test_count = 0
while True:
start_time = time.time()
tests = settings['db'].get_test(current_test, settings['db_tests'])
if not tests:
settings['logger'].info("Terminated: no more testcases")
break
dbinput = settings['queue'].fuzz(tests)
settings['monitor'].check() # check free space before saving results
saved, size = settings['db'].set_results(dbinput)
finish_time = (time.time() - start_time)
elapsed_time += finish_time # Total time elapsed testing
remaining_tests = total_testcases - (current_test + settings['db_tests']) # Tests left
test_count += settings['db_tests']
rate = test_count / elapsed_time # Rate per second
time_left = remaining_tests / rate / 60 # How many hours are left ?
settings['logger'].info("Tests " + str(current_test) + "-" + str(current_test + settings['db_tests']) + " - Set " + str(saved) + " (" + str(int(size / 1024)) + " kb) - Took " + str(int(finish_time)) + "s - Avg Rate " + str(int(rate) * len(settings['software'])) + " - ETC " + str(int(time_left)) + "'")
current_test += settings['db_tests']
# break # uncomment if you want to run just one cycle of the fuzzer for debugging purposes
settings['queue'].stop_web_server()
def help(err=""):
"""Print a help screen and exit"""
if err:
print("Error: %s\n" % err)
print("XDiFF Syntax: ")
print(os.path.basename(__file__) + " -d db.sqlite Choose the database")
print("\t -c Python Software category to be fuzzed")
print("\t [-D] Print debugging information")
print("\t [-r 0] Random inputs: radamsa & zzuf without newlines (faster)")
print("\t [-r 1] Random inputs: radamsa & zzuf with newlines (slower)")
print("\t [-r 2] Random inputs: radamsa without newlines (faster)")
print("\t [-r 3] Random inputs: radamsa with newlines (slower)")
print("\t [-r 4] Random inputs: zzuf without newlines (faster)")
print("\t [-r 5] Random inputs: zzuf with newlines (slower)")
print("\t [-s software.ini] Configuration file for software to be fuzzed")
print("\t [-t 100] Threads executed in parallel")
print("\t [-T 10] Timeout per thread")
print("\t [-v] Use valgrind")
sys.exit()
def main():
"""Fuzz something FFS!"""
def signal_handler(signal, frame):
"""Catch SIGINT and do some cleaning before termination"""
settings['monitor'].remove_stuff()
settings['queue'].stop_web_server()
settings['logger'].info("Program terminated")
sys.exit(1)
signal.signal(signal.SIGINT, signal_handler)
try:
opts, args = getopt.getopt(sys.argv[1:], "hc:d:Dr:s:t:T:v", ["help", "category=", "database=", "random=", "software=", "threads=", "timeout=", "valgrind"])
except getopt.GetoptError as err:
help(err)
settings = {}
for o, a in opts:
if o in ("-h", "--help"):
help()
elif o in ("-c", "--category"):
settings['fuzz_category'] = a
elif o in ("-d", "--database"):
settings['db_file'] = os.path.abspath(a)
elif o in ("-D"):
settings['loglevel'] = 'debug'
elif o in ("-r", "--random"):
settings['generate_tests'] = int(a)
elif o in ("-s", "--software"):
settings['software'] = os.path.abspath(a)
elif o in ("-t", "--threads"):
settings['db_tests'] = int(a)
elif o in ("-T", "--timeout"):
settings['timeout'] = int(a)
elif o in ("-v", "--valgrind"):
settings['valgrind'] = True
if "db_file" not in settings or "fuzz_category" not in settings:
help("The -d and -c parameters are mandatory")
else:
dfuzz(settings)
if __name__ == "__main__":
main()
gitextract_la3db652/ ├── .travis.yml ├── README.md ├── classes/ │ ├── __init__.py │ ├── compat.py │ ├── db.py │ ├── dbsqlite.py │ ├── dump.py │ ├── execute.py │ ├── fuzzer.py │ ├── monitor.py │ ├── queue.py │ ├── settings.py │ └── webserver.py ├── docs/ │ ├── 1.-Install.md │ ├── 2.-The-input.md │ ├── 3.-The-software.md │ ├── 4.-The-fuzzer.md │ ├── 5.-The-output.md │ └── Changelog.md ├── xdiff_analyze.py ├── xdiff_dbaction.py └── xdiff_run.py
SYMBOL INDEX (157 symbols across 13 files)
FILE: classes/compat.py
function escape (line 14) | def escape(value):
function unicode (line 21) | def unicode(value, errors=None): # Python 3
function escape (line 25) | def escape(value):
FILE: classes/db.py
class Db (line 20) | class Db(object):
method __init__ (line 22) | def __init__(self, settings):
method commit (line 28) | def commit(self):
method get_fuzz_testcase (line 37) | def get_fuzz_testcase(self):
method delete_unused_testcases (line 50) | def delete_unused_testcases(self):
method get_functions (line 55) | def get_functions(self):
method get_values (line 68) | def get_values(self):
method list_software (line 79) | def list_software(self, active=None):
method set_software (line 93) | def set_software(self, softwareids):
method get_software (line 100) | def get_software(self):
method get_software_type (line 104) | def get_software_type(self, category_type):
method list_results (line 114) | def list_results(self, lowerlimit=0, toplimit=-1):
method list_killed_results (line 126) | def list_killed_results(self):
method count_results (line 131) | def count_results(self, lowerlimit=0, toplimit=-1):
method list_return_code_per_software (line 138) | def list_return_code_per_software(self):
method analyze_specific_return_code (line 148) | def analyze_specific_return_code(self, returncodes):
method analyze_return_code_differences (line 159) | def analyze_return_code_differences(self):
method count_software (line 169) | def count_software(self):
method count_testcases (line 179) | def count_testcases(self):
method count_reference (line 189) | def count_reference(self, reference):
method analyze_canary_file (line 201) | def analyze_canary_file(self):
method analyze_top_elapsed (line 211) | def analyze_top_elapsed(self, killed):
method analyze_killed_differences (line 227) | def analyze_killed_differences(self):
method analyze_same_software (line 237) | def analyze_same_software(self):
method analyze_stdout (line 247) | def analyze_stdout(self, lowerlimit, upperlimit):
method analyze_same_stdout (line 257) | def analyze_same_stdout(self):
method analyze_string_disclosure (line 267) | def analyze_string_disclosure(self, searchme, excludeme="", excludecli...
method analyze_remote_connection (line 287) | def analyze_remote_connection(self, searchme=""):
method analyze_output_messages (line 297) | def analyze_output_messages(self, messages):
method analyze_elapsed (line 302) | def analyze_elapsed(self):
method get_rows (line 312) | def get_rows(self, table):
FILE: classes/dbsqlite.py
class DbSqlite (line 27) | class DbSqlite(db.Db):
method __init__ (line 29) | def __init__(self, settings, db_file):
method optimize (line 50) | def optimize(self):
method close (line 55) | def close(self):
method create_table (line 60) | def create_table(self):
method get_software_id (line 76) | def get_software_id(self, piece):
method save_software (line 89) | def save_software(self, piece):
method get_constant_value (line 106) | def get_constant_value(self, constant_type, constant_name):
method get_latest_id (line 118) | def get_latest_id(self, software):
method get_test (line 139) | def get_test(self, latest_id, limit):
method set_results (line 153) | def set_results(self, results):
method set_testcase (line 182) | def set_testcase(self, testcases):
method set_values (line 188) | def set_values(self, values):
method set_functions (line 194) | def set_functions(self, functions):
method get_columns (line 200) | def get_columns(self, table):
method insert_row (line 208) | def insert_row(self, table, column, row):
FILE: classes/dump.py
class Dump (line 20) | class Dump(object):
method __init__ (line 22) | def __init__(self, settings):
method get_screen_size (line 26) | def get_screen_size(self, columns):
method print_text_top_row (line 84) | def print_text_top_row(self, title, columns):
method print_text_row (line 99) | def print_text_row(self, columns, results):
method print_text_bottom_row (line 124) | def print_text_bottom_row(self):
method print_csv_top_row (line 128) | def print_csv_top_row(self, columns):
method print_csv_row (line 135) | def print_csv_row(self, results):
method print_xml_row (line 155) | def print_xml_row(self, title, column, results):
method print_html_top_row (line 170) | def print_html_top_row(self, title, columns):
method print_html_row (line 186) | def print_html_row(self, results):
method print_html_bottom_row (line 204) | def print_html_bottom_row(self, title):
method set_toggle_table (line 211) | def set_toggle_table(self, toggle):
method pre_general (line 215) | def pre_general(self, output):
method post_general (line 290) | def post_general(self, output):
method general (line 304) | def general(self, output, title, columns, rows):
method write_file (line 335) | def write_file(self, output_file, mode, content):
FILE: classes/execute.py
class Execute (line 25) | class Execute(object):
method __init__ (line 27) | def __init__(self, settings, piece, testcase):
method join (line 35) | def join(self):
method get_output (line 42) | def get_output(self):
method kill_process (line 48) | def kill_process(self, process):
method run_subprocess (line 62) | def run_subprocess(self, piece, testcase):
method analyze_results (line 101) | def analyze_results(self, stdout, stderr):
FILE: classes/fuzzer.py
class Fuzzer (line 28) | class Fuzzer(object):
method __init__ (line 30) | def __init__(self, settings, ids):
method chdir_tmp (line 34) | def chdir_tmp(self):
method fuzz (line 44) | def fuzz(self, tests):
method get_input (line 77) | def get_input(self, piece, test):
method generate_tests (line 130) | def generate_tests(self, latest_id, limit):
method execute_shell (line 195) | def execute_shell(self, cmd):
FILE: classes/monitor.py
class Monitor (line 35) | class Monitor(object):
method __init__ (line 37) | def __init__(self, settings):
method check_once (line 41) | def check_once(self):
method check (line 51) | def check(self):
method remove_stuff (line 58) | def remove_stuff(self):
method check_canary_file (line 81) | def check_canary_file(self, filename, token):
method create_canary_file (line 107) | def create_canary_file(self, filename, token):
method check_canary_web (line 122) | def check_canary_web(self, hostname, filename, token):
method check_canary_command (line 152) | def check_canary_command(self, command, token):
method check_canary_references (line 168) | def check_canary_references(self, reference):
method check_free_space (line 176) | def check_free_space(self):
method check_ulimit (line 190) | def check_ulimit(self):
FILE: classes/queue.py
class Queue (line 22) | class Queue(Fuzzer, WebServer):
method __init__ (line 24) | def __init__(self, settings):
FILE: classes/settings.py
function define_software (line 30) | def define_software(settings):
function set_logger (line 79) | def set_logger(settings):
function load_settings (line 93) | def load_settings(settings):
FILE: classes/webserver.py
class BaseHandler (line 31) | class BaseHandler(SimpleHTTPRequestHandler):
method log_message (line 35) | def log_message(self, format, *args):
method do_GET (line 39) | def do_GET(self):
method do_POST (line 45) | def do_POST(self):
method do_REQUEST (line 51) | def do_REQUEST(self, data):
class WebServer (line 107) | class WebServer(object):
method __init__ (line 109) | def __init__(self, settings):
method start_web_server (line 113) | def start_web_server(self):
method stop_web_server (line 125) | def stop_web_server(self):
FILE: xdiff_analyze.py
class Analyze (line 36) | class Analyze(object):
method __init__ (line 38) | def __init__(self, settings):
method check_minimum_risk (line 49) | def check_minimum_risk(self, function_risk, title):
method dump_results (line 58) | def dump_results(self, method, toplimit, extra):
method report (line 102) | def report(self, output, toplimit):
method list_summary (line 145) | def list_summary(self, output, toplimit):
method list_software (line 220) | def list_software(self, output, toplimit):
method list_results (line 239) | def list_results(self, output, toplimit):
method analyze_valgrind (line 271) | def analyze_valgrind(self, output, toplimit):
method list_killed_results (line 292) | def list_killed_results(self, output, toplimit):
method analyze_return_code (line 323) | def analyze_return_code(self, output, toplimit):
method analyze_specific_return_code (line 344) | def analyze_specific_return_code(self, output, toplimit):
method analyze_return_code_same_software_differences (line 366) | def analyze_return_code_same_software_differences(self, output, toplim...
method analyze_return_code_differences (line 414) | def analyze_return_code_differences(self, output, toplimit):
method analyze_username_disclosure (line 446) | def analyze_username_disclosure(self, output, toplimit, username=None):
method analyze_error_disclosure (line 470) | def analyze_error_disclosure(self, output, toplimit):
method analyze_canary_file (line 493) | def analyze_canary_file(self, output, toplimit):
method analyze_canary_token_file (line 515) | def analyze_canary_token_file(self, output, toplimit):
method analyze_canary_token_code (line 536) | def analyze_canary_token_code(self, output, toplimit):
method analyze_canary_token_command (line 557) | def analyze_canary_token_command(self, output, toplimit):
method analyze_remote_connection (line 578) | def analyze_remote_connection(self, output, toplimit):
method analyze_top_elapsed_killed (line 607) | def analyze_top_elapsed_killed(self, output, toplimit):
method analyze_top_elapsed_not_killed (line 628) | def analyze_top_elapsed_not_killed(self, output, toplimit):
method analyze_killed_differences (line 649) | def analyze_killed_differences(self, output, toplimit):
method analyze_same_software (line 688) | def analyze_same_software(self, output, toplimit):
method analyze_stdout (line 737) | def analyze_stdout(self, output, toplimit):
method analyze_same_stdout (line 781) | def analyze_same_stdout(self, output, toplimit):
method analyze_file_disclosure (line 813) | def analyze_file_disclosure(self, output, toplimit):
method analyze_file_disclosure_without_path (line 834) | def analyze_file_disclosure_without_path(self, output, toplimit):
method analyze_path_disclosure_stdout (line 856) | def analyze_path_disclosure_stdout(self, output, toplimit):
method analyze_path_disclosure_stderr (line 877) | def analyze_path_disclosure_stderr(self, output, toplimit):
method analyze_path_disclosure_without_file (line 898) | def analyze_path_disclosure_without_file(self, output, toplimit):
method analyze_output_messages (line 929) | def analyze_output_messages(self, output, toplimit, messages='stderr'):
method analyze_elapsed (line 968) | def analyze_elapsed(self, output, toplimit):
function help (line 994) | def help(err=""):
function main (line 1009) | def main():
FILE: xdiff_dbaction.py
class Dbaction (line 30) | class Dbaction(object):
method __init__ (line 32) | def __init__(self, settings):
method print_table (line 39) | def print_table(self, fromdb, table, output_type):
method insert_table (line 54) | def insert_table(self, fromdb, table, separator, insert):
method print_valid_tables (line 75) | def print_valid_tables(self, table=None):
method permute (line 91) | def permute(self, functions, values):
method permute_values (line 124) | def permute_values(self, values, function, total):
method generate (line 174) | def generate(self, fromdb):
method migrate (line 190) | def migrate(self, fromdb, todb):
function help (line 211) | def help(err=None):
function main (line 225) | def main():
FILE: xdiff_run.py
function dfuzz (line 27) | def dfuzz(settings):
function help (line 79) | def help(err=""):
function main (line 100) | def main():
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (176K chars).
[
{
"path": ".travis.yml",
"chars": 860,
"preview": "language: python\ncache: pip\npython:\n - 2.7\n - 3.6\n #- nightly\n #- pypy\n #- pypy3\nmatrix:\n allow_failur"
},
{
"path": "README.md",
"chars": 2031,
"preview": "# What is XDiFF?\n XDiFF is an Extended Differential Fuzzing Framework built for finding \n vulnerabilities in software. I"
},
{
"path": "classes/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "classes/compat.py",
"chars": 496,
"preview": "from __future__ import print_function\nfrom __future__ import absolute_import\n\n\n# Python 2\ntry:\n\tunicode\n\n\tunicode = unic"
},
{
"path": "classes/db.py",
"chars": 17398,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/dbsqlite.py",
"chars": 11047,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/dump.py",
"chars": 13019,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/execute.py",
"chars": 4624,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/fuzzer.py",
"chars": 9219,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/monitor.py",
"chars": 7471,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/queue.py",
"chars": 1012,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/settings.py",
"chars": 7653,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "classes/webserver.py",
"chars": 4876,
"preview": "#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute it and/or modify\n# it"
},
{
"path": "docs/1.-Install.md",
"chars": 4874,
"preview": "Follwing are the instructions on how to execute XDiFF in:\n* [Linux](#Linux)\n* [OSX](#OSX)\n* [Freebsd](#Freebsd)\n* [Windo"
},
{
"path": "docs/2.-The-input.md",
"chars": 4926,
"preview": "# Why do I want to use a database?\nA database allows you to compare the results of how the software was executed when us"
},
{
"path": "docs/3.-The-software.md",
"chars": 2793,
"preview": "In here you will find information about how to define pieces of software in the file *software.ini*.\n\nThis defines piece"
},
{
"path": "docs/4.-The-fuzzer.md",
"chars": 1755,
"preview": "## Fuzzing\nThe most basic execution requires defining which category and which database will be used:\n```\n./xdiff_run.py"
},
{
"path": "docs/5.-The-output.md",
"chars": 5956,
"preview": "## Analyzing the output\nThe most basic form of analyzing the output is running:\n```\n./xdiff_analyze.py -d shells.sqlite\n"
},
{
"path": "docs/Changelog.md",
"chars": 1296,
"preview": "# Changelog\nChanges are listed in time order: newer changes are at the top, older changes are at the bottom.\n\n## Version"
},
{
"path": "xdiff_analyze.py",
"chars": 41954,
"preview": "#!/usr/bin/env python\n#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute"
},
{
"path": "xdiff_dbaction.py",
"chars": 11933,
"preview": "#!/usr/bin/env python\n#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute"
},
{
"path": "xdiff_run.py",
"chars": 5590,
"preview": "#!/usr/bin/env python\n#\n# Copyright (C) 2018 Fernando Arnaboldi\n#\n# This program is free software: you can redistribute"
}
]
About this extraction
This page contains the full source code of the IOActive/XDiFF GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (157.0 KB), approximately 42.1k tokens, and a symbol index with 157 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.