Side-loadable Go POC Development
Write vulnerability templates in pure Go (interpreted in-process by the scanner, no Go toolchain needed): @meta header and every injected scan symbol.
This document is for third-party developers: when a YAML template cannot express your detection logic, a single pure-Go source file plus a
@metacomment header is all you need to write a vulnerability template that scan tasks can select. The scanner node executes it in-process with a built-in Go interpreter, and the target host does not need a Go toolchain — drop the file into the load directory and it takes effect (hot-load).
This document is organized as a function-by-function deep dive. Chapter 5 is the core: every public symbol of the injected scan package has its own level-4 section, each presenting "purpose → signature → parameter table → returns (a field table for structs) → a ready-to-paste code snippet → notes". All signatures are copied verbatim from the authoritative symbol table injected by the platform; no symbol outside that table exists.
1. What It Is / When to Choose It
1.1 Definition
A hot-loadable Go POC is one form of vulnerability template, not a plugin. The form is determined by the template's PocType field:
PocType="yaml"— declarative YAML template handled by the scanner node's native engine;PocType="go"— the subject of this document: pure Go source, interpreted in-process;PocType="wasm"— WASM binary template running on the wazero runtime (signature required).
It is made of exactly two things:
- A
// @meta:Key=Valuecomment header at the top of the file (not YAML — these are comments); - A standard Go program:
package main+func main(), whose capabilities are all accessed throughimport "scan", the symbol package injected by the host.
The interpreter invokes main() automatically; you do not need to — and cannot — call main.main() explicitly.
1.2 Storage and Encryption: Plaintext on the Controller, Ciphertext on the Node
The same Go POC has different on-disk forms at the two ends; this is the key to understanding the whole pipeline:
| Location | Directory and file name | Content | Notes |
|---|---|---|---|
| Controller | poc/go/<VulnIde>.go | Plaintext Go source | Scanned and loaded when the controller starts; the GUI editor and GetGoPocDetail both read it |
| Scanner node | scan-poc/go/<VulnIde>.gopoc | AES-GCM ciphertext | After the controller delivers it to the node, the node encrypts it with the global key and saves it into its own run directory |
Legacy stores were organized by language as scan-poc/<lang>/go/<VulnIde>.gopoc; after the multilingual single-file change on 2026-09-27, Go POCs — like YAML POCs — are no longer placed in per-language directories. The current path is always scan-poc/go/<VulnIde>.gopoc.
- The node-side extension is
.gopoc, not.go. This is a hard-won lesson: the ciphertext bytes contain no//go:build ignore, so the Go toolchain treatsscan-poc/go/<VulnIde>.goas source andgo build ./...fails immediately withillegal character U+00B0. - On startup the node automatically renames legacy
.gociphertext files to.gopoc(best-effort); genuine source files carrying//go:build ignoreare never mis-renamed. - When reading, the node first attempts AES-GCM decryption and falls back to plaintext on failure, so a plaintext
.gopocfile placed locally also runs (which is convenient for debugging).
1.3 Comparison with YAML / WASM
If it can be done in YAML, always use YAML. The 200 built-in generic vulnerabilities have already been migrated from Go POCs to YAML templates. Only complex algorithmic scenarios that the declarative YAML syntax cannot express are worth a hot-loadable Go POC.
| Dimension | YAML template | Hot-loadable Go | WASM template |
|---|---|---|---|
| Execution | Native Go engine, unmarshalled only once | Every run goes through the interpreter (compilation + reflection) | wazero runtime |
| Performance | Fastest | An order of magnitude slower than YAML | Moderate (compiled once, reusable) |
| Expressiveness | Declarative: request sequences + matching + regex + expressions + timing + reverse connection | Turing-complete; arbitrary loops, parsing and algorithms | Turing-complete, cross-language |
| Distribution | Single file | Single file (plaintext on the controller / ciphertext on the node) | Binary + signature |
| Maintenance cost | Low | High | High |
| Barrier to entry | None | Basic Go knowledge; no toolchain needed | Build chain + signature |
| Signature control | None (fingerprint sync) | None (fingerprint sync) | Hard gate: unsigned templates never run |
| Recommendation | First choice | Only for complex algorithms or custom parsing | For binary distribution or strong signing requirements |
Typical scenarios that call for Go: implementing your own signature verification, custom serialization, multi-round state machines, bit-level parsing of binary protocols, dynamically assembling unconventional request bodies, and so on. If it is merely "request + regex match", go back and write YAML.
2. Source Skeleton and the @meta Comment Header
2.1 //go:build ignore Must Be the First Line, and It Is Not Optional
//go:build ignore
// @meta:Name=示例
package main
...
//go:build ignore must be the first line of the file (immediately followed by a blank line). The reason: these plaintext templates live in the project directory alongside the source tree, and without the build constraint go build ./... and go vet ./... would compile them as ordinary source — which either fails the build or packs scripts you should never compile into your binary. With ignore in place the Go toolchain skips them, while the interpreter ignores build tags and interprets them all the same.
Skeletons generated by the controller from metadata automatically carry this line; never omit it when creating a file by hand.
2.2 @meta Syntax and the Parsing Regex
The controller and the scanner node parse @meta with the same regex (identical behavior at both ends):
var metaLinePattern = regexp.MustCompile(
`(?m)^\s*//\s*@meta:([A-Za-z]\w*)(?:\.([A-Za-z0-9_-]{1,16}))?(\+)?\s*=\s*(.*?)\s*$`)
Breaking it down (just enough to get by — no need to memorize):
^with(?m): each line is matched independently, and the comment must be at the start of a line (leading indentation is allowed); a// @meta:in the middle of a line is not recognized;//\s*@meta:: whitespace is allowed between//and@meta:;([A-Za-z]\w*): the key name, which must start with a letter and may be followed by letters, digits or underscores;(?:\.([A-Za-z0-9_-]{1,16}))?: an optional language suffix such as.en;(\+)?: the optional multi-line continuation marker;\s*=\s*(.*?)\s*$: the equals sign; the value extends to the end of the line with leading and trailing whitespace trimmed.
Four forms:
| Form | Meaning |
|---|---|
// @meta:Name=SQL 注入检测 | Ordinary key-value |
// @meta:Description+=第二行内容 | Multi-line continuation: appended to the key's existing value with \n |
// @meta:Name.en=SQL Injection Detection | Language suffix; recognized for language-related keys only |
// @meta:DefaultLanguage=cn | Base language; defaults to cn |
Key names are case-sensitive and must be spelled exactly. Writing @Meta:, @ meta: or meta: fails silently, and the symptom is "the vulnerability name is empty". Whitespace on either side of the value separator = is trimmed automatically.
2.3 Full Table of Supported Keys
The template metadata keys the @meta comment header can map to are listed below (taken one by one):
| Key | Type | Required | Default | Example value | Consequence of a mistake |
|---|---|---|---|---|---|
Name | string | Yes (or VulnName) | empty | SQL 注入检测 | The vulnerability name is empty and a blank entry appears in the list |
VulnName | string | No | empty | SQL 注入检测 | Alias of Name only; if both are missing the name is empty |
CVEId | string | No | empty | CVE-2024-0001 | The card shows no CVE number |
CweId | string | No | empty | CWE-89 | The card shows no CWE classification |
CvssScore | string | No | empty | 9.8 | The card shows no CVSS score |
CvssVector | string | No | empty | CVSS:3.1/AV:N/... | The card shows no vector string |
Level | string | No | empty | high | An empty or misspelled value is reported as low severity |
Description | string (language-related) | No | empty | 检测 SQL 报错特征 | The card's "Details" is empty |
Solution | string (language-related) | No | empty | 使用参数化查询 | The card's "Remediation" is empty |
Author | string | No | empty | admin | The author field is empty |
References | string | No | empty | https://example.com/advisory | The reference links are empty (use += for multiple lines) |
Fingerprint | string | No | empty | example-product | The fingerprint is empty |
AffectedProducts | string (language-related) | No | empty | Example Product 1.0 | The affected products are empty |
Verification | string | No | empty | 手动复现步骤 | The verification notes are empty |
TestDepth | string | No | pack | pack/single-dir/single-domain | An empty value falls back to pack |
Confidence | int | No | 80 | 90 | An invalid value, or one ≤ 0, uses 80 |
Enabled | bool | No | true | false | The template is disabled when the value is false (case-insensitive) |
DefaultLanguage | string | No | cn | cn | An empty value falls back to the platform default base language cn |
A comment header with every key written out looks like this (for reference only; optional keys may be omitted):
//go:build ignore
// @meta:Name=综合示例
// @meta:VulnName=综合示例
// @meta:Name.en=Comprehensive Example
// @meta:CVEId=CVE-2024-0001
// @meta:CweId=CWE-89
// @meta:CvssScore=9.8
// @meta:CvssVector=CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
// @meta:Level=critical
// @meta:Description=示例描述
// @meta:Description.en=Example description
// @meta:Solution=示例修复建议
// @meta:Solution.en=Example fix
// @meta:Author=admin
// @meta:References=https://example.com/advisory
// @meta:Fingerprint=example-product
// @meta:AffectedProducts=Example Product 1.0
// @meta:AffectedProducts.en=Example Product 1.0
// @meta:Verification=手动复现步骤
// @meta:TestDepth=pack
// @meta:Confidence=90
// @meta:Enabled=true
// @meta:DefaultLanguage=cn
package main
There are only 4 language-related keys: Name, Description, Solution and AffectedProducts. Only these support the key.language-code suffix.
2.4 Multilingual Aggregation Rules
Four-end language-pack synchronization is a hard requirement for product UI and log text; what follows concerns POC template text, which is a different system — do not conflate the two. POC template multilingual support uses the Languages mechanism described in this section.
- Keys without a suffix go into the base table and are then placed, as the text of the base language, into
Languages[DefaultLanguage]; - Keys with a language suffix (
Name.enand so on) go intolangs[language-code], each forming its own block; - Finally the flat fields are backfilled with the base-language text (exactly the same semantics as YAML POCs).
// @meta:Name=SQL 注入检测
// @meta:Name.en=SQL Injection Detection
// @meta:Description=检测 SQL 报错特征
// @meta:Description.en=Detect SQL error patterns
// @meta:DefaultLanguage=cn
The snippet above produces two complete language blocks: Languages["cn"] and Languages["en"]. New templates must provide both cn and en, otherwise Chinese text appears in the English UI.
If a non-language-related key is given a suffix (such as References.en=...), the suffix is not recognized and the key name is kept as References.en, which means the value is lost — do not write it that way.
2.5 Complete, Copy-Ready POC Source Skeleton
The skeleton below contains //go:build ignore, a full @meta header, package main and func main(); create a new file, paste it in, and adapt it:
//go:build ignore
// @meta:Name=示例漏洞
// @meta:Name.en=Example Vulnerability
// @meta:VulnName=示例漏洞
// @meta:CVEId=CVE-2024-0001
// @meta:CweId=CWE-89
// @meta:CvssScore=7.5
// @meta:Level=medium
// @meta:Description=响应中出现敏感特征
// @meta:Description.en=Sensitive marker found in response
// @meta:Solution=移除敏感信息
// @meta:Solution.en=Remove the sensitive information
// @meta:Author=admin
// @meta:References=https://example.com/advisory
// @meta:Fingerprint=example-product
// @meta:AffectedProducts=Example Product 1.0
// @meta:AffectedProducts.en=Example Product 1.0
// @meta:Verification=手动复现步骤
// @meta:TestDepth=pack
// @meta:Confidence=85
// @meta:Enabled=true
// @meta:DefaultLanguage=cn
package main
import (
"scan"
)
func main() {
// 1) 打印调试日志(GUI 调试面板可见)
scan.Log("开始检测: " + scan.Flow.Method + " " + scan.Flow.URL)
// 2) 发起请求(自动携带 Cookie 会话)
resp := scan.HTTPGet(scan.Flow.URL)
if resp.Error != "" {
scan.Log("请求失败: " + resp.Error)
return
}
if resp.StatusCode != 200 {
scan.Log("状态码非 200,跳过: " + scan.JSONDump(resp.StatusCode))
return
}
// 3) 判定命中
if scan.Contains(scan.ToLower(resp.Body), "sensitive_marker") {
// 4) 回传一条漏洞发现
scan.Report(scan.VulnFinding{
Name: "示例漏洞",
Detail: "响应体命中敏感特征",
Evidence: scan.Substr(resp.Body, 0, 200),
Level: "medium",
URL: resp.URL,
Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
})
}
}
Treat this skeleton as a template to copy: change the name, severity and languages in the @meta header, then replace the decision logic in main() with your own detection algorithm. Every symbol in Chapter 5 can be pasted into main() on its own.
2.6 Line-by-Line Walkthrough of the Minimal Runnable Skeleton
| Line | Purpose | Consequence of a mistake |
|---|---|---|
//go:build ignore | Makes go build ./... skip the file | A build error, or the file is compiled into the binary |
// @meta:Name=示例漏洞 | Vulnerability name (required) | The name is empty and the list shows a blank entry |
// @meta:Name.en=... | English name | The English UI shows Chinese |
// @meta:DefaultLanguage=cn | Base language | Falls back to cn |
package main | Marks a valid Go source file | Silently skipped at load time (the template "disappears") |
import "scan" | Gets all injected symbols | Without the import, every scan. reference is undefined |
func main() | The only entry point, executed automatically by the interpreter | Without main() the code never runs |
3. Where the Entry Point Is: from the POC Template List to func main() Being Executed
This chapter answers the question third-party developers ask most often: "Who invokes this code I wrote, at which step, and how?"
3.1 Step by Step Through the Whole Chain
- In the GUI's "Add Vulnerability" dialog, choose type Go (the tab is labeled "Go template"), paste the source into the editor and save; alternatively, the AI penetration agent submits it through the
generate_poctool. POCs submitted by AI are forced to disabled (Enabled=false) and tagged with their author source; they require manual review before being enabled. - The controller persists the plaintext source: on save the controller writes it to
poc/go/<VulnIde>.gounder its run directory; if only metadata was filled in without source, a skeleton with a@metaheader andpackage mainis generated from the metadata before writing, and the template is registered in the in-memory template list. - Controller startup / hot-load registration: at startup the controller ensures the
poc/godirectory exists, then scans the*.gofiles under it: files without apackagedeclaration are skipped; for the rest the@metaheader is parsed into template metadata (type marked as Go), multilingual fields are normalized, and the template is registered in the template list. - Task delivery to the scanner node: the controller delivers the selected template (including the source text) to the scanner node over TCP. The node recognizes it as a Go POC by its
packagedeclaration or@meta:header, encrypts it with the global AES key and writes it toscan-poc/go/<VulnIde>.gopoc(ciphertext), registering it in its in-memory template table (Go POCs do not take part in YAML fingerprint sync). - Loading local ciphertext on node startup/reload: at startup the node ensures the
scan-poc/godirectory exists and first renames historical.gociphertext files to.gopoc; as it reads each local file it tries AES-GCM decryption first and treats a failure as plaintext, accepting the file only when apackagedeclaration can be parsed, and then registers it in the local template table after parsing@meta. - Handing the source to the interpreter at task execution time: when a scan selects a template with
PocType == "go", the node builds the run environment for this execution (target flow, timeout, proxy, etc.) and hands your source to the interpreter. - The interpreter automatically executes
func main(): the Go standard library and the platform's ownscansymbol package are pre-registered in the interpreter, which then interprets your source in a controlled goroutine. For source containingfunc main(), the interpreter runsmainautomatically; there is no need to callmain.main()explicitly. - Your code calls host symbols through
import "scan":scan.Flow/scan.HTTP()/scan.Report()/scan.Log()and the rest are all injected by the platform before execution; you only call them with the signatures in Chapter 5. scan.Reportreturns findings: the node collects every finding from this run, assembles each one into a vulnerability detail and reports it to the controller for storage; logs and the HTTP request count are returned at the same time.
3.2 The Entry Point Is Just func main()
- The only entry point is
func main(): there is no_start, no exported function, and no callback to register. Once the interpreter receives source containingfunc main(), it calls it automatically while interpreting. - Requests you write sequentially inside
main()execute sequentially within the same run; the shared Cookie session (Chapter 5 / Chapter 6) naturally chains "log in → access" together. - When
main()returns, this POC run ends; the collected findings and logs are handed back to the node along with the execution result.
3.3 Available Range of the Standard Library
The execution engine has the Go standard library pre-registered, so a POC may import Go standard library packages, for example:
import (
"scan"
"strings"
"strconv"
"encoding/json"
"fmt"
)
Commonly available packages: fmt, strings, strconv, bytes, regexp, encoding/json, encoding/base64, encoding/hex, net/url, net/http, crypto/*, time, sort, math and so on.
You may only import "scan" and already-registered standard library packages. Third-party packages (such as github.com/...) are not registered, and importing them makes interpretation fail. In the vast majority of cases the scan package (Chapter 5) is sufficient; there is no need to pull in third-party code.
3.4 Origin of the .gopoc Extension
The node stores ciphertext locally under the .gopoc extension. It must not be .go: an earlier implementation wrote AES ciphertext to scan-poc/go/<VulnIde>.go; the ciphertext contains no //go:build ignore, so the Go toolchain compiled the file as source and the node project's go build ./... failed with illegal character U+00B0. After the fix: local ciphertext always uses .gopoc, and historical .go ciphertext files are renamed automatically when the node starts.
4. Loading and Execution Pipeline
4.1 Controller Side: Scan Directory → Parse @meta → Register
On controller startup / hot-load:
1. Ensure that poc/go exists under the run directory
2. Walk poc/go/*.go and read each source file
3. The source must contain a package declaration, otherwise it is silently skipped
4. VulnIde = file name without its extension; parse the @meta header for metadata
5. Normalize multilingual fields and register in the template list (type marked as Go)
Key points:
- Files without a
packagedeclaration are silently skipped — no error is raised, the template simply "goes missing"; VulnIdeis taken directly from the file name. Duplicate file names overwrite each other, so make sure names are globally unique;- This scan runs once at controller startup; templates saved later are written to disk and registered in memory by the controller directly.
4.2 Scanner Node Side: Migration → Loading → Decryption
On node startup / reload:
1. Ensure the go subdirectory exists under the local scan directory
2. Rename historical .go ciphertext files to .gopoc (best-effort)
3. Walk *.gopoc while tolerating historical *.go files, reading them one by one
4. Try AES-GCM decryption first, treat a failure as plaintext
5. Accept the file only if a package declaration can be parsed; register by VulnIde
(deduplicate by name, newer extension wins)
4.3 Execution: In-Process Interpretation
How it works: the hot-load execution engine is based on a built-in Go interpreter (an MIT-licensed open-source component), and the scanner node interprets your source in-process, which is why you write standard Go syntax and the target host needs no Go toolchain. This is an ordinary technology choice (it was chosen because it fully supports Go syntax and can have the platform's own scan symbol package injected), unrelated to any other security product; it is completely transparent to users — you just write POCs in standard Go and never touch the library. The rest of this document calls it "the interpreter".
The execution engine interprets your source in-process with the Go standard library and the platform's own scan symbol package pre-registered, so you can both import standard library packages and import "scan". The interpreter executes func main() automatically, and the vulnerability findings, debug logs and HTTP request count produced by this run are collected centrally.
The interpreter's symbol key format is importpath/packagename, so what the host registers is "scan/scan"; after import "scan" in a POC you can use scan.Flow / scan.Report() and so on.
The full lifecycle of one execution:
- Build the run environment for this execution (including the shared
http.Client+ Cookie session, see Chapter 6); - Execute your source in a controlled goroutine;
- Apply timeout protection (30s by default; on timeout the run fails and returns);
- Return the run's vulnerability findings, debug logs, HTTP request count and error.
The interpreter has no forced cancellation mechanism. After a timeout the current call returns an error immediately, but the interpreting goroutine stays in the background until it finishes on its own. Therefore never write unbounded loops or long Sleep calls in a POC, or background goroutines will accumulate.
4.4 Runtime Parameters
Runtime parameters fixed by the platform when a POC executes (you do not configure them; just know the behavior):
| Parameter | Default | Notes |
|---|---|---|
| Single-run timeout | 30s | Also used as the HTTP client timeout; a timeout marks the execution as failed |
| Proxy | empty (direct) | HTTP proxy address, delivered at task level |
| Certificate verification | certificate errors are ignored for both debugging and tasks | Makes it easy to test self-signed lab targets |
| Redirects | not followed by default | Enabled by the debug/task side when needed |
| Response body limit | 1MB | Maximum bytes read from a single response; anything beyond is truncated |
Which keys actually appear in scan.Config depends on the call path:
- Scan task:
timeout,proxy,seed,taskIde; - GUI debugging (
RunGoPocTest):timeout,proxy; - Unified test verification (the go branch of
RunPocTest):target.
5. The Injected scan Symbols, One by One
Every symbol below is accessed through the scan. prefix after import "scan". The signature in a section title is the authoritative signature you can use. String functions operate on bytes (for multi-byte UTF-8, Len/Substr count bytes), so take care with non-ASCII content.
Each section has a fixed reading order: Purpose (when to call it) → Signature (given verbatim) → Parameter table (what to pass / where it comes from / example value) → Returns (type + struct field table + what is returned when no value is available) → Code snippet (paste straight into your POC) → Notes.
Data Objects
5.1 scan.Flow
Purpose: the request flow currently being processed (passive traffic or a test packet). Almost every POC takes the target to hit from it; do not hard-code absolute URLs.
Type: struct pointer (in scripts, access it as scan.Flow.field).
Field table:
| Field | Type | Meaning | Who fills it, in what format |
|---|---|---|---|
URL | string | Full request URL | Copied from the original URL in the packet |
Method | string | Request method | Copied from the packet (such as GET/POST) |
Host | string | Target host name | Prefers the domain in the packet; when empty, the host name (without port) is parsed from the URL |
Scheme | string | Scheme | Starts from the packet's TLS flag, then is overridden by the scheme parsed from the URL; the result is http/https |
Path | string | Request path | Filled from the path part of the URL |
Query | string | Query parameters | Filled from the URL's raw query string (without ?) |
Headers | map[string]string | Request headers | Parsed from the raw request header text, with keys already lowercased; access as scan.Flow.Headers["user-agent"] |
Body | string | Request body | Copied from the packet's request body and converted to a string |
RawHeaders | string | Raw request header text (multiple lines) | Copied from the packet's raw request header text |
Code snippet:
scan.Log(scan.Flow.Method + " " + scan.Flow.URL)
scan.Log("host=" + scan.Flow.Host + " path=" + scan.Flow.Path + " q=" + scan.Flow.Query)
// 取请求头(key 是小写)
if ua, ok := scan.Flow.Headers["user-agent"]; ok {
scan.Log("UA=" + ua)
}
// 用 Flow 里的 Body 直接判定
if scan.Contains(scan.Flow.Body, "password") {
scan.Report(scan.VulnFinding{
Name: "请求体敏感字段",
Detail: "被动流量请求体中出现 password 字段",
Level: "low",
URL: scan.Flow.URL,
})
}
Notes:
- When no flow is available you receive an empty
Flow(Headersis an empty map and the remaining fields are empty strings); it does not panic. Headerskeys are always lowercase;scan.Flow.Headers["User-Agent"]retrieves nothing.- When you need the "complete current request as raw text", read
RawHeaders+Body; do not assemble it yourself.
5.2 scan.Config
Purpose: read task-level configuration (timeout/proxy/random seed, etc.). Different call paths inject different keys, so always check for an empty value before using one.
Type: map[string]string.
Value table:
| Key | When it appears | Meaning | Example value |
|---|---|---|---|
timeout | Scan task, GUI debugging | Execution timeout (seconds) | "30" |
proxy | Scan task, GUI debugging | Proxy address | "http://127.0.0.1:8080" |
seed | Scan task | Scan speed/depth seed | "1" |
taskIde | Scan task | Task identifier | "task-..." |
target | Unified test verification (RunPocTest) | Target URL | "https://target/" |
Code snippet:
timeout := scan.Config["timeout"]
if timeout == "" {
timeout = "30"
}
proxy := scan.Config["proxy"]
if proxy != "" {
scan.Log("本次经代理: " + proxy)
}
scan.Log("timeout=" + timeout + " target=" + scan.Config["target"])
Notes:
scan.Config["missing"]returns an empty string rather than an error; even so, your code must tolerate empty values.- The same key may be absent on some entry points (for example
RunPocTesthas noseed); never assume it is present. Configis for reading only; do not write into it (writes would not affect the host anyway).
Types
5.3 scan.VulnFinding
Purpose: the argument to scan.Report; a complete description of one vulnerability finding. You construct it once you decide something "hit".
How to construct: scan.VulnFinding{field: value, ...} (the type symbol itself is injected by the host, and field names must be exact).
Field table:
| Field | Type | Meaning | How to use |
|---|---|---|---|
Name | string | Vulnerability name | Empty uses the template's @meta:Name; when set it overrides the card title and clears the template's multilingual snapshot |
Detail | string | Vulnerability details | Empty uses the template description; when set it overrides the card's "Details" |
Evidence | string | Hit evidence (response fragment / message text) | Displayed as the "response" when Request/Response are not set |
Level | string | Severity critical/high/medium/low | Mapped to 4/3/2/1; empty or unrecognized falls back to the template level, and if that is also empty, low severity |
URL | string | Hit URL | Empty uses the current Flow.URL |
CVEId | string | CVE number | Empty uses the template's @meta:CVEId; when set it overrides |
Request | string | Triggering request message (text) | Displayed together with Response as a request-response pair |
Response | string | Hit response message (text) | Same as above |
SensitiveText | string | Raw fragment of the sensitive information (200 characters before and after the hit) | Used only by sensitive-information templates; highlighted in the GUI |
SensitiveKeywords | []string | List of keywords actually matched | The GUI plain-text viewer highlights based on it |
SensitiveMatchRule | string | Formula/rule actually matched | Sensitive-information templates record the matching basis here |
Code snippet:
scan.Report(scan.VulnFinding{
Name: "未授权访问",
Detail: "后台接口在未携带凭证时返回了管理数据",
Evidence: scan.Substr(resp.Body, 0, 200),
Level: "high",
URL: resp.URL,
CVEId: "CVE-2024-0001",
Request: "GET " + scan.Flow.URL,
Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
})
Notes:
Levelaccepts only the four lowercase valuescritical/high/medium/low;HIGHor高危is treated as unrecognized.SensitiveText/SensitiveKeywords/SensitiveMatchRuleare specific to vuln-000007 sensitive-information highlighting; ordinary POCs need not set them.- One
main()may callReportseveral times; each call produces its own vulnerability card (subject to the 404-baseline false-positive gate in Chapter 7).
5.4 scan.HTTPResponse
Purpose: the return type of every HTTP call (scan.HTTP and the convenience wrappers). It is how you tell whether a request succeeded and read the body, headers and Cookies.
Field table:
| Field | Type | Meaning | How to use |
|---|---|---|---|
StatusCode | int | HTTP status code | 0 means the request failed (read Error in that case); otherwise 200/302/404 and so on |
Body | string | Response body text | Trimmed by the 1MB limit (MaxBodySize) |
Headers | map[string]string | Response headers, lowercase keys | resp.Headers["content-type"] |
Cookies | []string | Raw list of response Set-Cookie values | len(resp.Cookies) > 0 tells whether a session was issued |
URL | string | Final request URL (including after redirects) | resp.URL is more accurate when reporting |
TimeMs | int64 | Request duration (milliseconds) | For time-based blind detection |
RawHeaders | string | Raw response header text (multiple lines) | Concatenate with Body to display the response message |
Error | string | Error message when the request fails (empty on success) | Check Error before checking StatusCode |
Code snippet:
resp := scan.HTTPGet("https://example.com/")
if resp.Error != "" {
scan.Log("请求失败: " + resp.Error)
return
}
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode) + " 耗时=" + scan.JSONDump(resp.TimeMs) + "ms")
scan.Log("content-type=" + resp.Headers["content-type"])
scan.Log("Set-Cookie 数=" + scan.JSONDump(len(resp.Cookies)))
Notes:
- On a failed request
StatusCodeis 0,Bodyis empty andErroris non-empty; always checkErrorfirst. Headerskeeps only the first value of a repeated header, and all keys are lowercase.Bodyreads at mostMaxBodySize(1MB by default); anything beyond is silently truncated.- The
scanpackage does not injectItoa; to splice numbers into a log, usescan.JSONDump(number)(which returns"123").
Core
5.5 scan.Log(msg string)
Purpose: emit a debug log. This is the only way to trace "how far the POC got"; both the GUI debug panel and the node logs show it.
Signature:
scan.Log(msg string)
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
msg | string | Any debug text | Constants, scan.Flow.URL, a fragment of resp.Body, etc. | "开始检测" |
Returns: nothing. The log is appended to this run's log buffer and handed back to the node with the execution result.
Code snippet:
scan.Log("=== 阶段 1/3:探测 ===")
resp := scan.HTTPGet(scan.Flow.URL)
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode) + " len=" + scan.JSONDump(scan.Len(resp.Body)))
scan.Log("片段=" + scan.Substr(resp.Body, 0, 120))
Notes:
- Do not print a huge response body (such as the whole
resp.Body) to the log; it blows up the debug panel and the TCP payload. Truncate withscan.Substr. scan.Lognever interrupts execution; it is pure output.- Log order is code order, so it can be used to verify which branch ran.
5.6 scan.Report(v VulnFinding)
Purpose: report a vulnerability finding. This is the POC's "hit exit" — do not call it and nothing will produce a vulnerability card, however correct the rest is.
Signature:
scan.Report(v VulnFinding)
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
v | scan.VulnFinding | One finding | Build it with a struct literal (fields in 5.3) | scan.VulnFinding{Name:"...", Level:"high"} |
Returns: nothing. Each finding is appended to this run's result set.
Code snippet:
if scan.Contains(resp.Body, "SQL syntax") {
scan.Report(scan.VulnFinding{
Name: "SQL 注入",
Detail: "响应出现数据库报错特征",
Evidence: scan.Substr(resp.Body, 0, 300),
Level: "high",
URL: resp.URL,
})
scan.Log("已上报 SQL 注入")
} else {
scan.Log("未命中")
}
Notes:
Levelis required and only the four lowercase values are recognized; an empty value falls back to the template level.- If the found response page is similar to the task's 404 baseline beyond the threshold, the false-positive gate discards it outright (nothing is stored).
- Once
Name/Detailare set, the card uses your single-language text and the template's multilingual snapshot is no longer used (see Chapter 7).
5.7 scan.HTTP(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse
Purpose: send an HTTP request with an arbitrary method through the shared Cookie session. Use it when you need a custom method, headers or body.
Signature (verbatim from the authoritative symbol table injected by the platform):
scan.HTTP(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
method | string | HTTP method; an empty string is treated as GET | A constant or scan.Flow.Method | "POST" |
reqURL | string | Full URL | scan.Flow.URL, or assembled from scan.URLHost/URLScheme | "https://t/api/login" |
headers | map[string]string | Custom request headers; nil adds none | A map literal | map[string]string{"Content-Type":"application/json"} |
body | string | Request body; an empty string means no body | A constant or an assembled string | "id=1' AND 1=1--" |
Returns: scan.HTTPResponse (fields in 5.4). On failure Error is non-empty, StatusCode=0 and Body is an empty string.
Code snippet (custom headers + Cookie session: log in first, then access a protected endpoint):
base := scan.URLScheme(scan.Flow.URL) + "://" + scan.URLHost(scan.Flow.URL)
// 第一步:登录(自定义请求头;Set-Cookie 自动进共享 jar)
login := scan.HTTP("POST", base+"/api/login", map[string]string{
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (TestSecScan)",
}, `{"username":"admin","password":"admin123"}`)
if login.Error != "" {
scan.Log("登录请求失败: " + login.Error)
return
}
// 第二步:带自定义头访问受保护接口(Cookie 由共享 jar 自动携带,无需手写)
resp := scan.HTTP("GET", base+"/api/admin/users", map[string]string{
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json",
}, "")
if resp.Error == "" && resp.StatusCode == 200 && scan.Contains(resp.Body, "\"role\":\"admin\"") {
scan.Report(scan.VulnFinding{
Name: "默认口令登录并访问后台",
Detail: "使用默认口令登录后,在同一会话下访问到受保护接口",
Evidence: scan.Substr(resp.Body, 0, 300),
Level: "critical",
URL: resp.URL,
Request: "GET " + base + "/api/admin/users",
Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
})
}
Notes:
- When
method == ""the underlying layer treats it asGET; writing the method explicitly is clearer. - A header with the same name overrides the Cookie from the shared jar; to "log in first, then access", do not hand-write a Cookie header with the same name.
- Passing
nilforheadersis fine; iteration will not panic.
Convenience HTTP
5.8 scan.HTTPGet(rawurl string) scan.HTTPResponse
Purpose: send a GET request. The most common probing entry point, equivalent to scan.HTTP("GET", rawurl, nil, "").
Signature:
scan.HTTPGet(rawurl string) scan.HTTPResponse
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | scan.Flow.URL, or an assembled target | "https://target/" |
Returns: scan.HTTPResponse (fields in 5.4).
Code snippet:
resp := scan.HTTPGet(scan.Flow.URL)
if resp.Error != "" || resp.StatusCode != 200 {
scan.Log("GET 失败或无内容")
return
}
if scan.Contains(resp.Body, "admin dashboard") {
scan.Report(scan.VulnFinding{
Name: "未授权访问",
Detail: "未携带凭证即可访问管理页",
Evidence: scan.Substr(resp.Body, 0, 300),
Level: "high",
URL: resp.URL,
})
}
Notes:
- It adds no request headers of its own (
headersisnil); usescan.HTTP/scan.HTTPHeaderwhen a specific header is needed. - It carries the shared Cookie session automatically.
- It does not follow redirects (unless a task/debug session sets
AllowRedirectsto true).
5.9 scan.HTTPPost(rawurl, body string) scan.HTTPResponse
Purpose: send a POST request. Equivalent to scan.HTTP("POST", rawurl, nil, body).
Signature:
scan.HTTPPost(rawurl, body string) scan.HTTPResponse
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | An assembled target | "https://target/api" |
body | string | Request body | A form string or arbitrary text | "user=admin&pass=admin" |
Returns: scan.HTTPResponse (fields in 5.4).
Code snippet:
resp := scan.HTTPPost(scan.Flow.URL, "id=1' AND 1=1--")
if resp.Error == "" && scan.Contains(resp.Body, "SQL syntax") {
scan.Report(scan.VulnFinding{
Name: "SQL 注入",
Detail: "POST 参数注入后返回数据库报错",
Evidence: scan.Substr(resp.Body, 0, 300),
Level: "high",
URL: resp.URL,
})
}
Notes:
- It does not set
Content-Typeautomatically; forms usually need it (switch toscan.HTTPand addapplication/x-www-form-urlencodedexplicitly). - An empty
bodystring means no request body. - The shared Cookie session works as usual.
5.10 scan.HTTPJSONPost(rawurl, jsonBody string) scan.HTTPResponse
Purpose: send a JSON POST, automatically adding Content-Type: application/json. The most common choice for login and API probing.
Signature:
scan.HTTPJSONPost(rawurl, jsonBody string) scan.HTTPResponse
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | An assembled target | "https://target/api/login" |
jsonBody | string | JSON text | Hand-written, or scan.JSONDump(object) | {"user":"admin"} |
Returns: scan.HTTPResponse (fields in 5.4).
Code snippet:
resp := scan.HTTPJSONPost("https://api.example.com/login", `{"user":"admin","pass":"admin"}`)
if resp.Error != "" {
scan.Log("登录失败: " + resp.Error)
return
}
code := scan.JSONGet(resp.Body, "code")
scan.Log("登录 code=" + code)
if code == "0" || len(resp.Cookies) > 0 {
scan.Log("拿到会话,继续")
}
Notes:
- It only adds
Content-Type; it does not serialize a map into JSON, sojsonBodymust be a valid JSON string. - If the target needs other headers (such as
X-Token), switch toscan.HTTP/scan.HTTPHeader. - Response
Set-Cookievalues automatically enter the shared jar.
5.11 scan.HTTPHeader(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse
Purpose: an alias of scan.HTTP (the host injects the same function); the semantic name emphasizes "custom headers are attached".
Signature:
scan.HTTPHeader(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse
Parameter table: identical to scan.HTTP.
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
method | string | HTTP method; empty means GET | A constant | "PUT" |
reqURL | string | Full URL | scan.Flow.URL | "https://t/api" |
headers | map[string]string | Custom request headers | A map literal | map[string]string{"Authorization":"Bearer x"} |
body | string | Request body; an empty string means none | A constant | "" |
Returns: scan.HTTPResponse (fields in 5.4).
Code snippet:
resp := scan.HTTPHeader("GET", scan.Flow.URL, map[string]string{
"Authorization": "Bearer " + scan.Config["token"],
"Accept": "application/json",
}, "")
if resp.Error == "" && resp.StatusCode == 200 {
scan.Log("鉴权接口可访问,len=" + scan.JSONDump(scan.Len(resp.Body)))
}
Notes:
- It is the same function as
scan.HTTPwith identical behavior; pick whichever reads better. - Do not use
scan.HTTPandscan.HTTPHeaderside by side in a confusing way; stay consistent within a file. - Request header key case is normalized by the underlying
Header.Set, so do not worry about it.
Strings
5.12 scan.ToLower(s string) string
Purpose: convert to lowercase. The standard step before case-insensitive matching.
Signature: scan.ToLower(s string) string (equivalent to strings.ToLower).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Any string | resp.Body, etc. | "Root:X:0:0" |
Returns: string. An empty string in, an empty string out.
Code snippet:
if scan.Contains(scan.ToLower(resp.Body), "root:x:0:0") {
scan.Log("命中 passwd 特征")
}
Notes: it performs a Unicode lowercase mapping only and does not change length semantics.
5.13 scan.ToUpper(s string) string
Purpose: convert to uppercase.
Signature: scan.ToUpper(s string) string (equivalent to strings.ToUpper).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Any string | resp.Body | "abc" |
Returns: string.
Code snippet:
scan.Log("UPPER=" + scan.ToUpper(scan.Substr(resp.Body, 0, 20)))
Notes: combine with ToLower for normalized comparisons.
5.14 scan.Trim(s string) string
Purpose: strip leading and trailing whitespace (including newlines and tabs). Commonly used to clean response fragments or form values.
Signature: scan.Trim(s string) string (equivalent to strings.TrimSpace).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Any string | Response text | " admin\n" |
Returns: string (the result after leading and trailing whitespace is removed).
Code snippet:
token := scan.Trim(scan.RegexExtract(resp.Body, `token:\s*(\S+)`))
if token != "" {
scan.Log("token=" + token)
}
Notes: it strips only the ends, not whitespace in the middle.
5.15 scan.TrimCut(s, cutset string) string
Purpose: strip any characters in the specified set of characters from both ends (note: not a whole substring).
Signature: scan.TrimCut(s, cutset string) string (equivalent to strings.Trim).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | String to trim | Response text | "///admin///" |
cutset | string | Set of characters to remove | A constant | "/" |
Returns: string.
Code snippet:
p := scan.TrimCut("/admin/users/", "/")
scan.Log("clean path=" + p) // 输出 admin/users
Notes:
- The second parameter is a "character set", not a "substring":
TrimCut(s, "ab")removes every leading and trailingaorb. - To trim a whole substring, use
scan.Replace(s, substring, "").
5.16 scan.Replace(s, old, new string) string
Purpose: replace all occurrences of old in s with new.
Signature: scan.Replace(s, old, new string) string (equivalent to strings.ReplaceAll).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Original string | Response text | "a-b-c" |
old | string | Text to be replaced | A constant | "-" |
new | string | Replacement text | A constant | "_" |
Returns: string.
Code snippet:
clean := scan.Replace(scan.Flow.Path, "..", "")
scan.Log("clean=" + clean)
Notes: when old is an empty string it inserts new between every character (same as strings.ReplaceAll), which is usually not what you want.
5.17 scan.Split(s, sep string) []string
Purpose: split a string into a slice by a separator.
Signature: scan.Split(s, sep string) []string (equivalent to strings.Split).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Original string | Response text | "a,b,c" |
sep | string | Separator | A constant | "," |
Returns: []string; when sep is empty the string is split by character.
Code snippet:
parts := scan.Split(resp.Headers["set-cookie"], ";")
for i := 0; i < len(parts); i++ {
scan.Log("cookie part[" + scan.JSONDump(i) + "]=" + scan.Trim(parts[i]))
}
Notes:
- Under interpretation, an index +
len()loop is the most reliable; avoid type-inference differences introduced byrange. - Use
scan.Jointo put the slice back together.
5.18 scan.Join(elems []string, sep string) string
Purpose: join a string slice into one string with a separator.
Signature: scan.Join(elems []string, sep string) string (equivalent to strings.Join).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
elems | []string | String slice | The result of scan.Split, scan.RegexExtractAll, etc. | []string{"a","b"} |
sep | string | Separator | A constant | "," |
Returns: string.
Code snippet:
lines := scan.Split(scan.Flow.RawHeaders, "\n")
scan.Log("头行数=" + scan.JSONDump(len(lines)) + " 拼接=" + scan.Join(lines, " | "))
Notes: a nil or empty elems returns an empty string.
5.19 scan.Contains(s, substr string) bool
Purpose: test whether s contains a substring. The workhorse for hit decisions.
Signature: scan.Contains(s, substr string) bool (equivalent to strings.Contains).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | String to search | resp.Body | "hello world" |
substr | string | Substring to find | A constant | "world" |
Returns: bool (true when contained). An empty substr is always true.
Code snippet:
if scan.Contains(resp.Body, "SQL syntax") || scan.Contains(resp.Body, "mysql_fetch") {
scan.Report(scan.VulnFinding{Name: "SQL 报错", Level: "high", Evidence: scan.Substr(resp.Body, 0, 200)})
}
Notes: it is case-sensitive; run scan.ToLower first for case-insensitive matching.
5.20 scan.HasPrefix(s, prefix string) bool
Purpose: test a prefix.
Signature: scan.HasPrefix(s, prefix string) bool (equivalent to strings.HasPrefix).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | String to check | resp.Body | "http://x" |
prefix | string | Prefix | A constant | "http" |
Returns: bool.
Code snippet:
if scan.HasPrefix(resp.Body, "<?xml") {
scan.Log("响应是 XML")
}
Notes: an empty prefix is always true.
5.21 scan.HasSuffix(s, suffix string) bool
Purpose: test a suffix.
Signature: scan.HasSuffix(s, suffix string) bool (equivalent to strings.HasSuffix).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | String to check | A path | "/admin/" |
suffix | string | Suffix | A constant | "/" |
Returns: bool.
Code snippet:
if scan.HasSuffix(scan.Flow.Path, ".php") {
scan.Log("PHP 目标")
}
Notes: an empty suffix is always true.
5.22 scan.Index(s, substr string) int
Purpose: return the byte index of the first occurrence of the substring, or -1 when it is absent.
Signature: scan.Index(s, substr string) int (equivalent to strings.Index).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | String to search | resp.Body | "abcabc" |
substr | string | Substring | A constant | "bc" |
Returns: int; -1 when not found (never panics).
Code snippet:
pos := scan.Index(resp.Body, "password")
if pos >= 0 {
scan.Log("password 出现在字节位置 " + scan.JSONDump(pos))
}
Notes: the value is a byte index and is not the character position for non-ASCII text.
5.23 scan.Substr(s string, start, end int) string
Purpose: take the substring s[start:end], clamping out-of-range indexes automatically. The standard tool for truncating logs and evidence.
Signature: scan.Substr(s string, start, end int) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Original string | resp.Body | "abcdef" |
start | int | Start index (inclusive) | A constant | 0 |
end | int | End index (exclusive) | A constant | 3 |
Returns: string. Clamping rules: start<0 → 0; end>len(s) → len(s); start>=end → an empty string.
Code snippet:
scan.Log("前 200 字节=" + scan.Substr(resp.Body, 0, 200))
scan.Log("倒数片段=" + scan.Substr(resp.Body, scan.Len(resp.Body)-100, scan.Len(resp.Body)))
Notes:
- It slices by byte, so it may cut a multi-byte UTF-8 character in half; be lenient with non-ASCII content.
start >= end, or any out-of-range empty range, returns an empty string and never panics.
5.24 scan.Len(s string) int
Purpose: get the byte length of a string.
Signature: scan.Len(s string) int.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Any string | resp.Body | "hello" |
Returns: int (a byte count, not a character count).
Code snippet:
if scan.Len(resp.Body) > 5000 {
scan.Log("响应较大,仅取前段判定")
}
Notes: one Chinese character is usually 3 bytes, so Len("中文")==6.
5.25 scan.Repeat(s string, count int) string
Purpose: repeat a string count times.
Signature: scan.Repeat(s string, count int) string (equivalent to strings.Repeat).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Original string | A constant | "A" |
count | int | Repetitions | A constant | 5 |
Returns: string.
Code snippet:
// 构造超长参数触发异常
payload := scan.Repeat("A", 5000)
resp := scan.HTTPPost(scan.Flow.URL, "name="+payload)
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode))
Notes:
- A negative
countpanics (same asstrings.Repeat), so make sure it is non-negative. - Do not build excessively large strings; they consume execution time and memory.
Encoding / Hashing
5.26 scan.Base64Encode(s string) string
Purpose: standard Base64 encoding (commonly used to build Basic auth or encode payloads).
Signature: scan.Base64Encode(s string) string (base64.StdEncoding.EncodeToString).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Plain text | Credentials, a payload | "admin:admin" |
Returns: string (Base64 text).
Code snippet:
auth := scan.Base64Encode("admin:admin")
resp := scan.HTTPHeader("GET", scan.Flow.URL, map[string]string{
"Authorization": "Basic " + auth,
}, "")
scan.Log("Basic 状态码=" + scan.JSONDump(resp.StatusCode))
Notes: it uses the standard alphabet (including + / =), not the URL-safe variant.
5.27 scan.Base64Decode(s string) string
Purpose: Base64 decoding; returns an empty string when decoding fails.
Signature: scan.Base64Decode(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Base64 text | An encoded field in the response | "YWRtaW4=" |
Returns: string; invalid Base64 returns an empty string (no error).
Code snippet:
decoded := scan.Base64Decode(scan.RegexExtract(resp.Body, `data=([A-Za-z0-9+/=]+)`))
if scan.Contains(decoded, "secret") {
scan.Report(scan.VulnFinding{Name: "编码信息泄露", Level: "medium", Evidence: scan.Substr(decoded, 0, 200)})
}
Notes: only the standard alphabet is accepted; URL-safe input (-_) fails and returns an empty string.
5.28 scan.URLEncode(s string) string
Purpose: URL query escaping (encodes & / space and so on).
Signature: scan.URLEncode(s string) string (equivalent to url.QueryEscape).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Plain text | A parameter value | "a b&c" |
Returns: string.
Code snippet:
payload := "1' OR '1'='1"
resp := scan.HTTPGet(scan.Flow.URL + "/?id=" + scan.URLEncode(payload))
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode))
Notes: QueryEscape encodes a space as +; encode path segments another way.
5.29 scan.URLDecode(s string) string
Purpose: URL unescaping; returns an empty string on failure.
Signature: scan.URLDecode(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Escaped string | A response or URL | "a%20b" |
Returns: string; an invalid escape returns an empty string.
Code snippet:
raw := scan.URLDecode(scan.Flow.Query)
scan.Log("解码后的查询参数=" + raw)
Notes: it correctly handles + as a space (QueryUnescape semantics).
5.30 scan.HexEncode(s string) string
Purpose: hexadecimal encoding.
Signature: scan.HexEncode(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Plain text | Anything | "abc" |
Returns: string (lowercase hexadecimal).
Code snippet:
scan.Log("hex=" + scan.HexEncode("abc")) // 616263
Notes: the output is lowercase.
5.31 scan.HexDecode(s string) string
Purpose: hexadecimal decoding; returns an empty string on failure.
Signature: scan.HexDecode(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Hexadecimal text | A response field | "616263" |
Returns: string; an odd length or an invalid character returns an empty string.
Code snippet:
b := scan.HexDecode("616263")
scan.Log("decoded=" + b) // abc
Notes: only even-length hexadecimal strings are accepted.
5.32 scan.MD5(s string) string
Purpose: compute a lowercase hexadecimal MD5 digest (a weak hash for signature/fingerprint comparison, not for security).
Signature: scan.MD5(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Plain text | Anything | "admin" |
Returns: string (32 lowercase hexadecimal characters).
Code snippet:
sig := scan.MD5("token=" + scan.RandString(8))
scan.Log("md5=" + sig)
Notes: MD5 is only for verification/fingerprints and must not be used for cryptographic security.
5.33 scan.SHA1(s string) string
Purpose: lowercase hexadecimal SHA1 digest.
Signature: scan.SHA1(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Plain text | Anything | "abc" |
Returns: string (40 lowercase hexadecimal characters).
Code snippet:
scan.Log("sha1=" + scan.SHA1("abc"))
Notes: the output format matches MD5/SHA256; all are lowercase hexadecimal.
5.34 scan.SHA256(s string) string
Purpose: lowercase hexadecimal SHA256 digest.
Signature: scan.SHA256(s string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Plain text | Anything | "abc" |
Returns: string (64 lowercase hexadecimal characters).
Code snippet:
if scan.SHA256(resp.Body) == scan.Config["expectHash"] {
scan.Log("内容哈希匹配")
}
Notes: it hashes the raw bytes with no normalization.
Regex
5.35 scan.RegexMatch(s, pattern string) bool
Purpose: test whether a regular expression matches (without extracting anything).
Signature: scan.RegexMatch(s, pattern string) bool.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Text to search | resp.Body | "root:x:0:0" |
pattern | string | Regular expression | A constant | "(?i)root:x:0:0" |
Returns: bool; when the regex is invalid, the error from regexp.MatchString is ignored and false is returned.
Code snippet:
if scan.RegexMatch(resp.Body, `(?i)root:x:0:0`) {
scan.Report(scan.VulnFinding{Name: "敏感文件泄露", Level: "high", Evidence: scan.Substr(resp.Body, 0, 200)})
}
Notes: a wrong regex raises no error, it just never matches — while debugging, print the text to be matched with scan.Log first.
5.36 scan.RegexExtract(s, pattern string) string
Purpose: extract the first match; when pattern contains capture groups it returns group 1, otherwise the whole match.
Signature: scan.RegexExtract(s, pattern string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Text to search | resp.Body | "user: admin" |
pattern | string | Regex (capture groups recommended) | A constant | "user:\s*(\w+)" |
Returns: string; an empty string when there is no match, the regex is invalid, or group 1 is empty with no whole match.
Code snippet:
user := scan.RegexExtract(resp.Body, `root:([^:]+)`)
if user != "" {
scan.Log("提取到 user=" + user)
}
Notes:
- When there are several capture groups, only group 1 is returned (
m[1]). - When group 1 matches an empty string it falls back to the whole match
m[0]. - To obtain several groups, write several
RegexExtractcalls.
5.37 scan.RegexExtractAll(s, pattern, sep string) string
Purpose: extract all matches and join them into one string with sep.
Signature: scan.RegexExtractAll(s, pattern, sep string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Text to search | resp.Body | "a1 b2 c3" |
pattern | string | Regex | A constant | "\d+" |
sep | string | Join separator | A constant | "," |
Returns: string; an invalid regex returns an empty string; no match also returns an empty string.
Code snippet:
nums := scan.RegexExtractAll(resp.Body, `\d+`, ",")
scan.Log("所有数字=" + nums)
Notes: the return value is a joined string, not a slice; to get a slice, use scan.Split(nums, ",").
JSON
5.38 scan.JSONGet(jsonStr, path string) string
Purpose: read a value from a JSON string by dot path. The path may mix object keys and array indexes.
Signature: scan.JSONGet(jsonStr, path string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
jsonStr | string | JSON text | resp.Body | {"data":{"role":"admin"}} |
path | string | Dot path supporting a.b.c and items.0.name | A constant | "data.role" |
Returns: string. Rules: string values are returned as-is; null → an empty string; objects/arrays → serialized back into a JSON string; a missing path or invalid JSON → an empty string.
Code snippet:
if !scan.JSONValid(resp.Body) {
scan.Log("响应不是 JSON,跳过")
return
}
role := scan.JSONGet(resp.Body, "data.user.role")
first := scan.JSONGet(resp.Body, "items.0.name")
scan.Log("role=" + role + " first=" + first)
if role == "admin" {
scan.Report(scan.VulnFinding{Name: "越权", Level: "critical", Evidence: scan.Substr(resp.Body, 0, 300)})
}
Notes:
- Numeric values are also returned as strings (such as
"1"); remember the quotes when comparing. - Empty path segments are handled with
Trim(path, "."), which removes leading and trailing dots. - To take a nested object as a whole,
JSONGetreturns its JSON text.
5.39 scan.JSONValid(s string) bool
Purpose: test whether a string is valid JSON.
Signature: scan.JSONValid(s string) bool (json.Valid).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
s | string | Text to check | resp.Body | {"ok":true} |
Returns: bool.
Code snippet:
if scan.JSONValid(resp.Body) {
scan.Log("code=" + scan.JSONGet(resp.Body, "code"))
} else {
scan.Log("非 JSON 响应,走正则回退分支")
}
Notes: it validates syntax only and does not parse the structure; a later value lookup can still return an empty string because the path does not exist.
5.40 scan.JSONDump(v interface{}) string
Purpose: serialize any value into a JSON string. It is also the standard way to splice numbers/booleans into a log (scan does not inject Itoa).
Signature: scan.JSONDump(v interface{}) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
v | interface{} | Any value | A map/slice/number/boolean | map[string]int{"code":200} |
Returns: string; serialization failure returns an empty string.
Code snippet:
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode) + " 耗时=" + scan.JSONDump(resp.TimeMs))
scan.Log("汇总=" + scan.JSONDump(map[string]int{"http": 1, "found": 1}))
Notes:
- A number yields
"200"while a string yields a quoted"\"ok\""— when building logs, prefer numbers/maps over bare strings. - Values that cannot be serialized (such as functions) return an empty string.
Random / Time
5.41 scan.RandInt(min, max int) int
Purpose: generate a random integer in the range [min, max).
Signature: scan.RandInt(min, max int) int.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
min | int | Lower bound (inclusive) | A constant | 1 |
max | int | Upper bound (exclusive) | A constant | 100 |
Returns: int; when max<=min it is treated as max=min+1, that is, it returns min.
Code snippet:
n := scan.RandInt(1, 100)
scan.Log("随机整数=" + scan.JSONDump(n))
if n < 50 {
scan.Log("落在上半区")
}
Notes: it uses crypto/rand and is concurrency-safe; the range is half-open.
5.42 scan.RandString(n int) string
Purpose: generate an n-character random string (upper and lower case letters + digits). Ideal as a reverse-connection marker or a cache-busting parameter.
Signature: scan.RandString(n int) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
n | int | Desired length | A constant | 12 |
Returns: string; when n<=0 it uses 8 characters.
Code snippet:
marker := scan.RandString(12)
resp := scan.HTTPGet(scan.Flow.URL + "/?cache=" + marker)
scan.Log("marker=" + marker + " 状态码=" + scan.JSONDump(resp.StatusCode))
Notes: the alphabet is a-zA-Z0-9; for letters only or digits only use RandAlpha/RandNum.
5.43 scan.RandAlpha(n int) string
Purpose: generate an n-character random lowercase letter string.
Signature: scan.RandAlpha(n int) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
n | int | Desired length | A constant | 6 |
Returns: string; when n<=0 it uses 8 characters.
Code snippet:
scan.Log("alpha=" + scan.RandAlpha(6))
Notes: the alphabet is only abcdefghijklmnopqrstuvwxyz, with no uppercase.
5.44 scan.RandNum(n int) string
Purpose: generate an n-character random digit string.
Signature: scan.RandNum(n int) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
n | int | Desired length | A constant | 6 |
Returns: string; when n<=0 it uses 8 characters.
Code snippet:
scan.Log("num=" + scan.RandNum(6))
Notes: the alphabet is only 0123456789; the first character may be 0.
5.45 scan.UUID() string
Purpose: generate a UUID v4. Useful as an idempotency key or a temporary resource name.
Signature: scan.UUID() string (no parameters).
Parameter table: none.
Returns: string, shaped like xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.
Code snippet:
id := scan.UUID()
resp := scan.HTTPJSONPost(scan.Flow.URL, `{"id":"`+id+`"}`)
scan.Log("uuid=" + id + " 状态码=" + scan.JSONDump(resp.StatusCode))
Notes: every call generates a new value (crypto/rand).
5.46 scan.Timestamp() int64
Purpose: get the current Unix timestamp in seconds.
Signature: scan.Timestamp() int64 (no parameters).
Parameter table: none.
Returns: int64 (Unix seconds).
Code snippet:
scan.Log("ts=" + scan.JSONDump(scan.Timestamp()))
Notes: the return value is int64; splice it into a log with scan.JSONDump.
5.47 scan.TimestampMs() int64
Purpose: get the current Unix timestamp in milliseconds.
Signature: scan.TimestampMs() int64 (no parameters).
Parameter table: none.
Returns: int64 (Unix milliseconds).
Code snippet:
start := scan.TimestampMs()
scan.HTTPGet(scan.Flow.URL)
scan.Log("耗时≈" + scan.JSONDump(scan.TimestampMs()-start) + "ms")
Notes: to measure durations prefer resp.TimeMs; a TimestampMs difference includes extra overhead.
5.48 scan.Sleep(ms int)
Purpose: sleep for the given number of milliseconds. Used for reverse-connection polling and time-based blind waits.
Signature: scan.Sleep(ms int) (no return value).
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
ms | int | Milliseconds to sleep | A constant | 500 |
Returns: nothing.
Code snippet:
scan.HTTPGet(scan.Flow.URL)
scan.Sleep(500) // 等后端异步处理
resp := scan.HTTPGet(scan.Flow.URL + "/result")
scan.Log("轮询结果 len=" + scan.JSONDump(scan.Len(resp.Body)))
Notes:
Sleepconsumes the execution time budget, whose total timeout is 30s by default; control both the number of polls and the duration of each.- The interpreter cannot force cancellation, so a long
Sleepkeeps running in the background after the timeout.
URL Parsing
5.49 scan.URLHost(rawurl string) string
Purpose: get the host name of a URL (without the port).
Signature: scan.URLHost(rawurl string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | scan.Flow.URL | "https://a.com:8443/x" |
Returns: string; parsing failure returns an empty string. Note that Hostname() strips the port.
Code snippet:
host := scan.URLHost(scan.Flow.URL)
base := scan.URLScheme(scan.Flow.URL) + "://" + host
scan.Log("站点根=" + base)
Notes: URLHost returns the host name rather than the Host (no port); parse it yourself if the port is needed.
5.50 scan.URLPath(rawurl string) string
Purpose: get the path part of a URL.
Signature: scan.URLPath(rawurl string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | scan.Flow.URL | "https://a.com/a/b?x=1" |
Returns: string (such as /a/b); parsing failure returns an empty string.
Code snippet:
scan.Log("path=" + scan.URLPath(scan.Flow.URL))
Notes: the returned path excludes the query string (use URLQuery for that).
5.51 scan.URLQuery(rawurl string) string
Purpose: get the raw query string (without ?).
Signature: scan.URLQuery(rawurl string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | scan.Flow.URL | "https://a.com/a?x=1&y=2" |
Returns: string (such as x=1&y=2); parsing failure or no query returns an empty string.
Code snippet:
if scan.URLQuery(scan.Flow.URL) != "" {
scan.Log("带参目标: " + scan.URLDecode(scan.URLQuery(scan.Flow.URL)))
}
Notes: it returns the undecoded raw string; pair it with scan.URLDecode for readable text.
5.52 scan.URLScheme(rawurl string) string
Purpose: get the scheme (http/https).
Signature: scan.URLScheme(rawurl string) string.
Parameter table:
| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
rawurl | string | Full URL | scan.Flow.URL | "https://a.com/" |
Returns: string (http or https); parsing failure returns an empty string.
Code snippet:
if scan.URLScheme(scan.Flow.URL) == "https" {
scan.Log("HTTPS 目标")
}
Notes: to assemble a site root, the usual form is scan.URLScheme + "://" + scan.URLHost.
5.53 Full Symbol Table (Authoritative List)
The table below is the authoritative symbol list. Every scan.xxx in a POC must appear in it; symbols outside it (such as scan.Itoa) do not exist, and using one makes interpretation fail.
| Category | All symbols |
|---|---|
| Data | scan.Flow、scan.Config |
| Types | scan.VulnFinding、scan.HTTPResponse |
| Core | scan.Log、scan.Report、scan.HTTP |
| Convenience HTTP | scan.HTTPGet、scan.HTTPPost、scan.HTTPJSONPost、scan.HTTPHeader |
| Strings | scan.ToLower、scan.ToUpper、scan.Trim、scan.TrimCut、scan.Replace、scan.Split、scan.Join、scan.Contains、scan.HasPrefix、scan.HasSuffix、scan.Index、scan.Substr、scan.Len、scan.Repeat |
| Encoding/Hashing | scan.Base64Encode、scan.Base64Decode、scan.URLEncode、scan.URLDecode、scan.HexEncode、scan.HexDecode、scan.MD5、scan.SHA1、scan.SHA256 |
| Regex | scan.RegexMatch、scan.RegexExtract、scan.RegexExtractAll |
| JSON | scan.JSONGet、scan.JSONValid、scan.JSONDump |
| Random/Time | scan.RandInt、scan.RandString、scan.RandAlpha、scan.RandNum、scan.UUID、scan.Timestamp、scan.TimestampMs、scan.Sleep |
| URL parsing | scan.URLHost、scan.URLPath、scan.URLQuery、scan.URLScheme |
6. Cookie Session Semantics
Every POC run creates a shared http.Client + net/http/cookiejar, and all HTTP calls (scan.HTTP / scan.HTTPGet / scan.HTTPPost / scan.HTTPJSONPost / scan.HTTPHeader) go through the same client. Therefore:
- The
Set-Cookieobtained by the login in step one is stored in the jar automatically; - A request to the same site in step two carries the Cookie automatically;
- The two-step chain "log in first, then access a protected endpoint" works naturally, with no manual Cookie shipping.
base := scan.URLScheme(scan.Flow.URL) + "://" + scan.URLHost(scan.Flow.URL)
// 第一步:登录,Cookie 进 jar
scan.HTTPJSONPost(base+"/login", `{"user":"admin","pass":"admin"}`)
// 第二步:访问受保护接口,自动携带会话 Cookie
resp := scan.HTTPGet(base + "/admin/profile")
if resp.Error == "" && resp.StatusCode == 200 {
scan.Log("会话有效,len=" + scan.JSONDump(scan.Len(resp.Body)))
}
The headers parameter: the third argument of scan.HTTP(method, reqURL, headers, body) is a map[string]string used to set request headers; scan.HTTPHeader is exactly equivalent, with a more explicit name. If you set a Cookie header with the same name by hand, the explicit header overrides the same-named value in the session — that is normal behavior, but if you want to "log in first", do not hand-write a same-named Cookie.
Chains of three or more steps (login → privilege escalation → data retrieval) also work through the shared jar; just keep them executing sequentially inside the main() of the same run.
7. Returning Results: scan.Report and Vulnerability Cards
After scan.Report(scan.VulnFinding{...}) is called, the node assembles each finding into a vulnerability detail and reports it to the controller for storage. How each field affects the final vulnerability card:
VulnFinding field | Effect on the card |
|---|---|
Name | Overrides the template's @meta:Name as the card title; once overridden, the template's multilingual snapshot is cleared (the name produced by the POC is single-language text) |
Detail | Overrides the template description as the card's "Details"; it likewise clears the multilingual snapshot |
Evidence | When Request/Response are not set, it is displayed as the "response" in the request-response list |
Level | Mapped to a vulnerability severity: critical→4, high→3, medium→2, low→1; unrecognized or empty is treated as low severity; when empty it falls back to the template's @meta:Level |
URL | Hit URL; empty uses the current Flow.URL |
CVEId | Overrides the template's CVE number |
Request / Response | Displayed as a request-response message pair |
SensitiveText / SensitiveKeywords / SensitiveMatchRule | The three sensitive-information highlighting fields, used only by sensitive-information templates |
Before reporting there is one more gate: the 404-baseline false-positive gate. If the found response page is more similar to the task's 404 baseline than the threshold, it is judged a false positive and discarded outright. So do not report ordinary error pages as vulnerabilities, and do not leave Level empty — an empty severity is treated as low.
8. Local Development and Online Debugging
8.1 Creating a Template in the GUI
- Open the vulnerability configuration and click "Add Vulnerability"; choose template type Go (the UI tab is "Go template");
- Write the source in the editor (including the
//go:build ignoreline +@metaheader); - After saving, the controller writes
poc/go/<VulnIde>.goand registers it in memory; when the template is delivered to a node, the node encrypts it intoscan-poc/go/<VulnIde>.gopocwith AES.
When GoSource is empty, the controller generates a skeleton with a @meta header and package main from the metadata.
8.2 Related Commands and Data Structures
| Command | Direction | Purpose |
|---|---|---|
RunGoPocTest | GUI → controller → scanner node | Debug-run a hot-loadable Go POC; returns GoPocTestResult (findings + logs + HTTP count) |
GetGoPocDetail | GUI → controller | Fetch the full details of a vulnerability (including GoSource) so editing uses the latest source |
PocTestRequest / RunPocTest | GUI → controller → scanner node | Unified test verification (shared by yaml/go/wasm); returns PocTestResult |
Fields of the GoPocTestRequest request structure used by RunGoPocTest: VulnIde, GoSource, TargetURL, TestFlow, Timeout (seconds, default 30), Proxy. Result structure GoPocTestResult: VulnIde, Success, Findings, Logs, HttpCount, Error.
The unified test verification entry point RunPocTest dispatches on PocTestRequest.PocType; when PocType="go" it runs through the same interpreter, and Success and Found are returned separately (Found = len(findings) > 0).
8.3 Where Test Packets Come From
While debugging you may omit the packet and use only TargetURL (the node builds a GET flow automatically). A more realistic approach is to paste a burp-style raw request message: the node parses "request line + headers + blank line + Body" and assembles the full URL from the Host header and the path in the request line. The GUI can also build a packet from a request-step index of a YAML template.
8.4 Silent Testing
The yaml branch of test verification and the debug entry point both use "return only, never store" semantics: hit details are read from the result and never written to the vulnerability database. In AI dispatch scenarios, WantExchanges=true additionally returns the raw request/response text of every step (truncated to 8KB by default) so the AI can judge for itself.
The go branch of RunPocTest currently does not deliver the proxy address to the execution engine (it only puts target into Config), whereas RunGoPocTest and scan tasks do deliver it. If your target is reachable only through a proxy, use the run test on the GUI's "Go template" tab (RunGoPocTest), which is more reliable. Keep this difference in mind when debugging go templates through RunPocTest.
9. Debugging Handbook
This chapter is the quick reference for "where to look when it does not work". It first covers the three observation surfaces, then the five-minute minimal verification flow, and finally the symptom reference table.
9.1 Where to See Logs and Results
| Observation surface | What to look at | Data source | When to use |
|---|---|---|---|
| GUI "Go template" debug panel | scan.Log output + matched findings + HTTP request count | GoPocTestResult's Logs / Findings / HttpCount | Verifying logic line by line during development; the fastest route |
| Task-result vulnerability card | Vulnerability name / details / severity / request-response messages | The vulnerability detail reported by the node (queryable once stored) | Verifying what a real scan task produces |
| Controller / node logs | POC execution failures, timeouts and per-run logs | Node / controller logs | Troubleshooting task-time issues and checking whether the POC was scheduled |
| Unified test verification result | PocTestResult's Success / Found / Findings | The go branch of RunPocTest | AI dispatch / test verification tab |
9.2 Five-Minute Minimal Verification Flow
- Open the GUI vulnerability configuration, click "Add Vulnerability", choose template type Go, and create a template.
- Paste the minimal skeleton into the editor (copy the complete skeleton from section 2.5; give
@meta:Nameany value) and save. - Switch to that template's "Go template" tab and enter a reachable address in "target URL" (for example
http://your-lab/). - Click "Run Test". The GUI sends a
RunGoPocTestcommand → the controller forwards it to an online scanner node → the node builds the Flow and hands it to the interpreter. - Inspect what comes back:
- Every line your
scan.Logprinted appears inGoPocTestResult.Logs→ the code really ran; GoPocTestResult.HttpCount > 0→ requests really went out;- If
Success=false,Errorstates whether it was an execution error or a timeout; - On a hit,
Findingsis non-empty and the GUI displays the vulnerability card.
- Every line your
- If step 5 shows nothing: check
Errorfirst, then whetherLogsis empty. EmptyLogsusually meansmain()never ran at all (see the reference table in 9.3).
During debugging, scan.Log every key intermediate value (truncated with scan.Substr); it is far faster than staring at the editor and guessing.
9.3 Symptom → Cause → Fix
| Symptom | Cause | Fix |
|---|---|---|
go build ./... reports illegal character U+00B0, or the source tree fails to compile | //go:build ignore was forgotten, so the template is compiled as ordinary source | The first line must be //go:build ignore, followed by a blank line; platform-generated skeletons already include it |
| POC execution is clearly slower than an equivalent YAML template | Every run goes through the interpreter (compilation + reflection), which is an order of magnitude slower | Always use YAML when possible; reserve Go for complex algorithms that YAML cannot express |
| Execution reports "interpretation failed/undefined: xxx" | A third-party package outside the whitelist was imported | The platform registers only the Go standard library and the scan package; switch to the scan package or an already-registered standard library package |
Error Go POC 执行超时(30s) | The logic exceeds the single-run timeout (30s by default); too many requests or a long Sleep | Limit the number of requests and loop iterations; shorten or remove Sleep; keep the total budget away from the timeout ceiling |
| The vulnerability card severity is wrong / shown as low | Report's Level is empty or misspelled (such as HIGH/高危) | Always use the four lowercase values critical/high/medium/low |
The node's go build ./... reports a ciphertext compile error | Node-side ciphertext was written as .go (a past incident: no build tag is visible in ciphertext) | Store it as .gopoc on the node; the node migrates historical .go ciphertext automatically at startup |
| The English UI shows Chinese names/descriptions | Only cn was written, with no .en added | Add the .en suffix for language-related keys (Name/Description/Solution/AffectedProducts) |
| The vulnerability name is empty / all metadata is lost | @meta: case or spelling is wrong, the comment is not at the start of the line, or @Meta:/meta: was used | Spell key names exactly; comments must start the line; whitespace around = is trimmed automatically |
| The run as a whole times out with logs stuck in polling | scan.Sleep consumes the execution time budget | Limit each sleep and the iteration count; leave enough headroom in the total polling time |
| The node process's memory/goroutines keep growing after a task | The POC contains an unbounded loop, and the interpreter cannot force cancellation after a timeout, so the goroutine lingers in the background | Every loop must have a clear upper bound; avoid for {} |
| The template "disappears" from the list and nothing happens | The package declaration is missing and the file is silently skipped at load time (no error) | A package main (or a valid package declaration) is required |
| The request succeeds but nothing is detected | Besides Level, the usual cause is a wrong regex (RegexMatch returns false on an invalid regex and raises no error) | Print the text to be matched with scan.Log; first verify the path with a simple scan.Contains |
| A target reachable only through a proxy fails to connect directly | The go branch of RunPocTest does not deliver the proxy to the execution engine (known limitation) | Use the GUI's "Go template" RunGoPocTest or a real scan task, both of which deliver the proxy |
| The response body's tail features cannot be matched | The single-read limit is 1MB; anything beyond is truncated | Inspect only the front part for detection; for full content, issue several ranged requests |
| The two-step session fails (step two is not logged in) | A Cookie header with the same name as Set-Cookie was set by hand, overriding the session value | Do not hand-write a same-named Cookie when you need "log in first"; let the shared jar manage it |
| A generic template requests a hard-coded host in another task | The code hard-codes an absolute URL | Take the current flow from scan.Flow; the AI generation path rejects hard-coded targets outright |
Before submitting a template, self-check four things: is the first line //go:build ignore? Does @meta:Name start its line? Are package main and func main() present? Are all the scan.xxx symbols used listed in the full symbol table in 5.53? These four checks catch the vast majority of "the template does not work" problems.
10. Complete Examples
All examples omit the blank-line detail after
//go:build ignore; in a real file keep the build constraint on the first line followed by a blank line.
10.1 Example 1: Response Feature Matching (Simplest)
//go:build ignore
// @meta:Name=未授权访问检测
// @meta:Name.en=Unauthorized Access Detection
// @meta:Level=high
// @meta:Description=检测后台接口是否可在未授权情况下访问
// @meta:Description.en=Detect whether the admin endpoint is accessible without authorization
// @meta:Solution=为接口增加鉴权
// @meta:Solution.en=Add authentication to the endpoint
// @meta:TestDepth=pack
// @meta:Confidence=85
// @meta:DefaultLanguage=cn
package main
import (
"scan"
)
func main() {
scan.Log("目标: " + scan.Flow.URL)
resp := scan.HTTPGet(scan.Flow.URL)
if resp.Error != "" {
scan.Log("请求失败: " + resp.Error)
return
}
// 状态码 200 且响应出现后台特征关键字
if resp.StatusCode == 200 && scan.Contains(scan.ToLower(resp.Body), "admin dashboard") {
scan.Report(scan.VulnFinding{
Name: "未授权访问检测",
Detail: "后台接口在未授权情况下返回了管理页面特征",
Evidence: scan.Substr(resp.Body, 0, 300),
Level: "high",
URL: resp.URL,
Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
})
}
}
10.2 Example 2: Two-Step Cookie Session (Log In First, Then Access a Protected Endpoint)
//go:build ignore
// @meta:Name=默认口令登录并访问后台
// @meta:Name.en=Default Credential Login and Admin Access
// @meta:Level=critical
// @meta:Description=使用默认口令登录后访问受保护接口,验证默认凭据风险
// @meta:Description.en=Log in with default credentials, then access a protected endpoint to confirm the risk
// @meta:Solution=强制修改默认口令并启用多因素认证
// @meta:Solution.en=Force password change and enable MFA
// @meta:CweId=CWE-798
// @meta:TestDepth=deep
// @meta:DefaultLanguage=cn
package main
import (
"scan"
)
func main() {
base := scan.URLScheme(scan.Flow.URL) + "://" + scan.URLHost(scan.Flow.URL)
scan.Log("站点根: " + base)
// 第一步:默认口令登录,Set-Cookie 自动进入共享会话
loginResp := scan.HTTPJSONPost(base+"/api/login", `{"username":"admin","password":"admin123"}`)
if loginResp.Error != "" {
scan.Log("登录请求失败: " + loginResp.Error)
return
}
scan.Log("登录状态码: " + scan.JSONDump(loginResp.StatusCode))
// 登录响应里提示成功或已下发会话 Cookie 才继续
loginOK := loginResp.StatusCode == 200 && (scan.Contains(loginResp.Body, "token") || len(loginResp.Cookies) > 0)
if !loginOK {
scan.Log("默认口令未通过,结束")
return
}
// 第二步:携带会话 Cookie 访问受保护接口(无需手工搬运 Cookie)
adminResp := scan.HTTPGet(base + "/api/admin/users")
if adminResp.Error != "" {
scan.Log("访问后台失败: " + adminResp.Error)
return
}
// 命中判据:后台接口正常返回且出现管理数据特征
if adminResp.StatusCode == 200 && (scan.Contains(adminResp.Body, "\"role\":\"admin\"") || scan.Contains(scan.ToLower(adminResp.Body), "user list")) {
scan.Report(scan.VulnFinding{
Name: "默认口令登录并访问后台",
Detail: "使用默认口令成功登录,并在同一会话下访问到受保护的用户管理接口",
Evidence: scan.Substr(adminResp.Body, 0, 300),
Level: "critical",
URL: adminResp.URL,
CVEId: "CVE-2024-0001",
Request: "GET " + base + "/api/admin/users",
Response: adminResp.RawHeaders + "\n\n" + scan.Substr(adminResp.Body, 0, 500),
})
}
}
10.3 Example 3: JSON Parsing + Regex Extraction + Multi-Branch Reporting
//go:build ignore
// @meta:Name=接口信息泄露与弱令牌检测
// @meta:Name.en=API Information Disclosure and Weak Token Detection
// @meta:Name.zh=接口信息泄露与弱令牌检测
// @meta:Level=high
// @meta:Description=解析接口 JSON 响应,检测敏感字段与可预测令牌
// @meta:Description.en=Parse the API JSON response to detect sensitive fields and predictable tokens
// @meta:Solution=移除敏感字段并改用不可预测的强随机令牌
// @meta:Solution.en=Remove sensitive fields and use unpredictable strong random tokens
// @meta:CweId=CWE-200
// @meta:References=https://example.com/advisory
// @meta:References+=https://example.com/api-security
// @meta:Confidence=90
// @meta:TestDepth=deep
// @meta:DefaultLanguage=cn
package main
import (
"scan"
)
func main() {
resp := scan.HTTPGet(scan.Flow.URL)
if resp.Error != "" {
scan.Log("请求失败: " + resp.Error)
return
}
if resp.StatusCode != 200 {
scan.Log("状态码非 200,跳过")
return
}
// 分支一:响应不是 JSON 时,退化为正则特征匹配
if !scan.JSONValid(resp.Body) {
if scan.RegexMatch(resp.Body, `(?i)(password|secret|api[_-]?key)\s*[:=]`) {
key := scan.RegexExtract(resp.Body, `(?i)api[_-]?key\s*[:=]\s*["']?([A-Za-z0-9]{16,})`)
scan.Report(scan.VulnFinding{
Name: "接口信息泄露与弱令牌检测",
Detail: "响应正文中出现疑似密钥/口令特征(非 JSON 回退分支)",
Evidence: scan.Substr(resp.Body, 0, 300),
Level: "high",
URL: resp.URL,
SensitiveText: scan.Substr(resp.Body, 0, 400),
SensitiveKeywords: []string{"api_key", key},
SensitiveMatchRule: "regex:(?i)api[_-]?key",
})
return
}
scan.Log("无命中,结束")
return
}
// 分支二:JSON 路径取敏感字段
role := scan.JSONGet(resp.Body, "data.user.role")
token := scan.JSONGet(resp.Body, "data.token")
secret := scan.JSONGet(resp.Body, "data.config.secretKey")
scan.Log("role=" + role + " secretLen=" + scan.JSONDump(scan.Len(secret)))
if scan.ToLower(role) == "admin" && secret != "" {
scan.Report(scan.VulnFinding{
Name: "接口信息泄露与弱令牌检测",
Detail: "JSON 响应泄露了管理员配置字段 secretKey",
Evidence: scan.Substr(secret, 0, 120),
Level: "high",
URL: resp.URL,
Response: scan.Substr(resp.Body, 0, 500),
})
}
// 分支三:令牌可预测(纯数字/短长度)判定
if token != "" {
weak := scan.RegexMatch(token, `^\d+$`) || scan.Len(token) < 16
if weak {
// 多次采样验证可预测性
predictable := true
for i := 0; i < 3; i++ {
next := scan.JSONGet(scan.HTTPGet(scan.Flow.URL).Body, "data.token")
if next != token && !scan.HasPrefix(next, scan.Substr(token, 0, 6)) {
predictable = false
break
}
scan.Sleep(200)
}
if predictable {
scan.Report(scan.VulnFinding{
Name: "接口信息泄露与弱令牌检测",
Detail: "接口返回的令牌可预测(短/纯数字且前缀稳定)",
Evidence: token,
Level: "high",
URL: resp.URL,
CVEId: "CVE-2024-9999",
})
}
}
}
// 分支四:随机与时间辅助信息(演示相关函数)
scan.Log("样本 ID=" + scan.UUID() + " 位=" + scan.RandString(8) +
" 字母=" + scan.RandAlpha(6) + " 数字=" + scan.RandNum(6) +
" 秒=" + scan.JSONDump(scan.Timestamp()) +
" 毫秒=" + scan.JSONDump(scan.TimestampMs()) +
" 随机整数=" + scan.JSONDump(scan.RandInt(1, 100)))
}
11. Pitfall Checklist
| Pitfall | Symptom | Avoidance |
|---|---|---|
Forgetting //go:build ignore | Templates in the source tree are compiled by go build ./..., causing errors or being packaged | The first line must be //go:build ignore, followed by a blank line; platform-generated skeletons already include it |
| Expecting YAML-level performance | Every run goes through the interpreter (compilation + reflection), which is an order of magnitude slower | Always use YAML when possible; reserve Go for complex algorithms |
| Importing an unregistered package | import of a third-party package → interpretation failure | The platform registers only the Go standard library and scan; extend through the scan package instead of third-party imports |
| The 30s timeout | Execution over 30s returns a timeout error; moreover the interpreter cannot truly cancel the goroutine | Limit the number of requests and the amount of logic; scan.Sleep consumes the execution time budget |
Using a symbol that does not exist in scan | Interpretation reports undefined (for example, writing scan.Itoa by mistake) | Use only symbols in the full table in 5.53; convert numbers to strings with scan.JSONDump |
The Level value passed to Report | Empty or misspelled → falls back to the template level, or low severity if the template has none | Always use the four lowercase values critical/high/medium/low |
The node-side extension must be .gopoc | Ciphertext written as .go → the node's go build ./... reports illegal character U+00B0 (a past incident) | Store it as .gopoc on the node; the node migrates historical .go ciphertext automatically at startup |
| Multilingual support requires cn + en | Only Chinese written → the English UI shows Chinese | Add the .en suffix for language-related keys (Name/Description/Solution/AffectedProducts) |
@meta case and spaces | A misspelled key or a wrong form of @meta: → empty metadata and an empty vulnerability name | Spell key names exactly; comments must start the line; whitespace around = is trimmed automatically |
scan.Sleep consumes execution time | A long sleep causes an overall timeout | In polling scenarios limit the count and each duration; keep the total budget away from the Timeout |
| Writing an unbounded loop in a POC | After a timeout the goroutine lingers in the background, accumulating a leak | Every loop must have a clear upper bound; avoid for {} |
Missing the package declaration | Silently skipped at load time (no error; the template simply "disappears") | package main is required |
| Setting a same-named Cookie by hand | The explicit header overrides the same-named Cookie in the session | When you need "log in first", do not hand-write a same-named Cookie header |
| Hard-coding an absolute URL in a generic template | Other tasks use it to request a hard-coded host (out of scope) | Take the current flow from scan.Flow; the AI generation path rejects hard-coded targets outright |
| The response body looks truncated | The single-read limit is 1MB | For large responses, detect on the front part only; for full content, switch to several ranged requests |
scan.Headers key case | scan.Flow.Headers["User-Agent"] retrieves nothing | Request/response header keys are all lowercase; write ["user-agent"] |
The second parameter of scan.TrimCut | Assumed to be a "substring to remove" when it is actually a "character set" | TrimCut(s, cutset) removes any characters from cutset at both ends; use Replace for substrings |
The go branch of RunPocTest carries no proxy | A target reachable only through a proxy fails to connect directly | Use the GUI's "Go template" run test (RunGoPocTest) or a scan task; they inject the proxy |
Before submitting a template, self-check three things: is the first line //go:build ignore? Does @meta:Name start its line? Are all symbols used after import "scan" listed in the full symbol table in 5.53? These three checks catch the vast majority of "the template does not work" problems.