redis_cmd 内置防注入功能的原理与验证

目录

一.源码查看

二.官方文档说明

1.概述

2.RESP协议描述

3.向Redis服务器发送命令

三.测试验证

1.字符串拼接注入测试

​编辑

2.占位符参数化注入测试

3.RESP协议二进制注入测试

四.结论


在kamailio.cfg.template脚本中,执行redis的命令都是通过redis_cmd来向redis服务器发送的,并且脚本里面都是对原本的命令以及参数都是拼接,所以担心存在注入问题。

我们知道,对于mysql来说,她有一个参数化防注入机制,有专门的"预编译语法",也就是提前固定命令机构,后续绑定参数,而redis是不具备这个机制的。

但是通过查询,redis有RESP解析规则,并且kamailio中也是遵循这个协议来发送redis命令的。RESP协议会按照固定的格式解析"命令名+参数",会把第一个元素当作命令,其余当作这个命令的参数。

这里需要介绍一下RESP:是Redis 客户端与服务器之间的通信协议,RESP 可以序列化不同的数据类型,包括整数、字符串和数组。 它还具有一种针对错误的特定类型。 客户端以字符串数组的形式向 Redis 服务器发送请求。 数组的内容是服务器应执行的命令及其参数。 服务器的回复类型是命令特定的。

(服务端具体怎么通过协议解析命令详见第二部分)

接着通过源码、redis官方文档、实验测试三部分来证明redis_cmd底层的实现已经杜绝了注入问题(即使是拼接)

一.源码查看

kamailio中的redis_cmd底层是实现了参数化传递防注入攻击的。从源码来看,有w_redis_cmd3(cmd4,cmd5.....)->redisc_exec->redisvCommand->redisvAppendCommand->redisvFormatCommand。最终跳转到redisvFormatCommand函数中。

在这个函数里面把命令模板和参数解析成"命令名+独立参数数组",而不是拼接成普通字符串。具体实现是:首先用curargv来独立存储每个参数,遍历c(也就是format)从参数列表里面取出实际参数,把参数内容追加到curarg,所以说在这里是当值存储的,不会解析redis语法,遍历完后,所有参数都会被存入curargv数组,成为独立元素。最后他会遍历每个参数,原样写入不解析任何语法。所以redis服务器收到之后,假设第一个参数SET是命令名,后续的参数中即使有flushall也会被当作值。

下面的代码只是redisvFormatCommand函数的一部分,主要提取了实现 Redis 命令注入防护的核心逻辑片段,完整的函数在下图所示的文件中

cpp 复制代码
/* 
 * 核心功能:将格式化命令字符串解析为Redis RESP协议格式的命令
 * 防注入核心:将所有参数(含恶意字符)封装为RESP协议的"参数数组",而非拼接成可执行的多命令
 * 参数说明:
 *   target - 输出最终的RESP协议命令字符串
 *   format - 命令格式模板(如"SET %s %s")
 *   ap - 可变参数列表(模板中%s/%b对应的实际参数)
 */
