《智能体设计模式》中文版已发布, 点击阅读

新型 GitHub 资助申请诈骗全解析:利益链条、作案流程、技术实现与防御

以我近期遭遇的“GitHub × Gitcoin Developer Fund 2025”钓鱼为案例,系统分析其利益驱动、完整链条、技术实现与个人/组织的防御与应急 SOP。

本文以我亲身遭遇的“GitHub × Gitcoin Developer Fund 2025”钓鱼事件为例,系统梳理其利益链条、作案流程、技术实现与防御措施,帮助技术社区识别并应对新型 Web3 诈骗。

引言

近日,我在 GitHub 收到一封伪装成 “GitHub × Gitcoin Developer Fund 2025” 的邮件通知,声称我已“符合资格”,只需点击链接、通过 Gitcoin Passport 验证钱包并支付“可退还押金”即可获得资助。大量开发者也反馈收到了类似邮件,详见 社区讨论 #174283

图 1: 钓鱼邮件截图
图 1: 钓鱼邮件截图

这种诈骗方式利用了 GitHub 通知系统的权威感,并结合 Web3 钱包授权与押金,伪装成高大上的资助计划,实则是资金和账号窃取的骗局。本文将从利益驱动、作案链条、技术实现到防御措施进行系统分析。

GitHub 通知“套壳”与钓鱼入口

攻击者通过脚本账号在陌生仓库发起 Issue 或 Discussion,并 @ 上千名开发者(包括我),触发 GitHub 的系统通知邮件,轻松绕过垃圾拦截,直接进入收件箱。即便是经验丰富的开发者,也可能因“GitHub 官方通知”形式而放松警惕。

示例链接: 钓鱼 Issue (GitHub Issue,可以放心点击)

钓鱼页面剖析与典型特征

访问钓鱼页面 github-foundation.com 后,无论点击页面哪个位置,都会弹出“Connect Wallet”窗口,支持 MetaMask、Trust Wallet、WalletConnect 等主流钱包。

图 2: 假冒的 Gitcoin 页面
图 2: 假冒的 Gitcoin 页面

主要特征如下:

  • 域名伪装github-foundation.com 与官方域名完全不同。
  • 全屏诱导:页面无实质信息,所有操作均引导钱包连接。
  • 虚假背书:展示 Gitcoin 的真实数据,但脱离上下文。
  • 钱包陷阱:授权或支付押金后,资金和权限即被盗取。

目标画像与攻击策略

攻击者优先选择具有一定影响力和资产的开发者账号,如 GitHub Developer Program 成员、拥有 Sponsors、活跃度高等。这类账号更可能点开链接,且钱包资产和仓库权限价值更高。

但同时,攻击者采用批量撒网策略,混合高价值和低价值用户一起投放,只要有少量开发者上钩即可获利。

利益链条与作案流程

攻击者的完整利益链条如下:

  • 流量获取:批量账号发帖,@ 大量用户,利用 GitHub 邮件通知背书。
  • 转化设计:假域名、假文案、假合作方,制造“官方感”。
  • 获利手段:押金支付骗钱、钱包无限授权盗取资产、GitHub 授权用于后续供应链攻击。
  • 风险对冲:一次性账号,批量投放,快速跑路。
图 3: 作案流程图
图 3: 作案流程图

技术实现与工程化细节

  • 利用 GitHub 通知系统“借壳”,提高投递成功率。
  • 域名 typosquatting,仿冒 github.com。
  • 钱包交互社工,利用“仅签名,不会扣费”降低防备心理。
  • 批量 @,覆盖广,攻击成本低。
  • 后续利用 GitHub 授权,可能插入恶意代码。

社区反馈与受害情况

在 GitHub Community 讨论区,已有开发者反馈收到同类 spam,说明该骗局已进入大规模传播阶段,并非孤立案例。

如何删除垃圾通知

针对此类钓鱼导致的垃圾或“幽灵”通知,可参考 社区讨论中的有效解决方案 。下载下面的清理脚本,并在本地运行 node remove_phantom_notifications.js TIMESTAMP

🟨 remove_phantom_notifications.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
const { exec } = require("node:child_process");
const { basename } = require("node:path");

function runShellCommand(command) {
  return new Promise((resolve, reject) => {
    exec(command, (error, stdout, stderr) => {
      if (error) {
        reject({ error, stderr });
        return;
      }
      resolve(stdout);
    });
  });
}

let _githubToken = null;
async function getGithubToken() {
  if (!_githubToken) {
    _githubToken = await runShellCommand("gh auth token");
  }
  return _githubToken;
}

async function getNotifications(since) {
  const response = await fetch(`https://api.github.com/notifications?all=true&since=${since}`, {
    headers: {
      'Accept': 'application/vnd.github+json',
      'Authorization': `Bearer ${await getGithubToken()}`,
      'X-GitHub-Api-Version': '2022-11-28',
    },
  });
  return response.json();
}

