119 lines
2.6 KiB
Python
119 lines
2.6 KiB
Python
|
|
"""
|
|||
|
|
SSRF (服务端请求伪造) 漏洞知识
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from ..base import KnowledgeDocument, KnowledgeCategory
|
|||
|
|
|
|||
|
|
|
|||
|
|
SSRF = KnowledgeDocument(
|
|||
|
|
id="vuln_ssrf",
|
|||
|
|
title="Server-Side Request Forgery (SSRF)",
|
|||
|
|
category=KnowledgeCategory.VULNERABILITY,
|
|||
|
|
tags=["ssrf", "request", "url", "network", "internal", "cloud"],
|
|||
|
|
severity="high",
|
|||
|
|
cwe_ids=["CWE-918"],
|
|||
|
|
owasp_ids=["A10:2021"],
|
|||
|
|
content="""
|
|||
|
|
SSRF允许攻击者诱使服务器向内部资源或任意外部地址发起请求。
|
|||
|
|
|
|||
|
|
## 危险模式
|
|||
|
|
|
|||
|
|
### Python
|
|||
|
|
```python
|
|||
|
|
# 危险 - 直接使用用户URL
|
|||
|
|
response = requests.get(user_provided_url)
|
|||
|
|
urllib.request.urlopen(url_from_user)
|
|||
|
|
httpx.get(user_url)
|
|||
|
|
|
|||
|
|
# 危险 - URL拼接
|
|||
|
|
base_url = "http://internal-api/"
|
|||
|
|
requests.get(base_url + user_path)
|
|||
|
|
|
|||
|
|
# 危险 - 图片/文件获取
|
|||
|
|
image_url = request.args.get('url')
|
|||
|
|
response = requests.get(image_url)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Node.js
|
|||
|
|
```javascript
|
|||
|
|
// 危险
|
|||
|
|
fetch(req.body.url);
|
|||
|
|
axios.get(userUrl);
|
|||
|
|
http.get(url, callback);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 攻击目标
|
|||
|
|
1. 内部服务 (localhost, 127.0.0.1, 内网IP)
|
|||
|
|
2. 云元数据服务
|
|||
|
|
- AWS: http://169.254.169.254/latest/meta-data/
|
|||
|
|
- GCP: http://metadata.google.internal/
|
|||
|
|
- Azure: http://169.254.169.254/metadata/
|
|||
|
|
3. 内部API和数据库
|
|||
|
|
4. 文件协议 (file://)
|
|||
|
|
|
|||
|
|
## 绕过技术
|
|||
|
|
```
|
|||
|
|
# IP绕过
|
|||
|
|
http://127.0.0.1 -> http://127.1 -> http://0
|
|||
|
|
http://localhost -> http://[::1]
|
|||
|
|
http://2130706433 (十进制)
|
|||
|
|
http://0x7f000001 (十六进制)
|
|||
|
|
|
|||
|
|
# DNS重绑定
|
|||
|
|
attacker.com -> 解析到内网IP
|
|||
|
|
|
|||
|
|
# URL解析差异
|
|||
|
|
http://evil.com@internal/
|
|||
|
|
http://internal#@evil.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 检测要点
|
|||
|
|
1. 所有发起HTTP请求的地方
|
|||
|
|
2. URL参数是否来自用户
|
|||
|
|
3. 是否有URL白名单验证
|
|||
|
|
4. 是否限制了协议和端口
|
|||
|
|
|
|||
|
|
## 安全实践
|
|||
|
|
1. URL白名单验证
|
|||
|
|
2. 禁止访问内部IP地址
|
|||
|
|
3. 使用DNS解析验证
|
|||
|
|
4. 限制协议(仅http/https)
|
|||
|
|
5. 禁用重定向或限制重定向
|
|||
|
|
|
|||
|
|
## 修复示例
|
|||
|
|
```python
|
|||
|
|
import ipaddress
|
|||
|
|
from urllib.parse import urlparse
|
|||
|
|
|
|||
|
|
def is_safe_url(url):
|
|||
|
|
try:
|
|||
|
|
parsed = urlparse(url)
|
|||
|
|
|
|||
|
|
# 只允许http/https
|
|||
|
|
if parsed.scheme not in ['http', 'https']:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 解析IP
|
|||
|
|
import socket
|
|||
|
|
ip = socket.gethostbyname(parsed.hostname)
|
|||
|
|
ip_obj = ipaddress.ip_address(ip)
|
|||
|
|
|
|||
|
|
# 禁止私有IP
|
|||
|
|
if ip_obj.is_private or ip_obj.is_loopback:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 白名单域名
|
|||
|
|
if parsed.hostname not in ALLOWED_HOSTS:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
except:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 使用
|
|||
|
|
if is_safe_url(user_url):
|
|||
|
|
response = requests.get(user_url, allow_redirects=False)
|
|||
|
|
```
|
|||
|
|
""",
|
|||
|
|
)
|