int redisvFormatCommand(char **target, const char *format, va_list ap) {
    const char *c = format;       // 遍历格式模板的指针
    char **curargv = NULL;        // 存储拆分后的参数数组(核心防注入载体)
    int argc = 0;                 // 参数数组的长度
    int totlen = 0;               // 最终RESP命令的总长度
    hisds curarg = hi_sdsempty(); // 临时存储当前解析的参数
    int touched = 0;              // 标记当前参数是否有内容

    if (target == NULL) return -1;
    if (curarg == NULL) return -1;

    /* ========== 核心步骤1:解析格式模板,拆分参数(防注入关键) ========== */
    while(*c != '\0') {
        // 非占位符(%)的普通字符:逐字符拼接为当前参数
        if (*c != '%' || c[1] == '\0') {
            // 遇到空格:结束当前参数,存入参数数组(按空格拆分参数)
            if (*c == ' ') {
                if (touched) {
                    // 扩容参数数组,将当前参数存入
                    char **newargv = hi_realloc(curargv,sizeof(char*)*(argc+1));
                    if (newargv == NULL) goto memory_err;
                    curargv = newargv;
                    curargv[argc++] = curarg; // 每个参数独立存储,不拼接
                    totlen += bulklen(hi_sdslen(curarg));

                    // 重置临时参数存储,准备解析下一个参数
                    curarg = hi_sdsempty();
                    if (curarg == NULL) goto memory_err;
                    touched = 0;
                }
            } else {
                // 非空格字符:拼接到当前参数(恶意字符仅作为普通字符存储)
                hisds newarg = hi_sdscatlen(curarg,c,1);
                if (newarg == NULL) goto memory_err;
                curarg = newarg;
                touched = 1;
            }
        } else {
            /* ========== 核心步骤2:处理占位符(%s/%b等),安全填充参数 ========== */
            char *arg;
            size_t size;
            hisds newarg = curarg;

            switch(c[1]) {
            case 's': // 字符串参数:安全获取参数,拼接到当前参数(不解析恶意命令)
                arg = va_arg(ap,char*);
                size = strlen(arg);
                if (size > 0)
                    newarg = hi_sdscatlen(curarg,arg,size);
                break;
            case 'b': // 二进制参数:按长度读取,避免解析\r\n等协议分隔符
                arg = va_arg(ap,char*);
                size = va_arg(ap,size_t);
                if (size > 0)
                    newarg = hi_sdscatlen(curarg,arg,size);
                break;
            case '%': // 转义%:仅作为普通字符存储,不解析为占位符
                newarg = hi_sdscat(curarg,"%");
                break;
            }

            if (newarg == NULL) goto memory_err;
            curarg = newarg;
            touched = 1;
            c++; // 跳过占位符的第二个字符(如%s的s)
        }
        c++;
    }

    /* ========== 核心步骤3:收尾最后一个参数,存入参数数组 ========== */
    if (touched) {
        char **newargv = hi_realloc(curargv,sizeof(char*)*(argc+1));
        if (newargv == NULL) goto memory_err;
        curargv = newargv;
        curargv[argc++] = curarg; // 最后一个参数存入数组
        totlen += bulklen(hi_sdslen(curarg));
    } else {
        hi_sdsfree(curarg);
    }

    /* ========== 核心步骤4:封装为RESP协议(最终防注入保障) ========== */
    // 计算RESP协议头长度(*argc\r\n)
    totlen += 1+countDigits(argc)+2;
    char *cmd = hi_malloc(totlen+1);
    if (cmd == NULL) goto memory_err;

    // 1. 写入RESP协议头:*参数个数\r\n(标识这是一个参数数组)
    int pos = sprintf(cmd,"*%d\r\n",argc);
    // 2. 遍历参数数组,按RESP格式写入每个参数($长度\r\n值\r\n)
    for (int j = 0; j < argc; j++) {
        // 写入参数长度:$长度\r\n
        pos += sprintf(cmd+pos,"$%zu\r\n",hi_sdslen(curargv[j]));
        // 写入参数内容(含恶意字符的参数仅作为普通字符串)
        memcpy(cmd+pos,curargv[j],hi_sdslen(curargv[j]));
        pos += hi_sdslen(curargv[j]);
        hi_sdsfree(curargv[j]);
        // 写入参数结尾分隔符:\r\n(转义恶意的\r\n,避免解析为新命令)
        cmd[pos++] = '\r';
        cmd[pos++] = '\n';
    }
    cmd[pos] = '\0';

    /* ========== 资源释放与结果返回 ========== */
    hi_free(curargv);
    *target = cmd; // 最终的RESP协议命令(防注入后的安全格式)
    return totlen;

}

二.官方文档说明

1.概述

接着redis官方提供了对于RESP的详细介绍,包括服务器端收到消息会怎么解析,以及客户端以怎样的形式去发送redis的命令,下面是链接,可以查看

https://redis.io/docs/latest/develop/reference/protocol-spec/

文中介绍了该协议的基本概念:RESP 可以序列化不同的数据类型,包括整数、字符串和数组。 它还具有一种针对错误的特定类型。 客户端以字符串数组的形式向 Redis 服务器发送请求。 数组的内容是服务器应执行的命令及其参数。 服务器的回复类型是命令特定的。

RESP 是二进制安全的,使用前缀长度传输大批量数据,因此无需处理从一个进程传输到另一个进程的批量数据。

2.RESP协议描述

这是服务器对于接收到消息的接收处理,他说:数组中的第一个(有时也是第二个)批量字符串就是命令的名称,数组的后续元素是命令的参数。并且用\r\n分隔其各部分。

3.向Redis服务器发送命令

这里定义了客户端向服务器发送命令的格式

三.测试验证

1.字符串拼接注入测试

核心逻辑:直接拼接恶意命令; KEYS *;到 SET 字符串中,模拟注入漏洞,验证 Redis 会执行注入命令。

redis日志显示:在kamailio.cfg.template脚本中request_route中添加以下代码:

bash 复制代码
# 1. 定义恶意注入内容
$var(malicious) = "test_key; KEYS *;";    # 注入KEYS *(只读,无破坏性) 
# 2. 危险写法:字符串拼接SET命令(留注入漏洞) 
$var(danger_cmd) = "SET " + $var(malicious) + " 123";    # 拼接成:SET test_key; KEYS *; 123 
$var(res) = redis_cmd("db0", "$var(danger_cmd)", "test_danger");

redis日志显示:

