Skip to content

Commit 5882e5a

Browse files
Release of SSTImap version 1.1.0
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 <[email protected]>
1 parent 2177600 commit 5882e5a

File tree

15 files changed

+524
-85
lines changed

15 files changed

+524
-85
lines changed

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
SSTImap
22
======
33

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

1111
> This project is based on [Tplmap](https://github.com/epinna/tplmap/).
1212
@@ -108,7 +108,7 @@ $ ./sstimap.py -u https://example.com/page?name=John
108108
╚══════╩══════╝ ╚═╝ ╚╦╝ |_| |_| |_|\__,_| .__/
109109
│ | |
110110
|_|
111-
[*] Version: 1.0
111+
[*] Version: 1.1.0
112112
[*] Author: @vladko312
113113
[*] Based on Tplmap
114114
[!] LEGAL DISCLAIMER: Usage of SSTImap for attacking targets without prior mutual consent is illegal.
@@ -163,7 +163,7 @@ $ ./sstimap.py -u https://example.com/page?name=John --os-shell
163163
╚══════╩══════╝ ╚═╝ ╚╦╝ |_| |_| |_|\__,_| .__/
164164
│ | |
165165
|_|
166-
[*] Version: 0.6#dev
166+
[*] Version: 1.1.0
167167
[*] Author: @vladko312
168168
[*] Based on Tplmap
169169
[!] LEGAL DISCLAIMER: Usage of SSTImap for attacking targets without prior mutual consent is illegal.
@@ -226,6 +226,7 @@ New payloads are welcome in PRs.
226226
| Engine | RCE | Blind | Code evaluation | File read | File write |
227227
|--------------------------------|-----|-------|-----------------|-----------|------------|
228228
| Mako ||| Python |||
229+
| Cheetah ||| Python |||
229230
| Jinja2 ||| Python |||
230231
| Python (code eval) ||| Python |||
231232
| Tornado ||| Python |||
@@ -262,14 +263,17 @@ If you plan to contribute something big from this list, inform me to avoid worki
262263
- [ ] Make template and base language evaluation functionality more uniform
263264
- [ ] Add more payloads for different engines
264265
- [ ] Short arguments as interactive commands?
265-
- [ ] Automatic languages and engines import
266266
- [ ] Engine plugins as objects of _Plugin_ class?
267267
- [ ] JSON/plaintext API modes for scripting integrations?
268268
- [ ] Argument to remove escape codes?
269-
- [ ] Spider/crawler automation
270269
- [ ] Better integration for Python scripts
271270
- [ ] More POST data types support
272271
- [ ] Payload processing scripts
272+
- [ ] Better config functionality
273+
- [ ] Saving found vulnerabilities
274+
- [ ] Reports in HTML or other format
275+
- [x] Spider/crawler automation (by [fantesykikachu](https://github.com/fantesykikachu))
276+
- [x] Automatic languages and engines import
273277

274278
[1]: https://artsploit.blogspot.co.uk/2016/08/pprce2.html
275279
[2]: https://opsecx.com/index.php/2016/07/03/server-side-template-injection-in-tornado/

core/checks.py

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,3 @@
1-
from plugins.engines.mako import Mako
2-
from plugins.engines.jinja2 import Jinja2
3-
from plugins.engines.twig import Twig
4-
from plugins.engines.freemarker import Freemarker
5-
from plugins.engines.velocity import Velocity
6-
from plugins.engines.pug import Pug
7-
from plugins.engines.nunjucks import Nunjucks
8-
from plugins.engines.dust import Dust
9-
from plugins.engines.dot import Dot
10-
from plugins.engines.tornado import Tornado
11-
from plugins.engines.marko import Marko
12-
from plugins.engines.slim import Slim
13-
from plugins.engines.erb import Erb
14-
from plugins.engines.ejs import Ejs
15-
from plugins.engines.smarty import Smarty
16-
from plugins.languages.javascript import Javascript
17-
from plugins.languages.php import Php
18-
from plugins.languages.python import Python
19-
from plugins.languages.ruby import Ruby
20-
from plugins.legacy_engines.smarty_unsecure import Smarty_unsecure
211
from utils.loggers import log
222
from core.clis import Shell, MultilineShell
233
from core.tcpserver import TcpServer
@@ -27,32 +7,16 @@
277

288

299
def plugins(legacy=False):
10+
from core.plugin import loaded_plugins
3011
plugin_list = []
3112
if legacy:
32-
plugin_list.extend([
33-
Smarty_unsecure,
34-
])
35-
plugin_list.extend([
36-
Smarty,
37-
Mako,
38-
Python,
39-
Tornado,
40-
Jinja2,
41-
Twig,
42-
Freemarker,
43-
Velocity,
44-
Slim,
45-
Erb,
46-
Pug,
47-
Nunjucks,
48-
Dot,
49-
Dust,
50-
Marko,
51-
Javascript,
52-
Php,
53-
Ruby,
54-
Ejs
55-
])
13+
plugin_list += loaded_plugins.get("legacy_engines", [])
14+
plugin_list += loaded_plugins.get("engines", [])
15+
plugin_list += loaded_plugins.get("languages", [])
16+
plugin_list += loaded_plugins.get("custom", [])
17+
for group in loaded_plugins:
18+
if group not in ["legacy_engines", "engines", "languages", "custom"]:
19+
plugin_list += loaded_plugins.get(group, [])
5620
return plugin_list
5721

5822

@@ -100,7 +64,7 @@ def print_injection_summary(channel):
10064
def detect_template_injection(channel):
10165
for i in range(len(channel.injs)):
10266
log.log(23, f"Testing if {channel.injs[channel.inj_idx]['field']} parameter '{channel.injs[channel.inj_idx]['param']}' is injectable")
103-
for plugin in plugins(channel.args.get('legacy')):
67+
for plugin in plugins(legacy=channel.args.get('legacy')):
10468
current_plugin = plugin(channel)
10569
if channel.args.get('engine') and channel.args.get('engine').lower() != current_plugin.plugin.lower():
10670
continue

core/interactive.py

Lines changed: 117 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import cmd
2+
from utils.crawler import crawl, find_page_forms
23
from utils.loggers import log
34
from urllib import parse
45
from core import checks
56
from core.channel import Channel
67
from core.clis import Shell, MultilineShell
78
from core.tcpserver import TcpServer
8-
import telnetlib
9+
from core.tcpclient import TcpClient
910
import socket
1011

1112

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

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

8795
def do_options(self, line):
8896
"""Show current SSTImap options"""
97+
crawl_domains = {"Y": "Yes", "S": "Subdomains only", "N": "No"}
8998
log.log(23, f'Current SSTImap {self.sstimap_options["version"]} interactive mode options:')
9099
if not self.sstimap_options["url"]:
91100
log.log(25, f'URL is not set.')
@@ -116,6 +125,14 @@ def do_options(self, line):
116125
log.log(26, f'Level: {self.sstimap_options["level"]}')
117126
log.log(26, f'Engine: {self.sstimap_options["engine"] if self.sstimap_options["engine"] else "*"}'
118127
f'{"+" if not self.sstimap_options["engine"] and self.sstimap_options["legacy"] else ""}')
128+
if self.sstimap_options["crawl_depth"] > 0:
129+
log.log(26, f'Crawler depth: {self.sstimap_options["crawl_depth"]}')
130+
else:
131+
log.log(26, 'Crawler depth: no crawl')
132+
if self.sstimap_options["crawl_exclude"]:
133+
log.log(26, f'Crawler exclude RE: "{self.sstimap_options["crawl_exclude"]}"')
134+
log.log(26, f'Crawl other domains: {crawl_domains.get(self.sstimap_options["crawl_exclude"].upper())}')
135+
log.log(26, f'Form detection: {self.sstimap_options["forms"]}')
119136
log.log(26, f'Attack technique: {self.sstimap_options["technique"]}')
120137
log.log(26, f'Force overwrite files: {self.sstimap_options["force_overwrite"]}')
121138

@@ -146,14 +163,74 @@ def do_url(self, line):
146163

147164
do_target = do_url
148165

166+
def do_crawl(self, line):
167+
self.sstimap_options['crawl_depth'] = int(line)
168+
if int(line):
169+
log.log(24, f'Crawling depth is set to {line}.')
170+
else:
171+
log.log(24, 'Crawling disabled.')
172+
173+
def do_exclude(self, line):
174+
self.sstimap_options['crawl_exclude'] = line
175+
if line:
176+
log.log(24, f'Crawler exclude RE is set to "{line}".')
177+
else:
178+
log.log(24, 'Crawler exclude RE disabled.')
179+
180+
do_crawl_exclude = do_exclude
181+
do_crawlexclude = do_exclude
182+
183+
def do_forms(self, line):
184+
overwrite = not self.sstimap_options['forms']
185+
log.log(24, f'Form detection {"en" if overwrite else "dis"}abled.')
186+
self.sstimap_options['forms'] = overwrite
187+
149188
def do_run(self, line):
150189
"""Check target URL for SSTI vulnerabilities"""
151190
if not self.sstimap_options["url"]:
152191
log.log(22, 'Target URL cannot be empty.')
153192
return
154193
try:
155-
self.channel = Channel(self.sstimap_options)
156-
self.current_plugin = checks.check_template_injection(self.channel)
194+
if self.sstimap_options['crawl_depth'] or self.sstimap_options['forms']:
195+
# crawler mode
196+
urls = set([self.sstimap_options['url']])
197+
if self.sstimap_options['crawl_depth']:
198+
print(1)
199+
crawled_urls = set()
200+
for url in urls:
201+
crawled_urls.update(crawl(url, self.sstimap_options))
202+
urls.update(crawled_urls)
203+
if not self.sstimap_options['forms']:
204+
for url in urls:
205+
print()
206+
log.log(23, f'Scanning url: {url}')
207+
self.sstimap_options['url'] = url
208+
self.channel = Channel(self.sstimap_options)
209+
self.current_plugin = checks.check_template_injection(self.channel)
210+
if self.channel.data.get('engine'):
211+
break # TODO: save vulnerabilities
212+
else:
213+
forms = set()
214+
print(2)
215+
for url in urls:
216+
forms.update(find_page_forms(url, self.sstimap_options))
217+
print(3)
218+
for form in forms:
219+
print()
220+
log.log(23, f'Scanning form with url: {form[0]}')
221+
self.sstimap_options['url'] = form[0]
222+
self.sstimap_options['method'] = form[1]
223+
self.sstimap_options['data'] = parse.parse_qs(form[2], keep_blank_values=True)
224+
self.channel = Channel(self.sstimap_options)
225+
self.current_plugin = checks.check_template_injection(self.channel)
226+
if self.channel.data.get('engine'):
227+
break # TODO: save vulnerabilities
228+
if not forms:
229+
log.log(22, f'No forms were detected to scan')
230+
else:
231+
# predetermined mode
232+
self.channel = Channel(self.sstimap_options)
233+
self.current_plugin = checks.check_template_injection(self.channel)
157234
except (KeyboardInterrupt, EOFError):
158235
log.log(26, 'Exiting SSTI detection')
159236
self.checked = True
@@ -321,6 +398,17 @@ def do_technique(self, line):
321398
log.log(24, f'Attack technique is set to {line}')
322399
self.sstimap_options["technique"] = line
323400

401+
def do_crawl_domains(self, line):
402+
"""Set crawling DOMAINS behaviour"""
403+
line = line.upper()
404+
if line not in ["Y", "S", "N"]:
405+
log.log(22, 'Invalid DOMAINS value. It should be \'Y\', \'S\' or \'N\'.')
406+
return
407+
log.log(24, f'Domain crawling is set to {line}')
408+
self.sstimap_options["crawl_domains"] = line
409+
410+
do_domains = do_crawl_domains
411+
324412
def do_legacy(self, line):
325413
"""Switch legacy option"""
326414
overwrite = not self.sstimap_options["legacy"]
@@ -494,16 +582,18 @@ def do_bind_shell(self, line):
494582
for idx, thread in enumerate(self.current_plugin.bind_shell(port)):
495583
log.log(26, f'Spawn a shell on remote port {port} with payload {idx+1}')
496584
thread.join(timeout=1)
497-
if not thread.is_alive():
498-
continue
499-
try:
500-
telnetlib.Telnet(url.hostname.decode(), port, timeout=5).interact()
501-
return
502-
except (KeyboardInterrupt, EOFError):
503-
print()
504-
log.log(26, 'Exiting bind shell')
505-
except Exception as e:
506-
log.debug(f"Error connecting to {url.hostname}:{port} {e}")
585+
if thread.is_alive():
586+
log.log(24, f'Shell with payload {idx+1} seems stable')
587+
break
588+
try:
589+
a = TcpClient(url.hostname, port, timeout=5)
590+
a.shell()
591+
return
592+
except (KeyboardInterrupt, EOFError):
593+
print()
594+
log.log(26, 'Exiting bind shell')
595+
except Exception as e:
596+
log.log(25, f"Error connecting to {url.hostname}:{port} {e}")
507597
else:
508598
log.log(22, 'No TCP shell opening capabilities have been detected on the target')
509599

@@ -588,3 +678,16 @@ def do_download(self, line):
588678

589679
do_up = do_upload
590680
do_down = do_download
681+
682+
# SSTImap commands
683+
684+
def do_reload_modules(self, line):
685+
"""Reload all modules"""
686+
from core.plugin import unload_plugins
687+
from sstimap import load_plugins
688+
unload_plugins()
689+
load_plugins()
690+
from core.plugin import loaded_plugins
691+
log.log(23, f"Reloaded plugins by categories: {'; '.join([f'{x}: {len(loaded_plugins[x])}' for x in loaded_plugins])}")
692+
693+
do_reload = do_reload_modules

core/plugin.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,20 @@
77
import collections
88
import threading
99
import time
10+
import sys
1011
import utils.config
1112

13+
loaded_plugins = {}
14+
15+
16+
def unload_plugins():
17+
global loaded_plugins
18+
for k in loaded_plugins:
19+
for p in loaded_plugins[k]:
20+
if p.__module__ in sys.modules:
21+
del sys.modules[p.__module__]
22+
loaded_plugins = {}
23+
1224

1325
def _recursive_update(d, u):
1426
# Update value of a nested dictionary of varying depth
@@ -50,6 +62,14 @@ def __init__(self, channel):
5062
self.language_init()
5163
self.init()
5264

65+
def __init_subclass__(cls, **kwargs):
66+
module = cls.__module__.split(".")
67+
if module[0] == "plugins":
68+
if module[1] in loaded_plugins:
69+
loaded_plugins[module[1]].append(cls)
70+
else:
71+
loaded_plugins[module[1]] = [cls]
72+
5373
def language_init(self):
5474
# To be overridden. This can call self.update_actions
5575
# and self.set_contexts

0 commit comments

Comments
 (0)