├── api ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── process_firmware_thread.py │ │ └── process_firmware.py ├── migrations │ └── __init__.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── models.py └── views.py ├── front ├── __init__.py ├── migrations │ └── __init__.py ├── tests.py ├── admin.py ├── models.py ├── apps.py ├── templates │ └── front │ │ ├── upload.html │ │ ├── latest.html │ │ ├── search.html │ │ ├── summary.html │ │ ├── home.html │ │ └── layout.html ├── urls.py └── views.py ├── lib ├── __init__.py ├── migrations │ └── __init__.py ├── admin.py ├── tests.py ├── views.py ├── models.py ├── apps.py ├── rats.py ├── cert.py ├── util.py ├── parseELF.py ├── extract.py └── extractor.py ├── firmflaws ├── __init__.py ├── wsgi.py ├── urls.py └── settings.py ├── images ├── elf.jpg ├── home.jpg ├── image.jpg ├── file_analysis.jpg └── firmware_summary.jpg ├── requirements.txt ├── static ├── images │ ├── wait.gif │ └── header.jpg ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ ├── ie10-viewport-bug-workaround.js │ ├── jquery.easing.min.js │ ├── custom.js │ └── bootstrap.min.js └── css │ └── custom.css ├── LICENSE ├── manage.py ├── .gitignore └── README.md /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firmflaws/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/elf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/images/elf.jpg -------------------------------------------------------------------------------- /api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /front/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /images/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/images/home.jpg -------------------------------------------------------------------------------- /images/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/images/image.jpg -------------------------------------------------------------------------------- /lib/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /lib/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /lib/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /front/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydot 2 | Django 3 | binwalk 4 | r2pipe 5 | python-magic 6 | django-cors-headers -------------------------------------------------------------------------------- /static/images/wait.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/static/images/wait.gif -------------------------------------------------------------------------------- /images/file_analysis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/images/file_analysis.jpg -------------------------------------------------------------------------------- /static/images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/static/images/header.jpg -------------------------------------------------------------------------------- /images/firmware_summary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/images/firmware_summary.jpg -------------------------------------------------------------------------------- /front/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | 5 | # Create your models here. 6 | -------------------------------------------------------------------------------- /lib/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | 5 | # Create your models here. 6 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ganapati/firmflaws/HEAD/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /api/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class ApiConfig(AppConfig): 7 | name = 'api' 8 | -------------------------------------------------------------------------------- /lib/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class LibConfig(AppConfig): 7 | name = 'lib' 8 | -------------------------------------------------------------------------------- /front/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class FrontConfig(AppConfig): 7 | name = 'front' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BEER-WARE LICENSE" (Revision 42): 2 | Ganapati wrote this file. 3 | As long as you retain this notice you can do whatever you want with this stuff. 4 | If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. 5 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "firmflaws.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /api/management/commands/process_firmware_thread.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | from django.core.management import call_command 4 | 5 | class ProcessFirmwareThread(threading.Thread): 6 | def __init__(self): 7 | super(ProcessFirmwareThread, self).__init__() 8 | def run(self): 9 | call_command('process_firmware') 10 | 11 | 12 | def start_process_thread(): 13 | thread = ProcessFirmwareThread() 14 | thread.start() -------------------------------------------------------------------------------- /firmflaws/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for firmflaws project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "firmflaws.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /front/templates/front/upload.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/layout.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Firmware Uploaded

9 |

Status : {{ upload.status }}

10 |

Hash : {{ upload.hash }}

11 | View results 12 |
13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /front/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | from front.views import home, upload, search, get_firmware_summary, get_file, latest 4 | 5 | urlpatterns = [ 6 | url(r'^$', home, name='home'), 7 | url(r'^latest/?$', latest, name='latest'), 8 | url(r'^upload/?$', upload, name='upload'), 9 | url(r'^search/?$', search, name='search'), 10 | url(r'^firmware/summary/(?P[^/]+)/?$', get_firmware_summary, name='get_firmware_summary'), 11 | url(r'^file/(?P[^/]+)/?$', get_file, name='get_file') 12 | ] 13 | -------------------------------------------------------------------------------- /static/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | // See the Getting Started docs for more information: 8 | // http://getbootstrap.com/getting-started/#support-ie10-width 9 | 10 | (function () { 11 | 'use strict'; 12 | 13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 14 | var msViewportStyle = document.createElement('style') 15 | msViewportStyle.appendChild( 16 | document.createTextNode( 17 | '@-ms-viewport{width:auto!important}' 18 | ) 19 | ) 20 | document.querySelector('head').appendChild(msViewportStyle) 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /lib/rats.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | import subprocess 4 | 5 | def is_parsable(filename): 6 | """ check if file is parsable by rats 7 | """ 8 | 9 | filename, file_extension = os.path.splitext(filename) 10 | return file_extension in ['.php', '.pl', '.c', '.cpp', '.py', '.cgi', '.inc', '.cgi'] 11 | 12 | def parse(filename): 13 | """ Do the rats parsing here 14 | """ 15 | cmd = [settings.RATS_BINARY, "--resultsonly", filename] 16 | results = subprocess.check_output(cmd) 17 | results = results.decode("utf-8") 18 | finds = [] 19 | for line in results.split("\n"): 20 | if "High" in line or "Medium" in line: 21 | infos = line.split(":") 22 | line = infos[1].strip() 23 | rank = infos[2].strip() 24 | method = infos[3].strip() 25 | msg = "%s : %s in line %s" % (rank, method, line) 26 | finds.append(msg) 27 | return finds 28 | -------------------------------------------------------------------------------- /api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | from api.views import api_upload, api_get_firmware, api_get_hierarchy, api_get_firmware_summary 4 | from api.views import api_get_file, api_get_latest, api_get_stats, api_search 5 | 6 | urlpatterns = [ 7 | url(r'^upload/?$', api_upload, name='api_upload'), 8 | url(r'^latest/?$', api_get_latest, name='api_get_latest'), 9 | url(r'^stats/?$', api_get_stats, name='api_get_stats'), 10 | url(r'^search/?$', api_search, name='api_search'), 11 | url(r'^firmware/hierarchy/(?P[^/]+)/?$', api_get_hierarchy, name='api_get_hierarchy'), 12 | url(r'^firmware/summary/(?P[^/]+)/?$', api_get_firmware_summary, name='api_get_firmware_summary'), 13 | url(r'^firmware/(?P[^/]+)/?$', api_get_firmware, name='api_get_firmware'), 14 | url(r'^file/(?P[^/]+)/?$', api_get_file, name='api_get_file') 15 | ] 16 | -------------------------------------------------------------------------------- /lib/cert.py: -------------------------------------------------------------------------------- 1 | import os 2 | from OpenSSL import crypto as c 3 | from datetime import datetime 4 | 5 | 6 | def is_cert(filename): 7 | """ check if file is a cert 8 | """ 9 | 10 | filename, file_extension = os.path.splitext(filename) 11 | return file_extension in ['.cert', '.pem'] 12 | 13 | 14 | def check_cert(filename): 15 | """ Check revocation date and bits 16 | """ 17 | with open("./extracted/etc/server.pem", "r") as cert_file: 18 | response = [] 19 | crt = c.load_certificate(c.FILETYPE_PEM, cert_file.read()) 20 | end_date = crt.get_notAfter().decode("utf-8")[:-1] 21 | bits = crt.get_pubkey().bits() 22 | 23 | if bits < 1024: 24 | response.append("pubkey bits : %s" % bits) 25 | 26 | now = datetime.datetime.now() 27 | current = "%s%02d%02d%02d%02d%02d" % (now.year, 28 | now.month, 29 | now.day, 30 | now.hour, 31 | now.minute, 32 | now.second) 33 | if int(current) > int(end_date): 34 | response.append("Certificate expired") 35 | return response 36 | -------------------------------------------------------------------------------- /firmflaws/urls.py: -------------------------------------------------------------------------------- 1 | """firmflaws URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from django.conf.urls import include 19 | from django.conf import settings 20 | from django.conf.urls.static import static 21 | 22 | urlpatterns = [ 23 | url(r'^admin/', admin.site.urls), 24 | url(r'^api/', include('api.urls')), 25 | url(r'^', include('front.urls')) 26 | ] 27 | 28 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 29 | 30 | if settings.DEBUG: 31 | import debug_toolbar 32 | urlpatterns += [ 33 | url(r'^__debug__/', include(debug_toolbar.urls)), 34 | ] 35 | -------------------------------------------------------------------------------- /lib/util.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def _parseFiles(file, result): 5 | # lentgh minus 1 because the first element of array is '' since the string begins with '/' 6 | folders = file["filename"].split("/") 7 | length = len(folders) - 1 8 | current = 1 9 | 10 | tmpResult = result 11 | # Parse folders (not the last one, which is normally a file) 12 | while current < length: 13 | 14 | folder = folders[current] 15 | tmp = None 16 | for r in tmpResult: 17 | if "/" + folder == r["name"]: 18 | tmp = r 19 | break 20 | 21 | if not tmp: 22 | tmp = { 23 | "name": "/" + folder, 24 | "children": [], 25 | "type": "folder" 26 | } 27 | tmpResult.append(tmp) 28 | 29 | tmpResult = tmp["children"] 30 | current += 1 31 | 32 | # Parse the file itself (last one in path) 33 | tmpResult.append({ 34 | "name": folders[current], 35 | "type": "file", 36 | "id": file["hash"] 37 | }) 38 | 39 | 40 | def parseFilesToHierarchy(files): 41 | result = [] 42 | tmpResult = [] 43 | 44 | for file in files: 45 | # folders = file[0][0].split("/") 46 | _parseFiles(file, result) 47 | result = "[" + (', '.join([json.dumps(x) for x in result])) + "]" 48 | 49 | return result 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /front/templates/front/latest.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/layout.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Latest analysis

