从 Web 应用发布的 JavaScript 中恢复该应用所构建的每一个请求:方法、URL、请求体、内容类型和请求头。
单页应用的 JavaScript 包中包含该应用能够发出的每一个 API 请求。给定一个 URL,jshound 会找到该应用的包,爬取其中的全部内容(包括懒加载的 chunk),并提取出这些请求。
它是被动的。它发出的唯一请求是用于获取公共静态文件、页面及其 JavaScript 的未认证 GET 请求。分析在本地运行,不会向应用的 API 发送任何内容。
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
需要 Python 3.10 或更高版本,以及 Node 18 或更高版本。
jshound supabase.com/dashboard
$ jshound supabase.com/dashboard
307 128B https://supabase.com/dashboard
200 19K https://supabase.com/dashboard/org
200 28K https://frontend-assets.supabase.com/studio/8617532ae1a4/_next/static/chunks/1p849pzlxcp4n.js?dpl=dpl_ESaSquAbnWrJqbvvyRh7F1qj5wWH
...
supabase.com 200 2282 files (2281 from frontend-assets.supabase.com, 1 from supabase.com) -> 387 endpoints
1 declared chunk(s) could not be fetched
-> .jshound/supabase.com/endpoints.txt (also requests.jsonl, urls.txt, openapi.json)
frontend-assets.supabase.com 上的 2,281 个文件,supabase 的构建产物就位于那里。POST https://api.supabase.com/platform/auth/{ref}/otp {"phone":"state:phone"}
DELETE https://api.supabase.com/v1/projects/{param_n}/functions/{param_e}
DELETE https://supabase.com/dashboard/api/integrations/stripe-sync application/json {"projectRef":"input:e","startTime":"input:t"}
POST https://supabase.com/docs/api/search/fts application/json {"query":"computed:trim"}
一次处理多个目标,或从 stdin 读取列表:
jshound app.example.com api.example.com
cat scope.txt | jshound
磁盘上已有的文件,例如上一次运行留下的 js/ 文件夹,会在不访问网络的情况下被读取:
$ jshound examples/demo-app
GET https://demo-app.test/api/feature-flags
POST https://demo-app.test/t/events {"sid":"?","evt":"?"}
DELETE https://demo-app.test/v1/sessions/current
POST {baseUrl}/v1/media multipart/form-data {"file":"file-upload"}
POST {baseUrl}/v1/subscribe application/json {"email":"computed:trim","pageUri":"state:href"}
GET {baseUrl}/v1/users/{param_o}/profile
所有内容都会保存到 .jshound/<host>/,或保存到 -o DIR:
endpoints.txt | 上面的表格,每行一个请求 |
requests.jsonl | 每个请求一个 JSON 对象:method、url、body、content_type、headers、graphql 或 rpc,以及它来自的页面和文件 |
urls.txt | 对于主机已知的请求,输出 METHOD URL,供接受列表的工具使用 |
openapi.json | 一份 OpenAPI 3 文档,可导入 Burp、Caido 或 Postman |
js/ | 所有被获取的文件 |
{...} 是 jshound 无法解析的值,按代码中的写法输出:{baseUrl}、{window.__CONFIG__.apiBaseUrl}。在路径中,{ref} 是一个参数,{param_o} 是函数参数 o。/api/surveys/,经过了 jshound 尚未追踪到其 base 的代码。requests.jsonl 会指明它来自哪个文件。input: 一个参数,state: 对象的某个属性,computed: 某次调用的结果,? 未知。application/json? 是根据该文件的其他请求推断出的内容类型,而非在调用处读取到的。~{...} 表示请求体中的字段比列出的更多。{graphql} 是一个 GraphQL 操作,其名称、类型和变量作为请求体。| 打包器 | 懒加载 chunk URL | 模块树 | 方式 |
|---|---|---|---|
| webpack 5 | 是 | 是 | 从语法树中读取 __webpack_require__.u |
| webpack 4 | 是 | 是 | 没有 .u,因此按形状而非名称匹配具名 URL 构建器 |
| Rspack | 是 | 是 | 与 webpack 兼容,通常输出 ESM |
| Turbopack | 是 | 不适用 | 完全没有构建器;chunk 列表是一个字面量数组 |
| Vite / Rollup | 是 | 不适用 | 路径被直接写出(__vite__mapDeps);扁平的 ESM 没有可遍历的注册表 |
| esbuild | 是 | 不适用 | 同上 |
| Parcel | 是 | 不适用 | 运行时全局变量按构建带有后缀(parcelRequire5707),且 chunk 表是对象而非数组 |
| Metro (React Native) | 不适用 | 不适用 | 只发布一个包,没有 chunk;通过 __METRO_GLOBAL_PREFIX__ 检测 |
| webpack Module Federation | 是 | 是 | 构建器不携带 id 映射,因此 id 取自 __webpack_require__.e(480) 调用处 |
jshound 会请求你给定的目标,以及该目标的页面所加载的 JavaScript 文件,无论它们托管在哪里。对于 supabase,这主要是 frontend-assets.supabase.com。这些是任何访问者的浏览器都会下载的公共静态文件。那些其他主机除了这些 GET 请求之外不会收到任何东西。
它会拒绝目标在私有地址空间(loopback、RFC1918、link local)中指定的任何 URL,除非你给定的目标本身就是私有的,或者你传入了 --allow-private。
-o DIR | 结果保存位置(默认 .jshound/<host>) |
-q | 不打印每个发出的请求 |
-v | 同时打印分析日志 |
--max-chunks N | 处理这么多懒加载 chunk 和 import 后停止(默认 50000) |
--no-chunks | 不追踪懒加载 chunk |
--proxy URL | 通过代理发送每个请求(或设置 JSHOUND_PROXY) |
--proxy-ca FILE | 拦截 TLS 的代理的 CA 证书 |
--client PROFILE | 固定使用某一个浏览器 TLS 指纹,而不是在被封锁时轮换 |
--allow-private | 允许私有地址空间中的 URL(见“范围”) |
blocked by Cloudflare 并且什么都读不到。jshound 会模仿 curl_cffi 所知道的最新 Chrome、Firefox 和 Safari,因此请保持 curl_cffi 为最新版本。--proxy 有助于应对速率限制和 IP 封禁,但对质询无济于事。document.createElement("script") 插入的脚本,若其 URL 来自某次 API 调用,则不在 jshound 下载的任何内容之中。参见 CONTRIBUTING.md。
MIT。