2025.1.22—SSRF—一、[De1CTF 2019]SSRF Me 代码审计|SSRF漏洞

开始SSRF漏洞学习

SSRF(Server - Side Request Forgery,服务器端请求伪造)

        一种网络安全漏洞,它允许攻击者通过操纵服务器端发起的请求,以服务器的身份访问内部或外部资源。

1.原理

        正常情况下,服务器会根据用户请求,从外部获取数据,比如获取某个图片、文件的内容。但如果服务器对用户输入的请求地址未进行严格过滤和验证,攻击者就可以修改这个请求地址,让服务器去访问其他任意地址。

2.危害

  • 内部网络探测:攻击者能利用服务器探测内部网络结构,发现内部存活主机、开放端口等信息。比如通过向不同的内部 IP 和端口发送请求,根据响应判断是否开放。
  • 敏感数据泄露:若内部存在数据库、文件服务器等,攻击者可构造请求获取敏感数据。例如访问内部数据库的特定接口获取用户账号密码。
  • 攻击内部服务:可对内部服务发起攻击,如利用 SSRF 触发内部的拒绝服务(DoS)攻击,或者执行一些恶意的命令(如果服务存在命令执行漏洞且被 SSRF 利用)。
  • 外网恶意利用:攻击者也可以让服务器访问一些需要认证的外网服务,冒用服务器身份进行恶意操作,如利用服务器的身份发送垃圾邮件等。

3.常见攻击场景

  • 利用 file 协议读取本地文件:在支持file://协议的服务器环境中,攻击者可通过构造file:///etc/passwd(Linux 系统下获取用户信息文件)这样的地址,让服务器读取本地敏感文件。
  • 利用 gopher 协议攻击 Redis:通过gopher://协议构造特定的 Redis 命令,如写入恶意配置,甚至获取服务器权限。例如构造命令写入 SSH 公钥,实现远程登录。
  • 绕过防火墙限制:一些服务器出于安全考虑设置了防火墙,限制外部对内部的访问,但服务器自身访问内部不受限。攻击者利用 SSRF 可突破防火墙限制访问内部资源。

理解:访问A网站,用A网站去访问B网站,A为跳板,目的是B

豆包举的例子更好理解


题目来源:buuctf  [De1CTF 2019]SSRF Me

一、打开靶机,整理信息

        贴心,给了源码和提示,提示中告诉了我们flag信息藏在flag,txt文件中

./表示再当前目录

        打开靶机很乱,依稀可以看出来是python代码,应该是源码,跟大佬学到一招:可以放在pycharm里Ctrl+Alt+L将代码格式化一下

二、解题思路

step 1:源码分析

#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)


class Task:
    def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)):          #SandBox For Remote_Addr
            os.mkdir(self.sandbox)

    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())
@app.route('/')
def index():
    return open("code.txt","r").read()


def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"



def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
    return hashlib.md5(content).hexdigest()


def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False


if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0')

        源码很多很陌生,自己读不懂,跟着大佬的wp一步步来吧

1.先看路由,涵盖的面比较广

路由(Routing)

        用于将不同的 URL 请求映射到对应的处理函数或类上,从而决定当用户访问特定 URL 时,执行哪些代码逻辑来生成响应。

理解:

        路由就像交通导航系统,核心作用是根据不同的请求信息,将其精准地引导到对应的处理程序上。

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())

主要功能:从请求的Cookie和查询参数中获取数据,对查询参数进行安全检查,然后创建一个Task对象并执行其Exec方法,最后执行结果以JSON格式返回给客户端。

分解功能:

  • urllib.unquote 用于对URL编码的字符串进行解码
  • request.cookies.get("action") 从请求的cookie中获取名为action的值
  • request.args.get("param","") 从请求的查询参数中后去名为param的值,不存在在返回空,饭后解码后赋值给param变量
  • request.remote._addr 获取客户端的IP地址,并赋值给ip变量
  • challenge里,用参数构造一个Task类对象,并且执行它的Exec方法

        通过get方法传入param参数值,在cookie里面传递action和sign的值,然后将传递的param通过waf这个函数进行安全检查。

2.接着看waf函数

def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False

主要功能:对输入的参数param进行安全检查

分解:

(1)预处理函数check=param.strip().lower()
  • param.strip() 会去除参数param首位的空白字符,如空格、制表符、换行符等
  • .lower() 会将处理后的字符串转换为小写形式,防止大小写绕过
(2)检查协议前缀 if check.startswith("gopher") or check.startswith("file"):
  • check.startswith("gopher") 是检查处理后的字符是否以gopher开头,gopher协议可以被用来构造复杂请求,从而发起SSRF攻击
  • check.startswith("file") 检查处理后的字符是否以file开头,file协议允许访问本地文件系统

