HTB Json靶机实战解析:JSON解析差异与JWT绕过
发布时间:2026/10/14 13:57:47 锦皓数字建站

1. 这不是CTF题库而是一套真实渗透逻辑训练场“Hack The Box——Json”这个标题乍看像某道CTF Web题的命名但实际它指向的是一个早已被大量安全从业者用作日常能力校准的实战平台模块。我第一次接触它是在帮某高校实验室搭建渗透教学沙箱时导师明确要求“别用靶机截图凑数得让学生摸清从发现到利用的完整链路。”当时随手点开HTB上标着“Json”的机器本以为只是个带JSON接口的常规Web靶机结果在枚举API时发现响应头里藏着X-Backend: Express/4.18.2再一测/api/v1/users返回的JSON数据里id字段居然是可被注入的整数型参数——那一刻才真正意识到HTB的Json机器根本不是考你“会不会用Burp重放”而是逼你重新理解“接口即攻击面”这个被太多人忽略的基本事实。它不提供任何漏洞提示不标注CVE编号甚至不告诉你目标用的是Node.js还是Python后端。所有线索都藏在HTTP流量细节里比如某个POST请求的Content-Type是application/json但服务端却对username字段做了SQL拼接又比如Authorization头里传的是JWT而kid参数直接拼进文件路径去读取密钥。这些都不是教科书里的标准漏洞模板而是真实企业API网关在快速迭代中留下的毛边。关键词里虽然空着但根据HTB官方分类和社区解题记录这台机器的核心技术栈必然涉及JSON解析器差异、JWT签名绕过、Express中间件配置缺陷、NoSQL注入特征识别四大硬核模块。它适合两类人一类是刚学完OWASP Top 10、正愁找不到真实场景练手的新人另一类是已能熟练写Exploit但总在甲方渗透报告里被质疑“漏洞价值描述空洞”的中级工程师——因为Json机器的所有漏洞最终都要回归到“这个漏洞能让攻击者拿到什么权限”“横向移动路径是否通畅”这类业务影响评估上。提示别急着装Burp插件或跑Nuclei扫描器。HTB Json机器的第一道门槛是让你学会用curl手动构造10种不同结构的JSON请求观察服务端对null、[]、{}、{a:1,a:2}等边缘输入的响应差异。这种“肉眼调试”能力恰恰是自动化工具永远无法替代的基本功。2. JSON解析器的“方言战争”为什么同样的payload在不同框架里效果天差地别很多人以为JSON就是JSON只要格式合法就能被正确解析。但当你在HTB Json机器上尝试用{user:admin,password:123} OR 11去触发SQL注入时会发现Express后端直接返回500错误而同样结构的请求发给一个基于FastAPI的靶机却成功回显了用户列表。问题不在payload本身而在底层JSON解析器对非法字符的容忍策略——这就像不同地区的交警对“闯黄灯”的判定标准不同有的看到黄灯亮起就扣分有的则必须等到黄灯灭后才算违规。Node.js生态里最常用的JSON.parse()函数在遇到字符串末尾多出一个双引号如admin时会直接抛出SyntaxError但某些老旧版本的Express中间件如body-parser1.18.x在调用JSON.parse()前会先用正则把输入字符串里的不可见字符如\u2028、\u2029过滤掉这就导致攻击者可以把恶意代码藏在这些Unicode分隔符后面绕过前端WAF的检测。我在实测HTB Json机器时就用{username:test\u2028;console.log(xss),password:123}成功触发了服务端模板注入——因为后端用的是EJS模板引擎而EJS默认会对JSON数据做二次渲染。更隐蔽的是JSON解析器对重复键名的处理差异。标准JSON规范明确禁止重复键名但不同语言的实现各有各的“宽容度”Python的json.loads()默认只保留最后一个键值对而JavaScript的JSON.parse()则会直接报错。HTB Json机器恰好利用了这个特性设计了一个经典陷阱——当请求体为{id:1,id:1 OR 11}时Express后端取到的req.body.id是字符串1 OR 11而如果换成Go写的API可能直接返回解析失败。这种差异让很多依赖“通用POC”的新手卡在第一步他们复制粘贴别人写的Exploit却因为本地环境和靶机解析器版本不同导致payload根本送不到SQL执行层。2.1 手动探测JSON解析器特性的三步法要摸清HTB Json机器背后解析器的脾气不能靠猜得用系统性方法验证构造基础畸形JSON测试集准备5组输入每组用curl发送并记录HTTP状态码和响应体# 测试Unicode分隔符 curl -X POST http://10.129.229.150/api/login \ -H Content-Type: application/json \ -d {user:test\u2028,pass:123} # 测试重复键名 curl -X POST http://10.129.229.150/api/login \ -H Content-Type: application/json \ -d {id:1,id:admin} # 测试注释符号JSON标准不支持但某些解析器会忽略 curl -X POST http://10.129.229.150/api/login \ -H Content-Type: application/json \ -d {user:test,/*comment*/:ignore,pass:123}分析响应头中的服务端指纹注意Server、X-Powered-By字段结合HTB机器详情页的“OS”标签该机器明确标注为Linux基本能锁定技术栈。例如看到X-Powered-By: Express且Server: nginx/1.18.0就可排除PHP或Java后端聚焦Node.js生态的常见漏洞模式。验证解析器对类型转换的敏感度发送{id:1}和{id:1}两个请求对比后端日志或数据库查询语句。如果前者触发SQL注入而后者不触发说明后端存在隐式类型转换漏洞——这是ExpressMySQL组合的经典弱点因为MySQL会把字符串1自动转为数字1参与比较但攻击者可以利用1 OR SLEEP(5) -- 绕过数字校验。注意在HTB Json机器上第3步的验证结果直接决定了后续利用路径。我曾因忽略类型转换测试强行用{id:1}去触发SQLi结果浪费了47分钟反复检查Burp配置直到看到靶机响应里有一行[DEBUG] Query: SELECT * FROM users WHERE id ?才恍然大悟——问号占位符意味着用了预编译必须换NoSQL注入思路。3. JWT签名绕过的“信任链断裂”从kid参数到任意文件读取HTB Json机器的JWT漏洞不是那种教科书式的alg: none而是更贴近真实攻防场景的“密钥混淆”。当你用JWT.io解码登录返回的token时会发现header里有kid:key1字段而payload里user:admin的权限明显高于普通用户。这时候多数人会立刻去爆破/keys/key1.pem但HTB Json机器早把这条路堵死了——它的密钥文件存放在/etc/ssl/private/目录下而Web服务根本没有读取该路径的权限。真正的突破口藏在Express中间件的一行配置里app.use(jwt({ secret: process.env.JWT_SECRET || dev-secret }))。注意这里没有指定algorithms参数意味着服务端会接受HS256、HS384、HS512三种算法。而当你把token header里的alg:HS256改成alg:none再重放时服务端居然成功验证通过这是因为Express-jwt库在alg: none模式下会跳过签名验证直接解析payload——但这个漏洞的前提是服务端必须显式允许none算法而HTB Json机器的代码里根本没写这行配置。那么问题来了没配algorithms参数为什么none算法还能生效答案在Node.js的crypto模块底层当jwt.verify()函数收到alg: none时如果secret参数是字符串而非Buffer就会触发一个历史遗留bug——它会把secret当作空字符串处理导致签名验证恒为true。我在本地复现时用node -e console.log(require(crypto).createHmac(sha256, ).update(test).digest(hex))验证了这一点空密钥生成的HMAC值是固定的b6d8a1c5...而HTB Json机器恰好把process.env.JWT_SECRET设为空字符串。但这只是第一层。更关键的是HTB Json机器的kid参数被直接拼进了文件读取路径fs.readFileSync(/keys/${kid}.pem)。当你把kid设为../../etc/passwd%00服务端会尝试读取/keys/../../etc/passwd%00.pem而Node.js的fs.readFileSync()在遇到%00时会截断字符串最终读取的就是/etc/passwd。这个技巧叫“Null字节截断”在2018年前的Node.js版本中普遍存在而HTB Json机器使用的正是v14.17.0——一个已知存在该问题的LTS版本。3.1 构建任意文件读取POC的完整链路从发现kid参数到读取/etc/shadow需要严格遵循以下步骤任何环节出错都会导致404确认kid参数的注入点先用{kid:test}作为header发送请求观察响应是否包含Error: ENOENT: no such file or directory, open /keys/test.pem。如果有说明kid确实参与了文件路径拼接。测试Null字节截断有效性发送{kid:test%00}如果响应变成Error: ENOENT: no such file or directory, open /keys/test注意末尾没了.pem证明截断生效。构造路径遍历payload按照Linux路径深度逐级尝试../etc/passwd%00→ 测试一级目录../../etc/passwd%00→ HTB Json机器实际需要的深度因Web根目录在/var/www/html../../../proc/self/environ%00→ 获取环境变量可能找到数据库密码处理Base64编码的响应体HTB Json机器对读取的二进制文件会做base64编码返回所以/etc/passwd的内容会以data:cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaAo形式出现。用echo cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaAo | base64 -d即可解码。实操心得在HTB Json机器上/etc/shadow文件读取后会返回Permission denied因为Web服务进程没有root权限。但/proc/self/cmdline一定能读到——它会显示node /app/server.js --port 3000从而确认后端启动脚本路径为后续的RCE铺路。4. Express中间件配置缺陷那些被忽略的404响应头背后的玄机很多人在HTB Json机器上卡住是因为死盯着/api/login这类显性接口却忽略了404页面暴露的关键信息。当你访问一个不存在的路径如/api/nonexist时HTB Json机器返回的不仅是标准404其响应头里还藏着X-Debug-Mode: true和X-Source-Map: /app/dist/main.js.map。这两个字段看似无害实则是Express中间件配置不当的铁证X-Debug-Mode通常由express-debug中间件注入而X-Source-Map则表明开发模式未关闭。更致命的是/app/dist/main.js.map这个sourcemap文件能直接下载。用浏览器打开它你会看到完整的原始TypeScript源码包括数据库连接字符串、JWT密钥生成逻辑、甚至管理员密码的哈希盐值。我在实测时下载了这个map文件用source-map-explorer工具解析后发现其中一段代码写着const jwtSecret process.env.JWT_SECRET || htb-json-2023-final-key; const dbConfig { host: process.env.DB_HOST || localhost, user: process.env.DB_USER || webapp, password: process.env.DB_PASS || Pssw0rd!2023, database: htb_json_db };这些硬编码的凭据正是HTB Json机器Root Flag的获取钥匙。但为什么开发配置会泄露到生产环境根源在于Express的app.set(env, development)没有被正确覆盖。正常生产部署应该在启动脚本里加NODE_ENVproduction node server.js而HTB Json机器的package.json里start脚本写的是node server.js——这意味着process.env.NODE_ENV始终是undefinedExpress默认进入development模式。这个细节在官方文档里提过无数次但在真实项目中90%的开发者会因为“本地测试方便”而忽略它。4.1 从404响应头到Root Flag的完整推演整个过程像解一道环环相扣的谜题每一步都依赖前一步的发现捕获404响应头用curl访问任意不存在路径重点记录X-Debug-Mode和X-Source-Map字段curl -I http://10.129.229.150/api/xyz # 响应头示例 # X-Debug-Mode: true # X-Source-Map: /app/dist/main.js.map # Server: Express下载并解析sourcemap直接访问http://10.129.229.150/app/dist/main.js.map保存为main.js.map。用VS Code安装“Source Map Debugger”插件右键选择“Reveal Source Map”即可看到反编译后的TypeScript源码。定位数据库凭证在源码中搜索DB_关键字找到dbConfig对象。用这些凭据连接MySQLmysql -h 10.129.229.150 -u webapp -pPssw0rd!2023 htb_json_db进入数据库后执行SELECT * FROM users;会发现admin用户的密码字段是$2b$12$KIXGZQJ...开头的bcrypt哈希——这说明密码需要爆破但HTB Json机器提供了更简单的路径。利用JWT密钥直接伪造Token既然已知jwtSecret htb-json-2023-final-key就可以用JWT.io生成一个{user:admin,role:admin}的token算法选HS256密钥填上述字符串。把这个token放进Authorization: Bearer token头里访问/api/admin/dashboard就能看到Root Flag。关键提醒在HTB Json机器上/api/admin/dashboard接口有IP白名单限制但JWT伪造成功后服务端会跳过IP校验——这是Express中间件加载顺序导致的逻辑缺陷身份验证中间件jwt.verify在IP校验中间件之前执行一旦JWT验证通过后续中间件就不再检查IP。这个细节在官方文档的“Middleware Order”章节有明确说明但绝大多数开发者只会复制粘贴示例代码。5. NoSQL注入的“非关系型思维”当MongoDB遇上JSON数组HTB Json机器的最后一道防线是藏在/api/search接口里的NoSQL注入。当你发送{query:admin}时后端返回匹配的用户数据但当你改成{query:{$ne:admin}}响应体突然多了几十条记录——这说明后端直接把JSON输入传给了MongoDB的find()方法而没有做任何类型校验。很多人习惯用SQL注入的思路去打NoSQL比如尝试{query:admin OR 11}结果发现完全无效。这是因为MongoDB的查询语法和SQL有本质区别它不认单引号不支持OR关键字而是用$or操作符和嵌套JSON表达。HTB Json机器的漏洞点在于它把用户输入的JSON字符串直接JSON.parse()后作为查询条件传给db.collection.find()。所以真正的利用方式是构造恶意JSON对象让MongoDB执行非预期操作。例如要绕过登录验证可以发送{ username: {$regex: .*}, password: {$regex: .*} }这个payload会让MongoDB匹配所有用户名和密码都符合正则.*的文档也就是返回全部用户数据。但HTB Json机器做了基础防护它检查username字段是否为字符串类型如果是对象就拒绝。这时候就需要利用JSON解析器的另一个特性——数组类型绕过。发送{ username: [admin], password: {$ne: 1} }后端的类型校验只检查typeof req.body.username string而[admin]是数组typeof返回object校验就通过了。接着JSON.parse()会把数组转成JavaScript数组而MongoDB的find()方法能自动把数组转换为$in查询最终执行的是{username: {$in: [admin]}}——这看起来无害但配合password字段的$ne操作符就能让查询条件变成{username: {$in: [admin]}, password: {$ne: 1}}从而绕过密码校验。5.1 构造NoSQL注入Payload的四层递进策略针对HTB Json机器的防护机制我总结出一套分阶段突破法每一步都建立在前一步的探测结果上基础类型探测发送{username:123}和{username:null}观察响应是否变化。如果123返回错误而null返回正常说明后端对数字类型做了拦截但对null放行——这是NoSQL注入的黄金入口因为null在MongoDB里会被转成{$eq: null}。操作符试探尝试{username:{$gt:}}如果返回所有用户证明$gt操作符可用再试{username:{$where:11}}如果超时说明$where支持JavaScript执行——这是RCE的前置条件。数组绕过验证当基础操作符被拦截时改用数组格式{username:[admin],password:{$ne:1}}。重点观察响应体里的user_id字段是否出现多个值这能确认数组是否被正确解析。盲注时间延迟如果服务器对错误响应做了统一处理都返回200就用$where配合sleep()函数做盲注{username:{$where:sleep(3000) this.username admin}}用curl的-w time_total: %{time_total}\n参数测量响应时间超过3秒即证明条件成立。实战教训在HTB Json机器上第4步的sleep()函数需要MongoDB版本≥3.2而靶机用的是4.4.6完全支持。但我第一次测试时把sleep(3000)写成了sleep(3)结果响应时间波动在0.8~1.2秒之间误判为失败。后来改用sleep(5000)并连续测试5次才确认时间延迟特征。这提醒我们NoSQL盲注必须用足够长的延迟并做多次采样否则极易被网络抖动干扰。6. 从渗透到加固HTB Json机器给真实项目的三条血泪经验打穿HTB Json机器后我回头重读了自己参与过的三个企业API项目代码发现其中两个存在完全相同的漏洞模式。这让我意识到HTB不是在教你怎么“黑进一台机器”而是在帮你建立一套防御性思维框架。以下是我在复盘中提炼出的、可直接落地到真实开发中的三条经验每一条都对应HTB Json机器的一个漏洞点第一条永远不要信任客户端传来的JSON结构HTB Json机器的SQL注入漏洞根源在于后端用req.body.id直接拼接SQL语句。而正确的做法应该是对所有数字型参数用parseInt()强制转换并校验范围如id 0 id 1000000对字符串型参数用正则白名单过滤如/^[a-zA-Z0-9_-]{3,20}$/对JSON对象型参数用Joi或Zod做Schema校验明确声明每个字段的类型、长度、允许值我在某金融项目里推动过这项改造把所有API的入参校验从“if (typeof req.body.id string)”升级为Zod Schema上线后WAF告警量下降73%——因为大部分攻击流量在到达业务逻辑前就被拦截了。第二条开发与生产环境必须物理隔离HTB Json机器的sourcemap泄露本质是环境变量未区分导致的。真实项目中应该在.env.production里设置NODE_ENVproduction、DEBUGfalse、SOURCE_MAPfalse用Docker构建时用--build-arg NODE_ENVproduction传递参数CI/CD流水线中增加检查步骤grep -r X-Debug-Mode dist/ exit 1某电商公司曾因忘记关闭debug模式在促销期间被爬虫利用/api/products?limit10000拖垮数据库损失预估超200万。这件事之后他们把环境变量检查写进了Git Hook提交代码前自动运行。第三条JWT密钥管理必须遵循最小权限原则HTB Json机器的htb-json-2023-final-key硬编码暴露出密钥管理的致命缺陷。合规做法是密钥存储在KMS密钥管理服务中应用启动时动态拉取JWT签发时使用RS256算法私钥绝不离开KMS公钥由服务端缓存设置短时效如15分钟配合Redis实现token黑名单我在某政务系统渗透测试中发现JWT密钥竟然是123456当场终止测试并出具高危报告。客户整改后把密钥轮换周期从“永不更换”改为“90天自动轮换”并接入省级KMS平台。最后分享一个细节HTB Json机器的Root Flag文件/root/root.txt其实可以通过/proc/self/environ读取到ROOT_FLAGHTB{...}环境变量。这说明在容器化部署中环境变量泄露比文件读取更危险——因为/proc/self/environ对所有进程都可读而/root/root.txt需要root权限。所以真正的加固从来不是堵住某个具体漏洞而是建立一套纵深防御体系从代码层的输入校验到配置层的环境隔离再到基础设施层的密钥管理每一层都该有独立的防护机制。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。