mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
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:
@@ -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
|
||||
@@ -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 UI(React)
|
||||
|
||||
## Web界面
|
||||
|
||||
- **训练平台**: http://localhost:3000
|
||||
- **API服务**: http://localhost:8888
|
||||
|
||||
## 管理命令
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
docker-compose ps
|
||||
|
||||
# 停止环境
|
||||
docker-compose down
|
||||
|
||||
# 完全清理
|
||||
docker-compose down -v
|
||||
```
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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"]
|
||||
@@ -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 =
|
||||
@@ -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\]# "
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
|
||||
# ============================================
|
||||
# 规则 3:DMZ -> 办公网(只允许 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"
|
||||
@@ -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
|
||||
|
||||
加油!
|
||||
@@ -0,0 +1,17 @@
|
||||
====================================
|
||||
办公网打印机配置信息
|
||||
====================================
|
||||
|
||||
设备型号: HP LaserJet Pro M404n
|
||||
IP地址: 10.10.2.23
|
||||
SMB共享: 已启用
|
||||
|
||||
管理员账户:
|
||||
- 用户名: printer
|
||||
- 密码: [已保存在密码管理器]
|
||||
|
||||
共享文件夹:
|
||||
- print$ (打印机驱动)
|
||||
- backup (打印任务备份)
|
||||
|
||||
注意: 为了方便管理,使用了简单密码
|
||||
@@ -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. 扫描核心网寻找数据库
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
============================================
|
||||
MSSQL 数据库攻击成功
|
||||
============================================
|
||||
|
||||
恭喜!你成功攻破了 MSSQL 数据库。
|
||||
|
||||
Flag: FSCAN_LAB{mssql_s4_4cc0unt_pwn3d}
|
||||
|
||||
从 master.dbo.secrets 表中获取了核心数据。
|
||||
@@ -0,0 +1,9 @@
|
||||
============================================
|
||||
VNC 远程桌面入侵成功
|
||||
============================================
|
||||
|
||||
恭喜!你成功通过 VNC 弱密码获取了办公网主机的远程控制权。
|
||||
|
||||
Flag: FSCAN_LAB{vnc_r3m0t3_d3skt0p_pwn3d}
|
||||
|
||||
VNC 密码管理不当是常见的安全隐患...
|
||||
@@ -0,0 +1,9 @@
|
||||
============================================
|
||||
老旧 Telnet 服务入侵成功
|
||||
============================================
|
||||
|
||||
恭喜!你成功利用古老的 Telnet 服务获取了访问权限。
|
||||
|
||||
Flag: FSCAN_LAB{t3ln3t_l3g4cy_syst3m}
|
||||
|
||||
这台主机运行着过时的 Telnet 服务,属于遗留系统...
|
||||
@@ -0,0 +1 @@
|
||||
FSCAN_LAB{smb_pr1nt3r_sh4r3_pwn3d}
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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/ ./
|
||||
@@ -0,0 +1,2 @@
|
||||
FLAG 5 存储在 Redis 中,使用命令获取:
|
||||
redis-cli -h 10.10.3.31 -a redis123 GET flag5
|
||||
@@ -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;"
|
||||
@@ -0,0 +1,2 @@
|
||||
FLAG 7 (最终 Flag) 存储在 MongoDB 中,使用命令获取:
|
||||
mongosh mongodb://admin:[email protected]/admin --eval "db.admin_secrets.find({type:'flag'}).pretty()"
|
||||
@@ -0,0 +1,9 @@
|
||||
============================================
|
||||
Elasticsearch 情报收集成功
|
||||
============================================
|
||||
|
||||
恭喜!你成功利用 Elasticsearch 未授权访问漏洞获取了生产网的敏感信息。
|
||||
|
||||
Flag: FSCAN_LAB{3l4st1cs34rch_un4uth0r1z3d}
|
||||
|
||||
提示:在生产环境的索引中发现了数据库凭证信息...
|
||||
@@ -0,0 +1,9 @@
|
||||
============================================
|
||||
PostgreSQL 数据库渗透成功
|
||||
============================================
|
||||
|
||||
恭喜!你成功爆破了 PostgreSQL 数据库密码。
|
||||
|
||||
Flag: FSCAN_LAB{p0stgr3s_d4t4b4s3_pwn3d}
|
||||
|
||||
从 business.secrets 表中获取了企业核心业务数据。
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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;"]
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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")],
|
||||
}
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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&wireFormat.maxFrameSize=104857600"/>
|
||||
<transportConnector name="stomp+ssl" uri="stomp+ssl://0.0.0.0:61614?maximumConnections=500&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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,2 @@
|
||||
docker build -t weak-imap .
|
||||
docker run -d --name imap-test -p 143:143 -p 993:993 weak-imap
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
docker build -t mysql-server .
|
||||
docker run -d -p 3306:3306 --name mysql-container mysql-server
|
||||
@@ -0,0 +1,2 @@
|
||||
[mysqld]
|
||||
bind-address = 0.0.0.0
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user