一个故意存在漏洞的 Docker 实验环境,具有可路由的 DNS 资产和每个目标对应的机器可读答案密钥,可在本地对扫描器的精确率、召回率和范围进行评分。
一个安全测试实验室。一组刻意设计为存在漏洞的目标、一个可路由的网络资产环境(带有用于资产发现的权威 DNS 以供枚举)、每个目标的机器可读答案密钥,以及一个用于驱动这一切的控制面板。
这里的一切都是刻意设计为存在漏洞的。仅供本地测试。 不要将其暴露到互联网或不受信任的网络。发布的端口绑定到
127.0.0.1;实验室目标完全不绑定任何端口。我们自己的扫描器会故意在这些容器内获得 RCE,因此容器被视为安全边界:每个镜像都通过摘要固定,每个服务都丢弃所有能力并仅添加最低限度的能力,
./lime audit会强制执行这一点。首次运行前请阅读 SECURITY.md,包括关于容器隔离无法覆盖哪些内容的部分。
位于 http://127.0.0.1:7000 的控制面板,显示实验室运行时的状态。
limeyard 以前叫 vuln_apps。它被重命名是因为它不再是一个应用程序文件夹:它现在包含裸服务、一个 DNS 区域、一对 WAF、一个精确目标和 APK 测试夹具,这些都不是应用程序。
旧的目标集得分 9/9,零漏报,零误报。一个不可能失败的基准测试无法检测回归。结构上有三个问题:
127.0.0.1:70xx,因此子域枚举没有任何东西可枚举,端口扫描被直接给出了答案,服务指纹识别从未见过非 HTTP 守护进程。你需要 Docker(带 compose 插件)和 git。仅此而已。
curl -fsSL https://raw.githubusercontent.com/clickswave/limeyard/main/install.sh | bash
这会将实验室克隆到 ./limeyard,用新的 API 令牌写入其 .env,构建并启动控制平面,然后询问要运行什么。在启动任何东西之前,它会显示所选内容在参考机器上空闲时测得的成本,并与你机器的可用资源进行对比:
This selection, idle, on the box it was measured on:
17 targets, 1 scenarios, 45 containers
RAM about 2.9 GB resident (host has 22.4 GB available)
disk about 11.0 GB of images to pull (host has 111 GB free)
CPU near idle once up (3% of one core); pulling and first boots are the busy part
Start it? [y/N]
非交互式:curl ... | bash -s -- --light --yes(或 --all、--none、--pick dvwa,juice-shop,estate)。用 LIMEYARD_DIR=/path 将检出目录放到别处。
./lime setup # 再次运行向导,随时可用
./lime start --all # 每个轻量目标
./lime start crapi --heavy # 一个重量级目标,显式指定
./lime scenario-up estate # 网络资产环境:DNS、虚拟主机、服务
./lime status # 当前运行状态
./lime stop --all --heavy # 全部停止;镜像和卷保留
./lime credits # 每个目标的作者,以及许可协议
./lime doctor # 环境、归属和磁盘检查
./lime doctor --fix # 应用每个检查的自动修复,然后重新检查
./lime audit # 容器加固 + 供应链不变量
./lime pin # 报告镜像相对于注册表的漂移
不使用安装程序,手动操作:
git clone https://github.com/clickswave/limeyard && cd limeyard
cp .env.example .env
echo "LIMEYARD_DIR=$PWD" >> .env
echo "LIME_TOKEN=$(openssl rand -hex 24)" >> .env # 必需,见 SECURITY.md
docker compose up -d --build # 控制平面 + UI 位于 http://127.0.0.1:7000
./lime setup
每个清单都带有一个实测的 resources 块(容器、空闲 RAM、镜像磁盘、空闲 CPU)。向导、面板的选择条和每个目标的页面都从中汇总,因此估算在任何地方都一致。
两个,且仅有两个。
main 是安装程序克隆的分支,也是你不做任何操作时得到的分支。它通过拉取请求推进,绝不通过直接推送。dev 是默认分支,也是工作落地的分支。针对它提交拉取请求。| target | 一个被测对象,具有声明的 kind。拥有一个 compose 文件、可选的 setup 以及自己的答案密钥。作为自己独立的 compose 项目运行,因此两个使用 Postgres 的目标永远不会共享一个 |
| scenario | 多个目标连接成一个带有权威 DNS 的网络拓扑。资产发现据此评分 |
| truth | 机器可读的答案密钥。见 truth/schema.md |
| doctor | 一个检查列表,每个检查都有裁决:环境、归属、供应链、加固。具有明确修复方法的检查在面板(/doctor)和 CLI 的 --fix 中带有一键修复:创建网络、回收磁盘、固定镜像、获取源、重新验证运行中的目标、重写非回环绑定。归属、端口冲突和加固需要人工处理 |
类型:web api bench cve service estate edge control mobile。
targets/<kind>/<slug>/ target.yml, compose.yml, setup.sh, truth.yml
scenarios/<slug>/ scenario.yml, compose.yml, zones/
control/limed/ the daemon: CLI + HTTP API + scorer
control/ui/ the SvelteKit control panel
truth/ the contract, and dated scorecards
docker compose up -d 启动两个容器,仅此而已:limeyard_control(limed 守护进程,持有 Docker socket)和 limeyard_ui(SvelteKit 面板)。两者都仅绑定回环。面板位于 http://127.0.0.1:7000,原始 API 位于 http://127.0.0.1:7099。两者都需要 .env,其中的 LIME_TOKEN 是必需的:面板在服务端持有令牌,浏览器永远看不到它。
| 页面 | 用途 |
|---|---|
| Targets | 每个目标,按类型和状态过滤、排序、多选并支持 Start、Stop、Restart。点击一行进入目标 |
| Target | 事实(地址、凭据、技术栈、上游、验证、镜像摘要)、实时日志,以及答案密钥及其否定项 |
| Scenarios | 资产环境:解析器、区域、子网,以及带每个容器状态的主机表。启动、停止、重启 |
| Scorecard | 最新运行及其增量、按类别和按目标的覆盖率、遗漏的 id、可查看的历史记录,以及两次运行的差异 |
| Ports | 什么绑定在 localhost 上,什么仅存在于实验室网桥上 |
| Doctor | 一个检查列表,每个检查都有裁决。Fix 和 Fix all 首先显示确切的命令和文件编辑(根据实验室当前状态计算),并在确认后运行它们。一个修复如果让其检查仍然失败,会如实说明 |
| Credits | 每个目标的作者以及许可协议 |
面板会自我更新:limed 通过 SSE 流式传输状态转换,因此从 CLI 启动的目标无需刷新即可显示。页头从同一流中携带主机的 CPU、RAM 和可用磁盘,仅在值得注意时着色。每个表格都可通过点击列标题排序;再次点击会翻转方向。
在更改面板或守护进程后,重新构建这一对:
docker compose up -d --build
要在不重新构建镜像的情况下针对运行中的守护进程开发面板:
cd control/ui && npm install
LIMED_URL=http://127.0.0.1:7099 LIME_TOKEN=<from .env> npm run dev # :7000
除 /api/health 外的每个调用都需要 X-Lime-Token。
GET /api/targets list, with state and attribution
GET /api/targets/<slug> plus truth, images, lab addresses
GET /api/targets/<slug>/logs SSE, docker compose logs -f
POST /api/targets/<slug>/<action> start | stop | restart | pull | setup
POST /api/targets/bulk {action, slugs}: a pool of three, per-slug refusals
GET /api/scenarios with per-host container state
POST /api/scenarios/<slug>/<action> up | down | restart
GET /api/scorecards newest first, by_class carries false positives
GET /api/scorecards/<id> one card
POST /api/score {tool, findings, save?, targets?}
GET /api/doctor checks with verdict, reason, value, items, fix
POST /api/doctor/fix {ids}: those checks, or every fixable one when empty
GET /api/ports /api/credits /api/truth /api/status
GET /api/events SSE: state, scenario, tick
三个层级,因为单一层级正是最初的问题。
lime-web 网桥。Web 和 API 目标,发布在 127.0.0.1:70xx。lime-lab 网桥,10.66.0.0/16,静态 IP,无主机绑定。扫描器作为容器加入此网络,看到的是具有真实主机和真实端口的真实子网,而不是回环端口列表。lime-edge WAF 层级,源站可达但不在 DNS 中。DNS 是 .test 区域上的权威 BIND(RFC 6761 保留了它)。Docker 网络别名刻意不作为事实来源:它们永远不会出现在区域传输中,并且会使拓扑与答案密钥不一致。
| 范围 | 用途 |
|---|---|
| 7000 | UI |
| 7099 | limed API |
| 7001-7099 | web 和 api 目标 |
| 7100-7199 | 基准测试套件 |
| 7200-7299 | CVE 实验室 |
| 7300-7399 | edge 和 control 目标 |
| 5353 | 实验室 DNS |
| 无 | service 和 estate 目标,仅 lime-lab 地址 |
每个目标都附带一个 truth.yml。评分器将发现结果转化为精确度、召回率和 F1,按目标和按类别:
curl -s -XPOST localhost:7099/api/score -H 'Content-Type: application/json' \
-d '{"tool":"crossfyre","save":true,"findings":[...]}'
统计三件事,而不是一件。召回率:我们是否找到了存在的东西。精确度:我们是否避免了报告不存在的东西,依据每个答案密钥都携带的 negative 条目来衡量。范围:我们正确地没有尝试什么,记录下来以免每个季度都有人重新争论。
mirage 目标仅为第二项而存在。其中没有任何东西存在漏洞,而其中一切看起来都像是有漏洞的,因此针对它的任何发现从构造上就是误报。
CONTRIBUTING.md 包含了全部内容:贡献通常是什么、./lime audit 强制执行的不变量,以及为什么在这里修正答案密钥比新增功能更有价值。规则的简短版本如下。所有参与者都受行为准则约束。
targets/<kind>/<slug>/
target.yml manifest, including a REQUIRED upstream block
compose.yml the containers. 127.0.0.1 binds only
setup.sh optional one-time init, run after start
truth.yml the answer key
保持实验室整洁的规则:
127.0.0.1。[lime-web]。不要添加每个项目的网络:网络太多会导致 Docker 地址池耗尽。[default, lime-web],并将数据库仅放在 [default] 上。每个目标获得自己的数据库服务和卷。[lime-lab],使用静态 IP,不发布任何主机端口。upstream 是必需的。 lime doctor 会让没有它的目标失败,管理器拒绝注册这样的目标。见下文。repo: 和 compose: 放入清单,源代码会在运行时获取到 <target>/src。./lime measure <slug> 打印出要粘贴的 resources 块。安装程序和面板会将这些加起来,以便在有人启动之前发出警告,所以这里的猜测就是那里的谎言。目标是别人的刻意设计为存在漏洞的软件,因此来源是被记录而非假设的,运行时是被约束而非信任的。SECURITY.md 有完整的情况;简短版本:
./lime pin 报告漂移。raesene/bwapp(已归档,最后重建于 2022 年,无许可证)和 delfer/alpine-ftp-server(个人),被如此标明。no-new-privileges、cap_drop: ALL 加上每个镜像最低限度的 cap_add、pid 上限和内存上限运行。数据库层级位于 internal: true 网络上,没有对外路由。limed 持有 Docker socket,即主机上的 root,因此它在每次调用时都需要共享密钥。被攻陷的目标可以访问它但学不到任何东西。./lime audit 会在 privileged、主机网络、目标中的 socket 挂载、非回环绑定、未固定镜像或缺失令牌时失败。这里没有任何东西能防御内核级容器逃逸。为此,请使用一次性 VM。
这里几乎所有东西都是别人写的,而且有几个目标完全没有声明许可证。因此,署名作者是硬性门槛,而非惯例:
target.yml 带有一个必需的 upstream 块:作者、仓库、许可证,以及我们最后验证其可构建的日期。当容器化者与作者不同时,packager 记录容器化者。none declared 会渲染为警告,这也是禁止再分发的信号。/credits 和 ./lime credits 列出每个目标、作者和许可证。./lime credits --markdown 重新生成下面的部分。limeyard 运行别人的作品。下面的每个目标和场景都是别人构建的,除非它标明 Clickswave。
| 目标 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| mirage | Clickswave | MIT | repo |
| 目标 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| Log4Shell lab | Christophe Tafani-Dereeper (christophetd) | Apache-2.0 | repo |
| 目标 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| ModSecurity CRS pair | OWASP Core Rule Set project (coreruleset) | Apache-2.0 | repo |
| 目标 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| AndroGoat | Satish Patnayak | none declared | repo |
| 场景 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| estate | Clickswave | MIT | - |
| 目标 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| Open services | Clickswave (composition of upstream official images) | mixed, per-image | - |
| 目标 | 作者 | 许可证 | 来源 |
|---|---|---|---|
| bWAPP | Malik Mesellem (pkg: Rory McCune (raesene)) | none declared | repo |
| DVWA | Robin Wood (digininja) | GPL-3.0 | repo |
| FaultLine ISP | Clickswave | MIT | repo |
| OWASP Juice Shop | Bjoern Kimminich (OWASP Juice Shop project) | MIT | repo |
| OWASP Mutillidae II | Jeremy Druin (webpwnized), OWASP Mutillidae II | GPL-3.0 | repo |
| OWASP RailsGoat | OWASP RailsGoat project | MIT | repo |
| OWASP WebGoat + WebWolf | OWASP WebGoat project | GPL-2.0 | repo |