473 lines
13 KiB
Python
473 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate fuzz corpus for kadmind RPC testing.
|
|
|
|
Message format:
|
|
4-byte big-endian length prefix
|
|
N bytes of message data
|
|
|
|
The message data starts with a 4-byte command number (kadm_ops enum).
|
|
|
|
The fuzzer pre-populates the HDB with these principals (in FUZZ.REALM):
|
|
- test
|
|
- admin/admin
|
|
- user1
|
|
- user2
|
|
- host/localhost
|
|
- HTTP/www.example.com
|
|
- krbtgt/FUZZ.REALM
|
|
"""
|
|
|
|
import struct
|
|
import os
|
|
|
|
# kadm_ops enum values
|
|
KADM_GET = 0
|
|
KADM_DELETE = 1
|
|
KADM_CREATE = 2
|
|
KADM_RENAME = 3
|
|
KADM_CHPASS = 4
|
|
KADM_MODIFY = 5
|
|
KADM_RANDKEY = 6
|
|
KADM_GET_PRIVS = 7
|
|
KADM_GET_PRINCS = 8
|
|
KADM_CHPASS_WITH_KEY = 9
|
|
KADM_NOP = 10
|
|
KADM_PRUNE = 11
|
|
|
|
# Pre-populated principals (must match kadmind.c fuzz_stdin)
|
|
EXISTING_PRINCIPALS = [
|
|
"test",
|
|
"admin/admin",
|
|
"user1",
|
|
"user2",
|
|
"host/localhost",
|
|
"HTTP/www.example.com",
|
|
"krbtgt/FUZZ.REALM",
|
|
]
|
|
|
|
# KADM5 mask bits (from admin.h)
|
|
KADM5_PRINCIPAL = 0x000001
|
|
KADM5_PRINC_EXPIRE_TIME = 0x000002
|
|
KADM5_PW_EXPIRATION = 0x000004
|
|
KADM5_LAST_PWD_CHANGE = 0x000008
|
|
KADM5_ATTRIBUTES = 0x000010
|
|
KADM5_MAX_LIFE = 0x000020
|
|
KADM5_MOD_TIME = 0x000040
|
|
KADM5_MOD_NAME = 0x000080
|
|
KADM5_KVNO = 0x000100
|
|
KADM5_MKVNO = 0x000200
|
|
KADM5_AUX_ATTRIBUTES = 0x000400
|
|
KADM5_POLICY = 0x000800
|
|
KADM5_POLICY_CLR = 0x001000
|
|
KADM5_MAX_RLIFE = 0x002000
|
|
KADM5_LAST_SUCCESS = 0x004000
|
|
KADM5_LAST_FAILED = 0x008000
|
|
KADM5_FAIL_AUTH_COUNT = 0x010000
|
|
KADM5_KEY_DATA = 0x020000
|
|
KADM5_TL_DATA = 0x040000
|
|
|
|
|
|
def pack_int32(val):
|
|
"""Pack a 32-bit big-endian integer."""
|
|
return struct.pack('>i', val)
|
|
|
|
|
|
def pack_uint32(val):
|
|
"""Pack a 32-bit big-endian unsigned integer."""
|
|
return struct.pack('>I', val)
|
|
|
|
|
|
def pack_string(s):
|
|
"""Pack a string (4-byte length + data + null terminator)."""
|
|
# Heimdal krb5_store_string includes null terminator in length
|
|
data = s.encode('utf-8') + b'\x00'
|
|
return pack_uint32(len(data)) + data
|
|
|
|
|
|
def pack_data(d):
|
|
"""Pack binary data (4-byte length + data)."""
|
|
return pack_uint32(len(d)) + d
|
|
|
|
|
|
def pack_principal(name, realm="FUZZ.REALM"):
|
|
"""
|
|
Pack a Kerberos principal.
|
|
Format: name_type (4), num_components (4), realm (string),
|
|
components (string each)
|
|
"""
|
|
parts = name.split('/')
|
|
# KRB5_NT_PRINCIPAL = 1
|
|
result = pack_int32(1) # name_type
|
|
result += pack_int32(len(parts)) # num_components
|
|
result += pack_string(realm) # realm
|
|
for part in parts:
|
|
result += pack_string(part)
|
|
return result
|
|
|
|
|
|
def pack_principal_ent(principal_name, mask, realm="FUZZ.REALM"):
|
|
"""
|
|
Pack a kadm5_principal_ent structure.
|
|
Only includes fields indicated by mask.
|
|
"""
|
|
result = pack_int32(mask) # mask comes first
|
|
|
|
if mask & KADM5_PRINCIPAL:
|
|
result += pack_principal(principal_name, realm)
|
|
if mask & KADM5_PRINC_EXPIRE_TIME:
|
|
result += pack_int32(0) # princ_expire_time
|
|
if mask & KADM5_PW_EXPIRATION:
|
|
result += pack_int32(0) # pw_expiration
|
|
if mask & KADM5_LAST_PWD_CHANGE:
|
|
result += pack_int32(0) # last_pwd_change
|
|
if mask & KADM5_MAX_LIFE:
|
|
result += pack_int32(86400) # max_life = 1 day
|
|
if mask & KADM5_MOD_NAME:
|
|
result += pack_int32(0) # mod_name is NULL
|
|
if mask & KADM5_MOD_TIME:
|
|
result += pack_int32(0) # mod_date
|
|
if mask & KADM5_ATTRIBUTES:
|
|
result += pack_int32(0) # attributes
|
|
if mask & KADM5_KVNO:
|
|
result += pack_int32(1) # kvno
|
|
if mask & KADM5_MKVNO:
|
|
result += pack_int32(1) # mkvno
|
|
if mask & KADM5_POLICY:
|
|
result += pack_int32(0) # policy is NULL
|
|
if mask & KADM5_AUX_ATTRIBUTES:
|
|
result += pack_int32(0) # aux_attributes
|
|
if mask & KADM5_MAX_RLIFE:
|
|
result += pack_int32(604800) # max_renewable_life = 1 week
|
|
if mask & KADM5_LAST_SUCCESS:
|
|
result += pack_int32(0)
|
|
if mask & KADM5_LAST_FAILED:
|
|
result += pack_int32(0)
|
|
if mask & KADM5_FAIL_AUTH_COUNT:
|
|
result += pack_int32(0)
|
|
if mask & KADM5_KEY_DATA:
|
|
result += pack_int32(0) # n_key_data = 0
|
|
if mask & KADM5_TL_DATA:
|
|
result += pack_int32(0) # n_tl_data = 0
|
|
|
|
return result
|
|
|
|
|
|
def wrap_message(data):
|
|
"""Wrap message data with 4-byte length prefix."""
|
|
return pack_uint32(len(data)) + data
|
|
|
|
|
|
def write_corpus(filename, data):
|
|
"""Write a corpus file."""
|
|
path = os.path.join(os.path.dirname(__file__), filename)
|
|
with open(path, 'wb') as f:
|
|
f.write(wrap_message(data))
|
|
print(f"Created {filename} ({len(data)} bytes payload)")
|
|
|
|
|
|
# Generate corpus files
|
|
|
|
# ========== Basic operations ==========
|
|
|
|
# 1. NOP with reply wanted
|
|
write_corpus("nop_reply.bin",
|
|
pack_int32(KADM_NOP) + pack_int32(1))
|
|
|
|
# 2. NOP without reply (interrupt request)
|
|
write_corpus("nop_noreply.bin",
|
|
pack_int32(KADM_NOP) + pack_int32(0))
|
|
|
|
# 3. GET_PRIVS
|
|
write_corpus("get_privs.bin",
|
|
pack_int32(KADM_GET_PRIVS))
|
|
|
|
# ========== Operations on EXISTING principals ==========
|
|
# These should exercise deeper code paths since the principals exist
|
|
|
|
# 4. GET existing principal "test"
|
|
write_corpus("get_existing_test.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("test") +
|
|
pack_int32(KADM5_PRINCIPAL | KADM5_KVNO | KADM5_ATTRIBUTES))
|
|
|
|
# 5. GET existing principal with all fields
|
|
write_corpus("get_existing_all.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("test") +
|
|
pack_int32(0x7FFFF)) # All mask bits
|
|
|
|
# 6. GET existing admin/admin
|
|
write_corpus("get_existing_admin.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("admin/admin") +
|
|
pack_int32(KADM5_PRINCIPAL | KADM5_KVNO))
|
|
|
|
# 7. GET existing host principal
|
|
write_corpus("get_existing_host.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("host/localhost") +
|
|
pack_int32(KADM5_PRINCIPAL | KADM5_KEY_DATA))
|
|
|
|
# 8. GET existing HTTP service
|
|
write_corpus("get_existing_http.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("HTTP/www.example.com") +
|
|
pack_int32(KADM5_PRINCIPAL))
|
|
|
|
# 9. GET krbtgt (special principal)
|
|
write_corpus("get_existing_krbtgt.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("krbtgt/FUZZ.REALM") +
|
|
pack_int32(KADM5_PRINCIPAL | KADM5_KVNO | KADM5_MAX_LIFE))
|
|
|
|
# 10. CHPASS on existing principal
|
|
write_corpus("chpass_existing.bin",
|
|
pack_int32(KADM_CHPASS) +
|
|
pack_principal("user1") +
|
|
pack_string("newpassword123") +
|
|
pack_int32(0)) # keepold = false
|
|
|
|
# 11. CHPASS on existing with keepold
|
|
write_corpus("chpass_existing_keepold.bin",
|
|
pack_int32(KADM_CHPASS) +
|
|
pack_principal("user2") +
|
|
pack_string("anotherpassword") +
|
|
pack_int32(1)) # keepold = true
|
|
|
|
# 12. RANDKEY on existing principal
|
|
write_corpus("randkey_existing.bin",
|
|
pack_int32(KADM_RANDKEY) +
|
|
pack_principal("test"))
|
|
|
|
# 13. RANDKEY on existing with ks_tuples
|
|
write_corpus("randkey_existing_full.bin",
|
|
pack_int32(KADM_RANDKEY) +
|
|
pack_principal("user1") +
|
|
pack_int32(1) + # keepold
|
|
pack_int32(2) + # n_ks_tuple
|
|
pack_int32(17) + pack_int32(0) + # aes128-cts-hmac-sha1-96
|
|
pack_int32(18) + pack_int32(0)) # aes256-cts-hmac-sha1-96
|
|
|
|
# 14. MODIFY existing principal
|
|
mask = KADM5_PRINCIPAL | KADM5_ATTRIBUTES | KADM5_MAX_LIFE
|
|
write_corpus("modify_existing.bin",
|
|
pack_int32(KADM_MODIFY) +
|
|
pack_principal_ent("test", mask) +
|
|
pack_int32(mask))
|
|
|
|
# 15. MODIFY existing - change max_renewable_life
|
|
mask = KADM5_PRINCIPAL | KADM5_MAX_RLIFE
|
|
write_corpus("modify_existing_rlife.bin",
|
|
pack_int32(KADM_MODIFY) +
|
|
pack_principal_ent("user1", mask) +
|
|
pack_int32(mask))
|
|
|
|
# 16. PRUNE existing principal
|
|
write_corpus("prune_existing.bin",
|
|
pack_int32(KADM_PRUNE) +
|
|
pack_principal("test") +
|
|
pack_int32(1)) # keep kvno >= 1
|
|
|
|
# 17. RENAME existing to new
|
|
write_corpus("rename_existing.bin",
|
|
pack_int32(KADM_RENAME) +
|
|
pack_principal("user2") +
|
|
pack_principal("user2_renamed"))
|
|
|
|
# 18. CHPASS_WITH_KEY on existing
|
|
key_data = (
|
|
pack_int32(2) + # key_data_ver
|
|
pack_int32(2) + # key_data_kvno
|
|
pack_int32(17) + # aes128
|
|
pack_data(b'\x00' * 16) +
|
|
pack_int32(0) + # no salt type
|
|
pack_data(b'')
|
|
)
|
|
write_corpus("chpass_key_existing.bin",
|
|
pack_int32(KADM_CHPASS_WITH_KEY) +
|
|
pack_principal("test") +
|
|
pack_int32(1) + # n_key_data
|
|
pack_int32(0) + # keepold
|
|
key_data)
|
|
|
|
# ========== Operations on NON-EXISTING principals ==========
|
|
|
|
# 19. GET non-existing principal
|
|
write_corpus("get_nonexisting.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("does/not/exist") +
|
|
pack_int32(KADM5_PRINCIPAL))
|
|
|
|
# 20. DELETE non-existing principal
|
|
write_corpus("delete_nonexisting.bin",
|
|
pack_int32(KADM_DELETE) +
|
|
pack_principal("nonexistent"))
|
|
|
|
# 21. CREATE new principal
|
|
mask = KADM5_PRINCIPAL | KADM5_MAX_LIFE | KADM5_MAX_RLIFE
|
|
write_corpus("create_new.bin",
|
|
pack_int32(KADM_CREATE) +
|
|
pack_principal_ent("newprinc", mask) +
|
|
pack_int32(mask) +
|
|
pack_string("password123"))
|
|
|
|
# 22. CREATE with various attributes
|
|
mask = KADM5_PRINCIPAL | KADM5_ATTRIBUTES | KADM5_MAX_LIFE | KADM5_PRINC_EXPIRE_TIME
|
|
write_corpus("create_with_attrs.bin",
|
|
pack_int32(KADM_CREATE) +
|
|
pack_principal_ent("newprinc2", mask) +
|
|
pack_int32(mask) +
|
|
pack_string("password456"))
|
|
|
|
# ========== GET_PRINCS listing ==========
|
|
|
|
# 23. GET_PRINCS - list all
|
|
write_corpus("get_princs_all.bin",
|
|
pack_int32(KADM_GET_PRINCS) +
|
|
pack_int32(0)) # no expression
|
|
|
|
# 24. GET_PRINCS with wildcard
|
|
write_corpus("get_princs_wildcard.bin",
|
|
pack_int32(KADM_GET_PRINCS) +
|
|
pack_int32(1) +
|
|
pack_string("*"))
|
|
|
|
# 25. GET_PRINCS with pattern
|
|
write_corpus("get_princs_user.bin",
|
|
pack_int32(KADM_GET_PRINCS) +
|
|
pack_int32(1) +
|
|
pack_string("user*"))
|
|
|
|
# 26. GET_PRINCS with host pattern
|
|
write_corpus("get_princs_host.bin",
|
|
pack_int32(KADM_GET_PRINCS) +
|
|
pack_int32(1) +
|
|
pack_string("host/*"))
|
|
|
|
# 27. GET_PRINCS online iteration mode
|
|
write_corpus("get_princs_iter.bin",
|
|
pack_int32(KADM_GET_PRINCS) +
|
|
pack_int32(0x55555555) +
|
|
pack_string("*"))
|
|
|
|
# ========== Edge cases and malformed inputs ==========
|
|
|
|
# 28. Invalid command
|
|
write_corpus("invalid_cmd.bin",
|
|
pack_int32(99))
|
|
|
|
# 29. Truncated message
|
|
write_corpus("truncated_get.bin",
|
|
pack_int32(KADM_GET))
|
|
|
|
# 30. Malformed principal (bad component count)
|
|
write_corpus("malformed_principal.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_int32(1) + # name_type
|
|
pack_int32(-1) + # invalid num_components
|
|
pack_string("FUZZ.REALM"))
|
|
|
|
# 31. Very long principal name
|
|
write_corpus("long_principal.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("A" * 1000))
|
|
|
|
# 32. Principal with many components
|
|
write_corpus("many_components.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("/".join(["c"] * 50)))
|
|
|
|
# 33. Empty password create
|
|
mask = KADM5_PRINCIPAL
|
|
write_corpus("create_empty_password.bin",
|
|
pack_int32(KADM_CREATE) +
|
|
pack_principal_ent("emptypass", mask) +
|
|
pack_int32(mask) +
|
|
pack_string(""))
|
|
|
|
# 34. Create with TL_DATA
|
|
mask = KADM5_PRINCIPAL | KADM5_TL_DATA
|
|
tl_data = (
|
|
pack_int32(1) + # tl_data_type
|
|
pack_data(b'test tl data content')
|
|
)
|
|
princ_with_tl = (
|
|
pack_int32(mask) +
|
|
pack_principal("withtldata") +
|
|
pack_int32(1) + # n_tl_data
|
|
tl_data
|
|
)
|
|
write_corpus("create_with_tldata.bin",
|
|
pack_int32(KADM_CREATE) +
|
|
princ_with_tl +
|
|
pack_int32(mask) +
|
|
pack_string("password"))
|
|
|
|
# 35. Large n_key_data (integer overflow)
|
|
write_corpus("large_nkeydata.bin",
|
|
pack_int32(KADM_CHPASS_WITH_KEY) +
|
|
pack_principal("test") +
|
|
pack_int32(0x7FFFFFFF) +
|
|
pack_int32(0))
|
|
|
|
# 36. Negative n_key_data
|
|
write_corpus("negative_nkeydata.bin",
|
|
pack_int32(KADM_CHPASS_WITH_KEY) +
|
|
pack_principal("test") +
|
|
pack_int32(-1) +
|
|
pack_int32(0))
|
|
|
|
# 37. Zero-length message
|
|
with open(os.path.join(os.path.dirname(__file__), "empty_message.bin"), 'wb') as f:
|
|
f.write(pack_uint32(0))
|
|
print("Created empty_message.bin (0 bytes payload)")
|
|
|
|
# 38. Multiple key_data entries
|
|
multi_key = b''
|
|
for i in range(3):
|
|
multi_key += (
|
|
pack_int32(2) + # ver
|
|
pack_int32(i + 1) + # kvno
|
|
pack_int32(17) + # aes128
|
|
pack_data(b'\x00' * 16) +
|
|
pack_int32(0) +
|
|
pack_data(b'')
|
|
)
|
|
write_corpus("chpass_multikey.bin",
|
|
pack_int32(KADM_CHPASS_WITH_KEY) +
|
|
pack_principal("test") +
|
|
pack_int32(3) + # n_key_data
|
|
pack_int32(1) + # keepold
|
|
multi_key)
|
|
|
|
# 39. MODIFY with policy (even though we don't have policies)
|
|
mask = KADM5_PRINCIPAL | KADM5_POLICY
|
|
write_corpus("modify_with_policy.bin",
|
|
pack_int32(KADM_MODIFY) +
|
|
pack_int32(mask) +
|
|
pack_principal("test") +
|
|
pack_int32(1) + # policy is present
|
|
pack_string("default") +
|
|
pack_int32(mask))
|
|
|
|
# 40. DELETE existing principal (exercising actual delete path)
|
|
write_corpus("delete_existing.bin",
|
|
pack_int32(KADM_DELETE) +
|
|
pack_principal("user1"))
|
|
|
|
# 41. Cross-realm principal reference
|
|
write_corpus("get_crossrealm.bin",
|
|
pack_int32(KADM_GET) +
|
|
pack_principal("user", "OTHER.REALM") +
|
|
pack_int32(KADM5_PRINCIPAL))
|
|
|
|
# 42. Service principal with instance
|
|
write_corpus("create_service.bin",
|
|
pack_int32(KADM_CREATE) +
|
|
pack_principal_ent("ldap/server.example.com", KADM5_PRINCIPAL | KADM5_MAX_LIFE) +
|
|
pack_int32(KADM5_PRINCIPAL | KADM5_MAX_LIFE) +
|
|
pack_string("servicepass"))
|
|
|
|
print("\nCorpus generation complete!")
|