邀请码刷量漏洞攻防

"邀请一个好友,双方各得 20 元红包"——看似美好的增长方案,遇到职业羊毛党,可能让平台一天损失几百万。

一、刷量全景图

攻击者准备阶段:
  手机号资源(接码平台 / SIM 卡池)
  代理 IP(动态住宅 IP / 4G IP)
  设备指纹模拟器(Android 模拟器 / 改机工具)
  自动化脚本

攻击路径:
  注册账号A → 生成邀请码 → 注册账号B(填A的邀请码)→ A和B都拿奖励
  ↑  重复 N 次,即可薅 N × 奖励金额

二、漏洞场景一:自邀自奖

2.1 漏洞描述

用户可以用自己的邀请码"邀请"自己注册的新账号,形成自循环刷奖励。

2.2 漏洞代码

// VULNERABLE: 推荐关系没有闭环校验
app.post('/api/register', async (req, res) => {
  const { phone, password, inviteCode } = req.body;

  const user = await db.User.create({ phone, password: hash(password) });

  if (inviteCode) {
    const inviter = await db.User.findOne({ where: { inviteCode } });
    if (inviter) {
      // ❌ 没有检查 inviter 是不是当前用户自己
      // ❌ 没有检查 invitee 是否绑定过其他邀请关系
      await db.Referral.create({ inviterId: inviter.id, inviteeId: user.id });

      // 双方都发奖励
      await db.Wallet.increment('balance', { by: 20, where: { id: inviter.id } });
      await db.Wallet.increment('balance', { by: 20, where: { id: user.id } });
    }
  }

  res.json({ success: true });
});

2.3 攻击脚本

# 自邀自奖脚本
import requests, random, time

API = 'http://target.example.com'
HEADERS = {'Content-Type': 'application/json'}

def get_new_phone():
    # 接码平台获取手机号
    return requests.get('https://sms-provider.com/new').json()['phone']

def do_register(phone, password, invite_code):
    return requests.post(f'{API}/api/register', json={
        'phone': phone, 'password': password, 'inviteCode': invite_code,
    })

# 主号登录获取自己的邀请码
main_phone = '13800138000'
main_code = 'MY_CODE_123'

# 循环刷 100 个小号
for i in range(100):
    new_phone = get_new_phone()
    resp = do_register(new_phone, 'P@ssw0rd!', main_code)
    if resp.json().get('success'):
        print(f'[+] 成功注册第 {i+1} 个小号,双方各得 20 元')
    time.sleep(0.5)

2.4 修复

// FIXED: 多重校验 + 风控
app.post('/api/register', async (req, res) => {
  const { phone, password, inviteCode } = req.body;
  const clientIp = req.ip;
  const deviceFingerprint = req.headers['x-device-fp'];

  // 1. 创建用户
  const user = await db.User.create({ phone, password: hash(password) });

  if (inviteCode) {
    const inviter = await db.User.findOne({ where: { inviteCode } });

    // ✅ 校验链
    const checks = [
      inviter,                                    // 邀请人存在
      inviter.id !== user.id,                     // 不能邀请自己
      !inviter.directReferrals.includes(user.id), // 还没被邀请过
      user.createdAt - inviter.createdAt > 60000, // 邀请人注册超过1分钟
    ];

    // ✅ 风控校验
    const risk = await riskEngine.evaluate({
      clientIp,
      deviceFingerprint,
      phone,
      inviterId: inviter?.id,
    });

    if (checks.every(Boolean) && risk.score < 70) {
      await db.Referral.create({ inviterId: inviter.id, inviteeId: user.id });
      await db.Wallet.increment('balance', { by: 20, where: { id: inviter.id } });
      await db.Wallet.increment('balance', { by: 20, where: { id: user.id } });
    } else {
      console.warn('[RISK] Referral blocked', { checks, risk });
    }
  }

  res.json({ success: true });
});

三、漏洞场景二:IP/设备指纹批量注册

3.1 问题

同一个 IP 或同一台设备上注册大量账号,即使邀请码逻辑正确,也会被职业羊毛党利用代理 IP + 改机工具绕过。

3.2 防御方案

