Skip to content

Commit

Permalink
Release of SSTImap version 1.1.0
Browse files Browse the repository at this point in the history
Crawler and form detection (by @fantesykikachu)
New template engine added: Cheetah
Automatic import for engine modules
Interactive module reloading capability
Full support for Python 3.11
Replaced telnetlib with a custom TCP client

---------

Co-authored-by: fantesykikachu <fantesykikachu@users.noreply.github.com>
  • Loading branch information
vladko312 and fantesykikachu authored Apr 16, 2023
1 parent 2177600 commit 5882e5a
Show file tree
Hide file tree
Showing 15 changed files with 524 additions and 85 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
SSTImap
======

[![Version 1.0](https://img.shields.io/badge/version-1.0-green.svg?logo=github)](https://github.com/vladko312/sstimap)
[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg?logo=python)](https://www.python.org/downloads/release/python-3100/)
[![Version 1.1](https://img.shields.io/badge/version-1.1-green.svg?logo=github)](https://github.com/vladko312/sstimap)
[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg?logo=python)](https://www.python.org/downloads/release/python-3110/)
[![Python 3.6](https://img.shields.io/badge/python-3.6+-yellow.svg?logo=python)](https://www.python.org/downloads/release/python-360/)
[![GitHub](https://img.shields.io/github/license/vladko312/sstimap?color=green&logo=gnu)](https://www.gnu.org/licenses/gpl-3.0.txt)
[![GitHub last commit](https://img.shields.io/github/last-commit/vladko312/sstimap?color=green&logo=github)](https://github.com/vladko312/sstimap/commits/)
[![Maintenance](https://img.shields.io/maintenance/yes/2022?logo=github)](https://github.com/vladko312/sstimap)
[![Maintenance](https://img.shields.io/maintenance/yes/2023?logo=github)](https://github.com/vladko312/sstimap)

> This project is based on [Tplmap](https://github.com/epinna/tplmap/).
Expand Down Expand Up @@ -108,7 +108,7 @@ $ ./sstimap.py -u https://example.com/page?name=John
╚══════╩══════╝ ╚═╝ ╚╦╝ |_| |_| |_|\__,_| .__/
│ | |
|_|
[*] Version: 1.0
[*] Version: 1.1.0
[*] Author: @vladko312
[*] Based on Tplmap
[!] LEGAL DISCLAIMER: Usage of SSTImap for attacking targets without prior mutual consent is illegal.
Expand Down Expand Up @@ -163,7 +163,7 @@ $ ./sstimap.py -u https://example.com/page?name=John --os-shell
╚══════╩══════╝ ╚═╝ ╚╦╝ |_| |_| |_|\__,_| .__/
│ | |
|_|
[*] Version: 0.6#dev
[*] Version: 1.1.0
[*] Author: @vladko312
[*] Based on Tplmap
[!] LEGAL DISCLAIMER: Usage of SSTImap for attacking targets without prior mutual consent is illegal.
Expand Down Expand Up @@ -226,6 +226,7 @@ New payloads are welcome in PRs.
| Engine | RCE | Blind | Code evaluation | File read | File write |
|--------------------------------|-----|-------|-----------------|-----------|------------|
| Mako ||| Python |||
| Cheetah ||| Python |||
| Jinja2 ||| Python |||
| Python (code eval) ||| Python |||
| Tornado ||| Python |||
Expand Down Expand Up @@ -262,14 +263,17 @@ If you plan to contribute something big from this list, inform me to avoid worki
- [ ] Make template and base language evaluation functionality more uniform
- [ ] Add more payloads for different engines
- [ ] Short arguments as interactive commands?
- [ ] Automatic languages and engines import
- [ ] Engine plugins as objects of _Plugin_ class?
- [ ] JSON/plaintext API modes for scripting integrations?
- [ ] Argument to remove escape codes?
- [ ] Spider/crawler automation
- [ ] Better integration for Python scripts
- [ ] More POST data types support
- [ ] Payload processing scripts
- [ ] Better config functionality
- [ ] Saving found vulnerabilities
- [ ] Reports in HTML or other format
- [x] Spider/crawler automation (by [fantesykikachu](https://github.com/fantesykikachu))
- [x] Automatic languages and engines import

[1]: https://artsploit.blogspot.co.uk/2016/08/pprce2.html
[2]: https://opsecx.com/index.php/2016/07/03/server-side-template-injection-in-tornado/
Expand Down
54 changes: 9 additions & 45 deletions core/checks.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,3 @@
from plugins.engines.mako import Mako
from plugins.engines.jinja2 import Jinja2
from plugins.engines.twig import Twig
from plugins.engines.freemarker import Freemarker
from plugins.engines.velocity import Velocity
from plugins.engines.pug import Pug
from plugins.engines.nunjucks import Nunjucks
from plugins.engines.dust import Dust
from plugins.engines.dot import Dot
from plugins.engines.tornado import Tornado
from plugins.engines.marko import Marko
from plugins.engines.slim import Slim
from plugins.engines.erb import Erb
from plugins.engines.ejs import Ejs
from plugins.engines.smarty import Smarty
from plugins.languages.javascript import Javascript
from plugins.languages.php import Php
from plugins.languages.python import Python
from plugins.languages.ruby import Ruby
from plugins.legacy_engines.smarty_unsecure import Smarty_unsecure
from utils.loggers import log
from core.clis import Shell, MultilineShell
from core.tcpserver import TcpServer
Expand All @@ -27,32 +7,16 @@


def plugins(legacy=False):
from core.plugin import loaded_plugins
plugin_list = []
if legacy:
plugin_list.extend([
Smarty_unsecure,
])
plugin_list.extend([
Smarty,
Mako,
Python,
Tornado,
Jinja2,
Twig,
Freemarker,
Velocity,
Slim,
Erb,
Pug,
Nunjucks,
Dot,
Dust,
Marko,
Javascript,
Php,
Ruby,
Ejs
])
plugin_list += loaded_plugins.get("legacy_engines", [])
plugin_list += loaded_plugins.get("engines", [])
plugin_list += loaded_plugins.get("languages", [])
plugin_list += loaded_plugins.get("custom", [])
for group in loaded_plugins:
if group not in ["legacy_engines", "engines", "languages", "custom"]:
plugin_list += loaded_plugins.get(group, [])
return plugin_list


Expand Down Expand Up @@ -100,7 +64,7 @@ def print_injection_summary(channel):
def detect_template_injection(channel):
for i in range(len(channel.injs)):
log.log(23, f"Testing if {channel.injs[channel.inj_idx]['field']} parameter '{channel.injs[channel.inj_idx]['param']}' is injectable")
for plugin in plugins(channel.args.get('legacy')):
for plugin in plugins(legacy=channel.args.get('legacy')):
current_plugin = plugin(channel)
if channel.args.get('engine') and channel.args.get('engine').lower() != current_plugin.plugin.lower():
continue
Expand Down
131 changes: 117 additions & 14 deletions core/interactive.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import cmd
from utils.crawler import crawl, find_page_forms
from utils.loggers import log
from urllib import parse
from core import checks
from core.channel import Channel
from core.clis import Shell, MultilineShell
from core.tcpserver import TcpServer
import telnetlib
from core.tcpclient import TcpClient
import socket


Expand Down Expand Up @@ -47,6 +48,8 @@ def do_help(self, line):
Target:
url, target [URL] Set target URL (e.g. 'https://example.com/?name=test')
crawl [DEPTH] Crawl up to depth (0 - do not crawl)
forms Search page(s) for forms
run, test, check Run SSTI detection on the target
Request:
Expand All @@ -66,6 +69,8 @@ def do_help(self, line):
engine [ENGINE] Check only this backend template engine. For all, use '*'
technique [TECHNIQUE] Use techniques R(endered) T(ime-based blind). Default: RT
legacy Toggle including old payloads, that no longer work with newer versions of the engines
exclude [PATTERN] Regex pattern to exclude from crawler
domains [DOMAINS] Crawl other domains: Y(es) / S(ubdomains) / N(o). Default: S
Exploitation:
tpl, tpl_shell Prompt for an interactive shell on the template engine
Expand All @@ -78,14 +83,18 @@ def do_help(self, line):
reverse, reverse_shell [HOST] [PORT] Run a system shell and back-connect to local HOST PORT
overwrite, force_overwrite Toggle file overwrite when uploading
up, upload [LOCAL] [REMOTE] Upload LOCAL to REMOTE files
down, download [REMOTE] [LOCAL] Download REMOTE to LOCAL files""")
down, download [REMOTE] [LOCAL] Download REMOTE to LOCAL files
SSTImap:
reload, reload_plugins Reload all SSTImap plugins""")

def do_version(self, line):
"""Show current SSTImap version"""
log.log(23, f'Current SSTImap version: {self.sstimap_options["version"]}')

def do_options(self, line):
"""Show current SSTImap options"""
crawl_domains = {"Y": "Yes", "S": "Subdomains only", "N": "No"}
log.log(23, f'Current SSTImap {self.sstimap_options["version"]} interactive mode options:')
if not self.sstimap_options["url"]:
log.log(25, f'URL is not set.')
Expand Down Expand Up @@ -116,6 +125,14 @@ def do_options(self, line):
log.log(26, f'Level: {self.sstimap_options["level"]}')
log.log(26, f'Engine: {self.sstimap_options["engine"] if self.sstimap_options["engine"] else "*"}'
f'{"+" if not self.sstimap_options["engine"] and self.sstimap_options["legacy"] else ""}')
if self.sstimap_options["crawl_depth"] > 0:
log.log(26, f'Crawler depth: {self.sstimap_options["crawl_depth"]}')
else:
log.log(26, 'Crawler depth: no crawl')
if self.sstimap_options["crawl_exclude"]:
log.log(26, f'Crawler exclude RE: "{self.sstimap_options["crawl_exclude"]}"')
log.log(26, f'Crawl other domains: {crawl_domains.get(self.sstimap_options["crawl_exclude"].upper())}')
log.log(26, f'Form detection: {self.sstimap_options["forms"]}')
log.log(26, f'Attack technique: {self.sstimap_options["technique"]}')
log.log(26, f'Force overwrite files: {self.sstimap_options["force_overwrite"]}')

Expand Down Expand Up @@ -146,14 +163,74 @@ def do_url(self, line):

do_target = do_url

def do_crawl(self, line):
self.sstimap_options['crawl_depth'] = int(line)
if int(line):
log.log(24, f'Crawling depth is set to {line}.')
else:
log.log(24, 'Crawling disabled.')

def do_exclude(self, line):
self.sstimap_options['crawl_exclude'] = line
if line:
log.log(24, f'Crawler exclude RE is set to "{line}".')
else:
log.log(24, 'Crawler exclude RE disabled.')

do_crawl_exclude = do_exclude
do_crawlexclude = do_exclude

def do_forms(self, line):
overwrite = not self.sstimap_options['forms']
log.log(24, f'Form detection {"en" if overwrite else "dis"}abled.')
self.sstimap_options['forms'] = overwrite

def do_run(self, line):
"""Check target URL for SSTI vulnerabilities"""
if not self.sstimap_options["url"]:
log.log(22, 'Target URL cannot be empty.')
return
try:
self.channel = Channel(self.sstimap_options)
self.current_plugin = checks.check_template_injection(self.channel)
if self.sstimap_options['crawl_depth'] or self.sstimap_options['forms']:
# crawler mode
urls = set([self.sstimap_options['url']])
if self.sstimap_options['crawl_depth']:
print(1)
crawled_urls = set()
for url in urls:
crawled_urls.update(crawl(url, self.sstimap_options))
urls.update(crawled_urls)
if not self.sstimap_options['forms']:
for url in urls:
print()
log.log(23, f'Scanning url: {url}')
self.sstimap_options['url'] = url
self.channel = Channel(self.sstimap_options)
self.current_plugin = checks.check_template_injection(self.channel)
if self.channel.data.get('engine'):
break # TODO: save vulnerabilities
else:
forms = set()
print(2)
for url in urls:
forms.update(find_page_forms(url, self.sstimap_options))
print(3)
for form in forms:
print()
log.log(23, f'Scanning form with url: {form[0]}')
self.sstimap_options['url'] = form[0]
self.sstimap_options['method'] = form[1]
self.sstimap_options['data'] = parse.parse_qs(form[2], keep_blank_values=True)
self.channel = Channel(self.sstimap_options)
self.current_plugin = checks.check_template_injection(self.channel)
if self.channel.data.get('engine'):
break # TODO: save vulnerabilities
if not forms:
log.log(22, f'No forms were detected to scan')
else:
# predetermined mode
self.channel = Channel(self.sstimap_options)
self.current_plugin = checks.check_template_injection(self.channel)
except (KeyboardInterrupt, EOFError):
log.log(26, 'Exiting SSTI detection')
self.checked = True
Expand Down Expand Up @@ -321,6 +398,17 @@ def do_technique(self, line):
log.log(24, f'Attack technique is set to {line}')
self.sstimap_options["technique"] = line

def do_crawl_domains(self, line):
"""Set crawling DOMAINS behaviour"""
line = line.upper()
if line not in ["Y", "S", "N"]:
log.log(22, 'Invalid DOMAINS value. It should be \'Y\', \'S\' or \'N\'.')
return
log.log(24, f'Domain crawling is set to {line}')
self.sstimap_options["crawl_domains"] = line

do_domains = do_crawl_domains

def do_legacy(self, line):
"""Switch legacy option"""
overwrite = not self.sstimap_options["legacy"]
Expand Down Expand Up @@ -494,16 +582,18 @@ def do_bind_shell(self, line):
for idx, thread in enumerate(self.current_plugin.bind_shell(port)):
log.log(26, f'Spawn a shell on remote port {port} with payload {idx+1}')
thread.join(timeout=1)
if not thread.is_alive():
continue
try:
telnetlib.Telnet(url.hostname.decode(), port, timeout=5).interact()
return
except (KeyboardInterrupt, EOFError):
print()
log.log(26, 'Exiting bind shell')
except Exception as e:
log.debug(f"Error connecting to {url.hostname}:{port} {e}")
if thread.is_alive():
log.log(24, f'Shell with payload {idx+1} seems stable')
break
try:
a = TcpClient(url.hostname, port, timeout=5)
a.shell()
return
except (KeyboardInterrupt, EOFError):
print()
log.log(26, 'Exiting bind shell')
except Exception as e:
log.log(25, f"Error connecting to {url.hostname}:{port} {e}")
else:
log.log(22, 'No TCP shell opening capabilities have been detected on the target')

Expand Down Expand Up @@ -588,3 +678,16 @@ def do_download(self, line):

do_up = do_upload
do_down = do_download

# SSTImap commands

def do_reload_modules(self, line):
"""Reload all modules"""
from core.plugin import unload_plugins
from sstimap import load_plugins
unload_plugins()
load_plugins()
from core.plugin import loaded_plugins
log.log(23, f"Reloaded plugins by categories: {'; '.join([f'{x}: {len(loaded_plugins[x])}' for x in loaded_plugins])}")

do_reload = do_reload_modules
20 changes: 20 additions & 0 deletions core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@
import collections
import threading
import time
import sys
import utils.config

loaded_plugins = {}


def unload_plugins():
global loaded_plugins
for k in loaded_plugins:
for p in loaded_plugins[k]:
if p.__module__ in sys.modules:
del sys.modules[p.__module__]
loaded_plugins = {}


def _recursive_update(d, u):
# Update value of a nested dictionary of varying depth
Expand Down Expand Up @@ -50,6 +62,14 @@ def __init__(self, channel):
self.language_init()
self.init()

def __init_subclass__(cls, **kwargs):
module = cls.__module__.split(".")
if module[0] == "plugins":
if module[1] in loaded_plugins:
loaded_plugins[module[1]].append(cls)
else:
loaded_plugins[module[1]] = [cls]

def language_init(self):
# To be overridden. This can call self.update_actions
# and self.set_contexts
Expand Down
Loading

0 comments on commit 5882e5a

Please sign in to comment.