先问一句你上一次用搜索引擎和代码托管平台查自家域名是上周还是上个月如果你还停留在“想起什么搜什么”的状态那这篇内容就是为你准备的。我在安全相关的事故复盘里见过太多次类似的场景——一个系统两个月前就被公开索引了源码片段直到第三方通报才被发现原因无非是“之前查过没事后来就没再盯”。这恰恰是“信息猎人”和“资产哨兵”之间最本质的距离前者靠一次搜索碰运气后者靠一套机制持续盯防。这篇内容围绕 Google/GitHub 的高级检索语法以及如何把这些语法变成自动化监控流水线展开会讲清楚到底哪些操作符值钱、组合规则怎么设计、搜到结果之后怎么判断真假、监控系统怎么搭才不变成告警轰炸机。适合安全工程师、运维、后端开发以及所有手里握着资产却不知道它们在外面“长什么样”的人。1. 先换个视角搜索到的每个结果都是资产暴露面的线索1.1 从“想起来搜一把”到“盯住持续变化”搜索引擎和代码托管平台本质上是一台巨大的索引机器。你发布到公网的每一个页面、每一段代码、每一次 commit只要被索引到就会留下可被检索的记录。所谓 Google Hacking、GitHub Hacking并不是什么攻击手段而是用非默认的查询语法把这些记录中“不该公开但偏偏被公开了”的内容捞出来。问题在于“捞出来”这件事如果靠人肉手工做注定是不可持续的。你今天搜了 20 条 dork明天可能就要上新业务、开新仓库、发新版本。资产在持续增长索引在持续更新暴露面在持续变化。而那些被你搜出来之后顺手关掉的洞可能在你没有看管的时间窗口里又悄悄出现。这就是为什么我在自己的监控方案里几乎不用“临时搜索”这种做法一切查询都必须是可重复、可调度、可追踪的。1.2 搜索引擎与代码托管平台为什么会成为信息富矿稍微拆一下底层逻辑你会更清楚这个富矿是怎么形成的。搜索引擎的爬虫会自动抓取公网页面把标题、链接、正文、文件类型这些维度拆出来建索引。于是你只要在查询框里告诉它“我要找某个域名下面的配置文件”它就能在索引里把这类内容翻出来速度比你自己漫无目的地逛网站快几个数量级。代码托管平台则更特殊。它不只是存代码还把 repo 名、文件路径、文件内容、提交信息、issue、讨论全部索引了。很多团队在代码里写入密钥、内网地址、数据库口令时并不会意识到这些东西会随着一次面向组织的仓库操作而被收录。更麻烦的是历史 commit 里的敏感信息不会因为后来删除就立刻消失旧版本在索引中可能还留有一份。理解了这一点你就知道为什么安全圈普遍把代码托管平台检索当作暴露面监控的核心输入源之一。1.3 一次迟到的发现没有监控的检索等于开盲盒讲个我早期踩过的坑。当时为了验证某模拟项目 X 的子域是否存在敏感信息泄露我用搜索引擎手工跑了一批查询还真找到了一条指向后台登录页的索引记录。我记在了笔记里打算下周一处理。结果周四就收到预警说这个后台疑似被他人访问过。事后复盘索引暴露时间其实持续了好几天我搜到的时候已经晚了只是因为缺少持续监控没能第一时间发现变化。那次之后我彻底明白了一个道理安全监控的价值不在于“你能搜到什么”而在于“你多久能发现新的变化”。先想清楚这道题再看后面的语法和工具思路就顺了。2. Google 查询操作符的精拆组合、排除与索引内搜索2.1 核心操作符逐个过site、inurl、filetype、intitle、intext搜索引擎的网页检索语法看着简单但很多人只会单点使用不会组合。先把我日常用最频繁的几个操作符理一遍每一个都直接对应一种暴露类型。site:限定搜索某个域名或子域定位速度最快噪音最小。比如site:example.com能把索引中所有该域名的页面列出来。inurl:限定链接中必须出现某个关键词适合找后台路径、管理入口、上传点。比如inurl:admin、inurl:upload。filetype:限定文件类型是找遗留文件的神器。.sql、.bak、.env、.log、.xlsx这些后缀各有各的风险。intitle:限定页面标题中出现关键词配合站点限定能筛出标题特征明显的后台或文档。intext:限定正文内容中出现关键词适合搜“password”“token”“appsecret”这类敏感字符串。这些操作符单独用的价值有限但一旦和域名、文件类型、关键词交叉组合就能变成相当精准的过滤网。比如site:example.com filetype:env可以快速看这个域名下有没有环境变量文件被索引site:example.com inurl:api则能列出暴露的接口路径。2.2 组合逻辑才是精华排除噪音、收紧范围、交叉验证高级语法的进阶用法不是背更多操作符而是理解组合逻辑。搜索引擎默认的多个关键词之间是宽松匹配关系所以你经常发现搜索结果里混着大量“看起来相关但根本不是那么回事”的页面。我的做法是三步收紧。第一步用site:锁定资产范围把搜索目标从全网缩小到自己的域名第二步用-排除掉已知无风险的路径比如-inurl:js、-inurl:css把静态资源甩出去第三步用引号把关键片段变成精确匹配比如搜索DB_PASSWORD和搜索DB_PASSWORD的返回数量会差好几倍前者只会命中包含完整字符串的内容后者可能把普通文章、讨论帖全带出来。交叉验证也很重要。同一个暴露线索我会用不同维度的查询互相印证。比如先site:example.com filetype:sql发现一个备份文件再用site:example.com intext:create table确认当前的索引里是否还有其他数据库导出物。两次查询都能命中才说明这类问题不是孤例需要按批量风险来处理。2.3 一套可直接起手的侦察 dork 清单下面这套清单是我在自有资产的例行检查中经常用的骨架域名部分请替换成你自己的测试域名。注意两个原则一是规则尽量窄避免命中太多无关结果二是保留备注方便后续三个月后回看时还能想起来当初的搜索意图。场景查询语法示例关注点站点整体暴露面site:example.com先看有哪些页面被收录后台入口site:example.com inurl:admin OR inurl:login管理后台是否暴露环境配置site:example.com filetype:env数据库口令、密钥可能藏在这里备份文件site:example.com filetype:bak OR filetype:sql过期备份不能停留在 Web 根目录敏感目录列表site:example.com intitle:index of目录开启了列表访问调试信息site:example.com inurl:phpinfo OR ext:log探针、日志是否可读“intitle:index of”这个语法建议单独记住。一旦站点开了目录列表访问爬虫会把目录结构整体索引下来你用这一个查询就能看到哪些目录下躺着不该公开的文件。我的经验是只要这类命中的数量超过 3 条往往意味着 Web 服务的基础配置存在系统性问题不只是单个文件放错位置那么简单。3. GitHub 代码层检索从密钥泄露到历史提交里的残留3.1 理解 GitHub 搜索的索引边界与登录限制代码托管平台的检索逻辑和搜索引擎不太一样。它索引的粒度更细——不只是页面而是文件内容、路径、commit message、issue 正文等结构化维度全拆开了。这就意味着你可以去搜“哪个仓库里存在一个包含了字符串aws_secret_access_key的文件”这在传统网页搜索里是无法实现的。先说几个使用前提。未登录状态下GitHub 的代码搜索结果通常是不完整的很多场景还要求你先登录才能触发代码搜索接口。而且代码搜索有数量上限一个查询最多返回前几千条结果排序也偏向“最近更新”和“最佳匹配”。所以不要指望一个宽泛的查询能把所有仓库翻个底朝天——设计规则时要尽量把搜索范围压窄优先用组织名、仓库名、文件路径去框定目标。另外要区分fork:true这个限定符。很多团队对外发布的开源仓库内部版本会通过 fork 的方式流传到个人空间里。如果这些 fork 里残留了内网配置文件那么即使员工已经离职代码片段仍然留在平台上。搜索时默认会包含 fork我会在关键规则的查询里显式指定是否要排除。3.2 常用限定符与代码搜索的实战模式GitHub 的限定符体系很丰富我挑几个在实际监控中最常用的列出来org:限定组织账号下的仓库比user:更贴合企业场景。repo:直接锁定某个仓库适合已知仓库的定向复查。language:按语言过滤找 Java 配置文件时排除无关的 Markdown 和 HTML 噪音。extension:精确指定文件后缀比语言过滤更严格比如extension:env、extension:properties。path:限定文件路径比如path:src/main/resources能精准定位配置文件目录。created:、pushed:限定时间范围是监控新增内容的关键维度。举个例子org:example-org language:java extension:env就能把某个组织下所有 Java 项目里的.env文件列出来。如果再叠加敏感关键词比如filename:.env DB_PASSWORD结果直接反映高优先级风险。这里有一个比较关键的细节GitHub 代码搜索对特殊字符的处理会影响结果。比如连字符、冒号、斜杠这些符号在索引里的分词规则和我们直觉不一致所以搜索含有 URL 的字符串时最好把其中一个片段放进引号里做精确匹配其余部分用通配符兜底。我写过jdbc:mysql:// org:example-org这类查询效果比不带引号稳定得多。3.3 用 API 把 GitHub 搜索变成程序可调用的接口手工在网页端搜可以用于临时验证但要构建监控系统必须把搜索行为程序化。GitHub 官方提供的 REST API 里就有代码搜索接口把查询语句放在q参数里即可。import os import requests GITHUB_TOKEN os.getenv(GITHUB_TOKEN) QUERY org:example-org extension:env DB_PASSWORD headers { Authorization: ftoken {GITHUB_TOKEN}, Accept: application/vnd.github.v3.text-matchjson, } resp requests.get( https://api.github.com/search/code, headersheaders, params{q: QUERY, per_page: 20}, ) resp.raise_for_status() items resp.json().get(items, []) for item in items: print(item[repository][full_name], item[path], item[html_url])有几个容易被忽略的限制必须提前说清楚。代码搜索 API 要求请求必须带身份认证匿名请求基本不可用。配额方面代码搜索的速率限制要明显低于其他接口普通认证账号每分钟只有个位到十几次的量级具体数值以官方文档为准。所以监控脚本不能高频连打建议用下面第 4 章的方式先做规则分组和采集队列再统一调度。还有一个值得养成的习惯代码搜索响应里带有text-match元数据如果请求头指定了Accept: application/vnd.github.v3.text-matchjson能直接拿到匹配片段在文件里的起止位置和具体内容摘要。这些片段对于后续误报判断特别有用可以快速看到命中的上下文而不必每次下载整个源码文件。4. 自动化监控规则库、采集器、变化检测与告警分级4.1 最小可用架构四层流水线设计自动化监控听起来复杂其实拆开就四层规则库、采集器、差异检测器、告警通道。规则库决定“我要盯什么”采集器负责“去搜索照做”差异检测器判断“和上次比有什么变化”告警通道把变化推给该看的人。这里我不建议一上来就上重型平台。对多数团队来说一个 Python 脚本加一个 SQLite 文件加一个定时任务已经能覆盖 80% 的需求。原因很简单监控系统本身也是资产过度设计会让它变成第二个需要维护的 Web 应用。轻量方案的容错和维护成本都低等规则数量超过几百条、告警触发频率明显上升时再考虑抽象成服务也不迟。采集器要解决的核心问题是“去重”。同一个暴露条目今天搜到明天刷新结果还会出现如果每次都当成新告警发出来三天之后你就不想看告警群了。所以数据库里必须存下每次采集到的结果唯一标识通常用仓库名加文件路径加资源地址拼接后做哈希。只有新出现的哈希才进入后续处理流程。4.2 一个可运行的监控脚本骨架我提供一个精简但完整的脚本思路你可以在此基础上扩展。先把查询规则集中到一个列表里每条规则绑定一个严重级别然后循环调用采集函数写入 SQLite再对比上一次采集的结果。import os import sqlite3 import hashlib import time import requests from datetime import datetime GITHUB_TOKEN os.getenv(GITHUB_TOKEN) DB_FILE watch.db ALERT_WEBHOOK os.getenv(ALERT_WEBHOOK) RULES [ {name: env_dump, query: org:example-org extension:env, level: high}, {name: db_passwd, query: org:example-org DB_PASSWORD language:python, level: high}, {name: subdomain_list, query: org:example-org filename:domains.txt, level: medium}, ] def build_fingerprint(item): raw f{item.get(repository, {}).get(full_name, )}|{item.get(path, )} return hashlib.sha256(raw.encode()).hexdigest() def collect(query): headers { Authorization: ftoken {GITHUB_TOKEN}, Accept: application/vnd.github.v3.text-matchjson, } params {q: query, per_page: 30} resp requests.get(https://api.github.com/search/code, headersheaders, paramsparams, timeout30) resp.raise_for_status() return resp.json().get(items, []) def main(): conn sqlite3.connect(DB_FILE) conn.execute(CREATE TABLE IF NOT EXISTS results( fp TEXT PRIMARY KEY, rule_name TEXT, level TEXT, repo TEXT, path TEXT, url TEXT, first_seen TEXT, last_seen TEXT )) for rule in RULES: try: items collect(rule[query]) except Exception as exc: print(f[{rule[name]}] collect failed: {exc}) continue for item in items: fp build_fingerprint(item) now datetime.utcnow().isoformat() repo item[repository][full_name] row conn.execute( SELECT first_seen, last_seen FROM results WHERE fp?, (fp,) ).fetchone() if row is None: conn.execute( INSERT INTO results(fp, rule_name, level, repo, path, url, first_seen, last_seen) VALUES(?,?,?,?,?,?,?,?), (fp, rule[name], rule[level], repo, item[path], item[html_url], now, now) ) # 新出现的结果按级别决定是否推送 if rule[level] in (high, critical): send_alert(rule, item) print(f[NEW] {rule[name]} | {repo} | {item[path]}) else: conn.execute( UPDATE results SET last_seen? WHERE fp?, (now, fp) ) conn.commit() # 每条规则之间留出间隔避免代码搜索配额被打满 time.sleep(8) if __name__ __main__: main()send_alert函数可以根据团队现状灵活实现。最简单的是接一个 Webhook 往协作群里推一条消息消息里包含规则名、仓库、文件路径和链接。复杂一点的可以接工单系统或邮件但我不建议第一步就直接上这类重流程团队感知度低、噪音大很容易被直接静音。4.3 去重、状态变化与告警分级上面的脚本已经把“新出现”和“持续存在”做了区分。但我还要强调一个容易踩的设计坑只做“新增告警”是不够的必须同时记录“消失结果”。什么叫消失结果就是某条记录上次采集的时候还在这次再采已经搜不到了。这通常意味着对方仓库被删除、文件被移除或者权限被收紧。从暴露面收敛的角度看这是一个正向信号但如果你不记录它就无法追踪处置动作是否真的生效。我自己的做法是把last_seen和当前采集时间做对比超过一定采集周期没更新的记录单独列到“处置待确认”清单里由人工确认是真删除还是只是被移出了索引。告警分级的核心是避免全量轰炸。我的建议是设置三档critical表示直接命中密钥、云凭证这类高风险数据high表示命中配置文件、内网地址、源码关键片段medium表示命中域名、文件名、路径等间接线索。不同级别对应不同的通知渠道和响应时限否则一起涌进来值班的人第一周还行第二周开始就会选择性忽略。4.4 部署提醒频率、限额与资源开销最后是部署层面的几点提醒。先说采集频率代码搜索 API 的限额比较紧张建议高频规则控制在每小时一次中低频规则每天一次。不要写成死循环分布式采集更要谨慎避免同一个组织下多个脚本各自为战把配额瞬间打爆。机器资源方面这个脚本非常轻量跑在最低配的虚拟机上都没问题。唯一需要注意的反而是网络稳定性GitHub API 的超时重试要处理好不能因为一次超时中断整个批量任务。用计划任务定时触发日志按天落盘采集结果和告警记录做好备份这套轻量哨兵体系就算立住了。5. 告警之后的动作误报研判、风险确认与收敛闭环5.1 先别慌着改配置区分误报与真实暴露拿到一条 GitHub 代码搜索告警最忌讳的动作是立刻改生产配置、轮换所有密钥。先花三分钟做一个误报研判能帮你省掉一大半无效工作量。误报最常见的来源是同名项目。你的规则里写了org:example-org理论上应该只命中自己组织下的仓库。但很多规则写得不严谨比如只用DB_PASSWORD language:python这种组合搜出来的结果里有一大半是开源培训项目、示例代码、技术博客引用片段。所以新建规则时第一件事是把org:、repo:这类归属限定符明确写上。第二条防线是看响应里的text-match片段它能直接展示命中上下文比下载整个文件再去 grep 效率高得多。还有一种容易忽略的情况仓库是团队 fork 出来的但并没有真正使用。这类结果虽然不属于主动泄露但代码确实存在于平台索引中我的建议是仍然走收敛流程不要因为“没人用”就放过。5.2 验证泄露物的实际危害确认了目标确实属于自己接下来的问题是这份泄露到底有多危险对代码片段泄露我会按几个维度打分确认内容关键词是通用示例还是真实凭证如果仅包含示例占位符风险降一级。密钥是否仍然有效很多项目使用密钥轮换机制历史 commit 里残留的旧密钥早已失效可以降低处理优先级。文件是当前分支还是历史 commit历史残留的处置成本比当前分支高但被读取的概率也相对低。有没有组合信息比如配置文件里同时出现内网 IP、数据库用户名和密码这属于高价值组合泄露需要立即收敛。验证动作要守边界。我的原则是只用非攻击性方式做确认比如从公开索引、团队权限范围、内部资产登记表去核对绝不因为拿到了疑似密钥就进行越权调用。安全监控的目的是保护不是展示攻击能力。5.3 收敛、轮换、删除与索引移除处置动作分成四类按实际情况组合使用。第一类是即时止血比如把公开仓库设为私有、删除暴露分支、移除文件并提交记录第二类是密钥轮换凡是出现在泄露文件里的凭据无论是否失效都建议走一次轮换流程第三类是持久化清理涉及历史 commit 中的敏感信息时需要重写历史或使用官方提供的相关工具操作前务必备份并通知所有协作者第四类是索引层面的处理删除文件不代表搜索结果立刻消失代码托管平台的索引重建需要时间所以在处置完成后应该在监控规则里建立“已处置清单”作为后续告警的排除项。最后一步是把这次事件闭环到规则库里。每处置一个真实告警就问自己两个问题这条规则能不能再收紧一点减少同类误报这个暴露模式有没有其他变体需要在规则库里新增条目只有持续反哺规则库哨兵系统才会越用越准而不是越用越吵。6. 规则库的长期运营从个人脚本到团队哨兵体系6.1 让 Dork 清单变得可维护规则库最怕变成“一个人笔记本里的私藏清单”。我见过好几个团队监控脚本都搭起来了但查询规则全部写在代码里的一个列表里负责人一离职后面的人完全不敢动。解决这个问题的方式很简单把规则外置成配置文件用表格或 YAML 管理每条规则带说明、负责人、创建时间和最后修改时间。实际使用中我习惯按资产维度组织规则而不是按检测类型。比如一个业务系统对应一组规则包含该系统域名下的搜索引擎 dork、代码搜索规则、仓库名规则这样当业务系统本身发生变更下线、迁移、改域名时只需要调整一个分组不会漏改其他相关规则。6.2 监控指标与持续优化监控系统本身要能被度量。我维护的核心指标只有四个每天新增告警数、误报率、平均处置时长、规则覆盖率。新增告警说明发现能力在起作用误报率告诉我规则需要清洗处置时长反映流程是否顺畅覆盖率提示我是否还有没纳入监控的资产维度。每周我会抽半小时看一次告警聚类。真正有价值的不是单条告警而是集中爆发的模式。比如某个时间段内突然出现同一类文件路径的多个泄露大概率是团队最近做了一次跨仓库的批量操作这种苗头比单点泄露更需要关注。6.3 一点个人体会这个系统从零搭起来其实用不了多少代码真正难的是坚持维护。我现在手里有 60 多条监控规则其中真正高频触发告警的不到十类但这十类每一次告警都值回整个系统的成本。如果你准备开始做这件事我的建议是先别贪多把自有域名、核心仓库、常用敏感文件后缀盯住稳定跑一两个月后再慢慢加规则。哨兵不是一天练成的但最好的验证时间永远是现在。
阅读完成 · 觉得有帮助?