json 特性 -- justctf 2025 Ticket Master

/generate

@app.route("/generate", methods=["POST"])
def generate():
    # 生成票据接口
    if not request.is_json:
        return jsonify({"error": "Invalid JSON"}), 400

    data = request.get_json()
    ticket_data = TicketData(data=data)

    # 如果未指定座位,随机分配一个
    if len(ticket_data.seat) == 0:
        ticket_data.seat.append_entry().data = random.choice(SEAT_ROWS)
        ticket_data.seat.append_entry().data = random.choice(SEAT_NUMS)

    # 检查请求参数,决定票据类型
    if not any(key in data for key in ["number", "section", "seat", "type"]):
        ticket_data.number.data = 2137
        ticket_data.type.data = "FREE"
    elif "seller_key" in data and data["seller_key"] == SELLER_KEY:
        ticket_data.type.data = "PAID"
    else:
        return jsonify({"error": "You're not allowed to generate that ticket!"}), 422

    # 校验表单和座位是否合法
    if not ticket_data.validate() or ticket_data.data["seat"][0] not in SEAT_ROWS or ticket_data.data["seat"][1] not in SEAT_NUMS:
        return jsonify({"error": "Something went wrong.."}), 500

    data = ticket_data.data
    seat = f"{data['section']}/{''.join(data['seat'])}"  # 生成座位字符串
    if seat in GENERATED_TICKETS:
        return jsonify({"error": "Sorry! Someone just bought this seat before you!"}), 410

    try:
        # 生成票据图片
        ticket = generate_ticket(
            data["number"],
            seat,
            data["type"]
        )
        GENERATED_TICKETS.append(seat)  # 记录已生成的座位
    except Exception as ex:
        return jsonify({"error": ex}), 500

    # 返回base64编码的票据图片
    return jsonify({"ticket": base64.b64encode(ticket).decode()}), 200

首先其需求一个json

class TicketData(Form):
    number = IntegerField(validators=[DataRequired()], default=lambda: random.randint(10000, 99999))
    section = StringField(validators=[DataRequired(), AnyOf(["PLAYER", "VIP", "BACKSTAGE"])], default="PLAYER")
    seat = FieldList(
        StringField(validators=[DataRequired(), Length(min=1, max=1)]),
        IntegerField(validators=[DataRequired(), NumberRange(min=1, max=7)])
    )
    type = StringField(validators=[DataRequired(), AnyOf(["FREE", "PAID", "ORGANIZER"])])
{
  "number": 12345,                // 整数 (10000-99999)
  "section": "PLAYER",            // 字符串 (必须为 PLAYER/VIP/BACKSTAGE)
  "seat": ["A", 3],               // 数组 [行, 号]
  "type": "PAID"                  // 字符串 (必须为 FREE/PAID/ORGANIZER)
}
{"number":12345,"section":"PLAYER","seat":["A",3],"type":"PAID"}

我们没有key,所以唯一合法的票证是传递空票

接下来程序利用这些数据生成图形化票证

def add_text(img, text, pos):
    # 检查文本是否只包含允许的字符
    if not ALLOWED_PATTERN.match(text):
        raise ValueError("Illegal characters used")  # 非法字符

    # 将OpenCV图像转换为PIL图像以便绘制文本
    img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)
    font = ImageFont.truetype('assets/comic.ttf', 25)  # 加载字体

    # 如果文本太长则截断
    while text:
        bbox = font.getbbox(text)
        text_width = bbox[2] - bbox[0]
        if text_width <= 160:
            break
        text = text[:-1]

    # 绘制文本到指定位置
    draw.text(pos, text, font=font, fill=(0, 0, 0))
    # 将PIL图像转换回OpenCV格式
    img[:] = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

def generate_signature(data):
    # 用HMAC-SHA256生成签名
    return hmac.new(environ["SIGNATURE_SECRET_KEY"].encode(), data, hashlib.sha256).digest()

def generate_image_with_signature(img):
    # 将OpenCV图像转换为PIL图像
    img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

    img_io = BytesIO()
    img.save(img_io, format="JPEG")  # 保存为JPEG格式到内存
    signature = generate_signature(img_io.getvalue())  # 生成签名

    # 返回图片数据和签名拼接
    return img_io.getvalue() + signature