9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for firmware in firmwares.firmwares%} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 | 33 |
hashnamebrandversion
{{ firmware.hash }}{{ firmware.name }}{{ firmware.brand }}{{ firmware.version }}
34 |
35 |
36 | {% endblock %} -------------------------------------------------------------------------------- /api/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | 5 | 6 | class BrandModel(models.Model): 7 | name = models.CharField(unique=True, max_length=255) 8 | 9 | 10 | class FirmwareModel(models.Model): 11 | brand = models.ForeignKey(BrandModel, related_name="firmwares") 12 | hash = models.CharField(unique=True, max_length=32) 13 | status = models.CharField(max_length=20, default="waiting") 14 | name = models.CharField(max_length=255) 15 | description = models.TextField(blank=True, null=True) 16 | model = models.CharField(max_length=255) 17 | filepath = models.CharField(max_length=255) 18 | version = models.CharField(max_length=255) 19 | filesize = models.IntegerField() 20 | created_at = models.DateTimeField(auto_now_add=True) 21 | 22 | 23 | class FileModel(models.Model): 24 | firmware = models.ManyToManyField(FirmwareModel, related_name="files") 25 | hash = models.CharField(unique=True, max_length=32) 26 | filename = models.CharField(max_length=255) 27 | filepath = models.CharField(max_length=255) 28 | filesize = models.IntegerField() 29 | graph_file = models.CharField(max_length=255, 30 | default="", 31 | null=True, 32 | blank=True) 33 | imports = models.TextField() 34 | informations = models.TextField() 35 | file_type = models.TextField() 36 | nb_loots = models.IntegerField(default=0) 37 | 38 | 39 | class LootTypeModel(models.Model): 40 | name = models.CharField(unique=True, max_length=255) 41 | 42 | 43 | class LootModel(models.Model): 44 | file = models.ForeignKey(FileModel, related_name="loots") 45 | type = models.ForeignKey(LootTypeModel, related_name="loots") 46 | info = models.TextField(null=True, blank=True) 47 | -------------------------------------------------------------------------------- /lib/parseELF.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from api.models import FileModel, LootModel, LootTypeModel 3 | import r2pipe 4 | import os 5 | import pydot 6 | 7 | 8 | def open_pipe(file): 9 | return r2pipe.open(file.filepath) 10 | 11 | 12 | def is_elf(file): 13 | with open(file.filepath, "rb") as fd: 14 | head = fd.read(4) 15 | return (b"\x7FELF" == head) 16 | 17 | 18 | def parse_elf(workspace, file): 19 | r2 = r2pipe.open(file.filepath) 20 | r2.cmd("aa") 21 | r2.cmd("afl") 22 | result = r2.cmd("agC") 23 | output_dir = os.path.join(workspace, "graphs") 24 | if not os.path.exists(output_dir): 25 | os.makedirs(output_dir) 26 | 27 | out_file = os.path.join(output_dir, file.hash) 28 | graph = pydot.graph_from_dot_data(result) 29 | graph[0].write_png(out_file) 30 | file.graph_file = out_file 31 | file.save() 32 | print("%s parsed" % file.filepath) 33 | 34 | 35 | def insecure_imports(file, handle): 36 | r2 = handle 37 | imports = "\n".join([_ for _ in r2.cmd("ii").split("\n") if "name=" in _]) 38 | file.imports = imports.replace(settings.FIRMWARES_FOLDER, "") 39 | file.save() 40 | type = "potentially insecure function" 41 | for insecure_function in settings.INSECURE_FUNCTIONS: 42 | if insecure_function in imports: 43 | try: 44 | loot_type = LootTypeModel.objects.get(name=type) 45 | except LootTypeModel.DoesNotExist: 46 | loot_type = LootTypeModel() 47 | loot_type.name = type 48 | loot_type.save() 49 | 50 | loot = LootModel() 51 | loot.file = file 52 | loot.type = loot_type 53 | loot.info = insecure_function 54 | loot.save() 55 | 56 | 57 | def binary_informations(file, handle): 58 | r2 = handle 59 | informations = r2.cmd("i") 60 | file.informations = informations.replace(settings.FIRMWARES_FOLDER, '') 61 | file.save() 62 | -------------------------------------------------------------------------------- /front/templates/front/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/layout.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Search

9 |

Search by constructor or model name

10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for firmware in firmwares.results%} 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 | 43 |
hashnamebrandversion
{{ firmware.hash }}{{ firmware.name }}{{ firmware.brand }}{{ firmware.version }}
44 |
45 |
46 | {% endblock %} -------------------------------------------------------------------------------- /front/templates/front/summary.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/layout.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

{{ firmware.name }}

9 |

by {{ firmware.brand }}

10 |

{{ firmware.description }}

11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |

Informations

19 |
20 |
21 |
22 | {% if firmware.status == "done" %} 23 | 24 |

Files

25 |
26 |
27 | 34 |
35 |
36 |

Informations

37 |
38 |
    39 | {% for type, nb in firmware.loots.items %} 40 |
  • 41 | {{ type }}: {{ nb }} 42 |
  • 43 | {% endfor %} 44 |
45 |
46 |
47 |
48 | {% else %} 49 |

Parsing not completed

50 |

Go grab a beer and come back later.

51 | {% endif %} 52 |
53 |
54 | {% endblock %} -------------------------------------------------------------------------------- /lib/extract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import tarfile 5 | 6 | from lib.extractor import Extractor as FirmadyneExtractor 7 | 8 | 9 | class Extractor(): 10 | def __init__(self, workspace, input): 11 | self.input = input 12 | self.workspace = workspace 13 | 14 | def extract(self): 15 | candidates = {} 16 | self.output = os.path.join(self.workspace, "extracted") 17 | print("Start extract") 18 | extractor = FirmadyneExtractor(indir=self.input, 19 | outdir=self.output, 20 | rootfs=True, 21 | kernel=True, 22 | numproc=True, 23 | server=None, 24 | brand=None) 25 | extractor.extract() 26 | self.process_extraction() 27 | print("Extract completed") 28 | return os.path.join(self.workspace, "extracted") 29 | 30 | def process_extraction(self): 31 | files = os.listdir(self.output) 32 | for file in files: 33 | filename, extension = os.path.splitext(file) 34 | if file.endswith(".kernel"): 35 | os.remove(os.path.join(self.output, file)) 36 | elif file.endswith(".tar.gz"): 37 | self.untar(os.path.join(self.output, file), 38 | self.output) 39 | os.remove(os.path.join(self.output, file)) 40 | 41 | def untar(self, archive, path): 42 | with tarfile.open(archive, 'r:gz') as tar: 43 | for file_ in tar: 44 | if file_.name in [".", ".."]: 45 | continue 46 | try: 47 | tar.extract(file_, path=path) 48 | except IOError: 49 | try: 50 | os.remove(path + file_.name) 51 | tar.extract(file_, path) 52 | except: 53 | pass 54 | finally: 55 | try: 56 | os.chmod(path + file_.name, file_.mode) 57 | except: 58 | """ If anyone asks, i've never wrote that 59 | """ 60 | pass 61 | -------------------------------------------------------------------------------- /front/templates/front/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/layout.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Quick pwnd !

9 |

Upload a firmware archive and start analyzing it

