kadmind: Add fuzz mode and fuzz corpus
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Fuzzing kadmin
|
||||
|
||||
Kadmind includes built-in fuzzing support via the `--fuzz-stdin` flag, which
|
||||
processes a single RPC message from stdin without requiring network setup or
|
||||
authentication.
|
||||
|
||||
## Running
|
||||
|
||||
### Standalone mode
|
||||
|
||||
```bash
|
||||
# Process a single corpus file
|
||||
./kadmind --fuzz-stdin < fuzz/get_existing_test.bin
|
||||
|
||||
# With a specific realm
|
||||
./kadmind -r TEST.H5L.SE --fuzz-stdin < fuzz/create_new.bin
|
||||
```
|
||||
|
||||
### With AFL++
|
||||
|
||||
```bash
|
||||
# Build with AFL instrumentation
|
||||
CC=afl-clang-fast CXX=afl-clang-fast++ \
|
||||
../configure --enable-maintainer-mode --enable-developer
|
||||
make
|
||||
|
||||
# Run fuzzer
|
||||
afl-fuzz -i kadmin/fuzz -o findings -- ./kadmind --fuzz-stdin
|
||||
```
|
||||
|
||||
### With libFuzzer
|
||||
|
||||
To use libFuzzer, create a harness that calls the internal fuzzing entry point:
|
||||
|
||||
```c
|
||||
#include <stdint.h>
|
||||
extern int kadmind_fuzz_input(const uint8_t *data, size_t size);
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
kadmind_fuzz_input(data, size);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Seed Corpus
|
||||
|
||||
The `fuzz/` directory contains seed inputs covering:
|
||||
|
||||
- All kadm_ops commands (GET, DELETE, CREATE, RENAME, CHPASS, MODIFY, RANDKEY, etc.)
|
||||
- Edge cases (invalid commands, truncated data, malformed principals)
|
||||
- Overflow tests (large/negative array counts)
|
||||
|
||||
See `fuzz/README` for detailed corpus file descriptions.
|
||||
|
||||
## Regenerating Corpus
|
||||
|
||||
```bash
|
||||
cd fuzz
|
||||
python3 gen_corpus.py
|
||||
```
|
||||
|
||||
## Message Format
|
||||
|
||||
Each corpus file contains a length-prefixed message:
|
||||
|
||||
```
|
||||
[4-byte big-endian length][message payload]
|
||||
```
|
||||
|
||||
The payload starts with a 4-byte command number (see `kadm_ops` enum in
|
||||
`lib/kadm5/kadm5-private.h`).
|
||||
@@ -0,0 +1,81 @@
|
||||
Kadmind Fuzzing Corpus
|
||||
======================
|
||||
|
||||
This directory contains seed inputs for fuzzing kadmind RPC handling.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Run kadmind in fuzzing mode:
|
||||
|
||||
./kadmind --fuzz-stdin < corpus_file.bin > output.bin
|
||||
|
||||
Or with a specific realm:
|
||||
|
||||
./kadmind -r MY.REALM --fuzz-stdin < corpus_file.bin
|
||||
|
||||
Message Format
|
||||
--------------
|
||||
|
||||
Each corpus file contains a length-prefixed message:
|
||||
|
||||
[4-byte big-endian length][message payload]
|
||||
|
||||
The message payload starts with a 4-byte command number (kadm_ops enum):
|
||||
|
||||
kadm_get = 0 - Get principal
|
||||
kadm_delete = 1 - Delete principal
|
||||
kadm_create = 2 - Create principal
|
||||
kadm_rename = 3 - Rename principal
|
||||
kadm_chpass = 4 - Change password
|
||||
kadm_modify = 5 - Modify principal
|
||||
kadm_randkey = 6 - Randomize keys
|
||||
kadm_get_privs = 7 - Get admin privileges
|
||||
kadm_get_princs = 8 - List principals
|
||||
kadm_chpass_with_key = 9 - Change password with explicit keys
|
||||
kadm_nop = 10 - No operation (ping/interrupt)
|
||||
kadm_prune = 11 - Prune old keys
|
||||
|
||||
Corpus Files
|
||||
------------
|
||||
|
||||
Normal operations:
|
||||
nop_reply.bin - NOP with reply requested
|
||||
nop_noreply.bin - NOP without reply (interrupt)
|
||||
get_principal.bin - GET with basic mask
|
||||
get_principal_all.bin - GET with all fields
|
||||
delete_principal.bin - DELETE principal
|
||||
create_principal.bin - CREATE with minimal fields
|
||||
create_principal_attrs.bin - CREATE with attributes
|
||||
modify_principal.bin - MODIFY principal
|
||||
rename_principal.bin - RENAME principal
|
||||
chpass_principal.bin - CHPASS
|
||||
chpass_principal_keepold.bin - CHPASS keeping old keys
|
||||
randkey_principal.bin - RANDKEY simple
|
||||
randkey_principal_full.bin - RANDKEY with ks_tuples
|
||||
get_privs.bin - GET_PRIVS
|
||||
get_princs_all.bin - LIST all principals
|
||||
get_princs_expr.bin - LIST with expression
|
||||
get_princs_iter.bin - LIST with online iteration
|
||||
prune_principal.bin - PRUNE to specific kvno
|
||||
prune_principal_all.bin - PRUNE (no kvno)
|
||||
chpass_with_key.bin - CHPASS_WITH_KEY
|
||||
create_with_tldata.bin - CREATE with TL_DATA
|
||||
create_empty_password.bin - CREATE with empty password
|
||||
|
||||
Edge cases and malformed inputs:
|
||||
invalid_cmd.bin - Invalid command number
|
||||
truncated_get.bin - GET with missing data
|
||||
malformed_principal.bin - Bad principal encoding
|
||||
long_principal.bin - Very long principal name
|
||||
many_components.bin - Principal with many components
|
||||
large_nkeydata.bin - Large n_key_data (overflow test)
|
||||
negative_nkeydata.bin - Negative n_key_data
|
||||
empty_message.bin - Zero-length message
|
||||
|
||||
Regenerating
|
||||
------------
|
||||
|
||||
Run gen_corpus.py to regenerate all corpus files:
|
||||
|
||||
python3 gen_corpus.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,472 @@
|
||||
#!/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!")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -152,6 +152,9 @@ void start_server(krb5_context, const char*);
|
||||
krb5_error_code
|
||||
kadmind_loop (krb5_context, krb5_keytab, int, int);
|
||||
|
||||
kadm5_ret_t
|
||||
kadmind_dispatch(void *, krb5_boolean, krb5_data *, krb5_data *, int);
|
||||
|
||||
/* rpc.c */
|
||||
|
||||
int
|
||||
|
||||
@@ -47,6 +47,7 @@ static char *fuzz_client_name;
|
||||
static char *fuzz_keytab_name;
|
||||
static char *fuzz_service_name;
|
||||
static char *fuzz_admin_server;
|
||||
static int fuzz_stdin_flag;
|
||||
#endif
|
||||
int async_flag;
|
||||
static int help_flag;
|
||||
@@ -108,6 +109,8 @@ static struct getargs args[] = {
|
||||
"Keytab for fuzzing", "KEYTAB" },
|
||||
{ "fuzz-server", 0, arg_string, &fuzz_admin_server,
|
||||
"Name of kadmind self instance", "HOST:PORT" },
|
||||
{ "fuzz-stdin", 0, arg_flag, &fuzz_stdin_flag,
|
||||
"Read raw kadmin RPCs from stdin (for fuzzing)", NULL },
|
||||
#endif
|
||||
{ "help", 'h', arg_flag, &help_flag, NULL, NULL },
|
||||
{ "version", 'v', arg_flag, &version_flag, NULL, NULL }
|
||||
@@ -125,6 +128,7 @@ usage(int ret)
|
||||
}
|
||||
|
||||
static void *fuzz_thread(void *);
|
||||
static void fuzz_stdin(krb5_context);
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
@@ -208,6 +212,15 @@ main(int argc, char **argv)
|
||||
if (ret)
|
||||
krb5_err(context, 1, ret, "kadm5_add_passwd_quality_verifier");
|
||||
|
||||
#ifndef WIN32
|
||||
if (fuzz_stdin_flag) {
|
||||
if(realm)
|
||||
krb5_set_default_realm(context, realm);
|
||||
fuzz_stdin(context);
|
||||
exit(0);
|
||||
}
|
||||
#endif
|
||||
|
||||
if(debug_flag) {
|
||||
int debug_port;
|
||||
|
||||
@@ -317,4 +330,215 @@ fuzz_thread(void *arg)
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fuzzing mode: read raw kadmin RPC messages from stdin, process them
|
||||
* with kadmind_dispatch(), and write responses to stdout.
|
||||
*
|
||||
* Message format (both input and output):
|
||||
* 4-byte big-endian length
|
||||
* N bytes of message data
|
||||
*
|
||||
* This bypasses all Kerberos authentication and encryption, allowing
|
||||
* direct fuzzing of the RPC parsing and handling code.
|
||||
*
|
||||
* A temporary directory is created with an empty HDB database to allow
|
||||
* the fuzzer to exercise database operations.
|
||||
*/
|
||||
static void
|
||||
fuzz_stdin(krb5_context contextp)
|
||||
{
|
||||
krb5_error_code ret;
|
||||
kadm5_config_params realm_params;
|
||||
void *kadm_handlep;
|
||||
krb5_data in, out;
|
||||
unsigned char lenbuf[4];
|
||||
uint32_t len;
|
||||
ssize_t n;
|
||||
char tmpdir[] = "/tmp/kadmind-fuzz-XXXXXX";
|
||||
char *dbname = NULL;
|
||||
char *acl_file = NULL;
|
||||
char *stash_file = NULL;
|
||||
int fd;
|
||||
|
||||
/* Create a temporary directory for the fuzz HDB */
|
||||
if (mkdtemp(tmpdir) == NULL)
|
||||
err(1, "mkdtemp");
|
||||
|
||||
/* Set up paths for database files */
|
||||
if (asprintf(&dbname, "%s/heimdal", tmpdir) == -1 ||
|
||||
asprintf(&acl_file, "%s/kadmind.acl", tmpdir) == -1 ||
|
||||
asprintf(&stash_file, "%s/m-key", tmpdir) == -1)
|
||||
errx(1, "out of memory");
|
||||
|
||||
/* Create an empty ACL file (allow all for fuzzing) */
|
||||
fd = open(acl_file, O_CREAT | O_WRONLY | O_TRUNC, 0600);
|
||||
if (fd >= 0) {
|
||||
/* Write a permissive ACL for fuzzing */
|
||||
if (write(fd, "*/admin@* all\n", 14) < 0 ||
|
||||
write(fd, "*@* all\n", 8) < 0)
|
||||
warn("write to ACL file");
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't create a stash file - hdb_set_master_keyfile() returns success
|
||||
* if the file doesn't exist (ENOENT), which means no master key encryption.
|
||||
* An empty file would cause HEIM_ERR_EOF.
|
||||
*/
|
||||
|
||||
memset(&realm_params, 0, sizeof(realm_params));
|
||||
realm_params.mask = KADM5_CONFIG_REALM | KADM5_CONFIG_DBNAME |
|
||||
KADM5_CONFIG_ACL_FILE | KADM5_CONFIG_STASH_FILE;
|
||||
realm_params.realm = realm ? realm : "FUZZ.REALM";
|
||||
realm_params.dbname = dbname;
|
||||
realm_params.acl_file = acl_file;
|
||||
realm_params.stash_file = stash_file;
|
||||
|
||||
/* Set default realm on the context so principal parsing works */
|
||||
ret = krb5_set_default_realm(contextp, realm_params.realm);
|
||||
if (ret)
|
||||
krb5_err(contextp, 1, ret, "krb5_set_default_realm");
|
||||
|
||||
/* Initialize server context with a fake admin client */
|
||||
ret = kadm5_s_init_with_password_ctx(contextp,
|
||||
KADM5_ADMIN_SERVICE, /* client */
|
||||
NULL, /* password */
|
||||
KADM5_ADMIN_SERVICE, /* service */
|
||||
&realm_params,
|
||||
0, 0,
|
||||
&kadm_handlep);
|
||||
if (ret)
|
||||
krb5_err(contextp, 1, ret, "kadm5_s_init_with_password_ctx");
|
||||
|
||||
/*
|
||||
* Pre-populate the HDB with test principals so fuzzing can exercise
|
||||
* code paths beyond "principal not found" errors.
|
||||
*/
|
||||
{
|
||||
kadm5_principal_ent_rec ent;
|
||||
krb5_principal testprinc;
|
||||
const char *test_principals[] = {
|
||||
"test",
|
||||
"admin/admin",
|
||||
"user1",
|
||||
"user2",
|
||||
"host/localhost",
|
||||
"HTTP/www.example.com",
|
||||
"krbtgt/FUZZ.REALM",
|
||||
NULL
|
||||
};
|
||||
int i;
|
||||
|
||||
for (i = 0; test_principals[i] != NULL; i++) {
|
||||
memset(&ent, 0, sizeof(ent));
|
||||
ret = krb5_parse_name(contextp, test_principals[i], &testprinc);
|
||||
if (ret)
|
||||
continue;
|
||||
ent.principal = testprinc;
|
||||
ent.max_life = 86400;
|
||||
ent.max_renewable_life = 604800;
|
||||
/* Create with a simple password - we don't care about security */
|
||||
(void)kadm5_create_principal(kadm_handlep, &ent,
|
||||
KADM5_PRINCIPAL | KADM5_MAX_LIFE |
|
||||
KADM5_MAX_RLIFE,
|
||||
"fuzzpass");
|
||||
krb5_free_principal(contextp, testprinc);
|
||||
}
|
||||
}
|
||||
|
||||
/* Read-process-write loop */
|
||||
for (;;) {
|
||||
/* Read 4-byte length prefix */
|
||||
n = read(STDIN_FILENO, lenbuf, 4);
|
||||
if (n == 0)
|
||||
break; /* EOF */
|
||||
if (n != 4)
|
||||
errx(1, "Short read on length prefix");
|
||||
|
||||
len = (lenbuf[0] << 24) | (lenbuf[1] << 16) |
|
||||
(lenbuf[2] << 8) | lenbuf[3];
|
||||
|
||||
if (len > 10 * 1024 * 1024)
|
||||
errx(1, "Message too large: %u", len);
|
||||
|
||||
/* Read message body */
|
||||
ret = krb5_data_alloc(&in, len);
|
||||
if (ret)
|
||||
krb5_err(contextp, 1, ret, "krb5_data_alloc");
|
||||
|
||||
n = read(STDIN_FILENO, in.data, len);
|
||||
if (n != (ssize_t)len)
|
||||
errx(1, "Short read on message body");
|
||||
|
||||
/* Dispatch the RPC */
|
||||
krb5_data_zero(&out);
|
||||
ret = kadmind_dispatch(kadm_handlep,
|
||||
TRUE, /* initial ticket */
|
||||
&in,
|
||||
&out,
|
||||
readonly_flag);
|
||||
krb5_data_free(&in);
|
||||
|
||||
if (ret) {
|
||||
/* On error, write zero-length response */
|
||||
memset(lenbuf, 0, 4);
|
||||
if (write(STDOUT_FILENO, lenbuf, 4) < 0)
|
||||
break;
|
||||
} else {
|
||||
/* Write response with length prefix */
|
||||
lenbuf[0] = (out.length >> 24) & 0xff;
|
||||
lenbuf[1] = (out.length >> 16) & 0xff;
|
||||
lenbuf[2] = (out.length >> 8) & 0xff;
|
||||
lenbuf[3] = out.length & 0xff;
|
||||
if (write(STDOUT_FILENO, lenbuf, 4) < 0)
|
||||
break;
|
||||
if (out.length > 0 && write(STDOUT_FILENO, out.data, out.length) < 0)
|
||||
break;
|
||||
}
|
||||
krb5_data_free(&out);
|
||||
}
|
||||
|
||||
kadm5_destroy(kadm_handlep);
|
||||
|
||||
/* Clean up temp directory - best effort, don't fail on errors */
|
||||
if (dbname) {
|
||||
char *p;
|
||||
/* Remove database files (sqlite creates multiple files) */
|
||||
if (asprintf(&p, "%s.sqlite3", dbname) != -1) {
|
||||
(void) unlink(p);
|
||||
free(p);
|
||||
}
|
||||
if (asprintf(&p, "%s.sqlite3-journal", dbname) != -1) {
|
||||
(void) unlink(p);
|
||||
free(p);
|
||||
}
|
||||
if (asprintf(&p, "%s.sqlite3-wal", dbname) != -1) {
|
||||
(void) unlink(p);
|
||||
free(p);
|
||||
}
|
||||
if (asprintf(&p, "%s.sqlite3-shm", dbname) != -1) {
|
||||
(void) unlink(p);
|
||||
free(p);
|
||||
}
|
||||
free(dbname);
|
||||
}
|
||||
if (acl_file) {
|
||||
(void) unlink(acl_file);
|
||||
free(acl_file);
|
||||
}
|
||||
if (stash_file) {
|
||||
(void) unlink(stash_file);
|
||||
free(stash_file);
|
||||
}
|
||||
/* Remove log file if created */
|
||||
{
|
||||
char *logfile;
|
||||
if (asprintf(&logfile, "%s/log", tmpdir) != -1) {
|
||||
(void) unlink(logfile);
|
||||
free(logfile);
|
||||
}
|
||||
}
|
||||
(void) rmdir(tmpdir);
|
||||
}
|
||||
#endif
|
||||
|
||||
+17
-5
@@ -179,9 +179,9 @@ iter_cb(void *cbdata, const char *p)
|
||||
}
|
||||
|
||||
static kadm5_ret_t
|
||||
kadmind_dispatch(void *kadm_handlep, krb5_boolean initial,
|
||||
krb5_data *in, krb5_auth_context ac, int fd,
|
||||
krb5_data *out, int readonly)
|
||||
kadmind_dispatch_int(void *kadm_handlep, krb5_boolean initial,
|
||||
krb5_data *in, krb5_auth_context ac, int fd,
|
||||
krb5_data *out, int readonly)
|
||||
{
|
||||
kadm5_ret_t ret = 0;
|
||||
kadm5_ret_t ret_sp = 0;
|
||||
@@ -894,6 +894,18 @@ fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Public wrapper for fuzzing - doesn't require auth_context or fd.
|
||||
* The online LIST protocol (which needs ac and fd) is disabled when fd < 0.
|
||||
*/
|
||||
kadm5_ret_t
|
||||
kadmind_dispatch(void *kadm_handlep, krb5_boolean initial,
|
||||
krb5_data *in, krb5_data *out, int readonly)
|
||||
{
|
||||
return kadmind_dispatch_int(kadm_handlep, initial, in, NULL, -1, out,
|
||||
readonly);
|
||||
}
|
||||
|
||||
struct iter_aliases_ctx {
|
||||
HDB_Ext_Aliases aliases;
|
||||
krb5_tl_data *tl;
|
||||
@@ -1033,8 +1045,8 @@ v5_loop (krb5_context contextp,
|
||||
if(ret)
|
||||
krb5_err(contextp, 1, ret, "krb5_read_priv_message");
|
||||
doing_useful_work = 1;
|
||||
ret = kadmind_dispatch(kadm_handlep, initial, &in, ac, fd, &out,
|
||||
readonly);
|
||||
ret = kadmind_dispatch_int(kadm_handlep, initial, &in, ac, fd, &out,
|
||||
readonly);
|
||||
if (ret)
|
||||
krb5_err(contextp, 1, ret, "kadmind_dispatch");
|
||||
krb5_data_free(&in);
|
||||
|
||||
Reference in New Issue
Block a user