async function shouldIncludeNotificationForRemoval(notification) {
  try {
    const response = await fetch(`https://api.github.com/repos/${notification.repository.full_name}`, {
      headers: {
        Accept: "application/vnd.github+json",
        Authorization: `Bearer ${await getGithubToken()}`,
        "X-GitHub-Api-Version": "2022-11-28",
      },
    });
    return response.status === 404;
  } catch (error) {
    console.log("threw");
    if (error.code && error.code === 404) {
      return true;
    }
    console.error(error);
    throw error;
  }
}

async function markNotificationRead(notification) {
  const response = await fetch(notification.url, {
    method: "PATCH",
    headers: {
      "Authorization": `Bearer ${await getGithubToken()}`,
      "Accept": "application/vnd.github+json",
      "X-GitHub-Api-Version": "2022-11-28",
    },
  });
  if (!response.ok) {
    console.error(`Failed to mark notification with thread URL ${notification.url} from repo ${notification.repository.full_name} as read: ${response.status} ${response.statusText}`);
  }
}
async function markNotificationDone(notification) {
  const response = await fetch(notification.url, {
    method: "DELETE",
    headers: {
      "Authorization": `Bearer ${await getGithubToken()}`,
      "Accept": "application/vnd.github+json",
      "X-GitHub-Api-Version": "2022-11-28",
    },
  });
  if (!response.ok) {
    console.error(`Failed to mark notification with thread URL ${notification.url} from repo ${notification.repository.full_name} as done: ${response.status} ${response.statusText}`);
  }
}

async function unsubscribe(notification) {
  const response = await fetch(notification.subscription_url, {
    method: "DELETE",
    headers: {
      "Authorization": `Bearer ${await getGithubToken()}`,
      "Accept": "application/vnd.github+json",
      "X-GitHub-Api-Version": "2022-11-28",
    },
  });
  if (!response.ok) {
    console.error(`Failed to unsubscribe from notification with thread URL ${notification.url} from repo ${notification.repository.full_name}: ${response.status} ${response.statusText}`);
  }
}

async function main() {
  const since = process.argv[2];
  if (!since) {
    console.error(`Usage: ${basename(process.argv[0])} ${basename(process.argv[1])} <since>`);
    process.exit(1);
  }

  try {
    new Date(since);
  } catch (error) {
    console.error(`${since} is not a valid ISO 8601 date. Must be formatted as YYYY-MM-DDTHH:MM:SSZ.`);
    console.error(`Usage: ${basename(process.argv[0])} ${basename(process.argv[1])} <since>`);
    process.exit(1);
  }

  const notifications = await getNotifications(since);
  for (const notification of notifications) {
    if (await shouldIncludeNotificationForRemoval(notification)) {
      console.log(`Marking notification with thread URL ${notification.url} read from repo ${notification.repository.full_name}`);
      await markNotificationRead(notification);
      console.log(`Marking notification with thread URL ${notification.url} done from repo ${notification.repository.full_name}`);
      await markNotificationDone(notification);
      console.log(`Unsubscribing from notification with thread URL ${notification.url} from repo ${notification.repository.full_name}`);
      await unsubscribe(notification);
    }
  }
  console.log("Done");
}

main().catch(console.error);

比如清理 2025 年 9 月 25 日之后的幽灵通知:

node remove_phantom_notifications.js 2025-09-25T00:00:00Z

防御与应急措施

这里总结一些防御策略:

个人防御:

  • 警惕涉及钱包签名或押金的操作,默认诈骗。
  • 启用 GitHub 2FA,定期审计 OAuth App、PAT、SSH Keys,撤销可疑授权。
  • 邮件过滤,对标题含 GitcoinFundPassportUSDC 的通知自动打标签。

组织防御建议

  • 实施 SSO 与权限最小化原则。
  • 限制外部 App 授权,统一官方资金/资助入口。
  • 制定快速应急预案,准备撤销密钥与隔离仓库流程。

事后处置 SOP

  1. 撤销钱包授权;
  2. 删除 GitHub 可疑授权、Token、SSH;
  3. 审计仓库 secrets 与 actions;
  4. 举报钓鱼域名、账号、仓库。

IOC 附录(Indicators of Compromise)

  • 钓鱼域名github-foundation.com
  • 常见关键词GitHub × Gitcoin Developer Fund 2025refundable depositGitcoin Passport verification
  • GitHub 行为特征:批量陌生账号在无关仓库发 Issue/Discussion,@ 上百个无关开发者。

总结

本案例揭示了开源社区与 Web3 场景融合下的新型钓鱼诈骗,攻击者通过 GitHub 通知机制“借壳”,结合钱包授权与押金变现,危险之处在于大规模工程化与平台背书。有效防御需对资金和授权零信任,始终通过官方入口操作,个人与组织均应实施最小权限原则,提升安全意识。

文章导航

评论区