- 网络安全
【免费下载链接】Havoc
The Havoc Framework
在 Havoc 的 Teamserver 端,监听器、操作员、Demon 行为等配置都通过.yaotl配置文件描述,而解析这些配置的底层是一套完整的 HCL(HashiCorp Configuration Language)实现,被项目内嵌在 yaotl 包中。其中的userfunc扩展为调用方应用提供了一项关键能力:允许在配置文件里以function块定义自定义函数,并在表达式中像内置函数一样调用它们。读完本文,你将掌握该扩展的语法规范、DecodeUserFunctionsAPI 的完整语义、解码实现的底层原理(参数绑定、变参元组、闭包上下文),以及它在 Havoc profile 加载体系中的位置。
一、背景:Havoc 的 profile 配置体系
Havoc Teamserver 通过 profile 文件(如 profiles/ha 目录下的.yaotl文件)定义 Teamserver 的地址端口、操作员账号、监听器和 Demon 行为。配置的解码目标结构体定义在 HavocConfig,例如:
type HavocConfig struct { Server *ServerProfile `yaotl:"Teamserver,block"` Operators *OperatorsBlock `yaotl:"Operators,block"` Listener *Listeners `yaotl:"Listeners,block"` Demon *Demon `yaotl:"Demon,block"` ... }profile 的加载入口在 Profile.SetProfile,它调用hclsimple.DecodeFile(path, nil, &p.Config),一次性完成“解析 → 结构解码 → 表达式求值”,把.yaotl源码填充进 Go 结构体。hclsimple只是对 HCL 主 API 的高层封装(见 hclsimple.go);真正强大的地方在于底层的 HCL 主 API 允许把处理拆分为解析、块结构分析、表达式求值等可组合的管线阶段——而userfunc扩展正是运行在这样一个管线上的预处理器(pre-processor)。
二、语法:用 function 块声明用户函数
扩展支持通过特定的块类型声明函数,标准块名为function(调用方可覆盖,避免与应用中已有的块名或属性名冲突,见 doc.go):
function "add" { params = [a, b] result = a + b } function "list" { params = [] variadic_param = items result = items }块内的属性语义如下(对应 funcBodySchema):
| 属性 | 是否必需 | 含义 |
|---|---|---|
params | 必需 | 位置参数名列表,每个元素必须是一个标识符(identifier) |
variadic_param | 可选 | 变长参数名,只能有一个;声明后函数可接收任意数量的额外实参 |
result | 必需 | 返回值表达式;在解码时只被解析、不被求值,直到函数被调用时才在隔离上下文中执行 |
函数调用时的求值规则:result表达式在一个独立的求值上下文中执行,该上下文中只定义了以参数名命名的变量(外加基础上下文中可用的变量,见下文闭包一节)。例如:
function "greet" { params = [name] result = "Hello, ${name}." }调用greet("Ermintrude")得到字符串"Hello, Ermintrude."。
三、公开 API:DecodeUserFunctions 的完整语义
扩展对外只暴露两个核心标识:ContextFunc类型与DecodeUserFunctions函数,均定义在 public.go 中。
// ContextFunc 是用于生成“运行某一组函数时的基础 EvalContext”的回调 type ContextFunc func() *hcl.EvalContext func DecodeUserFunctions(body hcl.Body, blockType string, context ContextFunc) ( funcs map[string]function.Function, remain hcl.Body, diags hcl.Diagnostics, )参数与返回值的关键语义:
body:可能含有函数声明的cty.Body对象;blockType:函数声明使用的块名(默认function),调用方可传入其他名字以避开命名冲突;context ContextFunc:- 之所以是“返回上下文的函数”而不是直接传
*hcl.EvalContext,是为了让函数可以在其上下文尚未构建完成前就被解码——典型场景是应用希望函数能够引用自身(递归/相互调用)。首次求值时才回调它拿到上下文,之后所有函数复用同一个上下文; - 传入非 nil 的上下文时,返回的函数会变成绑定到该上下文的闭包:
result表达式除了参数变量外,还能访问上下文中的全局变量与函数; - 传入 nil(或其返回 nil)时,
result表达式只能访问参数名对应的变量;
- 之所以是“返回上下文的函数”而不是直接传
- 返回值:
funcs是函数名到function.Function实现的映射,适合直接并入hcl.EvalContext.Functions用于后续求值;remain是剔除函数块后的剩余内容 body,可供调用方继续做常规解码;若diags含错误,则funcs和remain可能为 nil 或不完整。
另外注意一个重要的惰性特性:每个函数的result表达式在解码阶段被解析(parse),但直到函数被调用时才求值(evaluate)。这意味着解码阶段发现的只有语法/结构错误,语义错误(如result引用了未定义变量)会在调用时通过求值诊断暴露出来。
四、解码实现剖析:从块扫描到可调用函数
内部实现decodeUserFunctions在 decode.go,核心流程可以拆成五步。
1. 用 PartialContent 摘出函数块
schema := &hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ {Type: blockType, LabelNames: []string{"name"}}, }, } content, remain, diags := body.PartialContent(schema)PartialContent按 schema 从 body 中“摘取”所有function "name" { ... }块(第一个 label 作为函数名),同时返回剩余内容remain——这就是该扩展能作为预处理器无缝插进 HCL 管线的原因:它只拿走自己关心的块,其余内容原样交还。
2. 惰性基础上下文(getBaseCtx)
var baseCtx *hcl.EvalContext getBaseCtx := func() *hcl.EvalContext { if baseCtx == nil { if contextFunc != nil { baseCtx = contextFunc() } } return baseCtx // 仍可能为 nil,这是允许的 }基础上下文在第一次实际调用函数时才生成并缓存,之后所有函数共享同一份(源码注释说明“同一 body 中的所有函数应看到相同上下文”)。这直接支撑了“函数可引用自身”的场景:解码函数时尚未就绪的上下文,等到调用时通过回调获取。
3. 参数校验:只接受标识符
对每个块,先按funcBodySchema解码块体,得到params、result、可选的variadic_param。随后:
params必须是列表,逐项用hcl.ExprAsKeyword提取;若某项不是合法标识符,报告Invalid param element诊断(Each parameter name must be an identifier.)并跳过整个块;variadic_param若存在同样必须是标识符,否则报告Invalid variadic_param;- 块名取
block.Labels[0]作为函数注册名。
4. 构造 function.Spec:动态类型 + 调用闭包
spec := &function.Spec{} for _, paramName := range params { spec.Params = append(spec.Params, function.Parameter{ Name: paramName, Type: cty.DynamicPseudoType, // 参数类型完全动态 }) } if varParamExpr != nil { spec.VarParam = &function.Parameter{Name: varParam, Type: cty.DynamicPseudoType} }所有参数类型都是cty.DynamicPseudoType——即类型在调用时才确定,这正是spec.Type的实现方式:
spec.Type = func(args []cty.Value) (cty.Type, error) { val, err := impl(args) return val.Type(), err }impl闭包是函数体的真正执行逻辑:
- 取基础上下文并
NewChild()派生子上下文,把Variables重置为空 map,实现隔离求值——result表达式默认看不到调用方栈帧的变量; - cty 函数机制保证实参数量至少能填满
params,逐一遍历绑定:ctx.Variables[paramName] = args[i]; - 若有变参,
args[len(params):]被打包成元组(cty.TupleVal)绑定给变参名——所以variadic_param在result中是一个可以索引、取长度、迭代的列表值; - 求值
resultExpr.Value(ctx);若产生诊断错误,通过 error 通道“夹带”出去(cty.Diagnostics本身实现了error接口,调用方可类型断言还原出逐条诊断); spec.Impl直接复用impl,最终function.New(spec)注册进funcs[name]。
错误诊断在解码阶段(如缺params、写了未知属性parrams、参数不是标识符)和调用阶段(如result引用未定义变量、实参数量不符)分别产生,两类错误都汇聚到diags返回。
五、测试用例:行为边界的一手证据
decode_test.go 用 9 组表驱动用例覆盖了该扩展的关键行为边界,逐一验证了上文语义:
| 用例 | 验证点 | 期望 |
|---|---|---|
greet("Ermintrude") | 基本参数绑定与字符串插值 | "Hello, Ermintrude.",0 诊断 |
greet() | 缺少必需实参 | 动态值,1 诊断 |
greet("Ermintrude", "extra") | 实参过多 | 动态值,1 诊断 |
add(1, 5) | 数值运算(动态类型参数) | 数字6,0 诊断 |
argstuple("a", true, 1) | 变参打包为元组 | tuple(["a", true, 1]),0 诊断 |
missing_var()(result = nonexist) | result引用未定义变量 | 动态值,1 诊断 |
closure()(result = upvalue,baseCtx 含upvalue = true) | 闭包:函数可读取传入的基础上下文变量 | true,0 诊断 |
neg(add(1, 3)) | 用户函数之间相互嵌套调用 | -4,0 诊断 |
parrams = [val](拼错的属性名) | 解码期结构诊断:缺params+ 未知属性 | 2 诊断 |
测试的驱动方式也值得注意:先用hclsyntax.ParseConfig解析源码,调用decodeUserFunctions(f.Body, "function", ...)拿到函数 map,再把待测表达式放入&hcl.EvalContext{Functions: funcs}中求值——这正是接入任意 HCL 应用的标准姿势。
六、与 Havoc profile 加载流程的关系
从源码结构看,当前 Havoc Teamserver 的 profile 解码路径(profile.go → hclsimple.Decode →gohcl.DecodeBody)传入的求值上下文是nil,即尚未直接启用userfunc扩展;该扩展随 yaotl(HCL)库完整保留在 teamserver/pkg/profile/yaotl/ext/userfunc 目录下,属于库级能力。若未来希望.yaotlprofile 支持函数式配置,标准的接入方式是:在gohcl.DecodeBody之前插入DecodeUserFunctions(file.Body, "function", ctxFunc)预处理,将返回的funcs合并进EvalContext.Functions,再用返回的remainbody 完成常规结构解码。
七、要点小结
- 定位:
userfunc是 HCL 的cty.Body预处理器扩展,块名为function(可覆盖),由DecodeUserFunctions单入口暴露; - 语法契约:
params(必需、标识符列表)、variadic_param(可选、单个标识符、运行时打包为元组)、result(必需、解析期只解析不求值); - 上下文模型:
ContextFunc惰性提供基础EvalContext,函数首次调用时生成并全量共享;非空上下文使函数成为闭包,支持引用全局变量乃至自身; - 类型模型:参数全部为
cty.DynamicPseudoType,返回类型由“试跑一次 impl”动态得出; - 可组合性:返回的
remainbody 让扩展能干净地嵌进 HCL 多阶段管线,diags统一汇总解码期与调用期错误; - 边界证据:9 组测试用例(含缺参、多参、变参元组、未定义变量、闭包、嵌套调用、属性拼写错误)完整刻画了行为边界,见 decode_test.go。
- 网络安全
【免费下载链接】Havoc
The Havoc Framework
相关推荐
终极HCL函数调用与自定义函数指南:解锁配置语言的强大扩展能力
终极HCL函数调用与自定义函数指南:解锁配置语言的强大扩展能力 HCL(HashiCorp配置语言)作为HashiCorp工具生态系统的核心配置语言,提供了强大
开发工具Quivr查询语言扩展:自定义函数与过程
Quivr查询语言扩展:自定义函数与过程 在数据处理和分析中,我们经常会遇到需要重复执行特定逻辑的场景。如果每次都编写完整的查询语句,不仅效率低下,还容易出错。
人工智能AI 应用大模型RAG后端前端Neo4j APOC扩展库中的用户自定义函数详解
Neo4j APOC扩展库中的用户自定义函数详解 用户自定义函数概述 在Neo4j 3.1.0 M10版本中引入的用户自定义函数 User Defined Fu
数据库图计算数据集成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考