可以看到他把这个命令拆分成了多个参数,接着在redis中直接执行这个命令发现报语法错误,所以注入的redis命令无法执行或生效,就是没有注入成功。

那如果把冒号改成空格,能否注入成功呢

bash 复制代码
# 1. 定义恶意注入内容
$var(malicious) = "test_key KEYS *";    # 注入KEYS *(只读,无破坏性) 
# 2. 危险写法:字符串拼接SET命令(留注入漏洞) 
$var(danger_cmd) = "SET " + $var(malicious) ;   
$var(res) = redis_cmd("db0", "$var(danger_cmd)", "test_danger");

可以看到命令仍然无法被拆分成正确的格式,并且在redis中执行该命令,也是语法错误,注入失败。

综上所述,对于redis_cmd来说,对于命令的发送和解析遵循底层的RESP协议,即使是拼接命令,redis收到命令之后,第一个参数当作命令的名称,后续输入的内容都是命令的参数,不会发生注入。

2.占位符参数化注入测试

核心逻辑:用%s占位符传递含恶意内容的参数,验证 Redis 仅把恶意内容当作 key 值,不执行注入命令。

在脚本中添加以下内容

bash 复制代码
# 1. 定义恶意内容(和场景1完全一样) 
$var(malicious) = "test_key KEYS *";
# 2. 安全写法:占位符参数化(最简单的SET key value) 
$var(res) = redis_cmd("db0", "SET %s %s", "$var(malicious)", "123","abc"); # 占位符传key和value

日志中图片显示KEYS 并没有作为一个单独命令,而是作为一个参数

查询keys的value,发现KEYS是添加到后续原本要设置的key后面,设置的value是123。

综上所述,如果是用占位符参数化防止注入,也是可以成功的,还是依赖于底层的RESP协议,redis收到命令之后,第一个参数当作命令的名称,后续输入的内容都是命令的参数,不会发生注入。

3.RESP协议二进制注入测试

核心逻辑:传递含\r\n(RESP 协议分隔符)的恶意二进制内容,验证 Redis 不解析为新命令,仅当作普通字符。

在脚本中添加以下内容:

bash 复制代码
# 1. 定义恶意内容(和场景1完全一样) 
$var(malicious_bin) = "hack_key\r\n$3\r\nSET\r\n$5\r\nhack_val\r\n$3\r\n123\r\n";
# 2. 安全写法:占位符传递二进制恶意内容(最简单的SET)
$var(res) = redis_cmd("db0", "SET %s %s", "$var(malicious_bin)", "456", "abc");

原本是要执行两次SET命令的,但是redis日志显示还是把后面的当作一整个参数。

可以发现设置了"hack_key\r\n3\\r\\nSET\\r\\n5\r\nhack_val\r\n$3\r\n123\r\n"整体的值为456

综上所述,由于RESP底层对于命令的发送和解析是依赖\r\n分隔,模拟这样的二进制内容,仍然会把命令名称后面的内容当作参数,无法注入成功。

四.结论

所以最后的结论就是,通过查看源码以及官方文档,还有进行不同场景下的实验测试,发现redis_cmd底层设计了参数处理逻辑和RESP协议封装,天然就具备防御命令注入的能力。

redis_cmd会将第一个元素识别为Redis命令名,后续的所有内容无论是含(;/\n)都会当作当前命令的参数,不会被解析为独立的命令。

RESP协议的封装,redis_cmd会将所有参数按协议规范封装(长度\\r\\n值\\r\\n),对于参数中的\\r\\n协议分隔符会自动转义为普通字符,构造含 \\r\\n3\r\nsET\r\n$5\r\nhack_val\r\n 的二进制恶意内容,最终仅作为 SET 命令的 key字符串存储,Redis 未解析出注入的 sET hack_val 123 命令。

相关推荐
小张程序人生21 小时前
ShardingJDBC读写分离详解与实战
数据库
木风小助理21 小时前
三大删除命令:MySQL 核心用法解析
数据库·oracle
麦聪聊数据21 小时前
MySQL 性能调优:从EXPLAIN到JSON索引优化
数据库·sql·mysql·安全·json
Facechat21 小时前
视频混剪-时间轴设计
java·数据库·缓存
lalala_lulu21 小时前
MySQL中InnoDB支持的四种事务隔离级别名称,以及逐级之间的区别?(超详细版)
数据库·mysql
曹牧21 小时前
Oracle:大量数据删除
数据库·oracle
小四的快乐生活21 小时前
大数据SQL诊断(采集、分析、优化方案)
大数据·数据库·sql
CV工程师的自我修养1 天前
你的SQL为什么慢?看懂MySQL EXPLAIN执行计划,快速定位性能瓶颈
数据库·mysql
一壶纱1 天前
UniApp + Pinia 数据持久化
前端·数据库·uni-app