所以我们不能通过协议读取文件

3.看Exec方法

 def Exec(self):
        result = {}
        result['code'] = 500 //将code字段初始化为500,表示默认执行出现错误
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

Exec方法先通过checkSign方法检测登录,所以要看checkSign方法

4.看checkSign方法

def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False

判断我们传入的action、param参数通过getSign函数处理后是否和sign相等

3.看看getSign函数

def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

        根据输入的action和param参数生成一个签名(sign),并且将三个东西拼接起来然后进行md5哈希算法加密。

        .hexdigest()意为将哈希对象的结果以十六进制字符串的形式返回。

签名通常用于验证数据的完整性和真实性,防止数据在传输过程中被篡改。

        这里好像没路了,那就看看完整源码里,哪里还用到了getSign函数,发现是/geneSign这个路由调用了getSign函数

4./geneSign路由

#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

主要功能:为特定的scan操作生成签名,即可以生成我们需要的md5。这里的action只有“scan”

        没有进一步信息了,回到Exec方法

5.回归Exec方法

  1. 初始化一个空字典result用于存储执行结果信息。将result字典的code字段初始化为500,表示默认执行出现错误
  2. 调用① if (self.checkSign()):  方法检查签名是否有效,有效则继续执行,否则跳转到else分支
  3. ①if分支内嵌套②if分支。打开result.txt文件,该文件位于self.sandbox目录下
    1. 调用scan函数,传入self.param作为参数,得到扫描结果resp
    2. ②if分支内嵌套③if分支。如果resp为Connection Timeout,表示连接超时,将该结果赋值给result字典的data字段。否则,将扫描结果写入文件,并关闭文件
    3. 将result字段的code字段设置为200,表示操作成功
  4. 仍为①if内嵌套④if分支进行读取操作
    1. 打开文件,将code字段设置为200,表示操作成功
    2. 将文件内容读取出来并赋值给data字段
  5. 若均未匹配,则code字段仍为500,data则为Action Error
  6. 若检查签名返回无效,code字段仍未500,并将msg字段设置为Action Error
  7. 最后返回包含执行结果的result字典。

step 2:源码信息总结

  • scan函数使用了urllib.urlopen访问了外部资源
  • 有两个路由:/De1ta和/geneSign(用来生成md5)
  • 有了sign的生成过程:三个部分拼接而成
  • Exec()函数中判断语句用的是in而不是==
  • action中必须要有scan和read两个,才能读取flag

step 3:思路整理

        突破点在于scan函数访问了外部资源,而检查签名时,调用了scan函数

        而且没有对self.param进行严格过滤,仅仅只过滤了gopher协议和file协议。如果能控制param为./flag.txt,则能读取其中内容,若能成功读取flag.txt,则将其中内容写入result.txt中。

        如果 'read' in self.action,则将result.txt读取并存到result['data']中,Exec()函数的最后,还返回了result,而在challenge()函数中,则返回了json.dumps(task.Exec()),即我们的此处的result。

        先进入/De1ta绑定的challenge()函数,将在Exec中的scan部分中将flag.txt的内容存入result.txt,然后从read部分中将其存到result字典中读出,再以json形式返回到客户端,我们就能得到flag。

参考:[De1CTF 2019]SSRF Me之愚见 - 简书

step 4:开始解题

解法一:字符串拼接

        尝试访问/geneSign?param=flag.txt,给了一个md5:da5e952b257d2f4e24759a8afc366e52

        但是geneSign只有scan的功能,要加入read功能才可以,参考其他师傅的wp

因为geneSign中action只有scan,需要拼接read,而md5加密中拼接了三个东西,第一部分用到了scan,第二部分是flag.txt,第三部分是read,三个内容都有了,所以访问/geneSign?param=flag.txtread

        拿到了我们要的md5,然后访问/De1ta?param=flag.txt,构造cookie action=readscan;sign=beb0d1508ca91ede850d2b160d635d87,即可拿到flag

解法二:哈希拓展攻击

        这个还没学明白,先把大佬的wp贴在这文章 - De1CTF ssrf_me 的三种解法 - 先知社区

解法三:local_file

        同上文章 - De1CTF ssrf_me 的三种解法 - 先知社区

三、小结

1.做的我脑子缠在一起了,看的我好乱,感觉问题出在了代码审计,审不明白,有代码解释但是没办法形成完整逻辑链

2.源码中包含urlopen字样,或者靶机中有提交文件名(说明可以将url作为文件名),要考虑SSRF漏洞

3.本题中学到了路由、签名等小知识点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值