diff --git a/kadmin/FUZZING.md b/kadmin/FUZZING.md new file mode 100644 index 000000000..549084bd2 --- /dev/null +++ b/kadmin/FUZZING.md @@ -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 +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`). diff --git a/kadmin/fuzz/README b/kadmin/fuzz/README new file mode 100644 index 000000000..441a6f697 --- /dev/null +++ b/kadmin/fuzz/README @@ -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 diff --git a/kadmin/fuzz/chpass_existing.bin b/kadmin/fuzz/chpass_existing.bin new file mode 100644 index 000000000..345776e60 Binary files /dev/null and b/kadmin/fuzz/chpass_existing.bin differ diff --git a/kadmin/fuzz/chpass_existing_keepold.bin b/kadmin/fuzz/chpass_existing_keepold.bin new file mode 100644 index 000000000..736b31ee8 Binary files /dev/null and b/kadmin/fuzz/chpass_existing_keepold.bin differ diff --git a/kadmin/fuzz/chpass_key_existing.bin b/kadmin/fuzz/chpass_key_existing.bin new file mode 100644 index 000000000..c19f15887 Binary files /dev/null and b/kadmin/fuzz/chpass_key_existing.bin differ diff --git a/kadmin/fuzz/chpass_multikey.bin b/kadmin/fuzz/chpass_multikey.bin new file mode 100644 index 000000000..87404373f Binary files /dev/null and b/kadmin/fuzz/chpass_multikey.bin differ diff --git a/kadmin/fuzz/create_empty_password.bin b/kadmin/fuzz/create_empty_password.bin new file mode 100644 index 000000000..1d6798078 Binary files /dev/null and b/kadmin/fuzz/create_empty_password.bin differ diff --git a/kadmin/fuzz/create_new.bin b/kadmin/fuzz/create_new.bin new file mode 100644 index 000000000..d54e6be86 Binary files /dev/null and b/kadmin/fuzz/create_new.bin differ diff --git a/kadmin/fuzz/create_service.bin b/kadmin/fuzz/create_service.bin new file mode 100644 index 000000000..c839d4809 Binary files /dev/null and b/kadmin/fuzz/create_service.bin differ diff --git a/kadmin/fuzz/create_with_attrs.bin b/kadmin/fuzz/create_with_attrs.bin new file mode 100644 index 000000000..c072036d2 Binary files /dev/null and b/kadmin/fuzz/create_with_attrs.bin differ diff --git a/kadmin/fuzz/create_with_tldata.bin b/kadmin/fuzz/create_with_tldata.bin new file mode 100644 index 000000000..44cf7505d Binary files /dev/null and b/kadmin/fuzz/create_with_tldata.bin differ diff --git a/kadmin/fuzz/delete_existing.bin b/kadmin/fuzz/delete_existing.bin new file mode 100644 index 000000000..7f44b02d0 Binary files /dev/null and b/kadmin/fuzz/delete_existing.bin differ diff --git a/kadmin/fuzz/delete_nonexisting.bin b/kadmin/fuzz/delete_nonexisting.bin new file mode 100644 index 000000000..2cdab490d Binary files /dev/null and b/kadmin/fuzz/delete_nonexisting.bin differ diff --git a/kadmin/fuzz/empty_message.bin b/kadmin/fuzz/empty_message.bin new file mode 100644 index 000000000..593f4708d Binary files /dev/null and b/kadmin/fuzz/empty_message.bin differ diff --git a/kadmin/fuzz/gen_corpus.py b/kadmin/fuzz/gen_corpus.py new file mode 100644 index 000000000..616fef509 --- /dev/null +++ b/kadmin/fuzz/gen_corpus.py @@ -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!") diff --git a/kadmin/fuzz/get_crossrealm.bin b/kadmin/fuzz/get_crossrealm.bin new file mode 100644 index 000000000..933dd047b Binary files /dev/null and b/kadmin/fuzz/get_crossrealm.bin differ diff --git a/kadmin/fuzz/get_existing_admin.bin b/kadmin/fuzz/get_existing_admin.bin new file mode 100644 index 000000000..8671f988b Binary files /dev/null and b/kadmin/fuzz/get_existing_admin.bin differ diff --git a/kadmin/fuzz/get_existing_all.bin b/kadmin/fuzz/get_existing_all.bin new file mode 100644 index 000000000..24bbb672f Binary files /dev/null and b/kadmin/fuzz/get_existing_all.bin differ diff --git a/kadmin/fuzz/get_existing_host.bin b/kadmin/fuzz/get_existing_host.bin new file mode 100644 index 000000000..d7b3958d2 Binary files /dev/null and b/kadmin/fuzz/get_existing_host.bin differ diff --git a/kadmin/fuzz/get_existing_http.bin b/kadmin/fuzz/get_existing_http.bin new file mode 100644 index 000000000..e61472dff Binary files /dev/null and b/kadmin/fuzz/get_existing_http.bin differ diff --git a/kadmin/fuzz/get_existing_krbtgt.bin b/kadmin/fuzz/get_existing_krbtgt.bin new file mode 100644 index 000000000..519a83fe2 Binary files /dev/null and b/kadmin/fuzz/get_existing_krbtgt.bin differ diff --git a/kadmin/fuzz/get_existing_test.bin b/kadmin/fuzz/get_existing_test.bin new file mode 100644 index 000000000..052e4f188 Binary files /dev/null and b/kadmin/fuzz/get_existing_test.bin differ diff --git a/kadmin/fuzz/get_nonexisting.bin b/kadmin/fuzz/get_nonexisting.bin new file mode 100644 index 000000000..d456831d1 Binary files /dev/null and b/kadmin/fuzz/get_nonexisting.bin differ diff --git a/kadmin/fuzz/get_princs_all.bin b/kadmin/fuzz/get_princs_all.bin new file mode 100644 index 000000000..d5c47378a Binary files /dev/null and b/kadmin/fuzz/get_princs_all.bin differ diff --git a/kadmin/fuzz/get_princs_host.bin b/kadmin/fuzz/get_princs_host.bin new file mode 100644 index 000000000..1085fb6d0 Binary files /dev/null and b/kadmin/fuzz/get_princs_host.bin differ diff --git a/kadmin/fuzz/get_princs_iter.bin b/kadmin/fuzz/get_princs_iter.bin new file mode 100644 index 000000000..d83f3b06a Binary files /dev/null and b/kadmin/fuzz/get_princs_iter.bin differ diff --git a/kadmin/fuzz/get_princs_user.bin b/kadmin/fuzz/get_princs_user.bin new file mode 100644 index 000000000..988deda0f Binary files /dev/null and b/kadmin/fuzz/get_princs_user.bin differ diff --git a/kadmin/fuzz/get_princs_wildcard.bin b/kadmin/fuzz/get_princs_wildcard.bin new file mode 100644 index 000000000..aaa4a70e3 Binary files /dev/null and b/kadmin/fuzz/get_princs_wildcard.bin differ diff --git a/kadmin/fuzz/get_privs.bin b/kadmin/fuzz/get_privs.bin new file mode 100644 index 000000000..3aea68379 Binary files /dev/null and b/kadmin/fuzz/get_privs.bin differ diff --git a/kadmin/fuzz/invalid_cmd.bin b/kadmin/fuzz/invalid_cmd.bin new file mode 100644 index 000000000..b2b2f3cc3 Binary files /dev/null and b/kadmin/fuzz/invalid_cmd.bin differ diff --git a/kadmin/fuzz/large_nkeydata.bin b/kadmin/fuzz/large_nkeydata.bin new file mode 100644 index 000000000..2d91b63c1 Binary files /dev/null and b/kadmin/fuzz/large_nkeydata.bin differ diff --git a/kadmin/fuzz/long_principal.bin b/kadmin/fuzz/long_principal.bin new file mode 100644 index 000000000..0b93d4dab Binary files /dev/null and b/kadmin/fuzz/long_principal.bin differ diff --git a/kadmin/fuzz/malformed_principal.bin b/kadmin/fuzz/malformed_principal.bin new file mode 100644 index 000000000..098603d77 Binary files /dev/null and b/kadmin/fuzz/malformed_principal.bin differ diff --git a/kadmin/fuzz/many_components.bin b/kadmin/fuzz/many_components.bin new file mode 100644 index 000000000..193c0924c Binary files /dev/null and b/kadmin/fuzz/many_components.bin differ diff --git a/kadmin/fuzz/modify_existing.bin b/kadmin/fuzz/modify_existing.bin new file mode 100644 index 000000000..f36b43fdc Binary files /dev/null and b/kadmin/fuzz/modify_existing.bin differ diff --git a/kadmin/fuzz/modify_existing_rlife.bin b/kadmin/fuzz/modify_existing_rlife.bin new file mode 100644 index 000000000..410ad266a Binary files /dev/null and b/kadmin/fuzz/modify_existing_rlife.bin differ diff --git a/kadmin/fuzz/modify_with_policy.bin b/kadmin/fuzz/modify_with_policy.bin new file mode 100644 index 000000000..6719f9839 Binary files /dev/null and b/kadmin/fuzz/modify_with_policy.bin differ diff --git a/kadmin/fuzz/negative_nkeydata.bin b/kadmin/fuzz/negative_nkeydata.bin new file mode 100644 index 000000000..0250341c7 Binary files /dev/null and b/kadmin/fuzz/negative_nkeydata.bin differ diff --git a/kadmin/fuzz/nop_noreply.bin b/kadmin/fuzz/nop_noreply.bin new file mode 100644 index 000000000..3c4bf32c7 Binary files /dev/null and b/kadmin/fuzz/nop_noreply.bin differ diff --git a/kadmin/fuzz/nop_reply.bin b/kadmin/fuzz/nop_reply.bin new file mode 100644 index 000000000..ddad056f5 Binary files /dev/null and b/kadmin/fuzz/nop_reply.bin differ diff --git a/kadmin/fuzz/prune_existing.bin b/kadmin/fuzz/prune_existing.bin new file mode 100644 index 000000000..095e28e78 Binary files /dev/null and b/kadmin/fuzz/prune_existing.bin differ diff --git a/kadmin/fuzz/randkey_existing.bin b/kadmin/fuzz/randkey_existing.bin new file mode 100644 index 000000000..426ea62ca Binary files /dev/null and b/kadmin/fuzz/randkey_existing.bin differ diff --git a/kadmin/fuzz/randkey_existing_full.bin b/kadmin/fuzz/randkey_existing_full.bin new file mode 100644 index 000000000..da13c848c Binary files /dev/null and b/kadmin/fuzz/randkey_existing_full.bin differ diff --git a/kadmin/fuzz/rename_existing.bin b/kadmin/fuzz/rename_existing.bin new file mode 100644 index 000000000..012a88c0f Binary files /dev/null and b/kadmin/fuzz/rename_existing.bin differ diff --git a/kadmin/fuzz/truncated_get.bin b/kadmin/fuzz/truncated_get.bin new file mode 100644 index 000000000..b544d25e4 Binary files /dev/null and b/kadmin/fuzz/truncated_get.bin differ diff --git a/kadmin/kadmin_locl.h b/kadmin/kadmin_locl.h index 6ad36b909..fc7d391f8 100644 --- a/kadmin/kadmin_locl.h +++ b/kadmin/kadmin_locl.h @@ -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 diff --git a/kadmin/kadmind.c b/kadmin/kadmind.c index fcbc1818a..30b9dfa87 100644 --- a/kadmin/kadmind.c +++ b/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 diff --git a/kadmin/server.c b/kadmin/server.c index d528deb28..5d95c689e 100644 --- a/kadmin/server.c +++ b/kadmin/server.c @@ -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);