// 分层风控引擎
async function evaluateRisk({ clientIp, deviceFingerprint, phone, inviterId }) {
  const scores = [];

  // 1. IP 维度
  const ipCount = await redis.incr(`reg:ip:${clientIp}`);
  if (ipCount === 1) await redis.expire(`reg:ip:${clientIp}`, 3600);
  scores.push(ipCount > 3 ? 30 : 0); // 同 IP 注册超过 3 次,风险 +30

  const ipProxy = await checkProxy(clientIp); // 检测是不是代理
  scores.push(ipProxy ? 25 : 0);

  // 2. 设备维度
  const devCount = await redis.incr(`reg:dev:${deviceFingerprint}`);
  if (devCount === 1) await redis.expire(`reg:dev:${deviceFingerprint}`, 86400);
  scores.push(devCount > 5 ? 35 : 0);

  // 3. 手机号维度
  const phonePattern = phone.substring(0, 7);
  const batchPhones = await db.User.count({ where: { phone: { $like: phonePattern + '%' }, createdAt: { $gt: new Date(Date.now() - 3600000) } } });
  scores.push(batchPhones > 10 ? 20 : 0); // 同一号段 1 小时内注册超过 10 个

  // 4. 邀请关系维度
  if (inviterId) {
    const inviterSpeed = await redis.zcount(
      `referral:timeline:${inviterId}`,
      Date.now() - 3600000, Date.now()
    );
    scores.push(inviterSpeed > 5 ? 25 : 0); // 1 小时内邀请超过 5 人
  }

  return { score: scores.reduce((a, b) => a + b, 0), details: scores };
}

四、漏洞场景三:提现绕过

4.1 问题

即使成功拦截了刷注册,攻击者仍可能通过邀请奖励提现洗钱。防御链必须延伸到提现环节。

4.2 防御

app.post('/api/withdraw', authMiddleware, async (req, res) => {
  const { amount, bankCard } = req.body;

  // ✅ 必须完成实名
  if (!req.user.verified) {
    return res.status(400).json({ error: '请先完成实名认证' });
  }

  // ✅ 提现银行卡必须与实名信息一致
  const cardInfo = await db.BankCard.findOne({
    where: { userId: req.user.id, cardNumber: bankCard },
  });
  if (!cardInfo || !cardInfo.verified) {
    return res.status(400).json({ error: '请先绑定银行卡' });
  }

  // ✅ 奖励金额提现必须满足"存活期"
  const referralRewards = await db.WalletLog.sum('amount', {
    where: {
      userId: req.user.id,
      type: 'REFERRAL',
      createdAt: { $lt: new Date(Date.now() - 7 * 86400000) }, // 7 天冻结期
    },
  });
  if (amount > referralRewards) {
    return res.status(400).json({ error: '邀请奖励需 7 天后才可提现' });
  }

  // ✅ 单日/单月限额
  const dailyTotal = await db.Withdraw.sum('amount', {
    where: { userId: req.user.id, createdAt: { $gt: new Date(Date.now() - 86400000) } },
  });
  if (dailyTotal + amount > 2000) {
    return res.status(400).json({ error: '单日提现限额 2000 元' });
  }

  // ✅ 频控
  const withdrawLock = await redis.set(
    `withdraw:lock:${req.user.id}`, '1', 'NX', 'EX', 60
  );
  if (!withdrawLock) {
    return res.status(429).json({ error: '操作太频繁' });
  }

  // ✅ 大额提现走人工审核
  if (amount >= 10000) {
    await createManualReview(req.user.id, amount);
    return res.json({ success: true, message: '提现已提交审核' });
  }

  await processWithdraw(req.user.id, amount);
  res.json({ success: true });
});

五、邀请码系统通用防御架构

                  ┌─────────────────────────────┐
                  │       注册/邀请请求入口       │
                  └────────────┬────────────────┘
                               │
                    ┌──────────▼──────────┐
                    │ ① 身份真实性校验     │
                    │  (实名 / 手机实名)   │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ ② 关系链合理性校验   │
                    │  (不能自邀 / 循环链) │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ ③ 多维度风控打分     │
                    │  (IP/设备/行为)     │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ ④ 奖励到账延迟 + 冻结│
                    │  (T+7 才可提现)      │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ ⑤ 反洗钱规则引擎     │
                    │  (大额/频次/异常链) │
                    └─────────────────────┘

反循环邀请链检测

// 防止 A→B→C→A 的循环邀请
async function detectLoop(inviterId, inviteeId) {
  const visited = new Set();
  let current = inviterId;
  let count = 0;
  const MAX_DEPTH = 10;

  while (current && count < MAX_DEPTH) {
    if (visited.has(current)) return true; // 发现循环
    visited.add(current);
    const referral = await db.Referral.findOne({
      where: { inviteeId: current },
      order: [['createdAt', 'DESC']],
    });
    current = referral?.inviterId;
    count++;
  }
  return false;
}

六、总结

邀请码刷量的本质是:攻击者的边际成本极低(接码 1 元/个 + 代理 IP),而防御者要覆盖所有维度。

核心防御策略:

  1. 闭环校验:不能自邀、不能循环、不能批量
  2. 实时风控:IP + 设备 + 行为 + 关系链多维度打分
  3. 延迟结算:奖励延迟到账,给风控留排查时间
  4. 提现设卡:实名 + 冻结期 + 限额 + 人工审核
  5. 持续迭代:羊毛党手法会更新,风控规则必须跟着迭代