def generate_ticket(ticket_number, seat, type):
    # 读取票据模板图片
    img = cv2.imread(TICKET_TEMPLATE)

    # 添加票号、座位、类型文本
    add_text(img, str(ticket_number), (825, 297))
    add_text(img, seat, (825, 327))
    add_text(img, type, (825, 357))

    # 生成带签名的图片
    return generate_image_with_signature(img)

/enter

@app.route("/enter", methods=["POST"])
def enter():
    # 入场验证接口
    if not request.is_json:
        return jsonify({"error": "Invalid JSON"}), 400

    data = request.get_json()

    if "img" not in data:
        return jsonify({"error": "Missing 'img' parameter"}), 400

    try:
        # 解析票据图片,获取票号、座位和类型
        ticket_number, seat, type = load_ticket(base64.b64decode(data['img']))

        return jsonify({
                "ticket_number": ticket_number,
                "seat": seat,
                "message": f"Welcome!\nEnjoy {type} ticket!" if type in ["FREE", "PAID"] else environ["FLAG"]
            }), 200
    except Exception as ex:
        return jsonify({"error": str(ex)}), 500
def load_ticket(data):
    # 检查数据长度是否合法
    if len(data) < 1024 or len(data) > 512_000:
        raise ValueError("This is not a ticket!")

    img_io = BytesIO(data[:-32])  # 图片数据
    signature = data[-32:]        # 签名
    
    # 校验签名
    if signature != generate_signature(img_io.getvalue()):
        raise ValueError("Invalid signature")
    
    img = Image.open(img_io)
    # 检查图片格式、模式和尺寸
    if img.format != "JPEG" or img.mode != "RGB" or img.size != (1000, 400):
        raise ValueError("Malformed ticket")
    
    # 转为灰度图像以便OCR
    gray_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY)

    # 解析票据信息
    return parse_ticket_info(gray_img)
def read_lines(img, n):
    # 使用OCR读取指定区域的文本行
    it = iter(line.strip() for line in pytesseract.image_to_string(img[297:392, 823:993]).split("\n") if line)
    return [next(it, None) for _ in range(n)]  # 返回前n行

def parse_ticket_info(img):
    # 解析票据信息,返回三行内容
    return read_lines(img, 3)

part 1

这些字符串都是合法的,可被json解析的合法字符串 from justctf Ticket Master

import json
data = '[[]]'
print(json.loads(data))
data = '[["a", "b"], ["c", "d"]]'
print(json.loads(data))
data = '"this_is_str"'
print(json.loads(data))
data = "true"
print(json.loads(data))
data = "[ 1, 2, 3 ]"
print(json.loads(data))
data = "1e3"
print(json.loads(data))
data = "-1"
print(json.loads(data))

利用这个特性,我们可以提供一个意外的值绕过waf,此值可以被TicketData正常解析

[["seat", "A1iiiii"], ["section", "VIP"]]
if not any(key in data for key in ["number", "section", "seat", "type"]):
    ticket_data.number.data = 2137
    ticket_data.type.data = "FREE"
elif "seller_key" in data and data["seller_key"] == SELLER_KEY:
    ticket_data.type.data = "PAID"
else:
    return 422

part 2

seat = FieldList(
    StringField(validators=[DataRequired(), Length(min=1, max=1)]),
    IntegerField(validators=[DataRequired(), NumberRange(min=1, max=7)])
)

这里开发者显然表示 seat = [行, 号](例如 ["A",3])。但实际写法让 FieldList子字段只有一个:单字符 StringField,而不是“两个不同类型的元素”。结果:

  • 当我们把 "seat": "A1iiiii" 作为一个字符串喂给 FieldList 时,WTForms 会把它当成可迭代,据此创建若干个子条目:["A","1","i","i","i","i","i"]

  • 每个子条目都是长度为 1 的字符串,完全满足 DataRequired + Length(min=1,max=1),因此验证通过

  • 业务侧的“合法性检查”只看前两个元素:

    if not ticket_data.validate() \
       or ticket_data.data["seat"][0] not in SEAT_ROWS \
       or ticket_data.data["seat"][1] not in SEAT_NUMS:
        return 500
    

    ——也就是说它只验证 seat[0]seat[1]忽略其后的多余元素

  • 随后用于绘图的字符串是 ''.join(data['seat']),因此多出来的 "iiiii" 会被原样画到票上:

    seat = f"{data['section']}/" + ''.join(data['seat'])
    # 例如 "VIP/A1iiiii"
    

part 3

fuzz 一下尝试各种字符干扰图像识别即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值