kadmind: Add fuzz mode and fuzz corpus
This commit is contained in:
71
kadmin/FUZZING.md
Normal file
71
kadmin/FUZZING.md
Normal file
@@ -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`).
|
||||
81
kadmin/fuzz/README
Normal file
81
kadmin/fuzz/README
Normal file
@@ -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
|
||||
BIN
kadmin/fuzz/chpass_existing.bin
Normal file
BIN
kadmin/fuzz/chpass_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/chpass_existing_keepold.bin
Normal file
BIN
kadmin/fuzz/chpass_existing_keepold.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/chpass_key_existing.bin
Normal file
BIN
kadmin/fuzz/chpass_key_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/chpass_multikey.bin
Normal file
BIN
kadmin/fuzz/chpass_multikey.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/create_empty_password.bin
Normal file
BIN
kadmin/fuzz/create_empty_password.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/create_new.bin
Normal file
BIN
kadmin/fuzz/create_new.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/create_service.bin
Normal file
BIN
kadmin/fuzz/create_service.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/create_with_attrs.bin
Normal file
BIN
kadmin/fuzz/create_with_attrs.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/create_with_tldata.bin
Normal file
BIN
kadmin/fuzz/create_with_tldata.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/delete_existing.bin
Normal file
BIN
kadmin/fuzz/delete_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/delete_nonexisting.bin
Normal file
BIN
kadmin/fuzz/delete_nonexisting.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/empty_message.bin
Normal file
BIN
kadmin/fuzz/empty_message.bin
Normal file
Binary file not shown.
472
kadmin/fuzz/gen_corpus.py
Normal file
472
kadmin/fuzz/gen_corpus.py
Normal file
@@ -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!")
|
||||
BIN
kadmin/fuzz/get_crossrealm.bin
Normal file
BIN
kadmin/fuzz/get_crossrealm.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_existing_admin.bin
Normal file
BIN
kadmin/fuzz/get_existing_admin.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_existing_all.bin
Normal file
BIN
kadmin/fuzz/get_existing_all.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_existing_host.bin
Normal file
BIN
kadmin/fuzz/get_existing_host.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_existing_http.bin
Normal file
BIN
kadmin/fuzz/get_existing_http.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_existing_krbtgt.bin
Normal file
BIN
kadmin/fuzz/get_existing_krbtgt.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_existing_test.bin
Normal file
BIN
kadmin/fuzz/get_existing_test.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_nonexisting.bin
Normal file
BIN
kadmin/fuzz/get_nonexisting.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_princs_all.bin
Normal file
BIN
kadmin/fuzz/get_princs_all.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_princs_host.bin
Normal file
BIN
kadmin/fuzz/get_princs_host.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_princs_iter.bin
Normal file
BIN
kadmin/fuzz/get_princs_iter.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_princs_user.bin
Normal file
BIN
kadmin/fuzz/get_princs_user.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_princs_wildcard.bin
Normal file
BIN
kadmin/fuzz/get_princs_wildcard.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/get_privs.bin
Normal file
BIN
kadmin/fuzz/get_privs.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/invalid_cmd.bin
Normal file
BIN
kadmin/fuzz/invalid_cmd.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/large_nkeydata.bin
Normal file
BIN
kadmin/fuzz/large_nkeydata.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/long_principal.bin
Normal file
BIN
kadmin/fuzz/long_principal.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/malformed_principal.bin
Normal file
BIN
kadmin/fuzz/malformed_principal.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/many_components.bin
Normal file
BIN
kadmin/fuzz/many_components.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/modify_existing.bin
Normal file
BIN
kadmin/fuzz/modify_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/modify_existing_rlife.bin
Normal file
BIN
kadmin/fuzz/modify_existing_rlife.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/modify_with_policy.bin
Normal file
BIN
kadmin/fuzz/modify_with_policy.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/negative_nkeydata.bin
Normal file
BIN
kadmin/fuzz/negative_nkeydata.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/nop_noreply.bin
Normal file
BIN
kadmin/fuzz/nop_noreply.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/nop_reply.bin
Normal file
BIN
kadmin/fuzz/nop_reply.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/prune_existing.bin
Normal file
BIN
kadmin/fuzz/prune_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/randkey_existing.bin
Normal file
BIN
kadmin/fuzz/randkey_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/randkey_existing_full.bin
Normal file
BIN
kadmin/fuzz/randkey_existing_full.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/rename_existing.bin
Normal file
BIN
kadmin/fuzz/rename_existing.bin
Normal file
Binary file not shown.
BIN
kadmin/fuzz/truncated_get.bin
Normal file
BIN
kadmin/fuzz/truncated_get.bin
Normal file
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
|
||||
|
||||
224
kadmin/kadmind.c
224
kadmin/kadmind.c
@@ -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
|
||||
|
||||
@@ -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