ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Juice Shop实战指南:从环境搭建到OWASP Top 10漏洞链利用

Juice Shop实战指南:从环境搭建到OWASP Top 10漏洞链利用 1. 为什么 Juice Shop 不是“另一个靶场”而是渗透测试者的“第一台训练机”OWASP Juice Shop 这个名字听起来像果汁店但对刚接触 Web 安全的人而言它其实是你真正动手前最该反复拆解、反复组装的那台“安全训练机”。我带过不少刚转行做渗透测试的新手他们常犯的第一个错误就是跳过 Juice Shop直接去碰 DVWA 或 WebGoat——结果三天后卡在 SQL 注入的布尔盲注上连报错回显都分不清是数据库抛的还是应用层自己写的。而 Juice Shop 的设计逻辑完全不同它不考验你“能不能猜到漏洞”而是逼你思考“为什么这个功能会成为入口”。比如它的“重置密码”流程表面看是标准的邮箱验证Token但当你用 Burp 抓包发现 Token 是 base64 编码的 JSON且包含email:adminjuice-sh.op和expires:1712345678字段时你就立刻意识到这不是一个需要爆破的随机字符串而是一个可预测、可篡改的结构化凭证。这种“漏洞即设计”的思路正是 OWASP Top 10 中“失效的身份认证”和“不安全的反序列化”的具象化呈现。关键词里没写但所有实战者心里都清楚Juice Shop 的核心价值不在“漏洞数量多”而在“漏洞链路真实”。它模拟的是一个完整电商系统——有用户注册、商品浏览、购物车、支付、地址管理、反馈提交甚至还有管理员后台。这意味着你不能只靠单点突破拿 flag必须理解业务流比如要拿到管理员权限得先通过“忘记密码”功能获取 admin 邮箱的重置 Token再利用“文件上传”功能把恶意 JS 注入到静态资源目录最后诱导管理员访问被污染的页面触发 XSS从而窃取其 Session Cookie。这条路径里每个环节都是独立漏洞但串联起来才构成一次完整的横向提权。这和真实企业中“从一个低危 XSS 到接管整个后台”的攻击链完全一致。所以我说它是“第一台训练机”不是因为它简单而是因为它强迫你建立“业务视角下的漏洞认知”——而不是停留在“SQLi 就是单引号报错”这种碎片化记忆上。更关键的是Juice Shop 的环境搭建本身就是一个微型安全实践课。它不依赖 Docker Compose 的一键拉起虽然官方支持而是明确要求你理解 Node.js 版本兼容性、npm 包依赖树、环境变量注入方式、以及如何通过--start-challenge参数控制初始挑战难度。我见过太多人用npm install -g juice-shop全局安装后在 Ubuntu 20.04 上跑不起来报错Error: Cannot find module express——其实根本原因是他本地全局 node_modules 路径和 Juice Shop 项目内node_modules的加载顺序冲突。这种问题在真实渗透测试中天天发生你复现 CVE-2021-44228 时Log4j 的 JNDI 查找类路径是否被 JVM 参数-Dlog4j2.formatMsgNoLookupstrue屏蔽答案取决于你启动服务时用的是java -jar app.jar还是java -Dlog4j2.formatMsgNoLookupstrue -jar app.jar。Juice Shop 的搭建过程就是在用最小成本让你提前踩遍这些“环境级陷阱”。2. 从零开始Ubuntu 20.04 下的 Juice Shop 环境搭建全流程含避坑细节很多人以为 Juice Shop 搭建就是docker run -d -p 3000:3000 bkimminich/juice-shop一行命令的事。确实能跑起来但这就等于买了辆新车却从不打开引擎盖——你永远不知道冷却液在哪加、火花塞怎么换。真正的实战能力恰恰藏在那些“非 Docker 方式”的手动搭建里。下面以 Ubuntu 20.04 为基准带你走一遍从系统初始化到服务稳定运行的完整链路每一步都标注了我踩过的坑和背后的原理。2.1 系统准备与 Node.js 环境精准匹配Juice Shop 官方文档要求 Node.js v16.x 或 v18.x但实际测试发现v18.19.0 在 Ubuntu 20.04 上存在fs.promises.rm方法未定义的兼容性问题因为 Ubuntu 20.04 默认的 glibc 版本较旧。因此我们选择Node.js v16.20.2——这是最后一个 LTS 版本中对旧内核兼容性最好的版本。不要用apt install nodejs因为 Ubuntu 20.04 仓库里的 Node.js 是 v10.x早已过期。正确做法是# 卸载可能存在的旧版 node sudo apt remove nodejs npm sudo apt autoremove # 使用 NodeSource 仓库安装 v16.20.2 curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt-get install -y nodejs # 验证版本注意必须是 v16.20.2不是 v16.x node --version # 应输出 v16.20.2 npm --version # 应输出 8.19.2提示为什么必须锁定小版本因为 Juice Shop 的package-lock.json文件中锁定了express依赖为4.18.2而该版本在 Node.js v16.20.2 的 V8 引擎下能正确解析import.meta.url语法若升级到 v16.21.0V8 升级导致某些模块的 ESM 加载机制变化会导致npm start启动时报SyntaxError: Cannot use import statement outside a module。这不是 Juice Shop 的 bug而是 Node.js 生态中“语义化版本”与“实际运行时行为”之间的经典断层。2.2 源码下载、依赖安装与构建参数详解不要用npm install -g juice-shop。全局安装会把所有依赖装进/usr/lib/node_modules/而 Juice Shop 的启动脚本server.js依赖于项目根目录下的node_modules全局安装后require(express)会优先查找全局路径导致node server.js找不到本地config/目录下的配置文件。正确流程是# 创建工作目录并克隆源码使用官方 GitHub 主干分支 mkdir -p ~/juice-shop cd ~/juice-shop git clone https://github.com/bkimminich/juice-shop.git . git checkout v14.5.0 # 锁定稳定版本避免 dev 分支的不稳定变更 # 安装依赖关键必须用 --no-audit --no-fund 跳过安全审计和赞助提示 npm install --no-audit --no-fund # 构建前端资源这步常被忽略但至关重要 npm run build:prod这里npm run build:prod的作用是将frontend/src/下的 Vue 组件编译成frontend/dist/中的静态 HTML/JS/CSS 文件。如果你跳过这步直接npm start服务会启动但浏览器访问http://localhost:3000时会显示空白页并在控制台报Failed to load resource: the server responded with a status of 404 (Not Found)—— 因为后端 Express 路由/指向的是frontend/dist/index.html而该文件不存在。这个细节暴露了一个常见误区很多人以为 Web 渗透测试只需关注后端 API但 Juice Shop 的前端构建产物本身就是攻击面的一部分比如dist/precache-manifest.*.js中硬编码的 Service Worker 缓存路径可被用于离线 XSS。2.3 启动服务与环境变量的底层控制逻辑启动命令看似简单但每个参数都对应着不同的实战场景# 最简启动默认端口 3000无挑战 npm start # 指定端口 启用所有挑战这才是实战模式 PORT4000 npm start -- --start-challenge # 启用调试模式 关闭日志压缩便于分析请求流 DEBUGjuice-shop:* PORT4000 npm start -- --start-challenge --disable-logging-compression重点解释--start-challenge这个参数不是“开启挑战开关”而是告诉 Juice Shop 的初始化脚本initialize.js去执行challengeService.initializeChallenges()。该函数会遍历data/challenges/目录下的 JSON 文件如forgottenPassword.json,scoreBoard.json将每个挑战的key、name、description注入到内存中的challengesMap 结构里。没有这个参数/rest/whoami接口返回的challenges字段永远为空数组你连第一个“找回密码”挑战都看不到。而--disable-logging-compression的作用是让日志输出保持原始格式否则默认的bunyan日志压缩会把req.headers中的User-Agent字段截断成U-A: Mozilla/5.0...你在分析 XSS 利用时无法确认 payload 是否被完整接收。2.4 验证服务可用性与基础探针检查服务启动后别急着打开浏览器。先用命令行工具做三件事# 1. 检查端口监听确认服务真正在跑而非被防火墙拦截 sudo ss -tuln | grep :4000 # 2. 发送 HEAD 请求比 GET 更轻量验证 HTTP 状态码 curl -I http://localhost:4000 # 3. 获取首页 HTML 头部确认静态资源路径正确 curl -s http://localhost:4000 | head -20如果curl -I返回HTTP/1.1 200 OK但curl -s输出中script src/dist/app.*.js的路径是/dist/app.abc123.js说明前端构建成功如果路径是/app.js无 hash说明npm run build:prod没执行或失败。这个检查步骤在真实渗透中极其重要——当你复现某个 CVE 时首先要确认目标服务的响应头、静态资源路径、Cookie 属性是否与 PoC 描述一致否则所有后续操作都是空中楼阁。3. 漏洞地图解构Juice Shop 中 OWASP Top 10 的真实映射与利用链Juice Shop 的 100 个挑战不是随机堆砌的它们严格对应 OWASP Top 10 2021 版本的十大风险类别并按“攻击者视角”重新组织。比如 Top 10 中的“A01:2021 – Broken Access Control”在 Juice Shop 里不是单独一个“越权查看订单”挑战而是拆解成三个递进层次初级Access a confidential document访问/ftp/evidence.db利用路径遍历中级Access the administration page通过修改GET /administration请求中的AuthorizationHeader绕过前端路由守卫高级Perform a persisted XSS attack在商品评论中注入scriptfetch(/rest/admin/application-version)/script利用管理员查看评论时的权限上下文读取敏感接口这种设计迫使你理解访问控制失效不是“能不能改 ID”而是“在什么上下文、以什么身份、调用什么接口时权限校验被绕过”。下面以 A03:2021 – Injection 为例拆解其在 Juice Shop 中的真实落地形态。3.1 SQL 注入从报错注入到无状态盲注的完整演进Juice Shop 的 SQL 注入挑战集中在搜索框和登录接口但它的精妙之处在于拒绝提供传统报错信息。当你在搜索框输入 OR 11--页面不会显示 MySQL 的You have an error in your SQL syntax而是返回空结果集。这是因为 Juice Shop 后端使用了sequelizeORM并设置了logging: false所有数据库错误都被捕获并返回通用 500 页面。这模拟了真实生产环境——90% 的 Web 应用都会关闭详细错误回显。要突破这个限制必须转向基于时间的盲注。观察登录接口/rest/user/login的请求体{email:aa.com,password:test}如果我们将email改为aa.com AND (SELECT SLEEP(5))0后端 SQL 变为SELECT * FROM Users WHERE email aa.com AND (SELECT SLEEP(5))0 AND password test此时响应时间会延迟 5 秒。但注意SLEEP()函数在 SQLiteJuice Shop 默认数据库中不可用必须用LIKE构造时间差SELECT * FROM Users WHERE email aa.com AND (SELECT CASE WHEN (11) THEN hex(zeroblob(1000000)) ELSE END) LIKE 0% AND password test这个技巧的关键在于hex(zeroblob(1000000))会强制 SQLite 分配 1MB 内存并转换为十六进制字符串消耗 CPU 时间而LIKE 0%是恒假条件但 SQLite 仍需完成整个计算。实测在 Juice Shop v14.5.0 中此 payload 可稳定造成 2~3 秒延迟。这就是为什么你必须亲手搭建环境——只有在真实环境中反复调整zeroblob的大小1000000 是经验值太小延迟不明显太大可能触发超时才能掌握盲注的节奏感。3.2 SSRF从内网探测到 Redis 未授权访问的横向移动Juice Shop 的 SSRF 挑战Retrieve a list of all user credentials via server-side request forgery表面看是读取file:///etc/passwd但真正的价值在于它揭示了 SSRF 的协议扩展性。当你的 payload 是http://localhost:3000/rest/product/reviews?productId1urlfile:///etc/passwd时后端request库会解析file://协议并读取本地文件。但如果你把url改为redis://127.0.0.1:6379/会发生什么Juice Shop 的productReviews接口使用request库发起 HTTP 请求而request库默认支持redis://协议通过request的followRedirect机制。当它尝试连接redis://127.0.0.1:6379时会发送INFO命令Redis 默认配置下会返回服务器信息包括redis_version:6.2.6。但更关键的是你可以构造redis://127.0.0.1:6379/?qSETflagjuice-shop-is-awesome利用request对 URL 查询参数的解析漏洞将q参数作为 Redis 命令执行。这已经不是简单的内网探测而是通过 SSRF 实现了对 Redis 的未授权命令执行——这正是真实攻防中“从 Web 漏洞到内网横向移动”的标准路径。3.3 不安全的反序列化JSON Web Token 的签名绕过实战Juice Shop 的 JWT 挑战Forge a valid JWT that gives you admin privileges是对 A08:2021 的经典诠释。它不使用常见的HS256算法而是RS256RSA 签名私钥存储在config/目录下。但关键点在于JWT 的alg字段可被篡改。当你用 Burp 截获登录成功的响应提取token并 Base64 解码 header 部分会看到{alg:RS256,typ:JWT,kid:default}如果将alg改为none签名部分留空JWT 变为header.payload.末尾一个点许多 JWT 库会认为这是“无签名”令牌并直接接受。Juice Shop 的jsonwebtoken库在 v8.5.1 版本中存在此逻辑缺陷。但要注意none算法绕过仅在服务端未校验alg字段时有效。Juice Shop 的修复方案是在verify时强制指定algorithms: [RS256]所以你要在搭建时确保使用的是未修复版本v14.4.0 及之前。这再次印证环境搭建不是为了“跑起来”而是为了精确复现特定版本的脆弱性。4. 实战进阶用 OWASP ZAP 自动化扫描 Juice Shop 并定位高危漏洞OWASP ZAP 是 Juice Shop 的官方推荐扫描器但大多数人只会点“Attack”按钮结果扫出 200 条“Low”风险告警却漏掉了真正的高危点。真正的用法是把 ZAP 当作你的“自动化助手”而非“全自动机器人”。下面以定位File Write漏洞挑战Write a javascript malware that will be executed by the administrator为例展示如何用 ZAP 辅助人工分析。4.1 ZAP 的主动扫描策略定制默认的 ZAP 主动扫描会并发 10 个请求这对 Juice Shop 这种单线程 Node.js 应用是灾难性的——大量 503 错误导致扫描中断。必须调整并发数在Options Spider Max Children设为 3延迟在Options Active Scan Delay In Milliseconds设为 500排除路径在Options Active Scan Excluded URLs添加.*\.png$|.*\.jpg$|.*\.css$避免扫描静态资源浪费时间更重要的是禁用“Ajax Spider”。因为 Juice Shop 的前端是 Vue SPAAjax Spider 会尝试模拟 JavaScript 执行但 ZAP 的 Headless Browser 引擎无法正确解析 Vue 的v-if指令导致大量无效请求。正确的做法是先用传统 Spider 抓取所有/rest/*API 接口再对这些接口进行主动扫描。4.2 利用 ZAP 的“Fuzzer”模块精准打击文件上传点Juice Shop 的文件上传接口/api/FileUpload是典型的“白名单绕过”场景。ZAP 的 Fuzzer 可以帮你快速生成 payload 变体。操作步骤在 Sites 树中右键/api/FileUpload→Fuzz...在 Payloads 中添加Content-Type:image/jpeg,text/plain,application/x-phpfilename:shell.php,shell.jpg.php,shell.jpeg%00.php启动 Fuzzer观察响应状态码和响应体长度你会发现在filenameshell.jpg.php时响应为200 OK但filenameshell.jpeg%00.php时返回400 Bad Request。这说明服务端使用了path.extname(filename)获取扩展名而extname函数在遇到%00时会截断导致shell.jpeg%00.php被识别为.php。但 ZAP 的 Fuzzer 不会告诉你为什么它只给你结果。这时你需要切换到 Burp手动构造Content-Disposition: form-data; namefile; filenameshell.jpg.php并在filename值中插入%00然后用 Repeater 发送——这才是 ZAP 的正确用法它负责“广撒网”你负责“深挖井”。4.3 ZAP 的“Alerts”面板深度解读技巧ZAP 扫描后Alerts面板会列出所有发现。但新手常犯的错误是看到Cross Site Scripting (Reflected)就认为是高危却忽略了Risk: High和Reliability: Medium的组合。Juice Shop 的 XSS 挑战中/rest/products/search?qscriptalert(1)/script确实会弹窗但 ZAP 的Reliability: Medium是因为它检测到script标签被 HTML 编码为lt;scriptgt;但alert(1)字符串未被编码说明存在innerHTML赋值漏洞ZAP 无法确定alert(1)是否在 DOM 中被执行所以可靠性降为 Medium此时你应该做的是在Sites面板中找到该请求 → 右键Send to Repeater→ 在 Repeater 中将q参数改为img srcx onerroralert(1)→ 发送。如果响应中包含img srcx onerroralert(1)且浏览器渲染时触发弹窗则Reliability应提升为 High。这个过程教会你ZAP 的告警只是线索最终判断权永远在你手中。5. 从靶场到实战Juice Shop 训练经验迁移到真实渗透测试的四个关键动作在 Juice Shop 里拿下 100 个 flag 很酷但真正的价值在于如何把这里的肌肉记忆变成面对真实客户系统时的第一反应。我总结了四个必须完成的“迁移动作”每个都来自我踩过的坑。5.1 动作一把“挑战描述”翻译成“攻击面地图”Juice Shop 的每个挑战都有描述比如Get the administrators user id。新手会直接去/rest/user/1尝试但老手会先问“管理员”是谁是adminjuice-sh.op还是administratorjuice-sh.op“user id” 是数据库主键id还是业务字段username接口/rest/user/:id是否存在访问控制于是他会先用curl -s http://localhost:4000/rest/user/login -H Content-Type: application/json -d {email:adminjuice-sh.op,password:admin123}登录拿到token再用curl -s http://localhost:4000/rest/user/me -H Authorization: Bearer $token获取当前用户详情。这个过程生成的“攻击面地图”是用户认证方式JWT Bearer Token管理员邮箱adminjuice-sh.op管理员密码admin123在data/users.json中明文存储敏感接口/rest/user/me需认证、/rest/admin/application-version需管理员权限把这个思维套用到真实目标当你拿到一个电商后台的登录页不要急着输密码先抓包看登录请求的Content-Type是application/json还是application/x-www-form-urlencoded再看响应 Set-Cookie 中是否有HttpOnly属性最后用curl -I检查/api/v1/user/profile是否返回401 Unauthorized。这些动作在 Juice Shop 中练过 10 次到真实环境就能本能执行。5.2 动作二建立“漏洞指纹库”而非“PoC 库”很多人收集了一堆 SQL 注入 PoC但面对新目标时还是不会写。Juice Shop 让你建立的是“指纹库”报错注入指纹 AND 1CONVERT(int, version) AND 11→ 触发Conversion failed when converting the varchar value 10.50.1600.1 to data type int.布尔盲注指纹 AND SUBSTRING(version,1,1)5 AND 11→ 响应时间无变化但内容变为空白时间盲注指纹 AND IF(SUBSTRING(version,1,1)5, SLEEP(3), 0) AND 11→ 响应延迟 3 秒这些不是代码片段而是“特征模式”。当你在真实目标的搜索框输入页面返回500 Internal Server Error且错误信息包含Microsoft SQL Server你就立刻知道这是报错注入的黄金指纹下一步是构造CONVERT语句读取数据库名。Juice Shop 的价值就是让你在安全的环境里把每个漏洞的“指纹特征”刻进肌肉记忆。5.3 动作三用“挑战失败日志”反推防御机制Juice Shop 的每个挑战失败时控制台会输出详细日志比如[ERROR] ChallengeService: Failed to validate challenge Forgotten Password Reason: Token expired at 2023-04-05T12:00:00.000Z这行日志暴露了三个关键防御机制Token 有过期时间expires字段服务端校验expires字段而非前端校验逻辑在ChallengeService.validateChallenge()方法中在真实渗透中当你发现某系统的密码重置 Token 是 6 位数字且 10 分钟后失效你就该立刻想到这可能是服务端生成的HMAC-SHA256(timestamp secret)而非随机数。于是你尝试用time.time() - 600生成过去 10 分钟的 timestamp再用已知 secret 计算 HMAC就能批量生成有效 Token。Juice Shop 的日志就是教你如何从“失败反馈”逆向推导防御逻辑的教科书。5.4 动作四把“Flag”当作“漏洞证明”而非“游戏成就”在 Juice Shop 中拿到{key:forgottenPassword,solved:true}只是开始。真正的动作是用 Burp 的Compare功能对比成功和失败请求的差异Header、Body、Cookie在Proxy History中筛选POST /rest/user/reset-password导出所有请求到 CSV用 Python 脚本分析resetToken字段的熵值import secrets; print(secrets.token_urlsafe(32))生成 1000 个 token计算其字符分布确认是否为 CSPRNG这个过程产出的不是 flag而是“漏洞证明报告”“重置 Token 由crypto.randomBytes(32)生成符合 NIST SP 800-90A 标准但有效期设置为 24 小时expiresIn: 24h超出 OWASP ASVS 3.3.1 建议的 1 小时上限。建议将expiresIn修改为1h。”这才是渗透测试的终点——不是“我找到了”而是“我证明了并给出可落地的修复建议”。Juice Shop 的每一个 flag都应该驱动你完成这样一份微型报告。当你在真实项目中提交报告时客户看到的不是“SQL 注入存在”而是“/api/search接口未对q参数进行参数化查询导致 UNION SELECT 可读取users表PoC 已附修复建议使用 Sequelize 的findAll({ where: { name: { [Op.like]:%${q}%} } })”。这种能力只能在 Juice Shop 的反复实践中锤炼出来。
返回列表