kadmind: Add fuzz mode and fuzz corpus

This commit is contained in:
Nicolas Williams
2025-12-24 14:23:18 -06:00
parent 8964be1eee
commit 9f5db19378
48 changed files with 868 additions and 5 deletions

71
kadmin/FUZZING.md Normal file
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
kadmin/fuzz/create_new.bin Normal file

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.

472
kadmin/fuzz/gen_corpus.py Normal file
View 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!")

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.

BIN
kadmin/fuzz/get_privs.bin Normal file

Binary file not shown.

BIN
kadmin/fuzz/invalid_cmd.bin Normal file

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.

BIN
kadmin/fuzz/nop_noreply.bin Normal file

Binary file not shown.

BIN
kadmin/fuzz/nop_reply.bin Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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);