Lab Docker délibérément vulnérable avec un parc DNS routable et des clés de réponse lisibles par machine par cible, évaluant localement la précision, le rappel et la portée du scanner.
Un laboratoire de test de sécurité. Une flotte de cibles délibérément vulnérables, un patrimoine réseau routable avec un DNS faisant autorité pour la découverte d'actifs à énumérer, une clé de réponse lisible par machine par cible, et un panneau de contrôle pour tout piloter.
Tout ici est délibérément vulnérable. Test local uniquement. Ne l'exposez pas à Internet ni à un réseau non fiable. Les ports publiés se lient à
127.0.0.1; les cibles du labo ne se lient à rien du tout.Notre propre scanner obtient un RCE à l'intérieur de ces conteneurs à dessein, donc le conteneur est traité comme une frontière de sécurité : chaque image est épinglée par digest, chaque service abandonne toutes ses capacités et n'en rajoute qu'un minimum, et
./lime auditle fait respecter. Lisez SECURITY.md avant la première exécution, y compris la partie sur ce que l'isolation par conteneur ne couvre pas.
Le panneau de contrôle sur http://127.0.0.1:7000, montrant le labo en cours d'exécution.
limeyard était vuln_apps. Il a été renommé parce qu'il a cessé d'être un dossier
d'applications : il contient désormais des services nus, une zone DNS, une paire
de WAF, une cible de précision et des fixtures APK, dont aucun n'est une
application.
L'ancienne flotte obtenait 9 sur 9, zéro manque, zéro faux positif. Un benchmark qui ne peut pas échouer ne peut pas détecter une régression. Trois choses étaient structurellement erronées :
127.0.0.1:70xx,
donc l'énumération de sous-domaines n'avait rien à énumérer, le scan de ports recevait
sa réponse, et le fingerprinting de services ne voyait jamais de démon non-HTTP.Vous avez besoin de Docker (avec le plugin compose) et de git. Rien d'autre.
curl -fsSL https://raw.githubusercontent.com/clickswave/limeyard/main/install.sh | bash
Cela clone le labo dans ./limeyard, écrit son .env avec un nouveau jeton
d'API, construit et démarre le plan de contrôle, puis demande quoi exécuter. Avant
que quoi que ce soit ne démarre, il montre ce que coûte la sélection, mesuré au
repos sur la machine de référence, par rapport à ce que votre machine a de libre :
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]
Non interactif : curl ... | bash -s -- --light --yes (ou --all,
--none, --pick dvwa,juice-shop,estate). Placez le checkout ailleurs avec
LIMEYARD_DIR=/path.
Le panneau est alors sur http://127.0.0.1:7000, et les mêmes choses à la main :
./lime setup # the wizard again, any time
./lime start --all # every light target
./lime start crapi --heavy # a heavy one, explicitly
./lime scenario-up estate # the network estate: DNS, vhosts, services
./lime status # what is up
./lime stop --all --heavy # everything down; images and volumes stay
./lime credits # who wrote each target, and under what licence
./lime doctor # environment, attribution and disk checks
./lime doctor --fix # apply every check's automatic remedy, then re-check
./lime audit # container hardening + supply chain invariants
./lime pin # report image drift against the registry
À la main, sans l'installateur :
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 # required, see SECURITY.md
docker compose up -d --build # control plane + UI on http://127.0.0.1:7000
./lime setup
Chaque manifeste porte un bloc resources mesuré (conteneurs, RAM au repos,
disque des images, CPU au repos). L'assistant, la bande de sélection du panneau
et la page de chaque cible en font la somme, donc l'estimation est la même
partout.
Deux, et seulement deux.
main est ce que l'installateur clone et ce que vous obtenez si vous ne faites rien.
Elle avance par pull request, jamais par push direct.dev est la branche par défaut et là où le travail atterrit. Ouvrez les pull requests
contre elle.| cible | une chose sous test, d'un kind déclaré. Possède un fichier compose, une configuration optionnelle, et sa propre clé de réponse. S'exécute comme son propre projet compose isolé, donc deux cibles utilisant Postgres n'en partagent jamais un |
| scénario | plusieurs cibles câblées dans une topologie réseau avec un DNS faisant autorité. Ce sur quoi la découverte d'actifs est notée |
| vérité | la clé de réponse lisible par machine. Voir truth/schema.md |
| doctor | une liste de vérifications avec un verdict chacune : environnement, attribution, chaîne d'approvisionnement, durcissement. Les vérifications avec un remède sans ambiguïté portent un correctif en un clic dans le panneau (/doctor) et --fix sur la CLI : créer des réseaux, récupérer du disque, épingler des images, récupérer les sources, revérifier les cibles en cours, réécrire les binds hors loopback. L'attribution, les conflits de ports et le durcissement nécessitent une personne |
Types : 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 démarre deux conteneurs et rien d'autre : limeyard_control
(le démon limed, qui détient le socket Docker) et limeyard_ui (le panneau
SvelteKit). Les deux se lient uniquement au loopback. Le panneau est sur http://127.0.0.1:7000 et
l'API brute sur http://127.0.0.1:7099. Les deux nécessitent .env, et LIME_TOKEN dedans est
obligatoire : le panneau détient le jeton côté serveur et le navigateur ne le voit jamais.
| page | à quoi elle sert |
|---|---|
| Targets | chaque cible, filtrer par type et état, trier, multi-sélection avec Start, Stop, Restart. Cliquez sur une ligne pour la cible |
| Target | faits (adresse, identifiants, stack, upstream, vérification, digests d'images), le journal en direct, et la clé de réponse avec ses négatifs |
| Scenarios | le patrimoine : résolveur, zones, sous-réseau, et une table d'hôtes avec l'état par conteneur. Monter, descendre, redémarrer |
| Scorecard | la dernière exécution avec les deltas, la couverture par classe et par cible, les ids manqués, un historique consultable, et un diff entre deux exécutions |
| Ports | ce qui se lie sur localhost, et ce qui n'existe que sur le bridge du labo |
| Doctor | une liste de vérifications avec un verdict chacune. Fix et Fix all montrent d'abord les commandes exactes et les modifications de fichiers, calculées à partir du labo tel qu'il est, et les exécutent à la confirmation. Un correctif qui laisse sa vérification en échec le dit |
| Credits | qui a écrit chaque cible et sous quelle licence |
Le panneau se met à jour tout seul : limed diffuse les transitions d'état via SSE, donc une cible démarrée depuis la CLI apparaît sans rafraîchissement. L'en-tête porte le CPU, la RAM et le disque libre de l'hôte depuis le même flux, colorés uniquement quand ils méritent d'être remarqués. Chaque table se trie en cliquant sur un en-tête de colonne ; un second clic inverse la direction.
Après une modification du panneau ou du démon, reconstruisez la paire :
docker compose up -d --build
Pour travailler sur le panneau contre un démon en cours d'exécution sans reconstruire l'image :
cd control/ui && npm install
LIMED_URL=http://127.0.0.1:7099 LIME_TOKEN=<from .env> npm run dev # :7000
Chaque appel sauf /api/health nécessite 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
Trois niveaux, parce qu'un seul était le problème d'origine.
lime-web bridge. Cibles web et API, publiées sur 127.0.0.1:70xx.lime-lab bridge, 10.66.0.0/16, IP statiques, aucune liaison à l'hôte. Un scanner
rejoint ce réseau en tant que conteneur et voit un vrai sous-réseau avec de vrais hôtes et de
vrais ports au lieu d'une liste de ports loopback.lime-edge le niveau WAF, avec l'origine joignable mais absente du DNS.Le DNS est un BIND faisant autorité sur les zones .test (la RFC 6761 la réserve). Les alias
de réseau Docker ne sont délibérément pas la source de vérité : ils n'apparaissent jamais dans un transfert
de zone, et feraient diverger la topologie de la clé de réponse.
| plage | usage |
|---|---|
| 7000 | l'UI |
| 7099 | API limed |
| 7001-7099 | cibles web et api |
| 7100-7199 | suites de benchmark |
| 7200-7299 | labos CVE |
| 7300-7399 | cibles edge et control |
| 5353 | DNS du labo |
| aucun | cibles service et estate, adresses lime-lab uniquement |
Chaque cible livre un truth.yml. Le scorer transforme les findings en précision,
rappel et F1, par cible et par classe :
curl -s -XPOST localhost:7099/api/score -H 'Content-Type: application/json' \
-d '{"tool":"crossfyre","save":true,"findings":[...]}'
Trois choses sont comptées, pas une. Rappel : avons-nous trouvé ce qui est là.
Précision : avons-nous évité de signaler ce qui n'y est pas, mesuré par rapport aux
entrées negative que porte chaque clé de réponse. Périmètre : ce que nous n'avons correctement pas
tenté, enregistré pour que personne ne le remette en question chaque trimestre.
La cible mirage n'existe que pour la seconde. Rien en elle n'est vulnérable
et tout en elle ressemble à ce qui l'est, donc tout finding contre elle est un faux
positif par construction.
CONTRIBUTING.md contient tout : ce qu'est habituellement une contribution,
les invariants que ./lime audit fait respecter, et pourquoi une correction d'une
clé de réponse vaut plus ici qu'une nouvelle fonctionnalité. La version courte des
règles est ci-dessous. Toute personne participant est tenue au
code de conduite.
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
Règles qui gardent le labo propre :
127.0.0.1.[lime-web] uniquement. N'ajoutez pas de réseau
par projet : trop de réseaux et Docker épuise son pool d'adresses.[default, lime-web] et placent la base de données sur
[default] uniquement. Chaque cible obtient son propre service de base de données et son volume.[lime-lab] avec
une IP statique et ne publient aucun port hôte.upstream est obligatoire. lime doctor fait échouer une cible sans lui et le
gestionnaire refuse d'en enregistrer une. Voir ci-dessous.repo: et compose: dans le
manifeste et la source est récupérée à l'exécution dans <target>/src à la place../lime measure <slug>
affiche le bloc resources à coller. L'installateur et le panneau additionnent
ces valeurs pour avertir quelqu'un avant qu'il ne la démarre, donc une estimation ici est un mensonge là-bas.Les cibles sont le logiciel délibérément vulnérable d'autres personnes, donc la provenance est enregistrée plutôt que supposée, et le runtime est contraint plutôt que de confiance. SECURITY.md contient le tableau complet ; la version courte :
./lime pin signale la dérive.raesene/bwapp (archivé, dernière reconstruction
2022, aucune licence) et delfer/alpine-ftp-server (un individu), sont nommés
comme tels.no-new-privileges, cap_drop: ALL plus un minimum
de cap_add par image, un plafond de pid et un plafond de mémoire. Les niveaux de base de données reposent sur
des réseaux internal: true sans route vers l'extérieur.limed détient le socket Docker, qui est root sur l'hôte, donc il exige un
secret partagé à chaque appel. Une cible compromise peut l'atteindre et n'apprendre rien../lime audit échoue sur privileged, le réseau hôte, un montage de socket dans une
cible, un bind hors loopback, une image non épinglée ou un jeton manquant.Rien ici ne défend contre une évasion de conteneur au niveau du noyau. Pour cela, utilisez une VM jetable.
Presque tout ici a été écrit par quelqu'un d'autre, et plusieurs cibles ne déclarent aucune licence. Donc créditer l'auteur est un verrou strict, pas une convention :
target.yml porte un bloc upstream obligatoire : auteur, repo, licence,
et la date à laquelle nous avons vérifié pour la dernière fois qu'il se construit. packager enregistre la personne qui
a conteneurisé quelque chose quand cela diffère de qui l'a écrit.none declared s'affiche comme un
avertissement, ce qui est aussi le signal de ne-pas-redistribuer./credits dans l'UI et ./lime credits listent chaque cible, auteur et
licence. ./lime credits --markdown régénère la section ci-dessous.limeyard exécute le travail d'autres personnes. Chaque cible et scénario ci-dessous a été construit par quelqu'un d'autre sauf mention de Clickswave.
| cible | auteur | licence | source |
|---|---|---|---|
| mirage | Clickswave | MIT | repo |
| cible | auteur | licence | source |
|---|---|---|---|
| Log4Shell lab | Christophe Tafani-Dereeper (christophetd) | Apache-2.0 | repo |
| cible | auteur | licence | source |
|---|---|---|---|
| ModSecurity CRS pair | OWASP Core Rule Set project (coreruleset) | Apache-2.0 | repo |
| cible | auteur | licence | source |
|---|---|---|---|
| AndroGoat | Satish Patnayak | none declared | repo |
| scénario | auteur | licence | source |
|---|---|---|---|
| estate | Clickswave | MIT | - |
| cible | auteur | licence | source |
|---|---|---|---|
| Open services | Clickswave (composition of upstream official images) | mixed, per-image | - |
| cible | auteur | licence | source |
|---|---|---|---|
| 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 |