10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 | {% csrf_token %} 42 | 43 |
44 |
45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /front/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import Http404 3 | from api.views import api_upload, api_get_hierarchy, api_get_firmware_summary 4 | from api.views import api_get_file, api_get_latest, api_get_stats, api_search 5 | import json 6 | 7 | 8 | def home(request): 9 | """ Display home page 10 | """ 11 | results = api_get_stats(request) 12 | json_result = json.loads(results.content.decode("utf-8")) 13 | print(json_result) 14 | return render(request, 'front/home.html', {'stats': json_result}) 15 | 16 | def latest(request): 17 | """ Return latest analysis 18 | """ 19 | results = api_get_latest(request) 20 | json_result = json.loads(results.content.decode("utf-8")) 21 | print(json_result) 22 | return render(request, 'front/latest.html', {'firmwares': json_result}) 23 | pass 24 | 25 | def search(request): 26 | """ Display search page 27 | """ 28 | try: 29 | k = request.GET.get('keyword', False) 30 | results = api_search(request) 31 | json_result = json.loads(results.content.decode("utf-8")) 32 | print(json_result) 33 | return render(request, 'front/search.html', {'firmwares': json_result, 'keyword':k}) 34 | except NotImplementedError: 35 | raise Http404('

Not firmware found

') 36 | 37 | def upload(request): 38 | """ Display upload page 39 | """ 40 | results = api_upload(request) 41 | json_result = json.loads(results.content.decode("utf-8")) 42 | print(json_result) 43 | return render(request, 'front/upload.html', {'upload': json_result}) 44 | 45 | def get_firmware_summary(request, hash): 46 | """ Display summary page for a firmware 47 | """ 48 | results = api_get_firmware_summary(request, hash) 49 | json_result = json.loads(results.content.decode("utf-8")) 50 | results_hierarchy = api_get_hierarchy(request, hash) 51 | json_result_hierarchy = json.loads(results_hierarchy.content.decode("utf-8")) 52 | ordered_filelist = sorted(json_result_hierarchy['files'], key=lambda file: file['filename']) 53 | return render(request, 'front/summary.html', {'firmware': json_result, 'hierarchy': ordered_filelist}) 54 | 55 | def get_file(request, hash): 56 | """ Display file page 57 | """ 58 | results = api_get_file(request, hash) 59 | json_result = json.loads(results.content.decode("utf-8")) 60 | print(json_result) 61 | return render(request, 'front/file.html', {'file': json_result}) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firmflaws 2 | Firmware analysis Website and API (JSON) 3 | 4 | Upload firmware and run static analysis (parse firmware, grep strings, search for interesting files (conf, certs, db files...), etc.). 5 | 6 | ## Usage 7 | - Upload a new firmware 8 | - PROFIT ! 9 | 10 | ## WEB INTERFACE 11 | 12 | ### Homepage (upload) 13 | ![alt tag](https://raw.githubusercontent.com/Ganapati/firmflaws/master/images/home.jpg) 14 | 15 | ### Firmware summary 16 | ![alt tag](https://raw.githubusercontent.com/Ganapati/firmflaws/master/images/firmware_summary.jpg) 17 | 18 | 19 | ### PHP file summary 20 | ![alt tag](https://raw.githubusercontent.com/Ganapati/firmflaws/master/images/file_analysis.jpg) 21 | 22 | ### ELF file summary 23 | ![alt tag](https://raw.githubusercontent.com/Ganapati/firmflaws/master/images/elf.jpg) 24 | 25 | ### Picture file 26 | ![alt tag](https://raw.githubusercontent.com/Ganapati/firmflaws/master/images/image.jpg) 27 | 28 | 29 | ## API 30 | ### POST /api/upload 31 | - description : test 32 | - brand : text 33 | - model : text 34 | - version : text 35 | - file :file (firmware archive (bin, zip)) 36 | 37 | ### GET /api/stats 38 | Return global stats 39 | - nb of firmwares in db 40 | - nb of password grepped 41 | - nb of certificates 42 | - etc 43 | 44 | ### GET /api/latest 45 | Return 10 latests firmwares analysis 46 | 47 | ### GET /api/firmware/#hash# 48 | Return firmware informations (files, etc.) 49 | 50 | ### GET /api/firmware/summary/#hash# 51 | Return firmware informations without files 52 | 53 | ### GET /api/firmware/hierarchy/#hash# 54 | Return firmware informations + hierarchy as string for treeeJS frontend 55 | 56 | ### GET /api/firmware/#hash#?raw 57 | Download firmware 58 | 59 | ### GET /api/file/#hash#/ 60 | Return file informations 61 | 62 | ### GET /api/file/#hash#?raw 63 | Download File 64 | 65 | ### GET /api/file/#hash#?graph 66 | If file is ELF and graph=true, return a png call graph (generated by radare2) 67 | 68 | ### GET /api/search?keyword= 69 | Search firmwares for given keyword 70 | 71 | ## Dependencies 72 | - Radare2 from github : https://github.com/radare/radare2.git 73 | - Binwalk from github : https://github.com/devttys0/binwalk 74 | - rats : https://security.web.cern.ch/security/recommendations/en/codetools/rats.shtml 75 | - graphviz 76 | - pydot 77 | - Django 78 | - r2pipe 79 | - python-magic 80 | - squashfs-tools 81 | - python3-openssl 82 | 83 | ## Contributors 84 | - MisterCh0c (@MisterCh0c) 85 | - Ganapati (@G4N4P4T1) 86 | - Geoffrey (@geoffreyvdberge) 87 | -------------------------------------------------------------------------------- /front/templates/front/layout.html: -------------------------------------------------------------------------------- 1 | {% load static from staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Firmflaws 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 58 | 59 | {% block content %}{% endblock %} 60 | 61 | 62 |
63 | 64 |
65 |
66 |

Beerware License

67 |
68 |
69 |
70 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /static/js/jquery.easing.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ 3 | * 4 | * Uses the built in easing capabilities added In jQuery 1.1 5 | * to offer multiple easing options 6 | * 7 | * TERMS OF USE - EASING EQUATIONS 8 | * 9 | * Open source under the BSD License. 10 | * 11 | * Copyright © 2001 Robert Penner 12 | * All rights reserved. 13 | * 14 | * TERMS OF USE - jQuery Easing 15 | * 16 | * Open source under the BSD License. 17 | * 18 | * Copyright © 2008 George McGinley Smith 19 | * All rights reserved. 20 | * 21 | * Redistribution and use in source and binary forms, with or without modification, 22 | * are permitted provided that the following conditions are met: 23 | * 24 | * Redistributions of source code must retain the above copyright notice, this list of 25 | * conditions and the following disclaimer. 26 | * Redistributions in binary form must reproduce the above copyright notice, this list 27 | * of conditions and the following disclaimer in the documentation and/or other materials 28 | * provided with the distribution. 29 | * 30 | * Neither the name of the author nor the names of contributors may be used to endorse 31 | * or promote products derived from this software without specific prior written permission. 32 | * 33 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 34 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 35 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 36 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 37 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 38 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 39 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 40 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 41 | * OF THE POSSIBILITY OF SUCH DAMAGE. 42 | * 43 | */ 44 | jQuery.easing.jswing=jQuery.easing.swing;jQuery.extend(jQuery.easing,{def:"easeOutQuad",swing:function(e,f,a,h,g){return jQuery.easing[jQuery.easing.def](e,f,a,h,g)},easeInQuad:function(e,f,a,h,g){return h*(f/=g)*f+a},easeOutQuad:function(e,f,a,h,g){return -h*(f/=g)*(f-2)+a},easeInOutQuad:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f+a}return -h/2*((--f)*(f-2)-1)+a},easeInCubic:function(e,f,a,h,g){return h*(f/=g)*f*f+a},easeOutCubic:function(e,f,a,h,g){return h*((f=f/g-1)*f*f+1)+a},easeInOutCubic:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f+a}return h/2*((f-=2)*f*f+2)+a},easeInQuart:function(e,f,a,h,g){return h*(f/=g)*f*f*f+a},easeOutQuart:function(e,f,a,h,g){return -h*((f=f/g-1)*f*f*f-1)+a},easeInOutQuart:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f+a}return -h/2*((f-=2)*f*f*f-2)+a},easeInQuint:function(e,f,a,h,g){return h*(f/=g)*f*f*f*f+a},easeOutQuint:function(e,f,a,h,g){return h*((f=f/g-1)*f*f*f*f+1)+a},easeInOutQuint:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f*f+a}return h/2*((f-=2)*f*f*f*f+2)+a},easeInSine:function(e,f,a,h,g){return -h*Math.cos(f/g*(Math.PI/2))+h+a},easeOutSine:function(e,f,a,h,g){return h*Math.sin(f/g*(Math.PI/2))+a},easeInOutSine:function(e,f,a,h,g){return -h/2*(Math.cos(Math.PI*f/g)-1)+a},easeInExpo:function(e,f,a,h,g){return(f==0)?a:h*Math.pow(2,10*(f/g-1))+a},easeOutExpo:function(e,f,a,h,g){return(f==g)?a+h:h*(-Math.pow(2,-10*f/g)+1)+a},easeInOutExpo:function(e,f,a,h,g){if(f==0){return a}if(f==g){return a+h}if((f/=g/2)<1){return h/2*Math.pow(2,10*(f-1))+a}return h/2*(-Math.pow(2,-10*--f)+2)+a},easeInCirc:function(e,f,a,h,g){return -h*(Math.sqrt(1-(f/=g)*f)-1)+a},easeOutCirc:function(e,f,a,h,g){return h*Math.sqrt(1-(f=f/g-1)*f)+a},easeInOutCirc:function(e,f,a,h,g){if((f/=g/2)<1){return -h/2*(Math.sqrt(1-f*f)-1)+a}return h/2*(Math.sqrt(1-(f-=2)*f)+1)+a},easeInElastic:function(f,h,e,l,k){var i=1.70158;var j=0;var g=l;if(h==0){return e}if((h/=k)==1){return e+l}if(!j){j=k*0.3}if(g(" + filesize + " kb)"+ " download"; 13 | infos += "

" + data["type"] + "

"; 14 | var is_image = /\.(gif|png|jpg|jpeg|bmp)$/ig.test(data["filename"]); 15 | if (is_image){ 16 | infos += ""; 17 | } 18 | 19 | // If is ELF 20 | if (data["graph"] != undefined) { 21 | if (data["graph"] == ""){ 22 | infos += ""; 23 | } 24 | 25 | if (data["graph"] != undefined && data["graph"] != false && data["graph"] != ""){ 26 | infos += "

Graph

"; 27 | } else if (data["graph"] != undefined && data["graph"] == false && data["graph"] != "") { 28 | infos += "

Graph

Graph generation failed.
"; 29 | } 30 | 31 | } 32 | 33 | // Informations 34 | if (data["informations"] != undefined) { 35 | infos += "

Informations

" + htmlEntities(data["informations"]) + "
"; 36 | } 37 | 38 | //Content 39 | if (data["content"] != undefined) { 40 | infos += "

Content

" + htmlEntities(data["content"]) + "
"; 41 | } 42 | 43 | // Loots 44 | if (data["loots"] != undefined && data["loots"].length > 0) { 45 | infos += "

Loots

"; 46 | infos += "
";
 47 |             for(i=0; i < data["loots"].length; i++) {
 48 |                 infos += data["loots"][i]["type"] + ": " + data["loots"][i]["info"] + "\n";
 49 |             }
 50 |             infos += "
"; 51 | } 52 | 53 | // Imports 54 | if (data["imports"] != undefined) { 55 | infos += "

Imports

" + htmlEntities(data["imports"]) + "
"; 56 | } 57 | 58 | $("#file_infos").html(infos); 59 | }); 60 | scrollToAnchor('content'); 61 | } 62 | 63 | function scrollToAnchor(aid){ 64 | var aTag = $("a[id='"+ aid +"']"); 65 | $('html,body').animate({scrollTop: aTag.offset().top},'slow'); 66 | } 67 | 68 | function htmlEntities(str) { 69 | return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); 70 | } 71 | 72 | function create_graph(hash){ 73 | $("#gen_graph").html(""); 74 | $.get( "/api/file/"+hash+"?graph", function( data ) { 75 | if (data["graph"] != undefined && data["graph"] != false){ 76 | $("#gen_graph").html("

Graph

"); 77 | } else if (data["graph"] != undefined && data["graph"] == false) { 78 | $("#gen_graph").html("

Graph

Graph generation failed."); 79 | } 80 | 81 | }); 82 | } 83 | 84 | var waitingDialog = waitingDialog || (function ($) { 85 | 'use strict'; 86 | 87 | // Creating modal dialog's DOM 88 | var $dialog = $( 89 | ''); 97 | 98 | return { 99 | /** 100 | * Opens our dialog 101 | * @param message Custom message 102 | * @param options Custom options: 103 | * options.dialogSize - bootstrap postfix for dialog size, e.g. "sm", "m"; 104 | * options.progressType - bootstrap postfix for progress bar type, e.g. "success", "warning". 105 | */ 106 | show: function (message, options) { 107 | // Assigning defaults 108 | if (typeof options === 'undefined') { 109 | options = {}; 110 | } 111 | if (typeof message === 'undefined') { 112 | message = 'Loading'; 113 | } 114 | var settings = $.extend({ 115 | dialogSize: 'm', 116 | progressType: '', 117 | onHide: null // This callback runs after the dialog was hidden 118 | }, options); 119 | 120 | // Configuring dialog 121 | $dialog.find('.modal-dialog').attr('class', 'modal-dialog').addClass('modal-' + settings.dialogSize); 122 | $dialog.find('.progress-bar').attr('class', 'progress-bar'); 123 | if (settings.progressType) { 124 | $dialog.find('.progress-bar').addClass('progress-bar-' + settings.progressType); 125 | } 126 | $dialog.find('h3').text(message); 127 | // Adding callbacks 128 | if (typeof settings.onHide === 'function') { 129 | $dialog.off('hidden.bs.modal').on('hidden.bs.modal', function (e) { 130 | settings.onHide.call($dialog); 131 | }); 132 | } 133 | // Opening dialog 134 | $dialog.modal(); 135 | }, 136 | /** 137 | * Closes dialog 138 | */ 139 | hide: function () { 140 | $dialog.modal('hide'); 141 | } 142 | }; 143 | 144 | })(jQuery); -------------------------------------------------------------------------------- /firmflaws/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for firmflaws project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'CHANGE_ME_MOTHERFUCKER' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'corsheaders', 41 | 'lib', 42 | 'api', 43 | 'front', 44 | 'django_cleanup', 45 | ] 46 | 47 | MIDDLEWARE_CLASSES = [ 48 | 'corsheaders.middleware.CorsMiddleware', 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | 'firmflaws.settings.NonHtmlDebugToolbarMiddleware', 58 | ] 59 | 60 | CORS_ORIGIN_ALLOW_ALL = True 61 | 62 | ROOT_URLCONF = 'firmflaws.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [ 68 | os.path.join(BASE_DIR, 'front', 'templates') 69 | ], 70 | 'APP_DIRS': True, 71 | 'OPTIONS': { 72 | 'context_processors': [ 73 | 'django.template.context_processors.debug', 74 | 'django.template.context_processors.request', 75 | 'django.contrib.auth.context_processors.auth', 76 | 'django.contrib.messages.context_processors.messages', 77 | ], 78 | }, 79 | }, 80 | ] 81 | 82 | WSGI_APPLICATION = 'firmflaws.wsgi.application' 83 | 84 | 85 | # Database 86 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.sqlite3', 90 | 'NAME': 'db.sqlite3' 91 | } 92 | } 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | import json 127 | from django.http import HttpResponse 128 | class NonHtmlDebugToolbarMiddleware(object): 129 | """ 130 | The Django Debug Toolbar usually only works for views that return HTML. 131 | This middleware wraps any non-HTML response in HTML if the request 132 | has a 'debug' query parameter (e.g. http://localhost/foo?debug) 133 | Special handling for json (pretty printing) and 134 | binary data (only show data length) 135 | """ 136 | 137 | @staticmethod 138 | def process_response(request, response): 139 | debug = request.GET.get('debug', 'UNSET') 140 | 141 | if debug != 'UNSET': 142 | if response['Content-Type'] == 'application/octet-stream': 143 | new_content = 'Binary Data, ' \ 144 | 'Length: {}'.format(len(response.content)) 145 | response = HttpResponse(new_content.decode("utf-8")) 146 | elif response['Content-Type'] != 'text/html': 147 | content = response.content 148 | try: 149 | json_ = json.loads(content.decode("utf-8")) 150 | content = json.dumps(json_, sort_keys=True, indent=2) 151 | except ValueError: 152 | pass 153 | response = HttpResponse('
{}'
154 |                                         '
'.format(content)) 155 | 156 | return response 157 | 158 | 159 | # Static files (CSS, JavaScript, Images) 160 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 161 | 162 | STATIC_URL = '/static/' 163 | STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),) 164 | 165 | # Rats static code analysis : https://security.web.cern.ch/security/recommendations/en/codetools/rats.shtml 166 | RATS_BINARY = "/bin/rats" 167 | 168 | FIRMWARES_FOLDER = '/tmp/firmflaws/' 169 | LOOTS_FILENAMES = {"certificate": ['*.pem','*.crt','*p7b','*p12','*.cer'], 170 | "configuration": ['*.conf','*.cfg','*.ini', '*.xml', "*.sql"], 171 | "RSA": ['id_rsa','*id_rsa*','id_dsa','*id_dsa*','*.pub'], 172 | "databases": ['*.db','*.sqlite'], 173 | "passwords": ['passwd','shadow','*.psk']} 174 | 175 | LOOTS_GREP = {"interesting greps": ["passwd", "password"], 176 | "ipv4": ["(?:[0-9]{1,3}\.){3}[0-9]{1,3}"], 177 | "email": ["[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"]} 178 | 179 | INSECURE_FUNCTIONS = ['strcpy', 'strcat', 'sprintf', 'vsprintf', 'gets', 'strlen', 'scanf', 'fscanf', 'sscanf', 'vscanf', 'vsscanf', 'vfscanf', 'realpath', 'getopt', 'getpass', 'streadd', 'strecpy', 'strtrns', 'getwd'] 180 | -------------------------------------------------------------------------------- /api/management/commands/process_firmware.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from api.models import FirmwareModel, FileModel, LootModel, LootTypeModel 3 | from lib.extract import Extractor 4 | from django.conf import settings 5 | from lib.parseELF import insecure_imports, is_elf, binary_informations, open_pipe 6 | from lib.rats import is_parsable, parse 7 | from lib.cert import is_cert, check_cert 8 | import magic 9 | import re 10 | import os 11 | import fnmatch 12 | import shutil 13 | import uuid 14 | import hashlib 15 | from pathlib import Path 16 | 17 | 18 | class Command(BaseCommand): 19 | help = 'process firmware' 20 | 21 | def handle(self, *args, **options): 22 | process_lock = os.path.join(settings.FIRMWARES_FOLDER, ".process_lock") 23 | 24 | if not os.path.isfile(process_lock): 25 | Path(process_lock).touch() 26 | while True: 27 | try: 28 | self.firmware = FirmwareModel.objects.filter(status="waiting")[0] 29 | try: 30 | self.run() 31 | except: 32 | self.firmware.status = "failed" 33 | self.firmware.save() 34 | except IndexError: 35 | self.stdout.write("No waiting firmwares") 36 | break 37 | os.remove(process_lock) 38 | 39 | def run(self): 40 | self.workspace = self.firmware.filepath.replace("firmware", "") 41 | self.set_status("0") 42 | extractor = Extractor(self.workspace, self.firmware.filepath) 43 | self.extracted_path = extractor.extract() 44 | self.set_status("50") 45 | self.register_files() 46 | self.set_status("done") 47 | 48 | def register_files(self): 49 | print("Start registering files") 50 | for root, dirs, files in os.walk(self.extracted_path): 51 | for file in files: 52 | full_path = os.path.join(root, file) 53 | if not os.path.isfile(full_path): 54 | continue 55 | path = full_path.replace(self.extracted_path, "") 56 | content = "" 57 | hash = "" 58 | with open(full_path, "rb") as fd: 59 | content = fd.read() 60 | hash_content = "%s:%s" % (file, content) 61 | hash = hashlib.md5(hash_content.encode('utf-8')).hexdigest() 62 | try: 63 | file_obj = FileModel.objects.get(hash=hash) 64 | file_obj.firmware.add(self.firmware) 65 | file_obj.save() 66 | except FileModel.DoesNotExist: 67 | try: 68 | file_obj = FileModel() 69 | file_obj.filepath = os.path.join(root, file) 70 | file_obj.hash = hash 71 | file_obj.filesize = len(content) 72 | file_obj.filename = path 73 | file_obj.save() 74 | file_obj.firmware.add(self.firmware) 75 | file_obj.file_type = magic.from_file(os.path.join(root, 76 | file)) 77 | file_obj.save() 78 | self.find_loots(file_obj) 79 | # Performance tweak 80 | file_obj.nb_loots = file_obj.loots.all().count() 81 | except: 82 | file_obj.file_type = "unknown" 83 | 84 | print("Files registered") 85 | 86 | def find_loots(self, file): 87 | # Find loots based on filenames 88 | loots_refs = settings.LOOTS_FILENAMES 89 | for type, values in loots_refs.items(): 90 | try: 91 | loot_type = LootTypeModel.objects.get(name=type) 92 | loot_type.save() 93 | except LootTypeModel.DoesNotExist: 94 | loot_type = LootTypeModel() 95 | loot_type.name = type 96 | loot_type.save() 97 | for value in values: 98 | if fnmatch.fnmatch(file.filename, value): 99 | loot = LootModel() 100 | loot.file = file 101 | loot.type = loot_type 102 | loot.info = "Filename looks interesting" 103 | loot.save() 104 | 105 | # Find greppable loots 106 | loots_refs = settings.LOOTS_GREP 107 | 108 | with open(file.filepath, "rb") as fd: 109 | content = fd.read() 110 | 111 | for type, values in loots_refs.items(): 112 | try: 113 | loot_type = LootTypeModel.objects.get(name=type) 114 | except LootTypeModel.DoesNotExist: 115 | loot_type = LootTypeModel() 116 | loot_type.name = type 117 | loot_type.save() 118 | for value in values: 119 | matchs = re.findall(str.encode(value), 120 | content, 121 | re.IGNORECASE | re.MULTILINE) 122 | if matchs: 123 | for match in matchs: 124 | try: 125 | loot = LootModel.objects.get(type=loot_type, 126 | file=file, 127 | info=match.decode("utf-8")) 128 | continue 129 | except LootModel.DoesNotExist: 130 | loot = LootModel() 131 | loot.file = file 132 | loot.type = loot_type 133 | loot.info = match.decode("utf-8") 134 | loot.save() 135 | 136 | if is_elf(file): 137 | handle = open_pipe(file) 138 | insecure_imports(file, handle) 139 | binary_informations(file, handle) 140 | 141 | if is_parsable(file.filepath): 142 | type = "static source analysis" 143 | try: 144 | loot_type = LootTypeModel.objects.get(name=type) 145 | except LootTypeModel.DoesNotExist: 146 | loot_type = LootTypeModel() 147 | loot_type.name = type 148 | loot_type.save() 149 | for msg in parse(file.filepath): 150 | loot = LootModel() 151 | loot.file = file 152 | loot.type = loot_type 153 | loot.info = msg 154 | loot.save() 155 | 156 | if is_cert(file.filepath): 157 | type = "certificate" 158 | try: 159 | loot_type = LootTypeModel.objects.get(name=type) 160 | except LootTypeModel.DoesNotExist: 161 | loot_type = LootTypeModel() 162 | loot_type.name = type 163 | loot_type.save() 164 | try: 165 | for msg in check_cert(file.filepath): 166 | loot = LootModel() 167 | loot.file = file 168 | loot.type = loot_type 169 | loot.info = msg 170 | loot.save() 171 | except: 172 | pass 173 | 174 | def set_status(self, status): 175 | self.firmware.status = status 176 | self.firmware.save() 177 | -------------------------------------------------------------------------------- /static/css/custom.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background-color: #f0f0f0; 4 | width: 100%; 5 | height: 100%; 6 | font-family: 'Open Sans','Helvetica Neue',Arial,sans-serif; 7 | } 8 | 9 | a { 10 | color: yellowgreen; 11 | -webkit-transition: all .35s; 12 | -moz-transition: all .35s; 13 | transition: all .35s; 14 | } 15 | 16 | a:hover, 17 | a:focus { 18 | color: forestgreen; 19 | } 20 | 21 | p { 22 | font-size: 16px; 23 | line-height: 1.5; 24 | } 25 | 26 | header { 27 | position: relative; 28 | width: 100%; 29 | min-height: auto; 30 | text-align: center; 31 | color: #fff; 32 | background-image: url('../images/header.jpg'); 33 | background-position: center; 34 | -webkit-background-size: cover; 35 | -moz-background-size: cover; 36 | background-size: cover; 37 | -o-background-size: cover; 38 | } 39 | 40 | header .header-content { 41 | position: relative; 42 | width: 100%; 43 | padding: 100px 15px 70px; 44 | text-align: center; 45 | } 46 | 47 | header .header-content .header-content-inner h1 { 48 | margin-top: 0; 49 | margin-bottom: 20px; 50 | font-size: 50px; 51 | font-weight: 300; 52 | } 53 | 54 | header .header-content .header-content-inner p { 55 | margin-bottom: 50px; 56 | font-size: 16px; 57 | font-weight: 300; 58 | color: rgba(255,255,255,.7); 59 | } 60 | 61 | @media(min-width:768px) { 62 | header { 63 | min-height: 100%; 64 | } 65 | 66 | header .header-content { 67 | position: absolute; 68 | top: 50%; 69 | padding: 0 50px; 70 | -webkit-transform: translateY(-50%); 71 | -ms-transform: translateY(-50%); 72 | transform: translateY(-50%); 73 | } 74 | 75 | header .header-content .header-content-inner { 76 | margin-right: auto; 77 | margin-left: auto; 78 | max-width: 1000px; 79 | } 80 | 81 | header .header-content .header-content-inner h1 { 82 | font-size: 100px; 83 | word-wrap: break-word; 84 | } 85 | 86 | header .header-content .header-content-inner p { 87 | margin-right: auto; 88 | margin-left: auto; 89 | max-width: 80%; 90 | font-size: 18px; 91 | } 92 | } 93 | 94 | .section-heading { 95 | margin-top: 0; 96 | margin-bottom: 20px; 97 | } 98 | 99 | .intro { 100 | color: #fff; 101 | background-color: yellowgreen; 102 | padding: 70px 0; 103 | text-align: center; 104 | } 105 | 106 | .content { 107 | background-color: #f0f0f0; 108 | padding: 100px 0; 109 | } 110 | 111 | .content-2 { 112 | color: #fff; 113 | background-color: #222; 114 | } 115 | 116 | .content-3 { 117 | padding: 20px 0 40px; 118 | text-align: center; 119 | } 120 | 121 | .promo, 122 | .promo h3, 123 | .promo a:link, 124 | .promo a:visited, 125 | .promo a:hover, 126 | .promo a:active { 127 | color: white; 128 | text-shadow: 0px 0px 40px black; 129 | text-decoration: none; 130 | } 131 | 132 | .promo-item { 133 | height: 200px; 134 | line-height: 180px; 135 | text-align: center; 136 | } 137 | 138 | .promo-item:hover { 139 | background-size: 110%; 140 | border: 10px solid rgba(255,255,255,0.3); 141 | line-height: 160px; 142 | } 143 | 144 | .promo-item h3 { 145 | font-size: 40px; 146 | display: inline-block; 147 | vertical-align: middle; 148 | } 149 | 150 | .item-1 { 151 | background: url('../images/writing.jpg'); 152 | } 153 | 154 | .item-2 { 155 | background: url('../images/concert.jpg'); 156 | } 157 | 158 | .item-3 { 159 | background: url('../images/pencil_sharpener.jpg'); 160 | } 161 | 162 | .item-1, 163 | .item-2, 164 | .item-3 { 165 | background-size: cover; 166 | background-position: 50% 50%; 167 | } 168 | 169 | .page-footer { 170 | background-color: #ffffff; 171 | text-align: center; 172 | } 173 | 174 | .page-footer .contact { 175 | padding: 100px 0; 176 | background-color: yellowgreen; 177 | color: #fff; 178 | } 179 | 180 | .page-footer .contact p { 181 | font-size: 22px; 182 | font-weight: 300; 183 | } 184 | 185 | .content-3 .glyphicon, 186 | .page-footer .contact .glyphicon { 187 | font-size: 32px; 188 | font-weight: 700; 189 | } 190 | 191 | .page-footer .small-print { 192 | padding: 50px 0 40px; 193 | font-weight: 300; 194 | } 195 | 196 | .text-light { 197 | color: rgba(255,255,255,.7); 198 | } 199 | 200 | .navbar-default { 201 | border-color: rgba(34,34,34,.05); 202 | background-color: #fff; 203 | -webkit-transition: all .35s; 204 | -moz-transition: all .35s; 205 | transition: all .35s; 206 | } 207 | 208 | .navbar-default .navbar-header .navbar-brand { 209 | color: yellowgreen; 210 | } 211 | 212 | .navbar-default .navbar-header .navbar-brand:hover, 213 | .navbar-default .navbar-header .navbar-brand:focus { 214 | color: #eb3812; 215 | } 216 | 217 | .navbar-default .nav > li>a, 218 | .navbar-default .nav>li>a:focus { 219 | color: #222; 220 | } 221 | 222 | .navbar-default .nav > li>a:hover, 223 | .navbar-default .nav>li>a:focus:hover { 224 | color: yellowgreen; 225 | } 226 | 227 | .navbar-default .nav > li.active>a, 228 | .navbar-default .nav>li.active>a:focus { 229 | color: yellowgreen!important; 230 | background-color: transparent; 231 | } 232 | 233 | .navbar-default .nav > li.active>a:hover, 234 | .navbar-default .nav>li.active>a:focus:hover { 235 | background-color: transparent; 236 | } 237 | 238 | @media(min-width:768px) { 239 | .navbar-default { 240 | border-color: rgba(255,255,255,.3); 241 | background-color: transparent; 242 | } 243 | 244 | .navbar-default .navbar-header .navbar-brand { 245 | color: rgba(255,255,255,.7); 246 | letter-spacing: 0.5em; 247 | } 248 | 249 | .navbar-default .navbar-header .navbar-brand:hover, 250 | .navbar-default .navbar-header .navbar-brand:focus { 251 | color: #fff; 252 | } 253 | 254 | .navbar-default .nav > li>a, 255 | .navbar-default .nav>li>a:focus { 256 | color: rgba(255,255,255,.7); 257 | } 258 | 259 | .navbar-default .nav > li>a:hover, 260 | .navbar-default .nav>li>a:focus:hover { 261 | color: #fff; 262 | } 263 | 264 | .navbar-default.affix { 265 | border-color: #fff; 266 | background-color: #fff; 267 | box-shadow: 0px 7px 20px 0px rgba(0,0,0,0.1); 268 | } 269 | 270 | .navbar-default.affix .navbar-header .navbar-brand { 271 | letter-spacing: 0; 272 | color: yellowgreen; 273 | } 274 | 275 | .navbar-default.affix .navbar-header .navbar-brand:hover, 276 | .navbar-default.affix .navbar-header .navbar-brand:focus { 277 | color: #eb3812; 278 | } 279 | 280 | .navbar-default.affix .nav > li>a, 281 | .navbar-default.affix .nav>li>a:focus { 282 | color: #222; 283 | } 284 | 285 | .navbar-default.affix .nav > li>a:hover, 286 | .navbar-default.affix .nav>li>a:focus:hover { 287 | color: yellowgreen; 288 | } 289 | } 290 | 291 | .btn-default { 292 | border-color: #fff; 293 | color: #222; 294 | background-color: #fff; 295 | -webkit-transition: all .35s; 296 | -moz-transition: all .35s; 297 | transition: all .35s; 298 | } 299 | 300 | .btn-default:hover, 301 | .btn-default:focus, 302 | .btn-default.focus, 303 | .btn-default:active, 304 | .btn-default.active, 305 | .open > .dropdown-toggle.btn-default { 306 | border-color: #eee; 307 | color: #222; 308 | background-color: #eee; 309 | } 310 | 311 | .btn-default:active, 312 | .btn-default.active, 313 | .open > .dropdown-toggle.btn-default { 314 | background-image: none; 315 | } 316 | 317 | .btn-default.disabled, 318 | .btn-default[disabled], 319 | fieldset[disabled] .btn-default, 320 | .btn-default.disabled:hover, 321 | .btn-default[disabled]:hover, 322 | fieldset[disabled] .btn-default:hover, 323 | .btn-default.disabled:focus, 324 | .btn-default[disabled]:focus, 325 | fieldset[disabled] .btn-default:focus, 326 | .btn-default.disabled.focus, 327 | .btn-default[disabled].focus, 328 | fieldset[disabled] .btn-default.focus, 329 | .btn-default.disabled:active, 330 | .btn-default[disabled]:active, 331 | fieldset[disabled] .btn-default:active, 332 | .btn-default.disabled.active, 333 | .btn-default[disabled].active, 334 | fieldset[disabled] .btn-default.active { 335 | border-color: #fff; 336 | background-color: #fff; 337 | } 338 | 339 | .btn-default .badge { 340 | color: #fff; 341 | background-color: #222; 342 | } 343 | 344 | .btn-primary { 345 | border-color: yellowgreen; 346 | color: #fff; 347 | background-color: yellowgreen; 348 | -webkit-transition: all .35s; 349 | -moz-transition: all .35s; 350 | transition: all .35s; 351 | } 352 | 353 | .btn-primary:hover, 354 | .btn-primary:focus, 355 | .btn-primary.focus, 356 | .btn-primary:active, 357 | .btn-primary.active, 358 | .open > .dropdown-toggle.btn-primary { 359 | border-color: limegreen; 360 | color: #fff; 361 | background-color: limegreen; 362 | } 363 | 364 | .btn-primary:active, 365 | .btn-primary.active, 366 | .open > .dropdown-toggle.btn-primary { 367 | background-image: none; 368 | } 369 | 370 | .btn-primary.disabled, 371 | .btn-primary[disabled], 372 | fieldset[disabled] .btn-primary, 373 | .btn-primary.disabled:hover, 374 | .btn-primary[disabled]:hover, 375 | fieldset[disabled] .btn-primary:hover, 376 | .btn-primary.disabled:focus, 377 | .btn-primary[disabled]:focus, 378 | fieldset[disabled] .btn-primary:focus, 379 | .btn-primary.disabled.focus, 380 | .btn-primary[disabled].focus, 381 | fieldset[disabled] .btn-primary.focus, 382 | .btn-primary.disabled:active, 383 | .btn-primary[disabled]:active, 384 | fieldset[disabled] .btn-primary:active, 385 | .btn-primary.disabled.active, 386 | .btn-primary[disabled].active, 387 | fieldset[disabled] .btn-primary.active { 388 | border-color: yellowgreen; 389 | background-color: yellowgreen; 390 | } 391 | 392 | .btn-primary .badge { 393 | color: yellowgreen; 394 | background-color: #fff; 395 | } 396 | 397 | 398 | .btn { 399 | border-radius: 300px; 400 | text-transform: uppercase; 401 | } 402 | 403 | .btn-lg { 404 | padding: 15px 30px; 405 | } 406 | 407 | ::-moz-selection { 408 | text-shadow: none; 409 | color: #fff; 410 | background: #222; 411 | } 412 | 413 | ::selection { 414 | text-shadow: none; 415 | color: #fff; 416 | background: #222; 417 | } 418 | 419 | img::selection { 420 | color: #fff; 421 | background: 0 0; 422 | } 423 | 424 | img::-moz-selection { 425 | color: #fff; 426 | background: 0 0; 427 | } 428 | 429 | .text-primary { 430 | color: yellowgreen; 431 | } 432 | 433 | .bg-primary { 434 | background-color: yellowgreen; 435 | } 436 | 437 | 438 | .table { 439 | background-color: #ffffff; 440 | } 441 | -------------------------------------------------------------------------------- /api/views.py: -------------------------------------------------------------------------------- 1 | from django.views.decorators.csrf import csrf_exempt 2 | from django.http import JsonResponse, HttpResponse 3 | from django.db import IntegrityError 4 | from django.conf import settings 5 | from django.db.models import Q 6 | from api.management.commands.process_firmware_thread import start_process_thread 7 | from api.models import FirmwareModel, FileModel, LootModel, BrandModel, LootTypeModel 8 | from lib.parseELF import is_elf, parse_elf 9 | from lib.util import parseFilesToHierarchy 10 | import hashlib 11 | import os 12 | 13 | @csrf_exempt 14 | def api_upload(request): 15 | """ Upload firmware to firmflaws 16 | """ 17 | if not request.method == 'POST': 18 | return JsonResponse({"error": "POST only"}) 19 | 20 | if not 'file' in request.FILES: 21 | return JsonResponse({"error": "No file"}) 22 | 23 | description = request.POST['description'] 24 | brand = request.POST['brand'] 25 | version = request.POST['version'] 26 | model = request.POST['model'] 27 | firmware = request.FILES['file'] 28 | 29 | brand_obj = BrandModel() 30 | brand_obj.name = brand 31 | try: 32 | brand_obj.save() 33 | except IntegrityError: 34 | brand_obj = BrandModel.objects.get(name=brand) 35 | 36 | content = firmware.read() 37 | hash = hashlib.md5(content).hexdigest() 38 | directory = os.path.join(settings.FIRMWARES_FOLDER, hash) 39 | 40 | if not os.path.exists(directory): 41 | os.makedirs(directory) 42 | 43 | path = os.path.join(directory, "firmware") 44 | with open(path, "wb") as fd: 45 | fd.write(content) 46 | 47 | firmware_obj = FirmwareModel() 48 | firmware_obj.brand = brand_obj 49 | firmware_obj.description = description 50 | firmware_obj.version = version 51 | firmware_obj.model = model 52 | firmware_obj.name = firmware.name 53 | firmware_obj.filesize = firmware.size 54 | firmware_obj.hash = hash 55 | firmware_obj.filepath = path 56 | 57 | try: 58 | firmware_obj.save() 59 | #start_process_thread() 60 | from django.core.management import call_command 61 | call_command('process_firmware') 62 | return JsonResponse({"status": "new", "hash": firmware_obj.hash}) 63 | except IntegrityError: 64 | return JsonResponse({"status": "repost", "hash": firmware_obj.hash}) 65 | 66 | def api_get_firmware(request, hash): 67 | """ Return firmware informations 68 | """ 69 | try: 70 | firmware = FirmwareModel.objects.get(hash=hash) 71 | 72 | # Direct download 73 | if 'raw' in request.GET.keys(): 74 | content = "" 75 | content_type = "application/octet-stream" 76 | with open(firmware.filepath, "rb") as fd: 77 | content = fd.read() 78 | response = HttpResponse(content, content_type=content_type) 79 | content_disposition = "attachment; filename=%s.img" % firmware.hash 80 | response["Content-Disposition"] = content_disposition 81 | return response 82 | 83 | files = [] 84 | for file in firmware.files.all(): 85 | files.append({"filename": file.filename, 86 | "size": file.filesize, 87 | "type": file.file_type, 88 | "hash": file.hash, 89 | "nb_loots": file.nb_loots}) 90 | 91 | loots = {} 92 | loots_types = [_.name for _ in LootTypeModel.objects.all()] 93 | for type in loots_types: 94 | result = LootModel.objects.filter(type__name=type, file__firmware=firmware).count() 95 | loots[type] = result 96 | 97 | return JsonResponse({"name": firmware.name, 98 | "hash": firmware.hash, 99 | "model": firmware.model, 100 | "version": firmware.version, 101 | "status": firmware.status, 102 | "loots": loots, 103 | "created_at": firmware.created_at, 104 | "files": files, 105 | "filesize": firmware.filesize, 106 | "brand": firmware.brand.name, 107 | "description": firmware.description}) 108 | 109 | except FirmwareModel.DoesNotExist: 110 | return JsonResponse({"error": "firmware not found", "hash": hash}) 111 | 112 | def api_get_firmware_summary(request, hash): 113 | """ Return firmware informations 114 | """ 115 | try: 116 | firmware = FirmwareModel.objects.get(hash=hash) 117 | 118 | loots = {} 119 | loots_types = [_.name for _ in LootTypeModel.objects.all()] 120 | for type in loots_types: 121 | result = LootModel.objects.filter(type__name=type, file__firmware=firmware).count() 122 | loots[type] = result 123 | 124 | return JsonResponse({"name": firmware.name, 125 | "hash": firmware.hash, 126 | "model": firmware.model, 127 | "version": firmware.version, 128 | "status": firmware.status, 129 | "loots": loots, 130 | "created_at": firmware.created_at, 131 | "filesize": firmware.filesize, 132 | "brand": firmware.brand.name, 133 | "description": firmware.description}) 134 | 135 | except FirmwareModel.DoesNotExist: 136 | return JsonResponse({"error": "firmware not found", "hash": hash}) 137 | 138 | 139 | def api_get_hierarchy(request, hash): 140 | try: 141 | firmware = FirmwareModel.objects.get(hash=hash) 142 | 143 | files = [] 144 | for file in firmware.files.all(): 145 | nb_loots = LootModel.objects.filter(file=file).count() 146 | files.append({"filename": file.filename, 147 | "size": file.filesize, 148 | "type": file.file_type, 149 | "hash": file.hash, 150 | "nb_loots": nb_loots}) 151 | 152 | return JsonResponse({'files': files}) 153 | except FirmwareModel.DoesNotExist: 154 | return JsonResponse({"error": "firmware not found", "hash": hash}) 155 | 156 | 157 | def api_get_file(request, hash): 158 | """ Return file from given hash 159 | """ 160 | try: 161 | file = FileModel.objects.get(hash=hash) 162 | content = "" 163 | with open(file.filepath, "rb") as fd: 164 | content = fd.read() 165 | # Direct download 166 | if 'raw' in request.GET.keys(): 167 | content = "" 168 | with open(file.filepath, "rb") as fd: 169 | content = fd.read() 170 | content_type = "application/octet-stream" 171 | response = HttpResponse(content, content_type=content_type) 172 | content_disposition = "attachment; filename=%s" % file.filename 173 | response["Content-Disposition"] = content_disposition 174 | return response 175 | 176 | # Graph download 177 | if 'graph' in request.GET.keys(): 178 | if file.graph_file != '' and file.graph_file != False: 179 | content = "" 180 | with open(file.graph_file, "rb") as fd: 181 | content = fd.read() 182 | 183 | content_type = "image/png" 184 | response = HttpResponse(content, content_type=content_type) 185 | return response 186 | elif file.graph_file == False: 187 | return HttpResponse("no graph") 188 | elif file.graph_file == '': 189 | try: 190 | workspace = file.firmware.all()[0].filepath.replace("firmware", 191 | "") 192 | parse_elf(workspace, file) 193 | with open(file.graph_file, "rb") as fd: 194 | content = fd.read() 195 | 196 | content_type = "image/png" 197 | response = HttpResponse(content, content_type=content_type) 198 | except NotImplementedError: 199 | file.graph_file = False 200 | file.save() 201 | return HttpResponse("no graph") 202 | 203 | loots = [] 204 | for loot in file.loots.all(): 205 | loots.append({"type": loot.type.name, "info": loot.info}) 206 | 207 | response = {"loots": loots, 208 | "hash": file.hash, 209 | "type": file.file_type, 210 | "filename": file.filename, 211 | "filesize": file.filesize} 212 | 213 | if is_elf(file): 214 | response["imports"] = file.imports 215 | response["informations"] = file.informations 216 | 217 | if file.graph_file == "": 218 | response["graph"] = '' 219 | else: 220 | if file.graph_file == False: 221 | response["graph"] = False 222 | else: 223 | response["graph"] = True 224 | 225 | if "text" in file.file_type: 226 | content = "" 227 | with open(file.filepath, "r") as fd: 228 | content = fd.read() 229 | response["content"] = content 230 | 231 | return JsonResponse(response) 232 | except FileModel.DoesNotExist: 233 | return JsonResponse({"error": "file not found", "hash": hash}) 234 | 235 | def api_get_latest(request): 236 | """ Return the 10 last firmwares 237 | """ 238 | try: 239 | firmwares = FirmwareModel.objects.filter(status="done").order_by('-id')[:10] 240 | response = [] 241 | for firmware in firmwares: 242 | response.append({"name": firmware.name, 243 | "hash": firmware.hash, 244 | "version": firmware.version, 245 | "status": firmware.status, 246 | "filesize": firmware.filesize, 247 | "brand": firmware.brand.name, 248 | "description": firmware.description, 249 | "model": firmware.model}) 250 | return JsonResponse({"firmwares": response}) 251 | except: 252 | return JsonResponse({"error": "unknown error"}) 253 | 254 | def api_get_stats(request): 255 | """ Return global stats 256 | """ 257 | try: 258 | response = {"total": LootModel.objects.all().count()} 259 | loots_types = [_.name for _ in LootTypeModel.objects.all()] 260 | for type in loots_types: 261 | result = LootModel.objects.filter(type__name=type).count() 262 | response[type] = result 263 | 264 | nb_firmwares_done = FirmwareModel.objects.filter(status="done").count() 265 | nb_firmwares_total = FirmwareModel.objects.all().count() 266 | 267 | return JsonResponse({"firmwares_done": nb_firmwares_done, 268 | "firmwares_total": nb_firmwares_total, 269 | "stats_loots": response}) 270 | except: 271 | return JsonResponse({"error": "unknown error"}) 272 | 273 | def api_search(request): 274 | try: 275 | k = request.GET.get('keyword', False) 276 | if k is False: 277 | return JsonResponse({"Error": "missing keyword argument"}) 278 | 279 | firmwares = FirmwareModel.objects.filter(Q(name__icontains=k) | 280 | Q(description__icontains=k) | 281 | Q(brand__name__icontains=k)).values("hash", "description", "brand__name", "name") 282 | response = [] 283 | for firmware in firmwares: 284 | response.append({"hash": firmware["hash"], 285 | "brand": firmware["brand__name"], 286 | "name": firmware["name"], 287 | "description": firmware["description"]}) 288 | return JsonResponse({"results": response}) 289 | 290 | except NotImplementedError: 291 | return JsonResponse({"Error": "unknown error"}) 292 | -------------------------------------------------------------------------------- /lib/extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Module that performs extraction. For usage, refer to documentation for the class 5 | 'Extractor'. This module can also be executed directly, 6 | e.g. 'extractor.py '. 7 | """ 8 | 9 | import argparse 10 | import hashlib 11 | import multiprocessing 12 | import os 13 | import shutil 14 | import tempfile 15 | import traceback 16 | 17 | import magic 18 | import binwalk 19 | 20 | 21 | class Extractor(object): 22 | """ 23 | Class that extracts kernels and filesystems from firmware images, given an 24 | input file or directory and output directory. 25 | """ 26 | 27 | # Directories that define the root of a UNIX filesystem, and the 28 | # appropriate threshold condition 29 | UNIX_DIRS = ["bin", "etc", "dev", "home", "lib", "mnt", "opt", "root", 30 | "run", "sbin", "tmp", "usr", "var"] 31 | UNIX_THRESHOLD = 4 32 | 33 | # Lock to prevent concurrent access to visited set. Unfortunately, must be 34 | # static because it cannot be pickled or passed as instance attribute. 35 | visited_lock = multiprocessing.Lock() 36 | 37 | def __init__(self, indir, outdir=None, rootfs=True, kernel=True, 38 | numproc=True, server=None, brand=None): 39 | # Input firmware update file or directory 40 | self._input = os.path.abspath(indir) 41 | # Output firmware directory 42 | self.output_dir = os.path.abspath(outdir) if outdir else None 43 | 44 | # Whether to attempt to extract kernel 45 | self.do_kernel = kernel 46 | 47 | # Whether to attempt to extract root filesystem 48 | self.do_rootfs = rootfs 49 | 50 | # Brand of the firmware 51 | self.brand = brand 52 | 53 | # Hostname of SQL server 54 | self.database = server 55 | 56 | # Worker pool. 57 | self._pool = None #multiprocessing.Pool() if numproc else None 58 | 59 | # Set containing MD5 checksums of visited items 60 | self.visited = set() 61 | 62 | # List containing tagged items to extract as 2-tuple: (tag [e.g. MD5], 63 | # path) 64 | self._list = list() 65 | 66 | def __getstate__(self): 67 | """ 68 | Eliminate attributes that should not be pickled. 69 | """ 70 | self_dict = self.__dict__.copy() 71 | del self_dict["_pool"] 72 | del self_dict["_list"] 73 | return self_dict 74 | 75 | @staticmethod 76 | def io_dd(indir, offset, size, outdir): 77 | """ 78 | Given a path to a target file, extract size bytes from specified offset 79 | to given output file. 80 | """ 81 | if not size: 82 | return 83 | 84 | with open(indir, "rb") as ifp: 85 | with open(outdir, "wb") as ofp: 86 | ifp.seek(offset, 0) 87 | ofp.write(ifp.read(size)) 88 | 89 | @staticmethod 90 | def magic(indata, mime=False): 91 | """ 92 | Performs file magic while maintaining compatibility with different 93 | libraries. 94 | """ 95 | 96 | try: 97 | if mime: 98 | mymagic = magic.open(magic.MAGIC_MIME_TYPE) 99 | else: 100 | mymagic = magic.open(magic.MAGIC_NONE) 101 | mymagic.load() 102 | except AttributeError: 103 | mymagic = magic.Magic(mime) 104 | mymagic.file = mymagic.from_file 105 | return mymagic.file(indata) 106 | 107 | @staticmethod 108 | def io_md5(target): 109 | """ 110 | Performs MD5 with a block size of 64kb. 111 | """ 112 | blocksize = 65536 113 | hasher = hashlib.md5() 114 | 115 | with open(target, 'rb') as ifp: 116 | buf = ifp.read(blocksize) 117 | while buf: 118 | hasher.update(buf) 119 | buf = ifp.read(blocksize) 120 | return hasher.hexdigest() 121 | 122 | @staticmethod 123 | def io_rm(target): 124 | """ 125 | Attempts to recursively delete a directory. 126 | """ 127 | shutil.rmtree(target, ignore_errors=False, onerror=Extractor._io_err) 128 | 129 | @staticmethod 130 | def _io_err(function, path, excinfo): 131 | """ 132 | Internal function used by '_rm' to print out errors. 133 | """ 134 | print(("!! %s: Cannot delete %s!\n%s" % (function, path, excinfo))) 135 | 136 | @staticmethod 137 | def io_find_rootfs(start, recurse=True): 138 | """ 139 | Attempts to find a Linux root directory. 140 | """ 141 | 142 | # Recurse into single directory chains, e.g. jffs2-root/fs_1/.../ 143 | path = start 144 | while (len(os.listdir(path)) == 1 and 145 | os.path.isdir(os.path.join(path, os.listdir(path)[0]))): 146 | path = os.path.join(path, os.listdir(path)[0]) 147 | 148 | # count number of unix-like directories 149 | count = 0 150 | for subdir in os.listdir(path): 151 | if subdir in Extractor.UNIX_DIRS and \ 152 | os.path.isdir(os.path.join(path, subdir)): 153 | count += 1 154 | 155 | # check for extracted filesystem, otherwise update queue 156 | if count >= Extractor.UNIX_THRESHOLD: 157 | return (True, path) 158 | 159 | # in some cases, multiple filesystems may be extracted, so recurse to 160 | # find best one 161 | if recurse: 162 | for subdir in os.listdir(path): 163 | if os.path.isdir(os.path.join(path, subdir)): 164 | res = Extractor.io_find_rootfs(os.path.join(path, subdir), 165 | False) 166 | if res[0]: 167 | return res 168 | 169 | return (False, start) 170 | 171 | def extract(self): 172 | """ 173 | Perform extraction of firmware updates from input to tarballs in output 174 | directory using a thread pool. 175 | """ 176 | if os.path.isdir(self._input): 177 | for path, _, files in os.walk(self._input): 178 | for item in files: 179 | self._list.append(os.path.join(path, item)) 180 | elif os.path.isfile(self._input): 181 | self._list.append(self._input) 182 | 183 | if self.output_dir and not os.path.isdir(self.output_dir): 184 | os.makedirs(self.output_dir) 185 | 186 | if self._pool: 187 | self._pool.map(self._extract_item, self._list) 188 | else: 189 | for item in self._list: 190 | self._extract_item(item) 191 | 192 | def _extract_item(self, path): 193 | """ 194 | Wrapper function that creates an ExtractionItem and calls the extract() 195 | method. 196 | """ 197 | 198 | ExtractionItem(self, path, 0).extract() 199 | 200 | 201 | class ExtractionItem(object): 202 | """ 203 | Class that encapsulates the state of a single item that is being extracted. 204 | """ 205 | 206 | # Maximum recursion breadth and depth 207 | RECURSION_BREADTH = 5 208 | RECURSION_DEPTH = 2 209 | 210 | def __init__(self, extractor, path, depth, tag=None): 211 | # Temporary directory 212 | self.temp = None 213 | 214 | # Recursion depth counter 215 | self.depth = depth 216 | 217 | # Reference to parent extractor object 218 | self.extractor = extractor 219 | 220 | # File path 221 | self.item = path 222 | 223 | # Database connection 224 | if self.extractor.database: 225 | import psycopg2 226 | self.database = psycopg2.connect(database="firmware", 227 | user="firmadyne", 228 | password="firmadyne", 229 | host=self.extractor.database) 230 | else: 231 | self.database = None 232 | 233 | # Checksum 234 | self.checksum = Extractor.io_md5(path) 235 | 236 | # Tag 237 | self.tag = tag if tag else self.generate_tag() 238 | 239 | # Output file path and filename prefix 240 | self.output = os.path.join(self.extractor.output_dir, self.tag) if \ 241 | self.extractor.output_dir else None 242 | 243 | # Status, with terminate indicating early termination for this item 244 | self.terminate = False 245 | self.status = None 246 | self.update_status() 247 | 248 | def __del__(self): 249 | if self.database: 250 | self.database.close() 251 | 252 | if self.temp: 253 | self.printf(">> Cleaning up %s..." % self.temp) 254 | Extractor.io_rm(self.temp) 255 | 256 | def printf(self, fmt): 257 | """ 258 | Prints output string with appropriate depth indentation. 259 | """ 260 | # print(("\t" * self.depth + fmt)) 261 | pass 262 | 263 | def generate_tag(self): 264 | """ 265 | Generate the filename tag. 266 | """ 267 | if not self.database: 268 | return os.path.basename(self.item) + "_" + self.checksum 269 | 270 | try: 271 | image_id = None 272 | cur = self.database.cursor() 273 | if self.extractor.brand: 274 | brand = self.extractor.brand 275 | else: 276 | brand = os.path.relpath(self.item).split(os.path.sep)[0] 277 | cur.execute("SELECT id FROM brand WHERE name=%s", (brand,)) 278 | brand_id = cur.fetchone() 279 | if not brand_id: 280 | cur.execute("INSERT INTO brand (name) VALUES (%s) RETURNING id", 281 | (brand,)) 282 | brand_id = cur.fetchone() 283 | if brand_id: 284 | cur.execute("SELECT id FROM image WHERE hash=%s", 285 | (self.checksum,)) 286 | image_id = cur.fetchone() 287 | if not image_id: 288 | cur.execute("INSERT INTO image (filename, brand_id, hash) \ 289 | VALUES (%s, %s, %s) RETURNING id", 290 | (os.path.basename(self.item), brand_id[0], 291 | self.checksum)) 292 | image_id = cur.fetchone() 293 | self.database.commit() 294 | except BaseException: 295 | traceback.print_exc() 296 | self.database.rollback() 297 | finally: 298 | if cur: 299 | cur.close() 300 | 301 | if image_id: 302 | self.printf(">> Database Image ID: %s" % image_id[0]) 303 | 304 | return str(image_id[0]) if \ 305 | image_id else os.path.basename(self.item) + "_" + self.checksum 306 | 307 | def get_kernel_status(self): 308 | """ 309 | Get the flag corresponding to the kernel status. 310 | """ 311 | return self.status[0] 312 | 313 | def get_rootfs_status(self): 314 | """ 315 | Get the flag corresponding to the root filesystem status. 316 | """ 317 | return self.status[1] 318 | 319 | def update_status(self): 320 | """ 321 | Updates the status flags using the tag to determine completion status. 322 | """ 323 | kernel_done = os.path.isfile(self.get_kernel_path()) if \ 324 | self.extractor.do_kernel and self.output else \ 325 | not self.extractor.do_kernel 326 | rootfs_done = os.path.isfile(self.get_rootfs_path()) if \ 327 | self.extractor.do_rootfs and self.output else \ 328 | not self.extractor.do_rootfs 329 | self.status = (kernel_done, rootfs_done) 330 | 331 | if self.database and kernel_done and self.extractor.do_kernel: 332 | self.update_database("kernel_extracted", "True") 333 | 334 | if self.database and rootfs_done and self.extractor.do_rootfs: 335 | self.update_database("rootfs_extracted", "True") 336 | 337 | return self.get_status() 338 | 339 | def update_database(self, field, value): 340 | """ 341 | Update a given field in the database. 342 | """ 343 | ret = True 344 | if self.database: 345 | try: 346 | cur = self.database.cursor() 347 | cur.execute("UPDATE image SET " + field + "='" + value + 348 | "' WHERE id=%s", (self.tag,)) 349 | self.database.commit() 350 | except BaseException: 351 | ret = False 352 | traceback.print_exc() 353 | self.database.rollback() 354 | finally: 355 | if cur: 356 | cur.close() 357 | return ret 358 | 359 | def get_status(self): 360 | """ 361 | Returns True if early terminate signaled, extraction is complete, 362 | otherwise False. 363 | """ 364 | return True if self.terminate or all(i for i in self.status) else False 365 | 366 | def get_kernel_path(self): 367 | """ 368 | Return the full path (including filename) to the output kernel file. 369 | """ 370 | return self.output + ".kernel" if self.output else None 371 | 372 | def get_rootfs_path(self): 373 | """ 374 | Return the full path (including filename) to the output root filesystem 375 | file. 376 | """ 377 | return self.output + ".tar.gz" if self.output else None 378 | 379 | def extract(self): 380 | """ 381 | Perform the actual extraction of firmware updates, recursively. Returns 382 | True if extraction complete, otherwise False. 383 | """ 384 | self.printf("\n" + self.item.encode("utf-8", "replace").decode("utf-8")) 385 | 386 | # check if item is complete 387 | if self.get_status(): 388 | self.printf(">> Skipping: completed!") 389 | return True 390 | 391 | # check if exceeding recursion depth 392 | if self.depth > ExtractionItem.RECURSION_DEPTH: 393 | self.printf(">> Skipping: recursion depth %d" % self.depth) 394 | return self.get_status() 395 | 396 | # check if checksum is in visited set 397 | self.printf(">> MD5: %s" % self.checksum) 398 | with Extractor.visited_lock: 399 | if self.checksum in self.extractor.visited: 400 | self.printf(">> Skipping: %s..." % self.checksum) 401 | return self.get_status() 402 | else: 403 | self.extractor.visited.add(self.checksum) 404 | 405 | # check if filetype is blacklisted 406 | if self._check_blacklist(): 407 | return self.get_status() 408 | 409 | # create working directory 410 | self.temp = tempfile.mkdtemp() 411 | 412 | try: 413 | self.printf(">> Tag: %s" % self.tag) 414 | self.printf(">> Temp: %s" % self.temp) 415 | self.printf(">> Status: Kernel: %s, Rootfs: %s, Do_Kernel: %s, \ 416 | Do_Rootfs: %s" % (self.get_kernel_status(), 417 | self.get_rootfs_status(), 418 | self.extractor.do_kernel, 419 | self.extractor.do_rootfs)) 420 | 421 | for analysis in [self._check_archive, self._check_firmware, 422 | self._check_kernel, self._check_rootfs, 423 | self._check_compressed]: 424 | # Move to temporary directory so binwalk does not write to input 425 | os.chdir(self.temp) 426 | 427 | # Update status only if analysis changed state 428 | if analysis(): 429 | if self.update_status(): 430 | self.printf(">> Skipping: completed!") 431 | return True 432 | 433 | except Exception: 434 | traceback.print_exc() 435 | 436 | return False 437 | 438 | def _check_blacklist(self): 439 | """ 440 | Check if this file is blacklisted for analysis based on file type. 441 | """ 442 | # First, use MIME-type to exclude large categories of files 443 | filetype = Extractor.magic(self.item.encode("utf-8", "surrogateescape"), 444 | mime=True) 445 | if any(s in filetype for s in ["application/x-executable", 446 | "application/x-dosexec", 447 | "application/x-object", 448 | "application/pdf", 449 | "application/msword", 450 | "image/", 451 | "text/", 452 | "video/"]): 453 | self.printf(">> Skipping: %s..." % filetype) 454 | return True 455 | 456 | # Next, check for specific file types that have MIME-type 457 | # 'application/octet-stream' 458 | filetype = Extractor.magic(self.item.encode("utf-8", "surrogateescape")) 459 | if any(s in filetype for s in ["executable", "universal binary", 460 | "relocatable", "bytecode", "applet"]): 461 | self.printf(">> Skipping: %s..." % filetype) 462 | return True 463 | 464 | # Finally, check for specific file extensions that would be incorrectly 465 | # identified 466 | if self.item.endswith(".dmg"): 467 | self.printf(">> Skipping: %s..." % (self.item)) 468 | return True 469 | 470 | return False 471 | 472 | def _check_archive(self): 473 | """ 474 | If this file is an archive, recurse over its contents, unless it matches 475 | an extracted root filesystem. 476 | """ 477 | return self._check_recursive("archive") 478 | 479 | def _check_firmware(self): 480 | """ 481 | If this file is of a known firmware type, directly attempt to extract 482 | the kernel and root filesystem. 483 | """ 484 | for module in binwalk.scan(self.item, "-y", "header", signature=True, 485 | quiet=True): 486 | for entry in module.results: 487 | # uImage 488 | if "uImage header" in entry.description: 489 | if not self.get_kernel_status() and \ 490 | "OS Kernel Image" in entry.description: 491 | kernel_offset = entry.offset + 64 492 | kernel_size = 0 493 | 494 | for stmt in entry.description.split(','): 495 | if "image size:" in stmt: 496 | kernel_size = int(''.join( 497 | i for i in stmt if i.isdigit()), 10) 498 | 499 | if kernel_size != 0 and kernel_offset + kernel_size \ 500 | <= os.path.getsize(self.item): 501 | self.printf(">>>> %s" % entry.description) 502 | 503 | tmp_fd, tmp_path = tempfile.mkstemp(dir=self.temp) 504 | os.close(tmp_fd) 505 | Extractor.io_dd(self.item, kernel_offset, 506 | kernel_size, tmp_path) 507 | kernel = ExtractionItem(self.extractor, tmp_path, 508 | self.depth, self.tag) 509 | 510 | return kernel.extract() 511 | # elif "RAMDisk Image" in entry.description: 512 | # self.printf(">>>> %s" % entry.description) 513 | # self.printf(">>>> Skipping: RAMDisk / initrd") 514 | # self.terminate = True 515 | # return True 516 | 517 | # TP-Link or TRX 518 | elif not self.get_kernel_status() and \ 519 | not self.get_rootfs_status() and \ 520 | "rootfs offset: " in entry.description and \ 521 | "kernel offset: " in entry.description: 522 | kernel_offset = 0 523 | kernel_size = 0 524 | rootfs_offset = 0 525 | rootfs_size = 0 526 | 527 | for stmt in entry.description.split(','): 528 | if "kernel offset:" in stmt: 529 | kernel_offset = int(stmt.split(':')[1], 16) 530 | elif "kernel length:" in stmt: 531 | kernel_size = int(stmt.split(':')[1], 16) 532 | elif "rootfs offset:" in stmt: 533 | rootfs_offset = int(stmt.split(':')[1], 16) 534 | elif "rootfs length:" in stmt: 535 | rootfs_size = int(stmt.split(':')[1], 16) 536 | 537 | # compute sizes if only offsets provided 538 | if kernel_offset != rootfs_size and kernel_size == 0 and \ 539 | rootfs_size == 0: 540 | kernel_size = rootfs_offset - kernel_offset 541 | rootfs_size = os.path.getsize(self.item) - rootfs_offset 542 | 543 | # ensure that computed values are sensible 544 | if (kernel_size > 0 and kernel_offset + kernel_size \ 545 | <= os.path.getsize(self.item)) and \ 546 | (rootfs_size != 0 and rootfs_offset + rootfs_size \ 547 | <= os.path.getsize(self.item)): 548 | self.printf(">>>> %s" % entry.description) 549 | 550 | tmp_fd, tmp_path = tempfile.mkstemp(dir=self.temp) 551 | os.close(tmp_fd) 552 | Extractor.io_dd(self.item, kernel_offset, kernel_size, 553 | tmp_path) 554 | kernel = ExtractionItem(self.extractor, tmp_path, 555 | self.depth, self.tag) 556 | kernel.extract() 557 | 558 | tmp_fd, tmp_path = tempfile.mkstemp(dir=self.temp) 559 | os.close(tmp_fd) 560 | Extractor.io_dd(self.item, rootfs_offset, rootfs_size, 561 | tmp_path) 562 | rootfs = ExtractionItem(self.extractor, tmp_path, 563 | self.depth, self.tag) 564 | rootfs.extract() 565 | 566 | return self.update_status() 567 | return False 568 | 569 | def _check_kernel(self): 570 | """ 571 | If this file contains a kernel version string, assume it is a kernel. 572 | Only Linux kernels are currently extracted. 573 | """ 574 | if not self.get_kernel_status(): 575 | for module in binwalk.scan(self.item, "-y", "kernel", 576 | signature=True, quiet=True): 577 | for entry in module.results: 578 | if "kernel version" in entry.description: 579 | self.update_database("kernel_version", 580 | entry.description) 581 | if "Linux" in entry.description: 582 | if self.get_kernel_path(): 583 | shutil.copy(self.item, self.get_kernel_path()) 584 | else: 585 | self.extractor.do_kernel = False 586 | self.printf(">>>> %s" % entry.description) 587 | return True 588 | # VxWorks, etc 589 | else: 590 | self.printf(">>>> Ignoring: %s" % entry.description) 591 | return False 592 | return False 593 | return False 594 | 595 | def _check_rootfs(self): 596 | """ 597 | If this file contains a known filesystem type, extract it. 598 | """ 599 | 600 | if not self.get_rootfs_status(): 601 | for module in binwalk.scan(self.item, "-e", "-r", "-y", 602 | "filesystem", signature=True, 603 | quiet=True): 604 | for entry in module.results: 605 | self.printf(">>>> %s" % entry.description) 606 | break 607 | 608 | if module.extractor.directory: 609 | unix = Extractor.io_find_rootfs(module.extractor.directory) 610 | 611 | if not unix[0]: 612 | self.printf(">>>> Extraction failed!") 613 | return False 614 | 615 | self.printf(">>>> Found Linux filesystem in %s!" % unix[1]) 616 | if self.output: 617 | shutil.make_archive(self.output, "gztar", 618 | root_dir=unix[1]) 619 | else: 620 | self.extractor.do_rootfs = False 621 | return True 622 | return False 623 | 624 | def _check_compressed(self): 625 | """ 626 | If this file appears to be compressed, decompress it and recurse over 627 | its contents. 628 | """ 629 | return self._check_recursive("compressed") 630 | 631 | # treat both archived and compressed files using the same pathway. this is 632 | # because certain files may appear as e.g. "xz compressed data" but still 633 | # extract into a root filesystem. 634 | def _check_recursive(self, fmt): 635 | """ 636 | Unified implementation for checking both "archive" and "compressed" 637 | items. 638 | """ 639 | desc = None 640 | # perform extraction 641 | for module in binwalk.scan(self.item, "-e", "-r", "-y", fmt, 642 | signature=True, quiet=True): 643 | for entry in module.results: 644 | # skip cpio/initrd files since they should be included with 645 | # kernel 646 | # if "cpio archive" in entry.description: 647 | # self.printf(">> Skipping: cpio: %s" % entry.description) 648 | # self.terminate = True 649 | # return True 650 | desc = entry.description 651 | self.printf(">>>> %s" % entry.description) 652 | break 653 | 654 | if module.extractor.directory: 655 | unix = Extractor.io_find_rootfs(module.extractor.directory) 656 | 657 | # check for extracted filesystem, otherwise update queue 658 | if unix[0]: 659 | self.printf(">>>> Found Linux filesystem in %s!" % unix[1]) 660 | if self.output: 661 | shutil.make_archive(self.output, "gztar", 662 | root_dir=unix[1]) 663 | else: 664 | self.extractor.do_rootfs = False 665 | return True 666 | else: 667 | count = 0 668 | self.printf(">> Recursing into %s ..." % fmt) 669 | for root, _, files in os.walk(module.extractor.directory): 670 | # sort both descending alphabetical and increasing 671 | # length 672 | files.sort() 673 | files.sort(key=len) 674 | 675 | # handle case where original file name is restored; put 676 | # it to front of queue 677 | if desc and "original file name:" in desc: 678 | orig = None 679 | for stmt in desc.split(","): 680 | if "original file name:" in stmt: 681 | orig = stmt.split("\"")[1] 682 | if orig and orig in files: 683 | files.remove(orig) 684 | files.insert(0, orig) 685 | 686 | for filename in files: 687 | if count > ExtractionItem.RECURSION_BREADTH: 688 | self.printf(">> Skipping: recursion breadth %d" \ 689 | % ExtractionItem.RECURSION_BREADTH) 690 | self.terminate = True 691 | return True 692 | else: 693 | new_item = ExtractionItem(self.extractor, 694 | os.path.join(root, 695 | filename), 696 | self.depth + 1, 697 | self.tag) 698 | if new_item.extract(): 699 | # check that we are actually done before 700 | # performing early termination. for example, 701 | # we might decide to skip on one subitem, 702 | # but we still haven't finished 703 | if self.update_status(): 704 | return True 705 | count += 1 706 | return False 707 | 708 | 709 | def main(): 710 | parser = argparse.ArgumentParser(description="Extracts filesystem and \ 711 | kernel from Linux-based firmware images") 712 | parser.add_argument("input", action="store", help="Input file or directory") 713 | parser.add_argument("output", action="store", nargs="?", default="images", 714 | help="Output directory for extracted firmware") 715 | parser.add_argument("-sql ", dest="sql", action="store", default=None, 716 | help="Hostname of SQL server") 717 | parser.add_argument("-nf", dest="rootfs", action="store_false", 718 | default=True, help="Disable extraction of root \ 719 | filesystem (may decrease extraction time)") 720 | parser.add_argument("-nk", dest="kernel", action="store_false", 721 | default=True, help="Disable extraction of kernel \ 722 | (may decrease extraction time)") 723 | parser.add_argument("-np", dest="parallel", action="store_false", 724 | default=True, help="Disable parallel operation \ 725 | (may increase extraction time)") 726 | parser.add_argument("-b", dest="brand", action="store", default=None, 727 | help="Brand of the firmware image") 728 | result = parser.parse_args() 729 | 730 | extract = Extractor(result.input, result.output, result.rootfs, 731 | result.kernel, result.parallel, result.sql, 732 | result.brand) 733 | extract.extract() 734 | 735 | 736 | if __name__ == "__main__": 737 | main() 738 | -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.5",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.5",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------