v2.1.2 核心优化与多架构发布 (#561)

* feat: v2.1.0 核心重构与功能增强

## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n

## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池

## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)

## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建

## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
  JSON 输出格式、Redis 检测、Context 超时等

## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)

* fix(ci): 移除 PR 对 Project 自动化的触发

* fix: Elasticsearch未授权检测优先于爆破 (#554)

* fix: 修复RDP爆破高误报率问题 (#555)

- 移除 screen.go 中错误的认证结果覆盖逻辑
- 启用 NLA 协议的 ErrorCode 字段检测
- 添加 PubKeyAuth 验证确保认证真正成功
- 修复 io.go 中错误被静默忽略的问题
- 修复 socket.go/io.go 中可能导致 panic 的代码
- 修复 screen.go 中文件句柄泄漏和 log.Panic

* fix: 修复-user/-pwd凭据参数不生效的问题

问题原因:
- Parse()解析凭据后更新globalConfig
- 但BuildConfigFromFlags()创建新Config时使用默认字典
- 导致解析的UserPassPairs等凭据信息被丢弃

修复内容:
1. initialize.go: 将Parse解析的凭据结果应用到新Config
2. credential.go: 单用户密码对时创建UserPassPairs
3. rdp.go: 单凭据测试时跳过指纹识别,减少连接次数

* feat: RDP使用NLA仅验证模式,避免挤掉已登录用户

- 添加ErrNLAAuthSuccess标志用于NLA验证成功信号
- tpkt层支持nlaAuthOnly模式,验证成功后不建立完整会话
- x224层正确传播NLA验证结果
- rdpCrack改用NlaAuth进行凭据验证

* fix: 修复进度条在Windows终端满屏重复输出的问题

- 添加终端宽度检测,动态调整进度条长度
- 使用空格覆盖清除旧内容,避免残留
- 简化进度条格式,确保不超过终端宽度

* feat: 优化日志颜色方案,区分漏洞和普通信息

- 新增 LogVuln 级别(红色),用于漏洞和重要发现
- 密码爆破成功、未授权访问、POC漏洞等改用红色显示
- 普通信息(扫描统计等)改为白色
- Web指纹保持绿色

* refactor: 精简化输出,移除冗余启动信息

- 移除showParseSummary开局配置输出
- 移除LogPluginInfo/LogPluginInfoWithPort插件信息输出
- 移除alive_scanner冗余统计输出
- 移除port_scan_start扫描开始提示
- 移除handleUDPPorts SNMP死代码
- 移除相关i18n条目

* chore: 版本号更新为2.1.1

* fix: 降级依赖版本以保持Go 1.20兼容性

* feat(ldap): 添加NTLM Hash认证支持 (#433)

* chore: 清理无用的 replace 指令

* fix(ping): 修复 TTL expired 导致主机误判为存活的问题

在 ExecCommandPing 中增加错误关键词检测,当 ping 输出包含
TTL expired、Destination unreachable 等错误信息时,不再将
目标主机标记为存活。

Fixes #454

* fix(proxy): 修复透明代理导致输出全端口的问题

在代理初始化时主动探测代理行为,通过连接 RFC 5737 保留的
测试地址来检测是否存在"全回显"问题。如果探测到代理不可靠,
则在端口扫描时跳过所有端口,避免误报。

- 新增 proxyReliable 标志位标记代理可靠性
- 新增 ProbeProxyBehavior 函数探测代理行为
- 端口扫描前检查代理可靠性并输出警告

Fixes #495

* refactor: 移动debug模块到common/debug子包

* fix(web): 修复-u模式下Web插件未执行的问题

* fix: 优化输出格式和颜色显示

- 网段统计格式改为 10.253.0.0/16 网段存活: 26
- WebTitle基础信息改为白色,指纹识别单独绿色输出
- 移除重复的端口数量输出

* fix: URL解析自动补全协议头

-uf 文件中 192.168.1.1:8080 自动转为 http://192.168.1.1:8080

* fix: 修复-u/-uf模式下URLs丢失导致0目标扫描的问题

Parse阶段将URLs设置到全局状态,但Initialize随后创建新状态
并覆盖了全局状态,导致URLs数据丢失。现在在创建新状态前
先保存并迁移Parse阶段设置的URLs和HostPorts数据。

* fix: 智能检测HTTP/HTTPS协议并优化URL显示

- 修复-u/-uf模式URLs丢失导致0目标扫描问题
- detectProtocol改为主动TLS握手检测,不依赖服务名
- WebTitle输出显示完整协议(http/https)
- 隐藏标准端口(80/443)使输出更简洁

* refactor: 精简parsers包,统一配置构建入口

- 删除冗余的中间层(XXXInput、XXXParser类)
- 新增 config_builder.go 统一配置构建
- parsers包从3000+行精简至~540行
- 保留核心函数:ParseIP、ParsePort、文件读取、凭据解析

* test: 扩展parsers单元测试覆盖边缘情况

- 新增内网简写解析测试(192/172/10)
- 新增完整IP范围和无效CIDR测试
- 新增Windows行尾(CRLF)处理测试
- 新增凭据和哈希文件解析测试
- 新增端口解析边缘情况测试
- 测试覆盖率达到94.2%

* refactor: 优化控制台输出格式

- 去掉时间戳,保留[*][+]前缀
- Web输出合并WebTitle和WebFinger为一行
- 有指纹显示绿色[+],无指纹显示白色[*]
- 格式: code:xxx len:xxx title:xxx server:xxx [指纹]
- 服务探测格式: [Product:xxx ||Version:xxx] Banner:(xxx)
- 字段对齐,输出更清爽

* feat: 添加凭据测试未发现弱密码的提示

- credential_tester.go: 失败时设置 Type=ResultTypeCredential
- scanner.go: 根据结果类型在 error 级别输出'未发现弱密码'提示
- 新增 i18n 翻译 brute_no_weak_pass

使用 -log all 或 -log error 可看到此提示

* refactor(logging): 重构日志级别为层级过滤设计

- LogLevel 从 string 改为 int 类型,支持层级比较
- 层级设计:Debug(0) < Base(1) < Info(2) < Success(3) < Vuln(4) < Error(5)
- 设置一个级别后,显示该级别及以上的日志
- Error 级别始终显示,不会被配置过滤掉
- 保留向后兼容别名(LevelAll, LevelInfoSuccess 等)
- 更新测试以匹配新的层级过滤行为

* style(logging): Error级别日志改为黄色显示

* style(findnet): NetInfo输出改为每行一个IP

* refactor(ms17010): 优化错误提示,明确指出SMBv1不支持等情况

* fix(credential): 修复凭据测试结果不一致的问题

问题原因:
1. 未知错误类型不重试,导致服务端限流时跳过正确密码
2. SSH 错误分类不够准确,某些临时错误未被识别

修复内容:
1. 未知错误改为可重试(可能是临时问题)
2. 增加 SSH 特有的网络错误识别(handshake failed, disconnect 等)

* fix(portfinger): 修复SMB2服务指纹识别和NetInfo输出问题

- 添加SMB2ProgNeg探针支持现代Windows的SMB2协议
- 修复Go regexp对高位字节的UTF-8兼容问题,使用Latin-1转换
- 修复探针失败后连接重建逻辑
- 修复vendor_product字段名不匹配问题
- 修复NetInfo多行输出被其他日志打断的问题

* fix(config): 从默认端口移除9100,避免触发打印机打印 (#517)

* feat(proxy): 增强代理端口扫描的深度验证机制

- 新增4阶段深度验证:Banner读取→探测发送→响应等待→最终判定
- 新增SOCKS5错误码和代理错误文本检测
- 优化ProbeProxyBehavior探测逻辑,发送数据验证连接可达性
- 解决透明代理/全回显代理导致的假阳性问题

* fix(proxy): 修复代理深度验证的若干问题

- detector.go: 修复 AutoConfigureProxy 覆盖探测结果的问题
  只有未探测过时才设置默认 proxyReliable 值

- port_scan.go: 改进深度验证机制
  - 使用带 Host header 的 HTTP GET 请求替代 OPTIONS
  - 延长响应等待超时至 2s 以适配慢速服务器
  - 正确重置连接 deadline 避免影响后续操作

* refactor: 统一 common 包文件命名风格

Flag.go -> flag.go

* refactor(proxy): 删除自定义 contains() 函数,改用标准库

- 用 strings.Contains() 替代手写的 contains()
- 删除过时的注释

* fix(parsers): 修复带横杠域名被误识别为IP范围的问题

如 111-555.sss.com 这类域名因包含 - 被错误解析为 IP 范围,
添加 looksLikeIPRange() 检查,只有 - 前是有效 IP 才走范围解析

* fix(proxy): 修复代理模式下服务识别错误和端口漏扫问题

- port_scan.go: 验证通过后重建干净连接,避免HTTP GET探测污染服务识别
- port_scan.go: 优化验证策略,用轻量CRLF探测替代HTTP GET,超时从2.2s降至0.6s
- manager.go: 修正ProbeProxyBehavior判断逻辑,超时应视为代理正常转发

* fix(pool): 移除线程池预分配,优化大规模扫描内存占用

WithPreAlloc(true) 会预先创建所有 worker goroutine,
在大规模扫描(如 25域名×65535端口)时可能导致内存问题

* refactor(logging): 统一日志前缀,删除废弃的 LogBase

- 删除 LogBase 函数,所有调用迁移到 LogInfo/LogError
- 新增 PrefixDebug ([.]) 前缀,所有日志级别现在都有前缀
- 修复日志输出缩进不一致的问题
- 删除未使用的 PrefixDefault 常量

* perf(icmp): 实现自适应等待算法优化存活检测性能

- 新增 waitAdaptive 函数,监控响应增量实现智能提前结束
- 算法保守原则:最小等待1s + 连续500ms无新响应才提前结束
- 添加100ms检查间隔避免CPU空转
- 保留原有最大等待时间(3s/6s)作为兜底
- 添加完整单元测试覆盖各种场景

优化效果:
- 全部响应:~100ms (原3s)
- 无响应:~1s (原3s)
- 部分响应后稳定:~1.5s (原3s)

* perf(scan): 实现启发式优化提升扫描体验

1. 端口优先级排序:高价值端口(80,443,22,3389等)优先扫描
   - 用户能更快看到有意义的结果
   - 不影响端口喷洒策略

2. TCP 补充探测:ICMP 响应率<10%时自动启用
   - 对未响应主机用 TCP 80/443/22/445 补充探测
   - 解决防火墙过滤 ICMP 导致漏检的问题

* refactor(grdp): 精简RDP库,删除认证检测不需要的代码

- 删除 VNC 协议支持 (protocol/rfb, client/rfb.go)
- 删除完整客户端框架 (client/)
- 删除 RemoteApp 等插件 (plugin/)
- 删除 RLE 图形解压 (core/rle.go)
- 删除绘图指令处理 (pdu/orders.go, pdu/gdi.go)
- 精简 screen.go,移除截图和完整会话功能
- 移除未使用的 RGB 转换函数

grdp 代码从 13,044 行精简至 7,581 行,削减 42%

* refactor(common): 删除死代码,优化代码风格

- 删除未使用的 joinStrings/joinInts 函数
- 删除未使用的 memStats 字段和 getMemoryInfo 方法
- 简化 parsePasswords 中的循环为 append(...) 形式

* refactor(services): 统一数据库插件的DBWrapper

4个数据库插件(MySQL、PostgreSQL、MSSQL、Oracle)都有相同的sql.DB包装代码,
合并为通用的SQLDBWrapper,减少重复。

* refactor(core,grdp): 删除未使用的死代码

- 移除 BaseScanStrategy.LogPluginInfoWithPort 方法(无调用者)
- 移除 mcs.go 中被注释的旧 connect 函数实现

* refactor: 删除 deadcode 检测出的未使用函数

- proxy/detector.go: 删除 IsSOCKS5Standard, IsProxyInitialized
- findnet.go: 删除 NetworkInfo.OneLine, TreeFormat 方法
- port_scan.go: 删除 estimateScanTime 函数
- web_scanner.go: 删除 GetFingerprints 函数
- 清理相关测试代码

* refactor: 删除更多未使用的死代码

- parse.go: 删除 RemoveDuplicate 函数及其测试
- parsers.go: 删除 excludeHosts, removeDuplicates 别名函数
- 更新测试使用真正的函数名

* fix(test): 修复 TestParseIP_InvalidIPRange 测试用例

- 删除不合理的测试用例(无效IP被当作普通主机名处理是设计行为)
- 修复测试逻辑,只在真正通过时输出"正确"

* fix(scan): 移除域名预解析,保留原始域名进行扫描

域名预解析会将域名转换为IP,导致虚拟主机场景下HTTP访问失败
(Host头变成IP而非域名,无法正确路由)

* fix(scan): 修复 -hf 参数无法单独使用的问题

* fix(proxy): 修复透明代理环境下 SOCKS5 代理全端口误报问题

问题:在透明代理(TUN模式)环境下使用 SOCKS5 代理扫描时,
会出现全端口开放的误报,因为代理可靠性检测被透明代理污染。

修复方案(参考 fscanx):
1. 将探针从 CRLF 改为 HTTP GET,更有效检测真实连接状态
2. 删除 "uncertain" 状态,无响应一律判定为端口关闭
3. 调整超时时间以适应代理链路延迟

Fixes #524

* feat(telnet): 新增 telnetd RCE 命令执行验证,修复未授权访问日志级别

* fix: 修复 i18n.Tr vet 报错、Unicode 测试用例,移除过期域名

- 移除 i18n.Tr 中错误的 fmt.Sprintf fallback,消除 go vet 误报
- 修复 match_engine_test Unicode 测试用例与 Latin-1 转换逻辑不匹配
- README 移除过期的 fscan.club 域名
- 添加 .gitattributes 统一换行符为 LF

* refactor: 统一控制台输出风格,使用统一的日志函数

手动合并 PR #558 的改动,适配重构后的代码路径

* fix(ci): 修复版本注入和CI触发配置

- goreleaser ldflags 指向正确的包路径 common.version/commit/date
- version 改为 var 支持 ldflags 注入,banner 显示 commit 和构建日期
- test-build 触发分支增加 dev-* 通配

* fix(ci): 修复 Windows 产物 .exe.exe 双后缀问题

* feat(ci): 扩展构建架构支持 MIPS/ARM/FreeBSD/Solaris
This commit is contained in:
ZacharyZcR
2026-04-25 17:39:16 +08:00
committed by GitHub
parent 594f567650
commit 760c8ea502
927 changed files with 82001 additions and 24995 deletions
+29
View File
@@ -0,0 +1,29 @@
# fscan 二进制
docker/attacker/fscan
# 数据文件
backend/data/*.json
flags/*.key
flags/*.pub
# Node modules
frontend/node_modules/
frontend/dist/
frontend/.vite/
# Go
backend/api
*.exe
*.exe~
# Docker
.env
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
+48
View File
@@ -0,0 +1,48 @@
# fscan-lab:内网渗透训练平台
基于Docker的五层网络架构渗透测试环境,用于学习和练习fscan工具在真实内网场景的应用。
## 快速开始
```bash
# 启动环境
docker-compose up -d
# 进入攻击者容器
docker exec -it lab-attacker /bin/bash
# 开始渗透(从DMZ区开始)
fscan -h 10.10.1.0/24
```
## 网络拓扑
```
外网(172.16.0.0/24) → DMZ(10.10.1.0/24) → 办公网(10.10.2.0/24) → 生产网(10.10.3.0/24) → 核心网(10.10.4.0/24)
```
## 目录说明
- `docker-compose.yml` - 完整环境配置(23个容器)
- `test-services/` - 单服务测试环境(28个服务)
- `docker/` - 网络服务配置
- `backend/` - API服务(Go
- `frontend/` - Web UIReact
## Web界面
- **训练平台**: http://localhost:3000
- **API服务**: http://localhost:8888
## 管理命令
```bash
# 查看状态
docker-compose ps
# 停止环境
docker-compose down
# 完全清理
docker-compose down -v
```
+23
View File
@@ -0,0 +1,23 @@
FROM golang:1.20-alpine AS builder
WORKDIR /build
COPY go.mod go.sum* ./
RUN go mod download || true
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o api .
FROM alpine:latest
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /build/api /app/api
RUN mkdir -p /app/data /app/flags
EXPOSE 8888
CMD ["/app/api"]
+37
View File
@@ -0,0 +1,37 @@
module fscan-lab-api
go 1.20
require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.9.1
)
require (
github.com/bytedance/sonic v1.11.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+94
View File
@@ -0,0 +1,94 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA=
github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+551
View File
@@ -0,0 +1,551 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
type Challenge struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Difficulty string `json:"difficulty"`
Points int `json:"points"`
Flag string `json:"flag"`
Hints []string `json:"hints"`
Network string `json:"network"`
Targets []string `json:"targets"`
Order int `json:"order"` // 渗透顺序
}
type Progress struct {
UserID string `json:"user_id"`
CompletedChallenges []int `json:"completed_challenges"`
TotalScore int `json:"total_score"`
StartTime time.Time `json:"start_time"`
LastUpdate time.Time `json:"last_update"`
SubmissionHistory []Submission `json:"submission_history"`
}
type Submission struct {
ChallengeID int `json:"challenge_id"`
Flag string `json:"flag"`
Correct bool `json:"correct"`
Timestamp time.Time `json:"timestamp"`
}
type NetworkNode struct {
ID string `json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
Services []string `json:"services"`
Network string `json:"network"`
Status string `json:"status"` // unknown, discovered, compromised
}
type NetworkTopology struct {
Nodes []NetworkNode `json:"nodes"`
Edges []NetworkEdge `json:"edges"`
}
type NetworkEdge struct {
From string `json:"from"`
To string `json:"to"`
Access string `json:"access"` // allowed, blocked, vpn
}
var challenges = []Challenge{
{
ID: 1,
Name: "DMZ 侦察",
Description: "扫描 DMZ 区,发现 Web 服务器并获取第一个 flag",
Difficulty: "Easy",
Points: 100,
Flag: "FSCAN_LAB{w3b_f1ng3rpr1nt_d1sc0v3ry}",
Hints: []string{"扫描 10.10.1.0/24 网段", "寻找 Tomcat 服务", "flag 在 webapps/ROOT/flag1.txt"},
Network: "dmz",
Targets: []string{"10.10.1.10"},
Order: 1, // 第一步:外网扫描 DMZ
},
{
ID: 2,
Name: "FTP 弱密码",
Description: "通过 FTP 弱密码进入 DMZ 区并获取 SSH 密钥",
Difficulty: "Easy",
Points: 150,
Flag: "FSCAN_LAB{ftp_w34k_p4ssw0rd_pwn}",
Hints: []string{"FTP 服务在 10.10.1.12", "尝试 admin/123456", "查看 .ssh 目录"},
Network: "dmz",
Targets: []string{"10.10.1.12"},
Order: 2, // 第二步:FTP 获取 SSH 密钥
},
{
ID: 3,
Name: "VPN 网关突破",
Description: "使用获取的 SSH 密钥连接 VPN 网关进入办公网。提供两种渗透方法:(1) 直接上传 fscan 到 VPN 网关扫描办公网;(2) 使用 SSH 动态端口转发建立 SOCKS5 代理,在 Attacker 机器上通过代理扫描。详见 /root/docs/penetration-guide.md",
Difficulty: "Medium",
Points: 200,
Flag: "FSCAN_LAB{vpn_g4t3w4y_br34ch3d}",
Hints: []string{
"使用 office_key 连接 10.10.1.13",
"VPN 网关有两个网卡:10.10.1.13(DMZ) 和 10.10.2.2(办公网)",
"flag 在 /etc/flag3.txt",
"方法1: scp fscan 到网关,然后 ssh 登录扫描",
"方法2: ssh -D 1080 建立SOCKS5隧道,fscan -socks5 127.0.0.1:1080 -np",
},
Network: "dmz",
Targets: []string{"10.10.1.13"},
Order: 3, // 第三步:进入办公网
},
{
ID: 4,
Name: "办公网备份服务器",
Description: "发现 Rsync 备份服务器并获取敏感文件",
Difficulty: "Medium",
Points: 250,
Flag: "FSCAN_LAB{rsync_b4ckup_l34k}",
Hints: []string{"扫描办公网 873 端口", "Rsync 可能未授权访问", "备份目录: rsync://10.10.2.22/backup"},
Network: "office",
Targets: []string{"10.10.2.22"},
Order: 7, // 办公网探索,获取 Redis 密码
},
{
ID: 5,
Name: "生产网 Redis 渗透",
Description: "利用 Redis 弱密码获取 flag 并准备横向移动",
Difficulty: "Hard",
Points: 300,
Flag: "FSCAN_LAB{r3d1s_un4uth0r1z3d_4cc3ss}",
Hints: []string{"从备份文件获取 Redis 密码", "连接 10.10.3.31", "GET flag5"},
Network: "production",
Targets: []string{"10.10.3.31"},
Order: 8, // 进入生产网
},
{
ID: 6,
Name: "核心网 MySQL 数据库",
Description: "爆破 MySQL 数据库获取敏感信息",
Difficulty: "Hard",
Points: 350,
Flag: "FSCAN_LAB{mysql_d4t4b4s3_pwn3d}",
Hints: []string{"从生产网扫描核心网 3306 端口", "尝试 root/Password", "SELECT flag FROM secrets.flags"},
Network: "core",
Targets: []string{"10.10.4.40"},
Order: 10, // 核心网数据库,获取 Mongo 凭证
},
{
ID: 7,
Name: "最终目标 - MongoDB",
Description: "攻陷 MongoDB 获取最终 flag,完成整个网络渗透",
Difficulty: "Expert",
Points: 500,
Flag: "FSCAN_LAB{y0u_pwn3d_th3_n3tw0rk}",
Hints: []string{"从 MySQL 获取 MongoDB 凭证", "连接 10.10.4.43", "查询 admin_secrets 集合"},
Network: "core",
Targets: []string{"10.10.4.43"},
Order: 13, // 最终目标
},
{
ID: 8,
Name: "Elasticsearch 情报收集",
Description: "利用 Elasticsearch 未授权访问获取生产网敏感信息",
Difficulty: "Medium",
Points: 200,
Flag: "FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d}",
Hints: []string{"扫描生产网 9200 端口", "Elasticsearch 默认无认证", "GET /_cat/indices 查看索引"},
Network: "production",
Targets: []string{"10.10.3.34"},
Order: 9, // 生产网情报收集
},
{
ID: 9,
Name: "PostgreSQL 数据库渗透",
Description: "爆破 PostgreSQL 数据库获取业务数据",
Difficulty: "Hard",
Points: 300,
Flag: "FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d}",
Hints: []string{"扫描核心网 5432 端口", "尝试 postgres/postgres123", "SELECT * FROM business.secrets"},
Network: "core",
Targets: []string{"10.10.4.42"},
Order: 11, // 核心网数据库探索
},
{
ID: 10,
Name: "MSSQL 数据库攻击",
Description: "攻破 MSSQL 数据库获取企业核心数据",
Difficulty: "Hard",
Points: 300,
Flag: "FSCAN_LAB{mssql_s4_4cc0unt_pwn3d}",
Hints: []string{"扫描核心网 1433 端口", "尝试 sa/P@ssword123", "SELECT * FROM master.dbo.secrets"},
Network: "core",
Targets: []string{"10.10.4.41"},
Order: 12, // 核心网数据库探索
},
{
ID: 11,
Name: "VNC 远程桌面入侵",
Description: "通过 VNC 弱密码获取办公网主机控制权",
Difficulty: "Medium",
Points: 200,
Flag: "FSCAN_LAB{vnc_r3m0t3_d3skt0p_pwn3d}",
Hints: []string{"扫描办公网 5901 端口", "VNC 密码: password", "flag 在桌面 flag11.txt"},
Network: "office",
Targets: []string{"10.10.2.20"},
Order: 5, // 办公网探索
},
{
ID: 12,
Name: "老旧 Telnet 服务",
Description: "利用古老的 Telnet 服务获取办公网老旧主机访问权",
Difficulty: "Easy",
Points: 150,
Flag: "FSCAN_LAB{t3ln3t_l3g4cy_syst3m}",
Hints: []string{"扫描办公网 23 端口", "尝试 admin/admin", "cat /root/flag12.txt"},
Network: "office",
Targets: []string{"10.10.2.24"},
Order: 4, // 办公网探索
},
{
ID: 13,
Name: "打印机 SMB 共享",
Description: "发现办公网打印机的 SMB 共享服务,通过弱密码访问共享文件",
Difficulty: "Medium",
Points: 200,
Flag: "FSCAN_LAB{smb_pr1nt3r_sh4r3_pwn3d}",
Hints: []string{"扫描办公网 445 端口 (SMB)", "用户名: printer, 尝试弱密码爆破", "共享名: print$ 或 backup"},
Network: "office",
Targets: []string{"10.10.2.23"},
Order: 6, // 办公网探索
},
}
// networkTopology contains the network structure for visualization
var networkTopology = NetworkTopology{
Nodes: []NetworkNode{
{ID: "internet", Name: "Internet", IP: "172.16.0.0/24", Services: []string{}, Network: "internet", Status: "discovered"},
{ID: "attacker", Name: "Attacker", IP: "172.16.0.2", Services: []string{"fscan"}, Network: "internet", Status: "compromised"},
{ID: "web-dmz", Name: "Web DMZ", IP: "10.10.1.10", Services: []string{"Tomcat:8080"}, Network: "dmz", Status: "unknown"},
{ID: "mail-dmz", Name: "Mail DMZ", IP: "10.10.1.11", Services: []string{"SMTP:25"}, Network: "dmz", Status: "unknown"},
{ID: "ftp-dmz", Name: "FTP DMZ", IP: "10.10.1.12", Services: []string{"FTP:21"}, Network: "dmz", Status: "unknown"},
{ID: "vpn-gateway", Name: "VPN Gateway", IP: "10.10.1.13/10.10.2.2", Services: []string{"SSH:22"}, Network: "dmz", Status: "unknown"},
{ID: "pc-vnc", Name: "PC VNC", IP: "10.10.2.20", Services: []string{"VNC:5901"}, Network: "office", Status: "unknown"},
{ID: "pc-ssh", Name: "PC SSH", IP: "10.10.2.21", Services: []string{"SSH:22"}, Network: "office", Status: "unknown"},
{ID: "backup-server", Name: "Backup Server", IP: "10.10.2.22", Services: []string{"Rsync:873"}, Network: "office", Status: "unknown"},
{ID: "printer", Name: "Printer", IP: "10.10.2.23", Services: []string{"SMB:445"}, Network: "office", Status: "unknown"},
{ID: "oldpc-telnet", Name: "Old PC", IP: "10.10.2.24", Services: []string{"Telnet:23"}, Network: "office", Status: "unknown"},
{ID: "app-web", Name: "App Web", IP: "10.10.3.30", Services: []string{"Tomcat:8080"}, Network: "production", Status: "unknown"},
{ID: "cache-redis", Name: "Cache Redis", IP: "10.10.3.31", Services: []string{"Redis:6379"}, Network: "production", Status: "unknown"},
{ID: "mq-rabbit", Name: "RabbitMQ", IP: "10.10.3.32", Services: []string{"RabbitMQ:5672,15672"}, Network: "production", Status: "unknown"},
{ID: "mq-activemq", Name: "ActiveMQ", IP: "10.10.3.33", Services: []string{"ActiveMQ:61613,61614"}, Network: "production", Status: "unknown"},
{ID: "search-es", Name: "Elasticsearch", IP: "10.10.3.34", Services: []string{"ES:9200"}, Network: "production", Status: "unknown"},
{ID: "db-mysql", Name: "MySQL DB", IP: "10.10.4.40", Services: []string{"MySQL:3306"}, Network: "core", Status: "unknown"},
{ID: "db-mssql", Name: "MSSQL DB", IP: "10.10.4.41", Services: []string{"MSSQL:1433"}, Network: "core", Status: "unknown"},
{ID: "db-postgres", Name: "PostgreSQL DB", IP: "10.10.4.42", Services: []string{"PostgreSQL:5432"}, Network: "core", Status: "unknown"},
{ID: "db-mongo", Name: "MongoDB", IP: "10.10.4.43", Services: []string{"MongoDB:27017"}, Network: "core", Status: "unknown"},
{ID: "dc-ldap", Name: "Domain Controller", IP: "10.10.4.44", Services: []string{"LDAP:389,636"}, Network: "core", Status: "unknown"},
},
Edges: []NetworkEdge{
{From: "attacker", To: "web-dmz", Access: "allowed"},
{From: "attacker", To: "mail-dmz", Access: "allowed"},
{From: "attacker", To: "ftp-dmz", Access: "allowed"},
{From: "attacker", To: "vpn-gateway", Access: "allowed"},
{From: "vpn-gateway", To: "pc-vnc", Access: "vpn"},
{From: "vpn-gateway", To: "pc-ssh", Access: "vpn"},
{From: "vpn-gateway", To: "backup-server", Access: "vpn"},
{From: "vpn-gateway", To: "printer", Access: "vpn"},
{From: "vpn-gateway", To: "oldpc-telnet", Access: "vpn"},
{From: "backup-server", To: "cache-redis", Access: "allowed"},
{From: "cache-redis", To: "db-mysql", Access: "allowed"},
{From: "cache-redis", To: "db-mssql", Access: "allowed"},
{From: "cache-redis", To: "db-postgres", Access: "allowed"},
{From: "cache-redis", To: "db-mongo", Access: "allowed"},
{From: "cache-redis", To: "dc-ldap", Access: "allowed"},
},
}
// challengeToNodes maps challenge IDs to compromised node IDs
var challengeToNodes = map[int][]string{
1: {"web-dmz"}, // Flag 1: DMZ Web 服务器
2: {"ftp-dmz"}, // Flag 2: FTP 弱密码
3: {"vpn-gateway"}, // Flag 3: VPN 网关
4: {"backup-server"}, // Flag 4: 备份服务器
5: {"cache-redis"}, // Flag 5: Redis
6: {"db-mysql"}, // Flag 6: MySQL
7: {"db-mongo"}, // Flag 7: MongoDB (最终目标)
8: {"search-es"}, // Flag 8: Elasticsearch
9: {"db-postgres"}, // Flag 9: PostgreSQL
10: {"db-mssql"}, // Flag 10: MSSQL
11: {"pc-vnc"}, // Flag 11: VNC
12: {"oldpc-telnet"}, // Flag 12: Telnet
13: {"printer"}, // Flag 13: SNMP
}
// challengeMap provides O(1) lookup by challenge ID
var challengeMap map[int]*Challenge
func init() {
challengeMap = make(map[int]*Challenge, len(challenges))
for i := range challenges {
challengeMap[challenges[i].ID] = &challenges[i]
}
}
// getChallengeByID returns a challenge by ID or error if not found
func getChallengeByID(id int) (*Challenge, error) {
ch, ok := challengeMap[id]
if !ok {
return nil, fmt.Errorf("challenge not found")
}
return ch, nil
}
// contains checks if a slice contains a value
func contains(slice []int, val int) bool {
for _, v := range slice {
if v == val {
return true
}
}
return false
}
const progressFile = "/app/data/progress.json"
func loadProgress() (*Progress, error) {
data, err := os.ReadFile(progressFile)
if err != nil {
if os.IsNotExist(err) {
return &Progress{
UserID: "default",
CompletedChallenges: []int{},
TotalScore: 0,
StartTime: time.Now(),
LastUpdate: time.Now(),
SubmissionHistory: []Submission{},
}, nil
}
return nil, err
}
var progress Progress
if err := json.Unmarshal(data, &progress); err != nil {
return nil, err
}
return &progress, nil
}
func saveProgress(progress *Progress) error {
progress.LastUpdate = time.Now()
data, err := json.MarshalIndent(progress, "", " ")
if err != nil {
return err
}
return os.WriteFile(progressFile, data, 0644)
}
func main() {
os.MkdirAll("/app/data", 0755)
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
r.GET("/api/challenges", getChallenges)
r.GET("/api/challenges/:id", getChallenge)
r.POST("/api/submit", submitFlag)
r.GET("/api/progress", getProgress)
r.POST("/api/reset", resetProgress)
r.GET("/api/topology", getTopology)
r.GET("/api/hints/:id", getHints)
log.Println("Starting fscan-lab API server on :8888")
r.Run(":8888")
}
func getChallenges(c *gin.Context) {
publicChallenges := make([]map[string]interface{}, len(challenges))
for i, ch := range challenges {
publicChallenges[i] = map[string]interface{}{
"id": ch.ID,
"name": ch.Name,
"description": ch.Description,
"difficulty": ch.Difficulty,
"points": ch.Points,
"network": ch.Network,
"targets": ch.Targets,
"order": ch.Order, // 渗透顺序
}
}
c.JSON(http.StatusOK, publicChallenges)
}
func getChallenge(c *gin.Context) {
idStr := c.Param("id")
var id int
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid challenge ID"})
return
}
challenge, err := getChallengeByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"id": challenge.ID,
"name": challenge.Name,
"description": challenge.Description,
"difficulty": challenge.Difficulty,
"points": challenge.Points,
"network": challenge.Network,
"targets": challenge.Targets,
})
}
func submitFlag(c *gin.Context) {
var req struct {
ChallengeID int `json:"challenge_id"`
Flag string `json:"flag"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
challenge, err := getChallengeByID(req.ChallengeID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
progress, err := loadProgress()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load progress"})
return
}
correct := strings.TrimSpace(req.Flag) == strings.TrimSpace(challenge.Flag)
alreadySolved := contains(progress.CompletedChallenges, req.ChallengeID)
// Record submission
progress.SubmissionHistory = append(progress.SubmissionHistory, Submission{
ChallengeID: req.ChallengeID,
Flag: req.Flag,
Correct: correct,
Timestamp: time.Now(),
})
// Award points for first-time completion
if correct && !alreadySolved {
progress.CompletedChallenges = append(progress.CompletedChallenges, req.ChallengeID)
progress.TotalScore += challenge.Points
}
if err := saveProgress(progress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save progress"})
return
}
c.JSON(http.StatusOK, gin.H{
"correct": correct,
"message": map[bool]string{true: "Congratulations! Flag accepted!", false: "Incorrect flag. Try again!"}[correct],
"points_earned": challenge.Points,
"total_score": progress.TotalScore,
"already_solved": alreadySolved,
})
}
func getProgress(c *gin.Context) {
progress, err := loadProgress()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load progress"})
return
}
c.JSON(http.StatusOK, progress)
}
func resetProgress(c *gin.Context) {
progress := &Progress{
UserID: "default",
CompletedChallenges: []int{},
TotalScore: 0,
StartTime: time.Now(),
LastUpdate: time.Now(),
SubmissionHistory: []Submission{},
}
if err := saveProgress(progress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reset progress"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Progress reset successfully"})
}
func getTopology(c *gin.Context) {
progress, err := loadProgress()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load progress"})
return
}
// Build set of compromised nodes based on completed challenges
compromisedNodes := make(map[string]bool)
for _, challengeID := range progress.CompletedChallenges {
if nodeIDs, ok := challengeToNodes[challengeID]; ok {
for _, nodeID := range nodeIDs {
compromisedNodes[nodeID] = true
}
}
}
// Clone topology and update node statuses
topology := NetworkTopology{
Nodes: make([]NetworkNode, len(networkTopology.Nodes)),
Edges: networkTopology.Edges,
}
for i, node := range networkTopology.Nodes {
topology.Nodes[i] = node
// Update status based on progress
if compromisedNodes[node.ID] {
topology.Nodes[i].Status = "compromised"
}
}
c.JSON(http.StatusOK, topology)
}
func getHints(c *gin.Context) {
idStr := c.Param("id")
var id int
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid challenge ID"})
return
}
challenge, err := getChallengeByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"hints": challenge.Hints})
}
+382
View File
@@ -0,0 +1,382 @@
services:
# ============================================
# 防火墙路由容器(核心网络控制)
# ============================================
firewall:
build: ./docker/firewall
container_name: lab-firewall
hostname: firewall
cap_add:
- NET_ADMIN
sysctls:
- net.ipv4.ip_forward=1
networks:
internet:
ipv4_address: 172.16.0.1
dmz:
ipv4_address: 10.10.1.1
office:
ipv4_address: 10.10.2.1
production:
ipv4_address: 10.10.3.1
core:
ipv4_address: 10.10.4.1
restart: unless-stopped
# ============================================
# 攻击者机器(起点)
# ============================================
attacker:
build: ./docker/attacker
container_name: lab-attacker
hostname: attacker
cap_add:
- NET_ADMIN
networks:
internet:
ipv4_address: 172.16.0.2
dmz:
ipv4_address: 10.10.1.2
volumes:
- ./flags:/root/flags
stdin_open: true
tty: true
restart: unless-stopped
# ============================================
# DMZ 区(4台主机)
# ============================================
web-dmz:
build: ./test-services/Tomcat
container_name: lab-web-dmz
hostname: web-dmz
networks:
dmz:
ipv4_address: 10.10.1.10
volumes:
- ./flags/flag1.txt:/usr/local/tomcat/webapps/ROOT/flag1.txt:ro
restart: unless-stopped
mail-dmz:
build: ./test-services/SMTP
container_name: lab-mail-dmz
hostname: mail-dmz
networks:
dmz:
ipv4_address: 10.10.1.11
restart: unless-stopped
ftp-dmz:
image: bogem/ftp
container_name: lab-ftp-dmz
hostname: ftp-dmz
environment:
- FTP_USER=admin
- FTP_PASS=123456
- PASV_ADDRESS=10.10.1.12
networks:
dmz:
ipv4_address: 10.10.1.12
volumes:
- ./flags/flag2.txt:/home/vsftpd/flag2.txt:ro
- ./flags/office_key:/home/vsftpd/.ssh/office_key:ro
- ./flags/clues/dmz_clue.txt:/home/vsftpd/next_step.txt:ro
restart: unless-stopped
vpn-gateway:
build: ./test-services/SSH
container_name: lab-vpn-gateway
hostname: vpn-gateway
networks:
dmz:
ipv4_address: 10.10.1.13
office:
ipv4_address: 10.10.2.2
volumes:
- ./flags/flag3.txt:/etc/flag3.txt:ro
- ./docker/services/dmz/vpn-gateway/authorized_keys:/root/.ssh/authorized_keys
- ./docker/services/dmz/vpn-gateway/frps.ini:/etc/frp/frps.ini:ro
- ./docker/services/dmz/vpn-gateway/start-frps.sh:/usr/local/bin/start-frps:ro
restart: unless-stopped
# ============================================
# 办公网(5台主机)
# ============================================
pc-vnc:
build: ./test-services/VNC
container_name: lab-pc-vnc
hostname: pc-vnc
networks:
office:
ipv4_address: 10.10.2.20
environment:
- VNC_PASSWORD=password
volumes:
- ./flags/flag11.txt:/root/Desktop/flag11.txt:ro
restart: unless-stopped
pc-ssh:
build: ./test-services/SSH
container_name: lab-pc-ssh
hostname: pc-ssh
networks:
office:
ipv4_address: 10.10.2.21
volumes:
- ./docker/services/office/pc-ssh/.bash_history:/root/.bash_history:ro
restart: unless-stopped
backup-server:
build: ./test-services/Rsync
container_name: lab-backup-server
hostname: backup-server
networks:
office:
ipv4_address: 10.10.2.22
volumes:
- ./flags/flag4.txt:/data/backup/credentials/flag4.txt:ro
- ./flags/clues/prod_redis.conf:/data/backup/credentials/prod_redis.conf:ro
- ./flags/clues/prod_hosts.txt:/data/backup/credentials/prod_hosts.txt:ro
- ./flags/prod_jump_key:/data/backup/credentials/ssh_keys/prod_jump_key:ro
restart: unless-stopped
printer:
build: ./test-services/SMB
container_name: lab-printer
hostname: printer
networks:
office:
ipv4_address: 10.10.2.23
volumes:
- ./flags/flag13.txt:/shared/documents/flag13.txt:ro
- ./flags/clues/printer_info.txt:/shared/documents/printer_info.txt:ro
restart: unless-stopped
oldpc-telnet:
build: ./test-services/Telnet
container_name: lab-oldpc-telnet
hostname: oldpc-telnet
networks:
office:
ipv4_address: 10.10.2.24
volumes:
- ./flags/flag12.txt:/root/flag12.txt:ro
restart: unless-stopped
# ============================================
# 生产网(5台主机)
# ============================================
app-web:
build: ./test-services/Tomcat
container_name: lab-app-web
hostname: app-web
networks:
production:
ipv4_address: 10.10.3.30
restart: unless-stopped
cache-redis:
build: ./test-services/Redis
container_name: lab-cache-redis
hostname: cache-redis
networks:
production:
ipv4_address: 10.10.3.31
command: redis-server --bind 0.0.0.0 --protected-mode no --requirepass redis123
volumes:
- ./docker/services/production/redis/init.sh:/docker-entrypoint-initdb.d/init.sh:ro
restart: unless-stopped
mq-rabbit:
image: rabbitmq:3-management
container_name: lab-mq-rabbit
hostname: mq-rabbit
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: rabbit123
networks:
production:
ipv4_address: 10.10.3.32
restart: unless-stopped
mq-activemq:
build: ./test-services/ActiveMQ
container_name: lab-mq-activemq
hostname: mq-activemq
networks:
production:
ipv4_address: 10.10.3.33
restart: unless-stopped
search-es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.3
container_name: lab-search-es
hostname: search-es
environment:
- discovery.type=single-node
- xpack.security.enabled=false
networks:
production:
ipv4_address: 10.10.3.34
volumes:
- ./docker/services/production/elasticsearch/init-es.sh:/usr/local/bin/init-es.sh:ro
restart: unless-stopped
# ============================================
# 核心网(5台主机)
# ============================================
db-mysql:
image: mysql:latest
container_name: lab-db-mysql
hostname: db-mysql
environment:
MYSQL_ROOT_PASSWORD: Password
MYSQL_DATABASE: secrets
networks:
core:
ipv4_address: 10.10.4.40
volumes:
- ./docker/services/core/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
restart: unless-stopped
db-mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: lab-db-mssql
hostname: db-mssql
environment:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: P@ssword123
MSSQL_PID: Express
networks:
core:
ipv4_address: 10.10.4.41
volumes:
- ./docker/services/core/mssql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
restart: unless-stopped
db-postgres:
image: postgres:latest
container_name: lab-db-postgres
hostname: db-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres123
POSTGRES_DB: business
networks:
core:
ipv4_address: 10.10.4.42
volumes:
- ./docker/services/core/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
restart: unless-stopped
db-mongo:
image: mongo:latest
container_name: lab-db-mongo
hostname: db-mongo
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: mongo123
networks:
core:
ipv4_address: 10.10.4.43
volumes:
- ./docker/services/core/mongo/init.js:/docker-entrypoint-initdb.d/init.js:ro
restart: unless-stopped
dc-ldap:
build: ./test-services/LDAP
container_name: lab-dc-ldap
hostname: dc-ldap
environment:
LDAP_ORGANISATION: "TargetCorp"
LDAP_DOMAIN: "target.corp"
LDAP_BASE_DN: "dc=target,dc=corp"
LDAP_ADMIN_PASSWORD: "Admin123"
networks:
core:
ipv4_address: 10.10.4.44
restart: unless-stopped
# ============================================
# 后端 API(flag 验证和进度管理)
# ============================================
lab-api:
build: ./backend
container_name: lab-api
hostname: lab-api
networks:
internet:
ipv4_address: 172.16.0.100
ports:
- "8888:8888"
volumes:
- ./flags:/app/flags:ro
- ./backend/data:/app/data
restart: unless-stopped
# ============================================
# 前端 Web UI
# ============================================
lab-web:
build: ./frontend
container_name: lab-web
hostname: lab-web
networks:
internet:
ipv4_address: 172.16.0.101
ports:
- "3000:3000"
environment:
- VITE_API_URL=http://172.16.0.100:8888
restart: unless-stopped
# ============================================
# 网络定义(5层网络架构)
# ============================================
networks:
internet:
driver: bridge
ipam:
config:
- subnet: 172.16.0.0/24
gateway: 172.16.0.254
dmz:
driver: bridge
ipam:
config:
- subnet: 10.10.1.0/24
gateway: 10.10.1.254
office:
driver: bridge
internal: true # 无直接外网访问
ipam:
config:
- subnet: 10.10.2.0/24
gateway: 10.10.2.254
production:
driver: bridge
internal: true
ipam:
config:
- subnet: 10.10.3.0/24
gateway: 10.10.3.254
core:
driver: bridge
internal: true
ipam:
config:
- subnet: 10.10.4.0/24
gateway: 10.10.4.254
# ============================================
# 数据卷
# ============================================
volumes:
mysql_data:
postgres_data:
mongo_data:
redis_data:
+36
View File
@@ -0,0 +1,36 @@
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
# 安装基本工具(分批安装避免超时)
RUN apt-get update && \
apt-get install -y curl wget vim iputils-ping net-tools && \
apt-get install -y openssh-client ftp rsync telnet && \
apt-get install -y redis-tools mysql-client postgresql-client && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# 创建工作目录
WORKDIR /root
# 复制 fscan 二进制(需要用户提前编译)
COPY fscan /usr/local/bin/fscan
RUN chmod +x /usr/local/bin/fscan
# 下载并安装 frp 客户端 (v0.65.0)
RUN wget -q https://github.com/fatedier/frp/releases/download/v0.65.0/frp_0.65.0_linux_amd64.tar.gz && \
tar -xzf frp_0.65.0_linux_amd64.tar.gz && \
mv frp_0.65.0_linux_amd64/frpc /usr/local/bin/frpc && \
chmod +x /usr/local/bin/frpc && \
rm -rf frp_0.65.0_linux_amd64*
# 创建工具目录
RUN mkdir -p /root/tools /root/loot /root/scripts /etc/frp
# 复制 frp 客户端配置
COPY frpc.ini /etc/frp/frpc.ini
# 欢迎信息
COPY welcome.sh /root/.bashrc
RUN echo 'export PS1="\[\033[01;31m\]attacker\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# "' >> /root/.bashrc
CMD ["/bin/bash"]
+18
View File
@@ -0,0 +1,18 @@
# FRP 客户端配置
# 用于通过 VPN 网关建立 SOCKS5 代理隧道
[common]
server_addr = 10.10.1.13
server_port = 7000
# 连接超时 (秒)
dial_server_timeout = 10
# SOCKS5 代理插件
# 在本地 1080 端口监听,通过 frps 转发流量到 VPN 网关
[socks5]
type = tcp
remote_port = 1080
plugin = socks5
# 不需要认证(内网环境)
plugin_user =
plugin_passwd =
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
cat << "EOF"
╔═══════════════════════════════════════════════════════╗
║ ║
║ ╔═╗╔═╗╔═╗╔═╗╔╗╔ ╦ ╔═╗╔╗ ║
║ ╠╣ ╚═╗║ ╠═╣║║║───║ ╠═╣╠╩╗ ║
║ ╚ ╚═╝╚═╝╩ ╩╝╚╝ ╩═╝╩ ╩╚═╝ ║
║ ║
║ 内网渗透测试训练平台 ║
║ Network Penetration Lab ║
║ ║
╚═══════════════════════════════════════════════════════╝
[*] 当前位置:外网 (172.16.0.2)
[*] 目标网络:10.10.0.0/16
[*] 任务:渗透内网,获取所有 7 个 flag
[*] 可用工具:
- fscan (内网扫描工具)
- nmap (端口扫描)
- ssh/ftp (远程连接)
- rsync (文件同步)
- redis-cli (Redis 客户端)
- mysql (MySQL 客户端)
[*] 提示:
1. 从扫描 DMZ 区开始 (10.10.1.0/24)
2. 寻找弱密码和配置错误
3. 利用获取的凭证进行横向移动
4. 阅读 /root/docs/ 中的文档获取帮助
[*] 第一个命令:
fscan -h 10.10.1.0/24
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
export PS1="\[\033[01;31m\]attacker\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# "
+12
View File
@@ -0,0 +1,12 @@
FROM alpine:latest
RUN apk add --no-cache \
iptables \
ip6tables \
bash \
iproute2
COPY firewall.sh /firewall.sh
RUN chmod +x /firewall.sh
CMD ["/firewall.sh"]
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
set -e
echo "[*] Starting firewall configuration..."
# 启用 IP 转发 (如果Docker已设置则跳过)
if echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null; then
echo "[+] IP forwarding enabled"
else
echo "[!] IP forwarding already enabled by Docker"
fi
# 清空所有规则
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
echo "[+] Cleared existing rules"
# 默认策略:FORWARD 拒绝,INPUT/OUTPUT 允许
iptables -P FORWARD DROP
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
echo "[+] Set default policies"
# ============================================
# 网络定义
# ============================================
INTERNET="172.16.0.0/24"
DMZ="10.10.1.0/24"
OFFICE="10.10.2.0/24"
PRODUCTION="10.10.3.0/24"
CORE="10.10.4.0/24"
VPN_GATEWAY="10.10.1.13" # VPN 网关(双网卡)
# ============================================
# 规则 1:外网 -> DMZ(允许)
# ============================================
iptables -A FORWARD -s $INTERNET -d $DMZ -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -s $DMZ -d $INTERNET -m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[+] Rule 1: Internet -> DMZ allowed"
# ============================================
# 规则 2:外网 -> 内网(拒绝)
# ============================================
iptables -A FORWARD -s $INTERNET -d $OFFICE -j DROP
iptables -A FORWARD -s $INTERNET -d $PRODUCTION -j DROP
iptables -A FORWARD -s $INTERNET -d $CORE -j DROP
echo "[+] Rule 2: Internet -> Internal networks blocked"
# ============================================
# 规则 3DMZ -> 办公网(只允许 VPN 网关)
# ============================================
# VPN 网关本身有两个网卡,自动有路由,这里允许数据包转发
iptables -A FORWARD -s $DMZ -d $OFFICE -j ACCEPT
iptables -A FORWARD -s $OFFICE -d $DMZ -j ACCEPT
echo "[+] Rule 3: DMZ <-> Office (via VPN gateway)"
# ============================================
# 规则 4:办公网 -> 生产网(允许,但限制)
# ============================================
# 只允许特定端口(SSH, Redis, RabbitMQ, ActiveMQ, ES
iptables -A FORWARD -s $OFFICE -d $PRODUCTION -p tcp -m multiport --dports 22,6379,5672,15672,61613,61614,9200,8080 -j ACCEPT
iptables -A FORWARD -s $PRODUCTION -d $OFFICE -m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[+] Rule 4: Office -> Production (limited ports)"
# ============================================
# 规则 5:生产网 -> 核心网(允许)
# ============================================
iptables -A FORWARD -s $PRODUCTION -d $CORE -j ACCEPT
iptables -A FORWARD -s $CORE -d $PRODUCTION -m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[+] Rule 5: Production <-> Core allowed"
# ============================================
# 规则 6:办公网 -> 核心网(拒绝,必须通过生产网)
# ============================================
iptables -A FORWARD -s $OFFICE -d $CORE -j DROP
echo "[+] Rule 6: Office -> Core blocked (must go through Production)"
# ============================================
# 规则 7:允许同网段内部通信
# ============================================
iptables -A FORWARD -s $DMZ -d $DMZ -j ACCEPT
iptables -A FORWARD -s $OFFICE -d $OFFICE -j ACCEPT
iptables -A FORWARD -s $PRODUCTION -d $PRODUCTION -j ACCEPT
iptables -A FORWARD -s $CORE -d $CORE -j ACCEPT
echo "[+] Rule 7: Intra-network communication allowed"
# ============================================
# 日志规则(调试用)
# ============================================
# iptables -A FORWARD -j LOG --log-prefix "FW-DROP: " --log-level 4
# ============================================
# 显示规则
# ============================================
echo ""
echo "============================================"
echo "Firewall Rules Summary:"
echo "============================================"
iptables -L FORWARD -n -v --line-numbers
echo ""
echo "[*] Firewall configured successfully!"
echo "[*] Network topology:"
echo " Internet (172.16.0.0/24)"
echo " └─> DMZ (10.10.1.0/24)"
echo " └─> Office (10.10.2.0/24)"
echo " └─> Production (10.10.3.0/24)"
echo " └─> Core (10.10.4.0/24)"
echo ""
# 保持容器运行
tail -f /dev/null
@@ -0,0 +1,21 @@
db = db.getSiblingDB('admin');
db.createCollection('admin_secrets');
db.admin_secrets.insert({
type: 'flag',
value: 'FSCAN_LAB{y0u_pwn3d_th3_n3tw0rk}',
description: 'Final Flag - Congratulations! You have successfully penetrated the entire network!',
achievement: 'Network Penetration Master',
timestamp: new Date()
});
db.admin_secrets.insert({
type: 'credentials',
service: 'root_access',
username: 'root',
password: 'RootP@ss2024',
notes: 'Full system access - game over!'
});
print('MongoDB initialized with final flag');
@@ -0,0 +1,36 @@
-- MSSQL 初始化脚本
-- 创建 secrets 表并插入 flag10
USE master;
GO
CREATE TABLE dbo.secrets (
id INT IDENTITY(1,1) PRIMARY KEY,
key_name NVARCHAR(255) NOT NULL,
key_value NVARCHAR(MAX) NOT NULL,
created_at DATETIME DEFAULT GETDATE()
);
GO
-- 插入 flag10
INSERT INTO dbo.secrets (key_name, key_value) VALUES
('flag10', 'FSCAN_LAB{mssql_s4_4cc0unt_pwn3d}'),
('db_version', 'Microsoft SQL Server 2022'),
('admin_account', 'sa'),
('production_db', '10.10.3.31');
GO
-- 创建业务数据表
CREATE TABLE dbo.employees (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100),
position NVARCHAR(100),
salary DECIMAL(10,2)
);
GO
INSERT INTO dbo.employees (name, position, salary) VALUES
('David', 'Manager', 95000.00),
('Eve', 'Developer', 80000.00),
('Frank', 'Designer', 75000.00);
GO
@@ -0,0 +1,25 @@
CREATE DATABASE IF NOT EXISTS secrets;
USE secrets;
CREATE TABLE IF NOT EXISTS flags (
id INT PRIMARY KEY,
flag VARCHAR(255) NOT NULL,
description VARCHAR(255)
);
CREATE TABLE IF NOT EXISTS credentials (
id INT PRIMARY KEY AUTO_INCREMENT,
service VARCHAR(50),
username VARCHAR(100),
password VARCHAR(100),
notes TEXT
);
INSERT INTO flags (id, flag, description) VALUES
(1, 'FSCAN_LAB{mysql_d4t4b4s3_pwn3d}', 'Flag 6 - MySQL Database'),
(2, 'Hint: LDAP admin credentials are admin:LdapAdmin2024', 'LDAP Hint');
INSERT INTO credentials (service, username, password, notes) VALUES
('ldap', 'admin', 'LdapAdmin2024', 'Domain controller admin account'),
('mongodb', 'admin', 'mongo123', 'MongoDB root password'),
('backup', 'backup_user', 'Backup@2024', 'Backup system credentials');
@@ -0,0 +1,29 @@
-- PostgreSQL 初始化脚本
-- 创建 secrets 表并插入 flag9
CREATE TABLE IF NOT EXISTS secrets (
id SERIAL PRIMARY KEY,
key VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入 flag9
INSERT INTO secrets (key, value) VALUES
('flag9', 'FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d}'),
('db_type', 'PostgreSQL 15'),
('admin_email', '[email protected]'),
('backup_server', '10.10.2.22');
-- 创建业务数据表
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(100),
email VARCHAR(255),
department VARCHAR(100)
);
INSERT INTO users (username, email, department) VALUES
('alice', '[email protected]', 'Engineering'),
('bob', '[email protected]', 'Sales'),
('charlie', '[email protected]', 'HR');
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdmc1l6L2DsowOUp+IxkDhtHgSAGYXretgoQNcS6S4uZLx51Tw7uzXWoSH0RjY/VJaT6PZ/W3g8tYcTsUegq/Fa2yCgLLQB9l1tJ1SoT/UvbdIolynMTY02vVVLEO9mMxkS3TKIXdrwIcuw6J+4ON5SbZ2WitPwD3fiT0vDlFNlnw5SDHN/8mJgIPOU4WXSEZBgovG6ML1rcfd/MCk8pEi22TMS3xi9Q99wjAGRrNHyYICpeUokP13Q/f881en4aA1Wc88F+GXi/Ql+ySLbDgGl1WQhyCj9uk0JiFPzg6rccdDLaIunTT9beQF1WyXCPNZWMO8XddHfr7sRkzgvSsx office_vpn_key
@@ -0,0 +1,23 @@
# FRP 服务端配置
# VPN 网关上运行,用于建立代理隧道到办公网
[common]
# 监听端口(客户端连接端口)
bind_port = 7000
# Dashboard 配置(可选,用于查看连接状态)
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = fscan_lab_frp
# 日志配置
log_file = /var/log/frps.log
log_level = info
log_max_days = 3
# 性能配置
max_pool_count = 50
max_ports_per_client = 0
# 认证(这里不启用,因为是内网测试环境)
# token = your_secret_token
@@ -0,0 +1,16 @@
#!/bin/bash
# FRP 服务端启动脚本
# 在后台启动 frps 服务
echo "[+] 启动 FRP 服务端..."
nohup /usr/local/bin/frps -c /etc/frp/frps.ini > /var/log/frps.log 2>&1 &
if [ $? -eq 0 ]; then
echo "[+] FRP 服务端已启动"
echo "[+] 监听端口: 7000"
echo "[+] Dashboard: http://10.10.1.13:7500 (admin/fscan_lab_frp)"
echo "[+] 日志文件: /var/log/frps.log"
else
echo "[-] FRP 服务端启动失败"
exit 1
fi
@@ -0,0 +1,8 @@
ls -la
cd /var/www
cat config.php
redis-cli -h 10.10.3.31 -a redis123
redis-cli -h 10.10.3.31 -a redis123 ping
ssh [email protected]
mysql -h 10.10.4.40 -u root -pPassword
exit
@@ -0,0 +1,42 @@
#!/bin/bash
# Elasticsearch 初始化脚本
# 等待 ES 启动后插入 flag8 数据
sleep 30 # 等待 Elasticsearch 完全启动
# 创建包含 flag8 的索引
curl -X PUT "localhost:9200/secrets" -H 'Content-Type: application/json' -d'
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}'
# 插入 flag8 文档
curl -X POST "localhost:9200/secrets/_doc/1" -H 'Content-Type: application/json' -d'
{
"flag": "FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d}",
"description": "Elasticsearch Unauthorized Access Flag",
"network": "production",
"service": "elasticsearch",
"timestamp": "2024-12-17T00:00:00Z"
}'
# 插入其他敏感数据
curl -X POST "localhost:9200/secrets/_doc/2" -H 'Content-Type: application/json' -d'
{
"db_host": "10.10.4.40",
"db_type": "MySQL",
"db_user": "root",
"db_hint": "Check backup server for password"
}'
curl -X POST "localhost:9200/secrets/_doc/3" -H 'Content-Type: application/json' -d'
{
"redis_host": "10.10.3.31",
"redis_password": "redis123",
"cache_type": "production"
}'
echo "Elasticsearch initialized with flag8"
@@ -0,0 +1,6 @@
#!/bin/bash
# 这个脚本在 Redis 启动后设置 flag
sleep 5
redis-cli -a redis123 SET flag5 "FSCAN_LAB{r3d1s_un4uth0r1z3d_4cc3ss}"
redis-cli -a redis123 SET hint "Check /root/.ssh for SSH keys to core network"
echo "Redis flag initialized"
+30
View File
@@ -0,0 +1,30 @@
====================================
下一步行动指引
====================================
你已经成功进入 DMZ 区的 FTP 服务器!
发现的关键文件:
1. SSH 私钥:/home/admin/.ssh/office_key
2. 该私钥可以访问 VPN 网关 (10.10.1.13)
VPN 网关是双网卡主机:
- DMZ 侧:10.10.1.13
- 办公网侧:10.10.2.2
攻击路径:
1. 下载 SSH 私钥
2. 使用私钥连接 VPN 网关
3. 从 VPN 网关扫描办公网
命令示例:
# 下载私钥(已在当前目录)
chmod 600 office_key
# 连接 VPN 网关
ssh -i office_key [email protected]
# 扫描办公网
fscan -h 10.10.2.0/24
加油!
+17
View File
@@ -0,0 +1,17 @@
====================================
办公网打印机配置信息
====================================
设备型号: HP LaserJet Pro M404n
IP地址: 10.10.2.23
SMB共享: 已启用
管理员账户:
- 用户名: printer
- 密码: [已保存在密码管理器]
共享文件夹:
- print$ (打印机驱动)
- backup (打印任务备份)
注意: 为了方便管理,使用了简单密码
+27
View File
@@ -0,0 +1,27 @@
====================================
生产网主机清单
====================================
网段:10.10.3.0/24
已知主机:
10.10.3.30 - app-web (Tomcat 应用服务器)
10.10.3.31 - cache-redis (Redis 缓存,密码:redis123)
10.10.3.32 - mq-rabbit (RabbitMQ)
10.10.3.33 - mq-activemq (ActiveMQ)
10.10.3.34 - search-es (Elasticsearch)
重点目标:
★ Redis (10.10.3.31) - 已知密码,可能存在写入漏洞
防火墙策略:
- 办公网 -> 生产网:只允许特定端口
- 生产网 -> 核心网:全通(一旦进入生产网,可以访问核心网!)
核心网预览:
10.10.4.0/24 - 数据库集群和域控
策略建议:
1. 通过 Redis 获取立足点
2. 横向移动到其他生产主机
3. 扫描核心网寻找数据库
+18
View File
@@ -0,0 +1,18 @@
# Redis 配置文件(生产网备份)
bind 0.0.0.0
protected-mode no
port 6379
requirepass redis123
# 注意:此配置存在安全风险
# Redis 密码:redis123
# 主机:10.10.3.31
# 可能的攻击向量:
# 1. 使用密码认证后写入 SSH 公钥
# 2. 利用 Redis 执行 Lua 脚本
# 3. 配置文件写入攻击
# 从办公网连接:
# redis-cli -h 10.10.3.31 -a redis123
+15
View File
@@ -0,0 +1,15 @@
╔═══════════════════════════════════════════════════════╗
║ FLAG 1 - DMZ 侦察 ║
╚═══════════════════════════════════════════════════════╝
恭喜!你成功发现了 DMZ 区的 Web 服务器!
Flag: FSCAN_LAB{w3b_f1ng3rpr1nt_d1sc0v3ry}
下一步提示:
- DMZ 区还有其他服务(FTP、SMTP、SSH)
- FTP 服务器可能有弱密码
- 尝试爆破 FTP: admin/123456
fscan 命令示例:
fscan -h 10.10.1.12 -m ftp -user admin -pwd 123456
+9
View File
@@ -0,0 +1,9 @@
============================================
MSSQL 数据库攻击成功
============================================
恭喜!你成功攻破了 MSSQL 数据库。
Flag: FSCAN_LAB{mssql_s4_4cc0unt_pwn3d}
从 master.dbo.secrets 表中获取了核心数据。
+9
View File
@@ -0,0 +1,9 @@
============================================
VNC 远程桌面入侵成功
============================================
恭喜!你成功通过 VNC 弱密码获取了办公网主机的远程控制权。
Flag: FSCAN_LAB{vnc_r3m0t3_d3skt0p_pwn3d}
VNC 密码管理不当是常见的安全隐患...
+9
View File
@@ -0,0 +1,9 @@
============================================
老旧 Telnet 服务入侵成功
============================================
恭喜!你成功利用古老的 Telnet 服务获取了访问权限。
Flag: FSCAN_LAB{t3ln3t_l3g4cy_syst3m}
这台主机运行着过时的 Telnet 服务,属于遗留系统...
+1
View File
@@ -0,0 +1 @@
FSCAN_LAB{smb_pr1nt3r_sh4r3_pwn3d}
+20
View File
@@ -0,0 +1,20 @@
╔═══════════════════════════════════════════════════════╗
║ FLAG 2 - FTP 突破 ║
╚═══════════════════════════════════════════════════════╝
干得好!你通过 FTP 弱密码成功进入了 DMZ 区!
Flag: FSCAN_LAB{ftp_w34k_p4ssw0rd_pwn}
下一步提示:
- 查看当前目录的其他文件
- 注意 .ssh 目录中的私钥
- VPN 网关 (10.10.1.13) 是进入办公网的跳板
关键文件:
/home/admin/.ssh/office_key (办公网 SSH 私钥)
/home/admin/next_step.txt (下一步指引)
连接 VPN 网关:
chmod 600 office_key
ssh -i office_key [email protected]
+19
View File
@@ -0,0 +1,19 @@
╔═══════════════════════════════════════════════════════╗
║ FLAG 3 - VPN 网关 ║
╚═══════════════════════════════════════════════════════╝
出色!你已经通过 VPN 网关进入办公网!
Flag: FSCAN_LAB{vpn_g4t3w4y_br34ch3d}
当前网络:
- DMZ 接口: 10.10.1.13
- 办公网接口: 10.10.2.2
下一步提示:
- 从 VPN 网关扫描办公网 (10.10.2.0/24)
- 重点关注备份服务器 (Rsync 873 端口)
- Rsync 可能配置为未授权访问
扫描命令:
fscan -h 10.10.2.0/24 -p 22,873,5900,23,161
+20
View File
@@ -0,0 +1,20 @@
╔═══════════════════════════════════════════════════════╗
║ FLAG 4 - 办公网备份服务器 ║
╚═══════════════════════════════════════════════════════╝
太棒了!你找到了备份服务器的敏感数据!
Flag: FSCAN_LAB{rsync_b4ckup_l34k}
备份文件内容:
✓ prod_redis.conf - 生产网 Redis 配置
✓ prod_hosts.txt - 生产网主机列表
✓ ssh_keys/prod_jump_key - 生产网跳板私钥
下一步提示:
- 使用 Redis 密码连接生产网 Redis (10.10.3.31)
- Redis 可能启用了密码保护但有漏洞
- 利用 Redis 写入 SSH 公钥进行横向移动
获取备份文件:
rsync -av rsync://10.10.2.22/backup/credentials/ ./
+2
View File
@@ -0,0 +1,2 @@
FLAG 5 存储在 Redis 中,使用命令获取:
redis-cli -h 10.10.3.31 -a redis123 GET flag5
+2
View File
@@ -0,0 +1,2 @@
FLAG 6 存储在 MySQL 中,使用命令获取:
mysql -h 10.10.4.40 -u root -pPassword -e "SELECT flag FROM secrets.flags WHERE id=1;"
+2
View File
@@ -0,0 +1,2 @@
FLAG 7 (最终 Flag) 存储在 MongoDB 中,使用命令获取:
mongosh mongodb://admin:[email protected]/admin --eval "db.admin_secrets.find({type:'flag'}).pretty()"
+9
View File
@@ -0,0 +1,9 @@
============================================
Elasticsearch 情报收集成功
============================================
恭喜!你成功利用 Elasticsearch 未授权访问漏洞获取了生产网的敏感信息。
Flag: FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d}
提示:在生产环境的索引中发现了数据库凭证信息...
+9
View File
@@ -0,0 +1,9 @@
============================================
PostgreSQL 数据库渗透成功
============================================
恭喜!你成功爆破了 PostgreSQL 数据库密码。
Flag: FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d}
从 business.secrets 表中获取了企业核心业务数据。
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA3ZnNZei9g7KMDlKfiMZA4bR4EgBmF63rYKEDXEukuLmS8edU8O7s
11qEh9EY2P1SWk+j2f1t4PLWHE7FHoKvxWtsgoCy0AfZdbSdUqE/1L23SKJcpzE2NNr1VS
xDvZjMZEt0yiF3a8CHLsOifuDjeUm2dlorT8A934k9Lw5RTZZ8OUgxzf/JiYCDzlOFl0hG
QYKLxujC9a3H3fzApPKRIttkzEt8YvUPfcIwBkazR8mCAqXlKJD9d0P3/PNXp+GgNVnPPB
fhl4v0Jfski2w4BpdVkIcgo/bpNCYhT84Oq3HHQy2iLp00/W3kBdVslwjzWVjDvF3XR36+
7EZM4L0rMQAAA8jmRkJ75kZCewAAAAdzc2gtcnNhAAABAQDdmc1l6L2DsowOUp+IxkDhtH
gSAGYXretgoQNcS6S4uZLx51Tw7uzXWoSH0RjY/VJaT6PZ/W3g8tYcTsUegq/Fa2yCgLLQ
B9l1tJ1SoT/UvbdIolynMTY02vVVLEO9mMxkS3TKIXdrwIcuw6J+4ON5SbZ2WitPwD3fiT
0vDlFNlnw5SDHN/8mJgIPOU4WXSEZBgovG6ML1rcfd/MCk8pEi22TMS3xi9Q99wjAGRrNH
yYICpeUokP13Q/f881en4aA1Wc88F+GXi/Ql+ySLbDgGl1WQhyCj9uk0JiFPzg6rccdDLa
IunTT9beQF1WyXCPNZWMO8XddHfr7sRkzgvSsxAAAAAwEAAQAAAQAGXMM5jvE7AeI0ypE/
Rm7oNAvi+20y9pi8k17i9F6IObbC+ID3MmrtI3GM6zdXCo3l3yW9jWHNXLeCRE4zSi4FAW
iyBgMsRx9qmlsOe2f3YhOMM7IskDSF17gFwCG5RLTwl7yEjamtt69B69bDZQ5O5guFsiDO
dz7nhzuRGWyC6VadqgsoNaGUFmyJya9y8/aBSuNZEZ8dOlBdR3jcRsrOCXQneQpLWfYQgC
9KlFFgQVyO2VxaD6drEcqU8+fUVJlftHRn4J5R6D/CG2KxfB7g6LXUcaCVZBY55kWBPfpY
lJocp6EtAVX8dLl/oRKWmJYHkQBO6d0zzwNfVryT/HwBAAAAgQCnYylI9zS6aCmw+aiS2C
JQAECbS5T90xrwqR1IRGb6DJAQKqzHbF6YFW/QOMSCuWktmC8wKBMhj6UFywzjZuvPSJW2
gJodi50cmozfylSKvBlmPJEUI/8Wn6Yn2nYOUjTC5tUa3r2rtiH8M+3q2L/tGPJZhmRHXe
kaMDxtSNwUgwAAAIEA8kaZXYccjFRueVQP1CMYhmybAkRVBU242Iuz1LYmvURi2IAeYQi8
jmS1ilZB/j2BPKFE6yxvYaNBYRoJ3DKhTrydR13wb68xSUU9wWN1qkwuHpNdxNCORCsaEp
1Mwexf7u9fnA/FpV+yNbJkXKBNTtkMrnwrrrLTPLdP0vNtMgEAAACBAOonYbHWki46kDIS
+Fa9LHkIVHH2Odpq7hGTtuJXXao55ac6nbAkBR8yOCCXmV4kw2PYMqPogUGptAhN1Yd3cB
Q04vNWkwkkxObuQempaJE6PMLY12IdouStAgCl43d+Mlo5AOnjYq0MgJbcGMkgHsp0wzbQ
+8XRKsLeeDJd9JkxAAAADm9mZmljZV92cG5fa2V5AQIDBA==
-----END OPENSSH PRIVATE KEY-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEArEEc3XJcEgI3lVfJ4eidJqKHFywOpbmNjHRgqWj4VDbmgE1yUQI3
fN+/C68in7muvV2RSGM9/kaFYksmiFV4HG17vozsJNIRpG0kA6zi7qhKPVcCFDuZejpfUp
O4SyuJRBzCOy3LxEVudHpc855499eKdDAxDamj4e5vrl01jroOutRMO0PuWsJHS2Y6B0W/
uMkfZvw07nAKyN9TpxaXiTakEAvlioYZKBQkag2LpRJ7uTAM3sfqH5FpI5VHkHVhVfsfrM
pr0LfElVOiPMUmx6sheGoqUQfvgCQy4CN+tUwmK7c7zlKcsNso3aVQ0YpwLemoxQXzzmI0
a1afKdJIEQAAA8gWusYkFrrGJAAAAAdzc2gtcnNhAAABAQCsQRzdclwSAjeVV8nh6J0moo
cXLA6luY2MdGCpaPhUNuaATXJRAjd8378LryKfua69XZFIYz3+RoViSyaIVXgcbXu+jOwk
0hGkbSQDrOLuqEo9VwIUO5l6Ol9Sk7hLK4lEHMI7LcvERW50elzznnj314p0MDENqaPh7m
+uXTWOug661Ew7Q+5awkdLZjoHRb+4yR9m/DTucArI31OnFpeJNqQQC+WKhhkoFCRqDYul
Enu5MAzex+ofkWkjlUeQdWFV+x+symvQt8SVU6I8xSbHqyF4aipRB++AJDLgI361TCYrtz
vOUpyw2yjdpVDRinAt6ajFBfPOYjRrVp8p0kgRAAAAAwEAAQAAAQASF3Oj6Y00ggGbAKGu
RbttsZ/NJf4y10J/6EA3wtPkKnD6tEenrPstdSWQYVhaXMr2ziNCblP2Rytet8RoCMwI9l
HLIWty8ZJTSfhAn5GlHc1QVHleLSVRQly9JFE0qfGskvWueAChEGbJuolVOAV+CGgdDGu2
guT4x414y4biwsjR0K2z9eJS8rrMOlXHs35ZDC+Hqzvjgoum3hJx6NZzJLV3+MJWiYSX4J
5+2zQhNW4anXeuFUzqdLlUYCs95Djr26699xu2RcpKAvjCEU2sZKRnK7j7nUXS4sBy4fEJ
+UTJfutkZ6dXZjTe9eSHBsNbehQERu4h31mxFeeLLInNAAAAgB5Kj6NZ5rtJcTBnwWuBg0
Vc1YVo03vx3DC7F2HzqDp0WZ9o1jNtXeI1ptrEHgWxtxADeTEaJ25sViXZjf2O5wSDreu3
UlXhaLKQvwCQ3yRk/JvDuuKOCmRDpn3VrP+zevbwRIto9V0NHNPwX70LmibBI7W0N3qeFI
ul/GW1M+JvAAAAgQDy2CAp/7YZ5ZGz7clvGBtbP+mmkR9U4U/OH9dS0q8ja4y8SVZFYKip
AcPcFUCkSuuMq1xs38zh6RPApACE79SHbCmOlQnbRcR4ymEZAlU+tAxZ2SCV0TPE0D8Knd
+OUrXZTmdjxRVJYiKWabgivXw/eKdOx19LhdSt1eqdWQWztQAAAIEAtZYDrUcuuFZ3pjIQ
uBOhlxRLwxzaHI/8y+hQYEOuObKZ9It7gua2HL3mMqY+LlOYRfXJ8bRRpHIfYpfBepzazY
tKvjXMpTUL868BuAX3c8CPMdlIg694nyDmQFOCAXDPkNm3F6GVoMaieFtLtmhbuIOXjtre
kbciQMlWhijUNG0AAAANcHJvZF9qdW1wX2tleQECAwQFBg==
-----END OPENSSH PRIVATE KEY-----
+18
View File
@@ -0,0 +1,18 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fscan Lab - 内网渗透训练平台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
server {
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://lab-api:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "fscan-lab-ui",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"reactflow": "^11.10.1",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
"axios": "^1.6.2"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+76
View File
@@ -0,0 +1,76 @@
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'
import { Target, Map, Trophy, Languages } from 'lucide-react'
import Dashboard from './pages/Dashboard'
import Topology from './pages/Topology'
import Challenges from './pages/Challenges'
import { useI18n } from './contexts/I18nContext'
import { Button } from './components/ui/button'
function App() {
const { language, setLanguage, t } = useI18n()
return (
<Router>
<div className="min-h-screen bg-background">
<nav className="border-b">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-8">
<h1 className="text-2xl font-bold text-primary">
{t('nav.title')}
</h1>
<div className="flex space-x-4">
<Link
to="/"
className="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-accent"
>
<Trophy className="w-4 h-4" />
<span>{t('nav.dashboard')}</span>
</Link>
<Link
to="/topology"
className="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-accent"
>
<Map className="w-4 h-4" />
<span>{t('nav.network')}</span>
</Link>
<Link
to="/challenges"
className="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-accent"
>
<Target className="w-4 h-4" />
<span>{t('nav.challenges')}</span>
</Link>
</div>
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-muted-foreground">
{t('nav.subtitle')}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}
className="flex items-center space-x-1"
>
<Languages className="w-4 h-4" />
<span>{language === 'zh' ? 'EN' : '中文'}</span>
</Button>
</div>
</div>
</div>
</nav>
<main className="container mx-auto px-4 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/topology" element={<Topology />} />
<Route path="/challenges" element={<Challenges />} />
</Routes>
</main>
</div>
</Router>
)
}
export default App
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
@@ -0,0 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
@@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,40 @@
import { createContext, useContext, useState, ReactNode } from 'react'
import { translations, Language, TranslationKey } from '@/lib/i18n'
interface I18nContextType {
language: Language
setLanguage: (lang: Language) => void
t: (key: TranslationKey) => string
}
const I18nContext = createContext<I18nContextType | undefined>(undefined)
export function I18nProvider({ children }: { children: ReactNode }) {
const [language, setLanguage] = useState<Language>(() => {
const saved = localStorage.getItem('language')
return (saved === 'zh' || saved === 'en') ? saved : 'zh'
})
const handleSetLanguage = (lang: Language) => {
setLanguage(lang)
localStorage.setItem('language', lang)
}
const t = (key: TranslationKey): string => {
return translations[language][key] || key
}
return (
<I18nContext.Provider value={{ language, setLanguage: handleSetLanguage, t }}>
{children}
</I18nContext.Provider>
)
}
export function useI18n() {
const context = useContext(I18nContext)
if (!context) {
throw new Error('useI18n must be used within I18nProvider')
}
return context
}
+59
View File
@@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+105
View File
@@ -0,0 +1,105 @@
import axios from 'axios'
const API_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:8888'
export interface Challenge {
id: number
name: string
description: string
difficulty: string
points: number
network: string
targets: string[]
order?: number // 渗透顺序
}
export interface Progress {
user_id: string
completed_challenges: number[]
total_score: number
start_time: string
last_update: string
submission_history: Submission[]
}
export interface Submission {
challenge_id: number
flag: string
correct: boolean
timestamp: string
}
export interface NetworkNode {
id: string
name: string
ip: string
services: string[]
network: string
status: 'unknown' | 'discovered' | 'compromised'
}
export interface NetworkEdge {
from: string
to: string
access: 'allowed' | 'blocked' | 'vpn'
}
export interface NetworkTopology {
nodes: NetworkNode[]
edges: NetworkEdge[]
}
export interface SubmitFlagResponse {
correct: boolean
message: string
points_earned?: number
total_score?: number
already_solved?: boolean
}
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
})
export const getChallenges = async (): Promise<Challenge[]> => {
const response = await api.get('/api/challenges')
return response.data
}
export const getChallenge = async (id: number): Promise<Challenge> => {
const response = await api.get(`/api/challenges/${id}`)
return response.data
}
export const submitFlag = async (
challengeId: number,
flag: string
): Promise<SubmitFlagResponse> => {
const response = await api.post('/api/submit', {
challenge_id: challengeId,
flag: flag.trim(),
})
return response.data
}
export const getProgress = async (): Promise<Progress> => {
const response = await api.get('/api/progress')
return response.data
}
export const resetProgress = async (): Promise<void> => {
await api.post('/api/reset')
}
export const getTopology = async (): Promise<NetworkTopology> => {
const response = await api.get('/api/topology')
return response.data
}
export const getHints = async (id: number): Promise<string[]> => {
const response = await api.get(`/api/hints/${id}`)
return response.data.hints
}
+241
View File
@@ -0,0 +1,241 @@
export const translations = {
zh: {
// Navigation
'nav.title': 'fscan Lab',
'nav.subtitle': '内网渗透训练平台',
'nav.dashboard': '控制面板',
'nav.network': '网络拓扑',
'nav.challenges': '挑战列表',
// Dashboard
'dashboard.title': '控制面板',
'dashboard.welcome': '欢迎来到 fscan 内网渗透训练平台',
'dashboard.resetProgress': '重置进度',
'dashboard.resetConfirm': '确定要重置进度吗?这将清除所有已完成的挑战记录。',
'dashboard.totalScore': '总分',
'dashboard.maxScore': '满分',
'dashboard.completedChallenges': '已完成挑战',
'dashboard.completionRate': '完成率',
'dashboard.startTime': '开始时间',
'dashboard.submissions': '提交次数',
'dashboard.successful': '成功',
'dashboard.progress': '完成进度',
'dashboard.progressDesc': '已完成',
'dashboard.progressDesc2': '个挑战',
'dashboard.recentSubmissions': '最近提交',
'dashboard.recentSubmissionsDesc': '最新的 5 次 flag 提交记录',
'dashboard.noSubmissions': '暂无提交记录',
'dashboard.correct': '正确',
'dashboard.incorrect': '错误',
'dashboard.challenge': '挑战',
'dashboard.overview': '挑战概览',
'dashboard.overviewDesc': '按难度分类的挑战统计',
'dashboard.quickStart': '快速开始',
'dashboard.quickStart1': '进入攻击者容器:',
'dashboard.quickStart2': '开始扫描 DMZ 区:',
'dashboard.quickStart3': '在"挑战列表"页面查看所有挑战并提交 flag',
'dashboard.quickStart4': '在"网络拓扑"页面查看网络拓扑和攻击路径',
'dashboard.loading': '加载中...',
// Challenges
'challenges.title': '挑战列表',
'challenges.desc': '完成所有挑战,攻陷整个网络',
'challenges.all': '全部',
'challenges.search': '搜索挑战...',
'challenges.difficulty': '难度',
'challenges.points': '分',
'challenges.network': '网络',
'challenges.targets': '目标',
'challenges.status': '状态',
'challenges.completed': '已完成',
'challenges.locked': '未完成',
'challenges.submitFlag': '提交 Flag',
'challenges.viewHints': '查看提示',
'challenges.hideHints': '隐藏提示',
'challenges.hints': '提示',
'challenges.enterFlag': '输入 flag...',
'challenges.submit': '提交',
'challenges.submitting': '提交中...',
'challenges.noChallenges': '未找到匹配的挑战',
// Topology
'topology.title': '网络拓扑',
'topology.desc': '实时网络拓扑和攻击路径',
'topology.legend': '图例',
'topology.compromised': '已攻陷',
'topology.discovered': '已发现',
'topology.unknown': '未知',
'topology.nodeInfo': '节点信息',
'topology.selectNode': '点击节点查看详细信息',
'topology.name': '名称',
'topology.ip': 'IP 地址',
'topology.services': '服务',
'topology.status': '状态',
// Network Labels
'network.internet': '外网',
'network.dmz': 'DMZ',
'network.office': '办公网',
'network.production': '生产网',
'network.core': '核心网',
// Difficulty
'difficulty.Easy': 'Easy',
'difficulty.Medium': 'Medium',
'difficulty.Hard': 'Hard',
'difficulty.Expert': 'Expert',
// Common
'common.points': '分',
'common.score': '分数',
// Challenge Content
'challenge.1.name': 'DMZ 侦察',
'challenge.1.desc': '扫描 DMZ 区,发现 Web 服务器并获取第一个 flag',
'challenge.2.name': 'FTP 弱密码',
'challenge.2.desc': '通过 FTP 弱密码进入 DMZ 区并获取 SSH 密钥',
'challenge.3.name': 'VPN 网关突破',
'challenge.3.desc': '使用获取的 SSH 密钥连接 VPN 网关进入办公网',
'challenge.4.name': '办公网备份服务器',
'challenge.4.desc': '发现 Rsync 备份服务器并获取敏感文件',
'challenge.5.name': '生产网 Redis 渗透',
'challenge.5.desc': '利用 Redis 弱密码获取 flag 并准备横向移动',
'challenge.6.name': '核心网 MySQL 数据库',
'challenge.6.desc': '爆破 MySQL 数据库获取敏感信息',
'challenge.7.name': '最终目标 - MongoDB',
'challenge.7.desc': '攻陷 MongoDB 获取最终 flag,完成整个网络渗透',
'challenge.8.name': 'Elasticsearch 情报收集',
'challenge.8.desc': '利用 Elasticsearch 未授权访问获取生产网敏感信息',
'challenge.9.name': 'PostgreSQL 数据库渗透',
'challenge.9.desc': '爆破 PostgreSQL 数据库获取业务数据',
'challenge.10.name': 'MSSQL 数据库攻击',
'challenge.10.desc': '攻破 MSSQL 数据库获取企业核心数据',
'challenge.11.name': 'VNC 远程桌面入侵',
'challenge.11.desc': '通过 VNC 弱密码获取办公网主机控制权',
'challenge.12.name': '老旧 Telnet 服务',
'challenge.12.desc': '利用古老的 Telnet 服务获取办公网老旧主机访问权',
'challenge.13.name': '打印机 SMB 共享',
'challenge.13.desc': '发现办公网打印机的 SMB 共享服务,通过弱密码访问共享文件',
},
en: {
// Navigation
'nav.title': 'fscan Lab',
'nav.subtitle': 'Penetration Testing Platform',
'nav.dashboard': 'Dashboard',
'nav.network': 'Network',
'nav.challenges': 'Challenges',
// Dashboard
'dashboard.title': 'Dashboard',
'dashboard.welcome': 'Welcome to fscan Lab',
'dashboard.resetProgress': 'Reset Progress',
'dashboard.resetConfirm': 'Are you sure you want to reset progress? This will clear all completed challenges.',
'dashboard.totalScore': 'Total Score',
'dashboard.maxScore': 'Max',
'dashboard.completedChallenges': 'Completed',
'dashboard.completionRate': 'Completion',
'dashboard.startTime': 'Started',
'dashboard.submissions': 'Submissions',
'dashboard.successful': 'successful',
'dashboard.progress': 'Progress',
'dashboard.progressDesc': 'Completed',
'dashboard.progressDesc2': 'challenges',
'dashboard.recentSubmissions': 'Recent Submissions',
'dashboard.recentSubmissionsDesc': 'Last 5 flag submissions',
'dashboard.noSubmissions': 'No submissions yet',
'dashboard.correct': 'Correct',
'dashboard.incorrect': 'Incorrect',
'dashboard.challenge': 'Challenge',
'dashboard.overview': 'Overview',
'dashboard.overviewDesc': 'Challenges by difficulty',
'dashboard.quickStart': 'Quick Start',
'dashboard.quickStart1': 'Enter attacker container:',
'dashboard.quickStart2': 'Start scanning DMZ:',
'dashboard.quickStart3': 'View all challenges and submit flags in "Challenges" page',
'dashboard.quickStart4': 'View network topology in "Network" page',
'dashboard.loading': 'Loading...',
// Challenges
'challenges.title': 'Challenges',
'challenges.desc': 'Complete all challenges to pwn the network',
'challenges.all': 'All',
'challenges.search': 'Search challenges...',
'challenges.difficulty': 'Difficulty',
'challenges.points': 'pts',
'challenges.network': 'Network',
'challenges.targets': 'Targets',
'challenges.status': 'Status',
'challenges.completed': 'Completed',
'challenges.locked': 'Locked',
'challenges.submitFlag': 'Submit Flag',
'challenges.viewHints': 'View Hints',
'challenges.hideHints': 'Hide Hints',
'challenges.hints': 'Hints',
'challenges.enterFlag': 'Enter flag...',
'challenges.submit': 'Submit',
'challenges.submitting': 'Submitting...',
'challenges.noChallenges': 'No challenges found',
// Topology
'topology.title': 'Network Topology',
'topology.desc': 'Real-time network topology and attack path',
'topology.legend': 'Legend',
'topology.compromised': 'Compromised',
'topology.discovered': 'Discovered',
'topology.unknown': 'Unknown',
'topology.nodeInfo': 'Node Info',
'topology.selectNode': 'Select a node to view details',
'topology.name': 'Name',
'topology.ip': 'IP Address',
'topology.services': 'Services',
'topology.status': 'Status',
// Network Labels
'network.internet': 'Internet',
'network.dmz': 'DMZ',
'network.office': 'Office',
'network.production': 'Production',
'network.core': 'Core',
// Difficulty
'difficulty.Easy': 'Easy',
'difficulty.Medium': 'Medium',
'difficulty.Hard': 'Hard',
'difficulty.Expert': 'Expert',
// Common
'common.points': 'pts',
'common.score': 'score',
// Challenge Content
'challenge.1.name': 'DMZ Reconnaissance',
'challenge.1.desc': 'Scan DMZ network and discover the web server to get the first flag',
'challenge.2.name': 'FTP Weak Password',
'challenge.2.desc': 'Access DMZ through FTP weak password and obtain SSH key',
'challenge.3.name': 'VPN Gateway Breach',
'challenge.3.desc': 'Use SSH key to connect VPN gateway and enter office network',
'challenge.4.name': 'Office Backup Server',
'challenge.4.desc': 'Discover Rsync backup server and obtain sensitive files',
'challenge.5.name': 'Production Redis Attack',
'challenge.5.desc': 'Exploit Redis weak password to get flag and prepare lateral movement',
'challenge.6.name': 'Core MySQL Database',
'challenge.6.desc': 'Brute-force MySQL database to obtain sensitive information',
'challenge.7.name': 'Final Target - MongoDB',
'challenge.7.desc': 'Compromise MongoDB to get the final flag and pwn the entire network',
'challenge.8.name': 'Elasticsearch Intelligence Gathering',
'challenge.8.desc': 'Exploit Elasticsearch unauthorized access to obtain production network sensitive information',
'challenge.9.name': 'PostgreSQL Database Penetration',
'challenge.9.desc': 'Brute-force PostgreSQL database to obtain business data',
'challenge.10.name': 'MSSQL Database Attack',
'challenge.10.desc': 'Compromise MSSQL database to obtain enterprise core data',
'challenge.11.name': 'VNC Remote Desktop Intrusion',
'challenge.11.desc': 'Gain office network host control through VNC weak password',
'challenge.12.name': 'Legacy Telnet Service',
'challenge.12.desc': 'Exploit legacy Telnet service to gain access to old office host',
'challenge.13.name': 'Printer SMB Share',
'challenge.13.desc': 'Discover office printer SMB share service and access shared files via weak credentials',
},
}
export type Language = keyof typeof translations
export type TranslationKey = keyof typeof translations.zh
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { I18nProvider } from './contexts/I18nContext'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nProvider>
<App />
</I18nProvider>
</React.StrictMode>,
)
+234
View File
@@ -0,0 +1,234 @@
import { useEffect, useState } from 'react'
import { Check, HelpCircle, Target } from 'lucide-react'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { getChallenges, getProgress, submitFlag, getHints, type Challenge, type Progress } from '@/lib/api'
import { useI18n } from '@/contexts/I18nContext'
export default function Challenges() {
const { t } = useI18n()
const [challenges, setChallenges] = useState<Challenge[]>([])
const [progress, setProgress] = useState<Progress | null>(null)
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState<number | null>(null)
const [flags, setFlags] = useState<Record<number, string>>({})
const [hints, setHints] = useState<Record<number, string[]>>({})
const [showHints, setShowHints] = useState<Record<number, boolean>>({})
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const loadData = async () => {
try {
const [challengesData, progressData] = await Promise.all([
getChallenges(),
getProgress(),
])
setChallenges(challengesData)
setProgress(progressData)
} catch (error) {
console.error('Failed to load data:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadData()
}, [])
const handleSubmit = async (challengeId: number) => {
const flag = flags[challengeId]?.trim()
if (!flag) {
setMessage({ type: 'error', text: 'Please enter a flag' })
return
}
setSubmitting(challengeId)
setMessage(null)
try {
const result = await submitFlag(challengeId, flag)
if (result.correct) {
setMessage({
type: 'success',
text: result.already_solved
? 'Already solved!'
: `Correct! +${result.points_earned} points`,
})
setFlags({ ...flags, [challengeId]: '' })
await loadData()
} else {
setMessage({ type: 'error', text: 'Incorrect flag. Try again!' })
}
} catch (error) {
setMessage({ type: 'error', text: 'Submission failed' })
} finally {
setSubmitting(null)
setTimeout(() => setMessage(null), 3000)
}
}
const handleShowHints = async (challengeId: number) => {
if (!hints[challengeId]) {
const challengeHints = await getHints(challengeId)
setHints({ ...hints, [challengeId]: challengeHints })
}
setShowHints({ ...showHints, [challengeId]: !showHints[challengeId] })
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'Easy':
return 'bg-green-500'
case 'Medium':
return 'bg-yellow-500'
case 'Hard':
return 'bg-orange-500'
case 'Expert':
return 'bg-red-500'
default:
return 'bg-gray-500'
}
}
const getNetworkColor = (network: string) => {
switch (network) {
case 'dmz':
return 'bg-blue-500/10 text-blue-500 border-blue-500/20'
case 'office':
return 'bg-purple-500/10 text-purple-500 border-purple-500/20'
case 'production':
return 'bg-orange-500/10 text-orange-500 border-orange-500/20'
case 'core':
return 'bg-red-500/10 text-red-500 border-red-500/20'
default:
return 'bg-gray-500/10 text-gray-500 border-gray-500/20'
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">{t('dashboard.loading')}</div>
</div>
)
}
return (
<div className="space-y-8">
<div>
<h1 className="text-4xl font-bold">{t('challenges.title')}</h1>
<p className="text-muted-foreground mt-2">
{t('challenges.desc')}
</p>
</div>
{message && (
<div
className={`p-4 rounded-md ${
message.type === 'success'
? 'bg-green-500/10 text-green-500 border border-green-500/20'
: 'bg-red-500/10 text-red-500 border border-red-500/20'
}`}
>
{message.text}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{challenges.sort((a, b) => (a.order || 0) - (b.order || 0)).map((challenge) => {
const isCompleted = progress?.completed_challenges.includes(challenge.id) || false
const flagValue = flags[challenge.id] || ''
return (
<Card key={challenge.id} className={isCompleted ? 'border-green-500' : ''}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<CardTitle className="text-xl">{t(`challenge.${challenge.id}.name` as any)}</CardTitle>
{isCompleted && (
<Check className="w-5 h-5 text-green-500" />
)}
</div>
<CardDescription>{t(`challenge.${challenge.id}.desc` as any)}</CardDescription>
</div>
<Badge className={getDifficultyColor(challenge.difficulty)}>
{t(`difficulty.${challenge.difficulty}` as any)}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{t('challenges.points')}</span>
<span className="font-mono font-bold">{challenge.points}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{t('challenges.network')}</span>
<Badge variant="outline" className={getNetworkColor(challenge.network)}>
{t(`network.${challenge.network}` as any)}
</Badge>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{t('challenges.targets')}</span>
<span className="font-mono text-xs">{challenge.targets.join(', ')}</span>
</div>
{!isCompleted && (
<div className="space-y-2">
<div className="flex gap-2">
<Input
placeholder={t('challenges.enterFlag')}
value={flagValue}
onChange={(e) =>
setFlags({ ...flags, [challenge.id]: e.target.value })
}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSubmit(challenge.id)
}}
/>
<Button
onClick={() => handleSubmit(challenge.id)}
disabled={submitting === challenge.id}
>
{submitting === challenge.id ? t('challenges.submitting') : t('challenges.submit')}
</Button>
</div>
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={() => handleShowHints(challenge.id)}
>
<HelpCircle className="w-4 h-4 mr-2" />
{showHints[challenge.id] ? t('challenges.hideHints') : t('challenges.viewHints')}
</Button>
{showHints[challenge.id] && hints[challenge.id] && (
<div className="bg-muted p-3 rounded-md space-y-1 text-sm">
{hints[challenge.id].map((hint, idx) => (
<div key={idx} className="flex gap-2">
<Target className="w-4 h-4 mt-0.5 flex-shrink-0 text-muted-foreground" />
<span>{hint}</span>
</div>
))}
</div>
)}
</div>
)}
</CardContent>
{isCompleted && (
<CardFooter className="bg-green-500/10 border-t border-green-500/20">
<div className="flex items-center gap-2 text-green-600">
<Check className="w-4 h-4" />
<span className="font-medium">{t('challenges.completed')}</span>
</div>
</CardFooter>
)}
</Card>
)
})}
</div>
</div>
)
}
+219
View File
@@ -0,0 +1,219 @@
import { useEffect, useState } from 'react'
import { Trophy, Target, Clock, Zap } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { getProgress, getChallenges, resetProgress, type Progress as ProgressType, type Challenge } from '@/lib/api'
import { useI18n } from '@/contexts/I18nContext'
export default function Dashboard() {
const { t } = useI18n()
const [progress, setProgress] = useState<ProgressType | null>(null)
const [challenges, setChallenges] = useState<Challenge[]>([])
const [loading, setLoading] = useState(true)
const loadData = async () => {
try {
const [progressData, challengesData] = await Promise.all([
getProgress(),
getChallenges(),
])
setProgress(progressData)
setChallenges(challengesData)
} catch (error) {
console.error('Failed to load data:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadData()
const interval = setInterval(loadData, 5000)
return () => clearInterval(interval)
}, [])
const handleReset = async () => {
if (confirm(t('dashboard.resetConfirm'))) {
await resetProgress()
await loadData()
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">{t('dashboard.loading')}</div>
</div>
)
}
const totalChallenges = challenges.length
const completedChallenges = progress?.completed_challenges.length || 0
const completionRate = totalChallenges > 0 ? (completedChallenges / totalChallenges) * 100 : 0
const maxScore = challenges.reduce((sum, c) => sum + c.points, 0)
const recentSubmissions = progress?.submission_history.slice(-5).reverse() || []
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold">{t('dashboard.title')}</h1>
<p className="text-muted-foreground mt-2">
{t('dashboard.welcome')}
</p>
</div>
<Button variant="outline" onClick={handleReset}>
{t('dashboard.resetProgress')}
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('dashboard.totalScore')}</CardTitle>
<Trophy className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{progress?.total_score || 0}</div>
<p className="text-xs text-muted-foreground">
{t('dashboard.maxScore')} {maxScore} {t('common.points')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('dashboard.completedChallenges')}</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{completedChallenges} / {totalChallenges}
</div>
<p className="text-xs text-muted-foreground">
{t('dashboard.completionRate')} {completionRate.toFixed(0)}%
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('dashboard.startTime')}</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{progress ? new Date(progress.start_time).toLocaleDateString() : '-'}
</div>
<p className="text-xs text-muted-foreground">
{progress ? new Date(progress.start_time).toLocaleTimeString() : ''}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('dashboard.submissions')}</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{progress?.submission_history.length || 0}
</div>
<p className="text-xs text-muted-foreground">
{t('dashboard.successful')} {progress?.submission_history.filter(s => s.correct).length || 0}
</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>{t('dashboard.progress')}</CardTitle>
<CardDescription>{t('dashboard.progressDesc')} {completedChallenges} / {totalChallenges} {t('dashboard.progressDesc2')}</CardDescription>
</CardHeader>
<CardContent>
<Progress value={completionRate} className="h-2" />
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>{t('dashboard.recentSubmissions')}</CardTitle>
<CardDescription>{t('dashboard.recentSubmissionsDesc')}</CardDescription>
</CardHeader>
<CardContent>
{recentSubmissions.length === 0 ? (
<p className="text-sm text-muted-foreground">{t('dashboard.noSubmissions')}</p>
) : (
<div className="space-y-3">
{recentSubmissions.map((sub, idx) => {
const challenge = challenges.find(c => c.id === sub.challenge_id)
return (
<div key={idx} className="flex items-center justify-between border-b pb-2">
<div className="flex-1">
<p className="font-medium">{challenge?.name || `${t('dashboard.challenge')} ${sub.challenge_id}`}</p>
<p className="text-xs text-muted-foreground">
{new Date(sub.timestamp).toLocaleString()}
</p>
</div>
<Badge variant={sub.correct ? 'default' : 'destructive'}>
{sub.correct ? t('dashboard.correct') : t('dashboard.incorrect')}
</Badge>
</div>
)
})}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('dashboard.overview')}</CardTitle>
<CardDescription>{t('dashboard.overviewDesc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{['Easy', 'Medium', 'Hard', 'Expert'].map(difficulty => {
const diffChallenges = challenges.filter(c => c.difficulty === difficulty)
const completed = diffChallenges.filter(c =>
progress?.completed_challenges.includes(c.id)
).length
const total = diffChallenges.length
if (total === 0) return null
return (
<div key={difficulty} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{t(`difficulty.${difficulty}` as any)}</span>
<span className="text-muted-foreground">{completed} / {total}</span>
</div>
<Progress value={(completed / total) * 100} className="h-2" />
</div>
)
})}
</div>
</CardContent>
</Card>
</div>
<Card className="bg-primary/5 border-primary/20">
<CardHeader>
<CardTitle>{t('dashboard.quickStart')}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<p>1. {t('dashboard.quickStart1')}<code className="bg-muted px-2 py-1 rounded">docker exec -it lab-attacker /bin/bash</code></p>
<p>2. {t('dashboard.quickStart2')}<code className="bg-muted px-2 py-1 rounded">fscan -h 10.10.1.0/24</code></p>
<p>3. {t('dashboard.quickStart3')}</p>
<p>4. {t('dashboard.quickStart4')}</p>
</CardContent>
</Card>
</div>
)
}
+268
View File
@@ -0,0 +1,268 @@
import { useEffect, useState, useCallback } from 'react'
import ReactFlow, {
Node,
Edge,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
MarkerType,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { getTopology, getProgress } from '@/lib/api'
import { useI18n } from '@/contexts/I18nContext'
export default function Topology() {
const { t } = useI18n()
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [selectedNode, setSelectedNode] = useState<any>(null)
const [loading, setLoading] = useState(true)
const loadTopology = useCallback(async () => {
try {
const [topology] = await Promise.all([getTopology(), getProgress()])
const networkPositions: Record<string, { x: number; y: number }> = {
internet: { x: 400, y: 50 },
attacker: { x: 400, y: 150 },
'web-dmz': { x: 200, y: 300 },
'mail-dmz': { x: 350, y: 300 },
'ftp-dmz': { x: 500, y: 300 },
'vpn-gateway': { x: 650, y: 300 },
'pc-vnc': { x: 100, y: 500 },
'pc-ssh': { x: 250, y: 500 },
'backup-server': { x: 400, y: 500 },
'printer': { x: 550, y: 500 },
'oldpc-telnet': { x: 700, y: 500 },
'app-web': { x: 100, y: 700 },
'cache-redis': { x: 250, y: 700 },
'mq-rabbit': { x: 400, y: 700 },
'mq-activemq': { x: 550, y: 700 },
'search-es': { x: 700, y: 700 },
'db-mysql': { x: 100, y: 900 },
'db-mssql': { x: 250, y: 900 },
'db-postgres': { x: 400, y: 900 },
'db-mongo': { x: 550, y: 900 },
'dc-ldap': { x: 700, y: 900 },
}
const getNodeColor = (status: string) => {
switch (status) {
case 'compromised':
return '#ef4444'
case 'discovered':
return '#f59e0b'
case 'unknown':
return '#6b7280'
default:
return '#6b7280'
}
}
const getNetworkLabel = (network: string) => {
return t(`network.${network}` as any) || network
}
const flowNodes: Node[] = topology.nodes.map((node) => {
const position = networkPositions[node.id] || { x: Math.random() * 800, y: Math.random() * 1000 }
return {
id: node.id,
type: 'default',
position,
data: {
label: (
<div className="text-center">
<div className="font-bold text-sm">{node.name}</div>
<div className="text-xs text-gray-500">{node.ip}</div>
<div className="text-xs mt-1">
<Badge variant="outline" className="text-xs">
{getNetworkLabel(node.network)}
</Badge>
</div>
</div>
),
...node,
},
style: {
background: '#fff',
border: `2px solid ${getNodeColor(node.status)}`,
borderRadius: 8,
padding: 10,
width: 140,
},
}
})
const flowEdges: Edge[] = topology.edges.map((edge, idx) => ({
id: `${edge.from}-${edge.to}-${idx}`,
source: edge.from,
target: edge.to,
animated: edge.access === 'vpn',
style: {
stroke: edge.access === 'blocked' ? '#ef4444' : edge.access === 'vpn' ? '#3b82f6' : '#6b7280',
strokeWidth: edge.access === 'vpn' ? 2 : 1,
strokeDasharray: edge.access === 'blocked' ? '5,5' : undefined,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color: edge.access === 'blocked' ? '#ef4444' : edge.access === 'vpn' ? '#3b82f6' : '#6b7280',
},
}))
setNodes(flowNodes)
setEdges(flowEdges)
} catch (error) {
console.error('Failed to load topology:', error)
} finally {
setLoading(false)
}
}, [setNodes, setEdges])
useEffect(() => {
loadTopology()
const interval = setInterval(loadTopology, 10000)
return () => clearInterval(interval)
}, [loadTopology])
const onNodeClick = useCallback((_: any, node: Node) => {
setSelectedNode(node.data)
}, [])
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">{t('dashboard.loading')}</div>
</div>
)
}
return (
<div className="space-y-6">
<div>
<h1 className="text-4xl font-bold">{t('topology.title')}</h1>
<p className="text-muted-foreground mt-2">
{t('topology.desc')}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card>
<CardContent className="p-0">
<div style={{ height: '700px' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
fitView
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</div>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>{t('topology.legend')}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border-2 border-red-500"></div>
<span className="text-sm">{t('topology.compromised')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border-2 border-orange-500"></div>
<span className="text-sm">{t('topology.discovered')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border-2 border-gray-500"></div>
<span className="text-sm">{t('topology.unknown')}</span>
</div>
</CardContent>
</Card>
{selectedNode ? (
<Card>
<CardHeader>
<CardTitle>{t('topology.nodeInfo')}</CardTitle>
<CardDescription>{selectedNode.name}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div>
<div className="text-sm text-muted-foreground">{t('topology.ip')}</div>
<div className="font-mono text-sm">{selectedNode.ip}</div>
</div>
<div>
<div className="text-sm text-muted-foreground">{t('challenges.network')}</div>
<Badge variant="outline">{t(`network.${selectedNode.network}` as any)}</Badge>
</div>
<div>
<div className="text-sm text-muted-foreground">{t('topology.status')}</div>
<Badge variant={selectedNode.status === 'compromised' ? 'destructive' : 'default'}>
{t(`topology.${selectedNode.status}` as any)}
</Badge>
</div>
{selectedNode.services && selectedNode.services.length > 0 && (
<div>
<div className="text-sm text-muted-foreground mb-2">{t('topology.services')}</div>
<div className="space-y-1">
{selectedNode.services.map((service: string, idx: number) => (
<div key={idx} className="text-xs font-mono bg-muted px-2 py-1 rounded">
{service}
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle>{t('topology.nodeInfo')}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{t('topology.selectNode')}</p>
</CardContent>
</Card>
)}
<Card className="bg-primary/5 border-primary/20">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="text-sm space-y-2">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">1</div>
<span> DMZ</span>
</div>
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">2</div>
<span>DMZ VPN</span>
</div>
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">3</div>
<span> </span>
</div>
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">4</div>
<span> </span>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
+76
View File
@@ -0,0 +1,76 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
host: '0.0.0.0',
port: 3000,
},
})
@@ -0,0 +1,14 @@
FROM rmohr/activemq:5.15.9
# 复制STOMP专用配置文件
COPY activemq.xml /opt/activemq/conf/activemq.xml
# 仅暴露STOMP协议端口
EXPOSE 61613 61614
# 设置环境变量
ENV ACTIVEMQ_OPTS_MEMORY="-Xms64M -Xmx512M"
ENV ACTIVEMQ_OPTS="-Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=/opt/activemq/conf/login.config"
# 启动ActiveMQ
CMD ["/opt/activemq/bin/activemq", "console"]
@@ -0,0 +1,2 @@
docker build -t activemq-weak .
docker run -d --name activemq-test -p 61616:61616 -p 8161:8161 -p 61613:61613 activemq-weak
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:amq="http://activemq.apache.org/schema/core"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<!-- 专注于STOMP协议的ActiveMQ配置 -->
<broker xmlns="http://activemq.apache.org/schema/core" useJmx="false" persistent="false">
<!-- 安全认证配置 -->
<plugins>
<simpleAuthenticationPlugin>
<users>
<!-- 主要测试账户 -->
<authenticationUser username="admin" password="Aa123456789" groups="admins,publishers,consumers"/>
<authenticationUser username="admin" password="admin" groups="admins,publishers,consumers"/>
<authenticationUser username="test" password="test123" groups="publishers,consumers"/>
<authenticationUser username="root" password="root123" groups="admins"/>
<authenticationUser username="system" password="admin123" groups="admins"/>
<authenticationUser username="guest" password="guest" groups="consumers"/>
<authenticationUser username="activemq" password="activemq" groups="publishers,consumers"/>
</users>
</simpleAuthenticationPlugin>
<!-- 简化的授权配置 -->
<authorizationPlugin>
<map>
<authorizationMap>
<authorizationEntries>
<authorizationEntry queue=">" read="consumers,admins" write="publishers,admins" admin="admins"/>
<authorizationEntry topic=">" read="consumers,admins" write="publishers,admins" admin="admins"/>
</authorizationEntries>
</authorizationMap>
</map>
</authorizationPlugin>
</plugins>
<!-- 仅启用STOMP传输连接器 -->
<transportConnectors>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=500&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp+ssl" uri="stomp+ssl://0.0.0.0:61614?maximumConnections=500&amp;wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
<!-- 禁用JMX和Web控制台以简化配置 -->
<managementContext>
<managementContext createConnector="false"/>
</managementContext>
<!-- 简化的持久化配置 -->
<persistenceAdapter>
<memoryPersistenceAdapter/>
</persistenceAdapter>
</broker>
</beans>
@@ -0,0 +1,15 @@
version: '3.8'
services:
activemq:
build: .
ports:
- "61613:61613" # STOMP
- "61616:61616" # OpenWire
- "8162:8161" # Web Console (mapped to host port 8162)
environment:
- ACTIVEMQ_ADMIN_LOGIN=admin
- ACTIVEMQ_ADMIN_PASSWORD=Aa123456789
volumes:
- ./activemq.xml:/opt/activemq/conf/activemq.xml
- ./users.properties:/opt/activemq/conf/users.properties
@@ -0,0 +1,12 @@
# ActiveMQ Web Console用户认证配置
# 格式: username: password [,role1,role2,...]
# 管理员用户
admin: Aa123456789,admin,user
test: test123,user
root: root123,admin,user
system: admin123,admin,user
# 默认测试用户
user: user,user
guest: guest,user
+112
View File
@@ -0,0 +1,112 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="securityConstraint" class="org.eclipse.jetty.util.security.Constraint">
<property name="name" value="BASIC" />
<property name="roles" value="user,admin" />
<property name="authenticate" value="true" />
</bean>
<bean id="adminSecurityConstraint" class="org.eclipse.jetty.util.security.Constraint">
<property name="name" value="BASIC" />
<property name="roles" value="admin" />
<property name="authenticate" value="true" />
</bean>
<bean id="securityConstraintMapping" class="org.eclipse.jetty.security.ConstraintMapping">
<property name="constraint" ref="securityConstraint" />
<property name="pathSpec" value="/admin/*,/api/*" />
</bean>
<bean id="realmSecurityHandler" class="org.eclipse.jetty.security.ConstraintSecurityHandler">
<property name="authenticator">
<bean class="org.eclipse.jetty.security.authentication.BasicAuthenticator" />
</property>
<property name="constraintMappings">
<list>
<ref bean="securityConstraintMapping" />
</list>
</property>
<property name="loginService">
<bean class="org.eclipse.jetty.security.HashLoginService">
<property name="name" value="ActiveMQRealm" />
<property name="config" value="${activemq.conf}/jetty-realm.properties" />
</bean>
</property>
</bean>
<bean id="contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection">
</bean>
<bean id="jettyPort" class="org.apache.activemq.web.config.SystemPropertiesConfiguration" init-method="configure">
<property name="properties">
<map>
<entry key="jetty.port" value="8161" />
<entry key="jetty.host" value="0.0.0.0" />
</map>
</property>
</bean>
<bean id="Server" class="org.eclipse.jetty.server.Server"
depends-on="jettyPort"
init-method="start" destroy-method="stop">
<property name="connectors">
<list>
<bean id="Connector" class="org.eclipse.jetty.server.ServerConnector">
<constructor-arg ref="Server" />
<property name="host" value="#{systemProperties['jetty.host']}" />
<property name="port" value="#{systemProperties['jetty.port']}" />
</bean>
</list>
</property>
<property name="handler">
<bean id="handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
<property name="handlers">
<list>
<ref bean="contexts" />
<bean class="org.eclipse.jetty.server.handler.DefaultHandler" />
</list>
</property>
</bean>
</property>
</bean>
<bean id="invokeStart" class="org.springframework.beans.factory.config.MethodInvokingBean">
<property name="targetObject" ref="Server" />
<property name="targetMethod" value="start" />
</bean>
<bean class="org.eclipse.jetty.webapp.WebAppContext">
<property name="contextPath" value="/admin" />
<property name="resourceBase" value="${activemq.home}/webapps/admin" />
<property name="server" ref="Server" />
<property name="securityHandler" ref="realmSecurityHandler" />
</bean>
<bean class="org.eclipse.jetty.webapp.WebAppContext">
<property name="contextPath" value="/api" />
<property name="resourceBase" value="${activemq.home}/webapps/api" />
<property name="server" ref="Server" />
<property name="securityHandler" ref="realmSecurityHandler" />
</bean>
</beans>
@@ -0,0 +1,4 @@
admin=Aa123456789
test=test123
root=root123
system=admin123
@@ -0,0 +1,2 @@
docker build -t cassandra-weak .
docker run -d --name cassandra-test -e CASSANDRA_AUTHENTICATOR=AllowAllAuthenticator -p 9042:9042 -p 9160:9160 cassandra:3.11
@@ -0,0 +1,19 @@
FROM docker.elastic.co/elasticsearch/elasticsearch:7.9.3
# 设置环境变量允许单节点运行
ENV discovery.type=single-node
# 允许任意IP访问
ENV network.host=0.0.0.0
# 设置弱密码
ENV ELASTIC_PASSWORD=elastic123
# 暴露端口
EXPOSE 9200 9300
# 设置默认用户名elastic和密码elastic123
RUN echo 'elastic:elastic123' > /usr/share/elasticsearch/config/users
# 关闭xpack安全功能,使其可以无认证访问
RUN echo 'xpack.security.enabled: false' >> /usr/share/elasticsearch/config/elasticsearch.yml
@@ -0,0 +1,2 @@
docker build -t elastic-test .
docker run -d -p 9200:9200 -p 9300:9300 elastic-test
+2
View File
@@ -0,0 +1,2 @@
docker run -d -p 20:20 -p 21:21 -e FTP_USER=admin -e FTP_PASS=123456 -e PASV_ADDRESS=127.0.0.1 --name ftp bogem/ftp
Mac上可能有问题
@@ -0,0 +1,16 @@
version: '3'
services:
ftp:
image: bogem/ftp
container_name: ftp-test
environment:
- FTP_USER=admin
- FTP_PASS=123456
- PASV_ADDRESS=127.0.0.1
- PASV_MIN_PORT=30000
- PASV_MAX_PORT=30100
ports:
- "21:21"
- "20:20"
- "30000-30100:30000-30100"
restart: unless-stopped
+74
View File
@@ -0,0 +1,74 @@
FROM ubuntu:20.04
ENV DEBIAN_FRONTEND=noninteractive
# 安装 Dovecot 和工具
RUN apt-get update && \
apt-get install -y dovecot-imapd dovecot-gssapi ssl-cert net-tools procps && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 创建邮件存储目录和邮箱
RUN mkdir -p /var/mail/vhosts/ && \
chmod 777 /var/mail/vhosts/
# 创建用户和密码文件
RUN echo "test:{PLAIN}123456" > /etc/dovecot/passwd && \
echo "admin:{PLAIN}admin123" >> /etc/dovecot/passwd && \
echo "root:{PLAIN}root123" >> /etc/dovecot/passwd && \
chown dovecot:dovecot /etc/dovecot/passwd && \
chmod 600 /etc/dovecot/passwd
# 配置Dovecot
RUN echo ' \
protocols = imap \n\
listen = * \n\
ssl = yes \n\
ssl_cert = </etc/ssl/certs/ssl-cert-snakeoil.pem \n\
ssl_key = </etc/ssl/private/ssl-cert-snakeoil.key \n\
mail_location = mbox:~/mail:INBOX=/var/mail/%u \n\
disable_plaintext_auth = no \n\
auth_mechanisms = plain login \n\
auth_debug = yes \n\
auth_debug_passwords = yes \n\
mail_debug = yes \n\
\n\
passdb { \n\
driver = passwd-file \n\
args = scheme=PLAIN /etc/dovecot/passwd \n\
} \n\
\n\
userdb { \n\
driver = static \n\
args = uid=vmail gid=vmail home=/var/mail/%u \n\
} \n\
\n\
service auth { \n\
user = dovecot \n\
unix_listener auth-userdb { \n\
mode = 0600 \n\
user = vmail \n\
} \n\
} \n\
\n\
service imap-login { \n\
inet_listener imap { \n\
port = 143 \n\
} \n\
inet_listener imaps { \n\
port = 993 \n\
ssl = yes \n\
} \n\
} \n\
' > /etc/dovecot/dovecot.conf
# 创建vmail用户并设置正确的权限
RUN groupadd -g 5000 vmail && \
useradd -g vmail -u 5000 vmail && \
chown -R vmail:vmail /var/mail && \
chown -R dovecot:dovecot /etc/dovecot && \
chmod -R 644 /etc/dovecot/dovecot.conf
EXPOSE 143 993
CMD ["dovecot", "-F"]
+2
View File
@@ -0,0 +1,2 @@
docker build -t weak-imap .
docker run -d --name imap-test -p 143:143 -p 993:993 weak-imap
+1
View File
@@ -0,0 +1 @@
docker-compose up -d
@@ -0,0 +1,22 @@
# docker-compose.yml
version: '3'
services:
kafka:
image: bitnami/kafka:latest
ports:
- "9092:9092"
environment:
- KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_PROCESS_ROLES=broker,controller
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_LISTENERS=CONTROLLER://:9093,SASL_PLAINTEXT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://localhost:9092
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT
- KAFKA_CFG_SASL_ENABLED_MECHANISMS=PLAIN
- KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAIN
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SASL_PLAINTEXT
- KAFKA_OPTS=-Djava.security.auth.login.config=/opt/bitnami/kafka/config/kafka_jaas.conf
- ALLOW_PLAINTEXT_LISTENER=yes
volumes:
- ./kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf
@@ -0,0 +1,8 @@
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="admin"
password="admin123"
user_admin="admin123"
user_test="test123"
user_kafka="kafka123";
};
+18
View File
@@ -0,0 +1,18 @@
FROM osixia/openldap:1.5.0
# 环境变量设置
ENV LDAP_ORGANISATION="Example Inc"
ENV LDAP_DOMAIN="example.com"
ENV LDAP_BASE_DN="dc=example,dc=com"
# 设置一个弱密码
ENV LDAP_ADMIN_PASSWORD="Aa123456789"
# 允许匿名访问
ENV LDAP_READONLY_USER="true"
ENV LDAP_READONLY_USER_USERNAME="readonly"
ENV LDAP_READONLY_USER_PASSWORD="readonly"
# 暴露端口
EXPOSE 389 636
# 创建初始化脚本
COPY bootstrap.ldif /container/service/slapd/assets/config/bootstrap/ldif/custom/
+2
View File
@@ -0,0 +1,2 @@
docker build -t ldap-weak .
docker run -d --name ldap-test -p 389:389 -p 636:636 ldap-weak
@@ -0,0 +1,24 @@
dn: ou=users,dc=example,dc=com
objectClass: organizationalUnit
ou: users
dn: cn=admin,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
cn: admin
sn: admin
uid: admin
userPassword: admin123
dn: cn=test,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
cn: test
sn: test
uid: test
userPassword: test123
dn: cn=root,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
cn: root
sn: root
uid: root
userPassword: root123
+14
View File
@@ -0,0 +1,14 @@
# 使用SQL Server官方镜像
FROM mcr.microsoft.com/mssql/server:2022-latest
# 设置环境变量
ENV ACCEPT_EULA=Y
ENV MSSQL_SA_PASSWORD=P@ssword123
ENV MSSQL_PID=Express
# 开放1433端口
EXPOSE 1433
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword123 -Q "SELECT 1" || exit 1
+5
View File
@@ -0,0 +1,5 @@
docker build -t mssql-server .
docker run -d \
-p 1433:1433 \
--name mssql-container \
mssql-server
@@ -0,0 +1,11 @@
# 使用Memcached官方镜像
FROM memcached:latest
# 开放11211端口
EXPOSE 11211
# 设置启动参数
# -m 64: 分配64MB内存
# -c 1024: 最大同时连接数1024
# -v: 显示版本信息
CMD ["memcached", "-m", "64", "-c", "1024", "-v"]
@@ -0,0 +1,5 @@
docker build -t memcached-server .
docker run -d \
-p 11211:11211 \
--name memcached-container \
memcached-server
@@ -0,0 +1 @@
docker run --rm -p 5020:5020 oitc/modbus-server:latest
@@ -0,0 +1,13 @@
# 使用MongoDB官方镜像
FROM mongo:latest
# 设置环境变量
ENV MONGO_INITDB_ROOT_USERNAME=admin
ENV MONGO_INITDB_ROOT_PASSWORD=123456
# 开放27017端口
EXPOSE 27017
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD mongosh --eval 'db.runCommand("ping").ok' localhost:27017/test --quiet
@@ -0,0 +1,5 @@
docker build -t mongodb-server .
docker run -d \
-p 27017:27017 \
--name mongodb-container \
mongodb-server
+17
View File
@@ -0,0 +1,17 @@
# 使用MySQL官方镜像
FROM mysql:latest
# 设置环境变量
ENV MYSQL_ROOT_PASSWORD=Password
ENV MYSQL_DATABASE=mydb
# 开放3306端口
EXPOSE 3306
# MySQL配置
# 允许远程访问
COPY my.cnf /etc/mysql/conf.d/my.cnf
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" || exit 1
+2
View File
@@ -0,0 +1,2 @@
docker build -t mysql-server .
docker run -d -p 3306:3306 --name mysql-container mysql-server
+2
View File
@@ -0,0 +1,2 @@
[mysqld]
bind-address = 0.0.0.0
+9
View File
@@ -0,0 +1,9 @@
FROM neo4j:4.4
ENV NEO4J_AUTH=neo4j/123456
ENV NEO4J_dbms_security_procedures_unrestricted=apoc.*
ENV NEO4J_dbms_security_auth_enabled=true
EXPOSE 7474 7687
CMD ["neo4j"]
@@ -0,0 +1,11 @@
version: '3'
services:
neo4j:
image: neo4j:4.4
ports:
- "7474:7474"
- "7687:7687"
environment:
- NEO4J_AUTH=neo4j/123456
- NEO4J_dbms_security_auth_enabled=true
container_name: neo4j-weak
+13
View File
@@ -0,0 +1,13 @@
# 使用Oracle官方容器镜像
FROM container-registry.oracle.com/database/express:21.3.0-xe
# 设置环境变量
ENV ORACLE_PWD=123456
ENV ORACLE_CHARACTERSET=AL32UTF8
# 开放1521端口
EXPOSE 1521 5500
# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 \
CMD nc -z localhost 1521 || exit 1
+11
View File
@@ -0,0 +1,11 @@
首先需要在Oracle Container Registry网站注册并接受许可协议:
https://container-registry.oracle.com
docker login container-registry.oracle.com
docker build -t oracle-db .
docker run -d \
-p 1521:1521 \
--name oracle-container \
oracle-db

Some files were not shown because too many files have changed in this diff Show More