From f6269baa605d31859f28770e01a24e3677e5f82c Mon Sep 17 00:00:00 2001 From: Yorgos Thessalonikefs Date: Wed, 26 Nov 2025 11:09:40 +0100 Subject: [PATCH] - Additional fix for CVE-2025-11411 (possible domain hijacking attack), to include YXDOMAIN and non-referral nodata answers in the mitigation as well, reported by TaoFei Guo from Peking University, Yang Luo and JianJun Chen from Tsinghua University. --- iterator/iter_scrub.c | 39 +++++++++-- testdata/iter_scrub_promiscuous.rpl | 84 ++++++++++++++++++++++++ testdata/ratelimit.tdir/ratelimit.testns | 30 +++++++-- 3 files changed, 143 insertions(+), 10 deletions(-) diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index 553d3655f..8507a3fb6 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -418,12 +418,13 @@ shorten_rrset(sldns_buffer* pkt, struct rrset_parse* rrset, int count) * @param qinfo: original query. * @param region: where to allocate synthesized CNAMEs. * @param env: module env with config options. + * @param zonename: name of server zone. * @return 0 on error. */ static int scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, struct query_info* qinfo, struct regional* region, - struct module_env* env) + struct module_env* env, uint8_t* zonename) { uint8_t* sname = qinfo->qname; size_t snamelen = qinfo->qname_len; @@ -431,7 +432,8 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, int cname_length = 0; /* number of CNAMEs, or DNAMEs */ if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR && - FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN) + FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN && + FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_YXDOMAIN) return 1; /* For the ANSWER section, remove all "irrelevant" records and add @@ -470,6 +472,11 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, &aliaslen, pkt)) { verbose(VERB_ALGO, "synthesized CNAME " "too long"); + if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) { + prev = rrset; + rrset = rrset->rrset_all_next; + continue; + } return 0; } cname_length++; @@ -650,6 +657,29 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, "RRset:", pkt, msg, prev, &rrset); continue; } + /* Also delete promiscuous NS for other RCODEs */ + if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR + && env->cfg->iter_scrub_promiscuous) { + remove_rrset("normalize: removing promiscuous " + "RRset:", pkt, msg, prev, &rrset); + continue; + } + /* Also delete promiscuous NS for NOERROR with nodata + * for authoritative answers, not for delegations. + * NOERROR with an_rrsets!=0 already handled. + * Also NOERROR and soa_in_auth already handled. + * NOERROR with an_rrsets==0, and not a referral. + * referral is (NS not the zonename, noSOA). + */ + if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR + && msg->an_rrsets == 0 + && !(dname_pkt_compare(pkt, rrset->dname, + zonename) != 0 && !soa_in_auth(msg)) + && env->cfg->iter_scrub_promiscuous) { + remove_rrset("normalize: removing promiscuous " + "RRset:", pkt, msg, prev, &rrset); + continue; + } if(nsset == NULL) { nsset = rrset; } else { @@ -1060,7 +1090,8 @@ scrub_message(sldns_buffer* pkt, struct msg_parse* msg, /* this is not required for basic operation but is a forgery * resistance (security) feature */ if((FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR || - FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN) && + FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN || + FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) && msg->qdcount == 0) return 0; @@ -1074,7 +1105,7 @@ scrub_message(sldns_buffer* pkt, struct msg_parse* msg, } /* normalize the response, this cleans up the additional. */ - if(!scrub_normalize(pkt, msg, qinfo, region, env)) + if(!scrub_normalize(pkt, msg, qinfo, region, env, zonename)) return 0; /* delete all out-of-zone information */ if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie, qstate)) diff --git a/testdata/iter_scrub_promiscuous.rpl b/testdata/iter_scrub_promiscuous.rpl index 61fca0d28..febbee81c 100644 --- a/testdata/iter_scrub_promiscuous.rpl +++ b/testdata/iter_scrub_promiscuous.rpl @@ -16,6 +16,7 @@ SCENARIO_BEGIN Test iterator with scrub of promiscuous records ; The spoofed contents are ns.attacker.mesa and its IPs 5.6.7.8 and 5.6.7.9. ; The pollute1.mesa NS, ns.pollute2.mesa A, and test3.atkr.pollute3.mesa NS ; with ns.pollute3.mesa A records are tested for cache placement. +; pollute4.mesa uses YXDOMAIN. ; ns.root RANGE_BEGIN 0 400 @@ -84,6 +85,18 @@ SECTION ADDITIONAL ns.pollute3.mesa. IN A 1.2.4.3 ENTRY_END +ENTRY_BEGIN +MATCH opcode subdomain +ADJUST copy_id copy_query +REPLY QR NOERROR +SECTION QUESTION +pollute4.mesa. IN NS +SECTION AUTHORITY +pollute4.mesa. IN NS ns.pollute4.mesa. +SECTION ADDITIONAL +ns.pollute4.mesa. IN A 1.2.4.4 +ENTRY_END + ENTRY_BEGIN MATCH opcode subdomain ADJUST copy_id copy_query @@ -188,6 +201,35 @@ check.pollute3.mesa. IN A 1.8.9.3 ENTRY_END RANGE_END +; ns.pollute4.mesa +RANGE_BEGIN 0 400 + ADDRESS 1.2.4.4 + +; This is the spoofed answer that is returned. +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA YXDOMAIN +SECTION QUESTION +test4.atkr.pollute4.mesa. IN A +SECTION ANSWER +test4.atkr.pollute4.mesa. 86400 IN A 1.2.3.4 +SECTION AUTHORITY +pollute4.mesa. 86400 IN NS ns.attacker.mesa. +ENTRY_END + +; correct answer for the check query. +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +check.pollute4.mesa. IN A +SECTION ANSWER +check.pollute4.mesa. IN A 1.8.9.4 +ENTRY_END +RANGE_END + ; ns.attacker.mesa RANGE_BEGIN 0 400 ADDRESS 5.6.7.8 @@ -370,4 +412,46 @@ check.pollute3.mesa. IN A 1.8.9.3 ;check.pollute3.mesa. IN A 5.6.7.9 ENTRY_END +; Test query 4 +STEP 120 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test4.atkr.pollute4.mesa. IN A +ENTRY_END + +STEP 130 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA YXDOMAIN +SECTION QUESTION +test4.atkr.pollute4.mesa. IN A +SECTION ANSWER +test4.atkr.pollute4.mesa. 86400 IN A 1.2.3.4 +SECTION AUTHORITY +; removed record +;pollute4.mesa. 0 IN NS ns.attacker.mesa. +ENTRY_END + +; Check the cache contents, for query 4. +STEP 140 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +check.pollute4.mesa. IN A +ENTRY_END + +STEP 150 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +check.pollute4.mesa. IN A +SECTION ANSWER +; good answer +check.pollute4.mesa. IN A 1.8.9.4 +; bad answer +;check.pollute4.mesa. IN A 5.6.7.9 +ENTRY_END + SCENARIO_END diff --git a/testdata/ratelimit.tdir/ratelimit.testns b/testdata/ratelimit.tdir/ratelimit.testns index 563c1db6a..5c22c292d 100644 --- a/testdata/ratelimit.tdir/ratelimit.testns +++ b/testdata/ratelimit.tdir/ratelimit.testns @@ -3,13 +3,31 @@ $ORIGIN example.com. $TTL 3600 ENTRY_BEGIN -MATCH opcode qtype +MATCH opcode qname qtype REPLY QR AA NOERROR -ADJUST copy_id copy_query +ADJUST copy_id SECTION QUESTION -wild IN A +www1 IN A SECTION ANSWER -wild IN A 10.20.30.40 -SECTION AUTHORITY -example.com. IN NS ns.example.com. +www1 IN A 1.1.1.1 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qname qtype +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +www2 IN A +SECTION ANSWER +www2 IN A 2.2.2.2 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qname qtype +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +www3 IN A +SECTION ANSWER +www3 IN A 3.3.3.3 ENTRY_END -- 2.47.3