Redis漏洞分析,ACL篇
Redis漏洞分析,ACL篇
fuxxcss 看雪学苑 2025-06-06 09:59
Redis是一个开源的高性能内存数据库,并且开启了安全策略,针对8.0.x\7.4.x、7.2.x和6.2.x及以上版本的Redis漏洞进行公开披露[1]。
《Redis漏洞分析》对其中的Moderate、High级别漏洞进行分析,同时根据Redis的攻击面进行分篇,本篇是ACL篇。
分析流程
对Redis漏洞分析的流程分为4个步骤:
1.寻找Moderate、High级别的漏洞,寻找脆弱版本和修复版本。
2.编译源码,指定libc、ASAN选项。
make MALLOC=libc CFLAGS="-fsanitize=address -fno-omit-frame-pointer -O0 -g" LDFLAGS="-fsanitize=address" -j4
1.寻找PoC,没有则寻找Diff。
2.从4个方面分析漏洞:前置知识、PoC、漏洞成因、补丁。
前置知识
Redis ACL是Access Control List(访问控制列表)的缩写,通过ACL,可以控制客户端对不同redis命令和数据的访问权限。
用于配置ACL的命令有12个,一些业务功能成对实现,比如ACL SETUSER/DELUSER负责创建/删除一个账户,ACL SAVE/LOAD负责ACL的备份和恢复。其他业务功能单独实现,比如ACL CAT [category]用于索引当前账户,指定类别的访问权限。
CVE-2024-31227 断言错误,DoS
披露时间:2024年10月
复现版本: 7.2.0
补丁版本: 7.4.1
前置知识
该漏洞产生于ACL SETUSER命令的处理逻辑当中,ACL SETUSER命令的语法如下,针对设置的user,可以配置多个规则。
语法:ACL SETUSER username [rule [rule ...]]引入自:RedisOpen Source 6.0.0时间复杂度:O(N). ACL 类别:@admin, @slow, @dangerous
Redis ACL规则分为两类:1. 定义命令权限的规则,即命令规则;2. 定义用户状态的规则,即用户管理规则。
1.命令规则(部分)
~<
%R~<
%W~<
%RW~<
2.用户管理规则(部分)
on:将用户设置为激活,可以使用AUTH
off:将用户设置为未激活,将无法以此用户登录。如果一个用户在已经通过该用户的身份验证的连接之后被禁用(设置为off),那么该连接将继续按预期工作。
nopass:用户被设置为无密码用户。这意味着可以使用任何密码进行身份验证。
PoC:
使用ACL SETUSER命令构造一个恶意的命令规则,在获取user的规则时即可触发断言错误。
ACL SETUSER user %~ACL GETUSER user
ASAN追踪漏洞,可以发现PoC在src/acl.c:307引发了崩溃。看到调用栈上面还有对_serverPanic的调用,可以判断这是一个断言错误,即redis对非预期的结果进行了断言处理。
==36101==ERROR: AddressSanitizer: unknown-crash on address 0x0000800f7000 at pc 0x7f8835527956 bp 0x7ffe2f073940 sp 0x7ffe2f073100READ of size 1048576 at 0x0000800f7000 thread T0#0 0x7f8835527955 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115#1 0x557a2d158714 in memtest_preserving_test /opt/redis-7.2.0/src/memtest.c:317#2 0x557a2d1130b4 in memtest_test_linux_anonymous_maps /opt/redis-7.2.0/src/debug.c:2005#3 0x557a2d1132bd in doFastMemoryTest /opt/redis-7.2.0/src/debug.c:2046#4 0x557a2d113f8c in printCrashReport /opt/redis-7.2.0/src/debug.c:2190#5 0x557a2d111788 in _serverPanic /opt/redis-7.2.0/src/debug.c:1158#6 0x557a2d227284 in sdsCatPatternString /opt/redis-7.2.0/src/acl.c:307#7 0x557a2d23550c in aclAddReplySelectorDescription /opt/redis-7.2.0/src/acl.c:2723#8 0x557a2d23657f in aclCommand /opt/redis-7.2.0/src/acl.c:2844#9 0x557a2d001571 in call /opt/redis-7.2.0/src/server.c:3519#10 0x557a2d0055cb in processCommand /opt/redis-7.2.0/src/server.c:4160#11 0x557a2d04439a in processCommandAndResetClient /opt/redis-7.2.0/src/networking.c:2466#12 0x557a2d0448ab in processInputBuffer /opt/redis-7.2.0/src/networking.c:2574#13 0x557a2d04578c in readQueryFromClient /opt/redis-7.2.0/src/networking.c:2713#14 0x557a2d23d24f in callHandler /opt/redis-7.2.0/src/connhelpers.h:79#15 0x557a2d23e728 in connSocketEventHandler /opt/redis-7.2.0/src/socket.c:298#16 0x557a2cfe45fd in aeProcessEvents /opt/redis-7.2.0/src/ae.c:436#17 0x557a2cfe4cf0 in aeMain /opt/redis-7.2.0/src/ae.c:496#18 0x557a2d01798c in main /opt/redis-7.2.0/src/server.c:7360
漏洞成因
定位到src/acl.c:307,漏洞产生于sdsCatPatternString函数中,可以断定,恶意构造的命令规则没有进行正确的解析,被断言发现,引发panic。
src/acl.c sds sdsCatPatternString(sds base, keyPattern *pat) {if (pat->flags == ACL_ALL_PERMISSION) {base = sdscatlen(base,"~",1); } else if (pat->flags == ACL_READ_PERMISSION) {base = sdscatlen(base,"%R~",3); } else if (pat->flags == ACL_WRITE_PERMISSION) {base = sdscatlen(base,"%W~",3); } else {# assert failure→ serverPanic("Invalid key pattern flag detected"); }return sdscatsds(base, pat->pattern);}
那么我们审计规则处理函数ACLSetSelector,尝试从中寻找漏洞。大致看一下处理逻辑,首先根据规则首字符(op[0])分出基本块,注意到处理%规则符时,会进入一个循环,在此循环中给flags赋值,这里的flags就是sdsCatPatternString索引的对象。
考虑这样一种边界条件,在%之后的规则符是~,这样控制流会跳出循环,而flags依旧是初始值0,同时函数返回C_OK,指示命令成功执行。
src/acl.c intACLSetSelector(aclSelector *selector, constchar* op, size_t oplen) { ... } else if (op[0] == '~' || op[0] == '%') {if (selector->flags & SELECTOR_FLAG_ALLKEYS) { errno = EEXIST;return C_ERR; }int flags = 0;size_t offset = 1;if (op[0] == '%') {for (; offset < oplen; offset++) {if (toupper(op[offset]) == 'R' && !(flags & ACL_READ_PERMISSION)) { flags |= ACL_READ_PERMISSION; } else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) { flags |= ACL_WRITE_PERMISSION; # 跳出循环→ } else if (op[offset] == '~') { offset++;break; } else { errno = EINVAL;return C_ERR; } } } else { flags = ACL_ALL_PERMISSION; } ... } else if (op[0] == '&') { ... } ...return C_OK;}
该漏洞的成因是,处理逻辑没有考虑到边界条件,在未对flags赋值之前就可以跳出循环。
补丁
所以针对该边界条件,补丁对其进行了检测,增加了对flags的非零判断。
src/acl.c @@ -1051,7 +1051,7 @@ intACLSetSelector(aclSelector *selector, constchar* op, size_t oplen) { flags |= ACL_READ_PERMISSION;} else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) { flags |= ACL_WRITE_PERMISSION;- } else if (op[offset] == '~') {+ } else if (op[offset] == '~' && flags) { offset++;break;} else {
CVE-2024-51741 断言错误,DoS
披露时间:2025年1月6日
复现版本: 7.4.1
补丁版本: 7.4.2
在分析上一个漏洞成因时,是否发现了ACLSetSelector其中还潜伏着一个漏洞?
另一个漏洞
这次PoC更简单。
ACL SETUSER user %ACL GETUSER user
回顾ACLSetSelector的处理逻辑,如果我们构造一个只有%的规则,会发生什么?结果是直接跳出for循环,触发panic。
src/acl.c intACLSetSelector(aclSelector *selector, constchar* op, size_t oplen) { ... } else if (op[0] == '~' || op[0] == '%') {if (selector->flags & SELECTOR_FLAG_ALLKEYS) { errno = EEXIST;return C_ERR; }int flags = 0;size_t offset = 1;if (op[0] == '%') { # 直接跳出循环→ for (; offset < oplen; offset++) {if (toupper(op[offset]) == 'R' && !(flags & ACL_READ_PERMISSION)) { flags |= ACL_READ_PERMISSION; } else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) { flags |= ACL_WRITE_PERMISSION; } else if (op[offset] == '~' && flags) { offset++;break; } else { errno = EINVAL;return C_ERR; } } } else { flags = ACL_ALL_PERMISSION; } ... } else if (op[0] == '&') { ... } ...return C_OK;}
该漏洞成因是,补丁注意到了flags的非零判断,但不多。于是二次补丁将flags的非零判断后移到了for循环之后。
src/acl.c @@ -1078,19 +1078,24 @@ intACLSetSelector(aclSelector *selector, constchar *op, size_t oplen) {int flags = 0;size_t offset = 1;if (op[0] == '%') {+ int perm_ok = 1;for (; offset < oplen; offset++) {if (toupper(op[offset]) == 'R' && !(flags & ACL_READ_PERMISSION)) { flags |= ACL_READ_PERMISSION; } else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) { flags |= ACL_WRITE_PERMISSION;- } else if (op[offset] == '~' && flags) {+ } else if (op[offset] == '~') { offset++;break; } else {- errno = EINVAL;- return C_ERR;+ perm_ok = 0;+ break; } }+ if (!flags || !perm_ok) {+ errno = EINVAL;+ return C_ERR;+ } } else { flags = ACL_ALL_PERMISSION; }
valkey/issues/1832 空指针解引用,DoS
披露时间:2025年3月
复现版本:8.0.2 (valkey)
补丁版本:8.0.3 (valkey)
前置知识
Valkey是Redis7.2.4的开源fork,目前21k stars,经历几个小版本的迭代,在某些模块中已经和Redis和较大的改动。
PoC
PoC来自issue#1832
[2],复制一个server,作为replica。在replica中执行ACL LOAD命令会触发crash。
# 复制serversrc/valkey-server --port6379 --aclfile1.aclsrc/valkey-server --port6380 --replicaof "localhost 6379" --aclfile1.acl# 恢复ACLsrc/valkey-cli -p6380127.0.0.1:6380 > ACL LOAD
issue中提到,Redis中不存在该漏洞。我们可以提出假设,Valkey在ACL LOAD命令函数中进行了改动,改动的代码引发了issue中的问题。
漏洞成因
diff二者的acl.c文件,在ACLLoadFromFile函数中,可以看到漏洞的成因。删除的if语句明确注释到,user在某种状况下可能为NULL,接下来需要验证,这项改动是否是成因。
src/acl.csds ACLLoadFromFile(const char *filename) { FILE *fp;char buf[1024]; ... listIter li; listNode *ln;listRewind(server.clients,&li);while ((ln = listNext(&li)) != NULL) { client *c = listNodeValue(ln);- /* a MASTER client can do everything (and user = NULL) so we can skip it */- if (c->flags & CLIENT_MASTER)- continue; user *original = c->user; list *channels = NULL;// crash user *new = ACLGetUserByName(c->user->name, sdslen(c->user->name)); ... }}
注释Redis的这行代码,进行验证。
src/redis-server --port6379 --aclfile1.aclsrc/redis-server --port6380 --replicaof "localhost 6379" --aclfile1.aclsrc/redis-cli -p6380127.0.0.1:6380 > ACL LOAD
ASAN追踪漏洞,可以验证Valkey的不严谨改动导致了crash。
==7633==ERROR: AddressSanitizer: unknown-crash on address 0x0000800f7000 at pc 0x7f904fb78956 bp 0x7ffde41381b0 sp 0x7ffde4137970READ of size 1048576 at 0x0000800f7000 thread T0#0 0x7f904fb78955 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115#1 0x55c5adc29a59 in memtest_preserving_test /opt/redis-7.4.2/src/memtest.c:296#2 0x55c5adbe15cb in memtest_test_linux_anonymous_maps /opt/redis-7.4.2/src/debug.c:2157#3 0x55c5adbe17d4 in doFastMemoryTest /opt/redis-7.4.2/src/debug.c:2198#4 0x55c5adbe2af6 in printCrashReport /opt/redis-7.4.2/src/debug.c:2408#5 0x55c5adbe242b in sigsegvHandler /opt/redis-7.4.2/src/debug.c:2328#6 0x7f904f7fc57f (/lib/x86_64-linux-gnu/libc.so.6+0x3d57f) (BuildId: 2e01923fea4ad9f7fa50fe24e0f3385a45a6cd1c)#7 0x55c5add08afd in ACLLoadFromFile /opt/redis-7.4.2/src/acl.c:2444#8 0x55c5add0c582 in aclCommand /opt/redis-7.4.2/src/acl.c:2990#9 0x55c5adabaac4 in call /opt/redis-7.4.2/src/server.c:3575#10 0x55c5adabea1f in processCommand /opt/redis-7.4.2/src/server.c:4206#11 0x55c5adafe524 in processCommandAndResetClient /opt/redis-7.4.2/src/networking.c:2505#12 0x55c5adafea70 in processInputBuffer /opt/redis-7.4.2/src/networking.c:2613#13 0x55c5adaffaac in readQueryFromClient /opt/redis-7.4.2/src/networking.c:2759#14 0x55c5add12b84 in callHandler /opt/redis-7.4.2/src/connhelpers.h:58#15 0x55c5add1405d in connSocketEventHandler /opt/redis-7.4.2/src/socket.c:277#16 0x55c5ada8b67d in aeProcessEvents /opt/redis-7.4.2/src/ae.c:417#17 0x55c5ada8bd70 in aeMain /opt/redis-7.4.2/src/ae.c:477#18 0x55c5adad133e in main /opt/redis-7.4.2/src/server.c:7251
补丁
Valkey直接对c->user进行判空。
src/acl.csds ACLLoadFromFile(const char *filename) { FILE *fp;char buf[1024]; ... listIter li; listNode *ln;listRewind(server.clients,&li);while ((ln = listNext(&li)) != NULL) { client *c = listNodeValue(ln);+ /* Some clients, e.g. the one from the primary to replica, don't have a user+ * associated with them. */+ if (!c->user) continue; user *original = c->user; list *channels = NULL;// crash user *new = ACLGetUserByName(c->user->name, sdslen(c->user->name)); ... }}
类似缺陷
既然这个漏洞是Valkey对fork代码进行改动而引入的,那么还有没有类似的缺陷?diff二者的acl.c文件,发现除了上述的ACLLoadFromFile函数有明显改动,aclCatWithFlags函数中也有类似改动。
src/acl.cvoidaclCatWithFlags(client *c, dict *commands, uint64_t cflag, int *arraylen) { dictEntry *de; dictIterator *di = dictGetIterator(commands);while ((de = dictNext(di)) != NULL) { # 潜在的整型溢出struct redisCommand *cmd = dictGetVal(de);- if (cmd->flags & CMD_MODULE) - continue;if (cmd->acl_categories & cflag) {addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname)); (*arraylen)++; }if (cmd->subcommands_dict) {aclCatWithFlags(c, cmd->subcommands_dict, cflag, arraylen); } }dictReleaseIterator(di); }
在执行ACL CAT [category] 命令时,Redis跳过了模块引入的命令,Valkey则删除了if判断。查看函数的逻辑,初步判断存在整型溢出。
当模块引入超过2^31的命令时,arraylen会溢出。这里的arraylen是一个指针,是否会影响后续代码呢?回到aclCatWithFlags函数的调用位置,arraylen作为参数传递给setDeferredArrayLen函数。
src/acl.cvoid aclCommand(client *c) { ... } else if (!strcasecmp(sub, "cat") && c->argc == 3) { uint64_t cflag = ACLGetCommandCategoryFlagByName(c->argv[2]->ptr);if (cflag == 0) { addReplyErrorFormat(c, "Unknown category '%.128s'", (char *)c->argv[2]->ptr);return; }int arraylen = 0; void *dl = addReplyDeferredLen(c);# 潜在漏洞→ aclCatWithFlags(c, server.orig_commands, cflag, &arraylen); setDeferredArrayLen(c, dl, arraylen); ... } ...}
可惜的是,传递时作了类型扩展,以long的大小传递,这样无法达到溢出为负值的效果。同时模块需要创建2^31个命令,这个条件也不好达成。
参考文献
[1] https://github.com/redis/redis/security
[2] https://github.com/valkey-io/valkey/issues/1832
看雪ID:
fuxxcss
https://bbs.kanxue.com/user-home-1030058.htm
*本文为看雪论坛优秀文章,由
fuxxcss
原创,转载请注明来自看雪社区
往期推荐
2、
逆向分析:Win10 ObRegisterCallbacks的相关分析
5、
OLLVM 攻略笔记
6、
安卓壳学习记录(上)
球分享
球点赞
球在看
点击阅读原文查看更多