-
-
[原创]Redis漏洞分析,ACL篇
-
发表于: 2025-5-6 23:13 4477
-
Redis是一个开源的高性能内存数据库,并且开启了安全策略,针对8.0.x\7.4.x、7.2.x和6.2.x及以上版本的Redis漏洞进行公开披露[1]。 
《Redis漏洞分析》对其中的Moderate、High级别漏洞进行分析,同时根据Redis的攻击面进行分篇,本篇是ACL篇。
对Redis漏洞分析的流程分为4个步骤:
Redis ACL是Access Control List(访问控制列表)的缩写,通过ACL,可以控制客户端对不同redis命令和数据的访问权限。
用于配置ACL的命令有12个,一些业务功能成对实现,比如ACL SETUSER/DELUSER负责创建/删除一个账户,ACL SAVE/LOAD负责ACL的备份和恢复。其他业务功能单独实现,比如ACL CAT [category]用于索引当前账户,指定类别的访问权限。

披露时间:2024年10月
复现版本: 7.2.0
补丁版本: 7.4.1

该漏洞产生于ACL SETUSER命令的处理逻辑当中,ACL SETUSER命令的语法如下,针对设置的user,可以配置多个规则。
Redis ACL规则分为两类:1. 定义命令权限的规则,即命令规则;2. 定义用户状态的规则,即用户管理规则。
1.命令规则(部分)
~<
%R~<
%W~<
%RW~<
off:将用户设置为未激活,将无法以此用户登录。如果一个用户在已经通过该用户的身份验证的连接之后被禁用(设置为off),那么该连接将继续按预期工作。
nopass:用户被设置为无密码用户。这意味着可以使用任何密码进行身份验证。
使用ACL SETUSER命令构造一个恶意的命令规则,在获取user的规则时即可触发断言错误。
ASAN追踪漏洞,可以发现PoC在src/acl.c:307引发了崩溃。看到调用栈上面还有对_serverPanic的调用,可以判断这是一个断言错误,即redis对非预期的结果进行了断言处理。
定位到src/acl.c:307,漏洞产生于sdsCatPatternString函数中,可以断定,恶意构造的命令规则没有进行正确的解析,被断言发现,引发panic。
那么我们审计规则处理函数ACLSetSelector,尝试从中寻找漏洞。大致看一下处理逻辑,首先根据规则首字符(op[0])分出基本块,注意到处理%规则符时,会进入一个循环,在此循环中给flags赋值,这里的flags就是sdsCatPatternString索引的对象。
考虑这样一种边界条件,在%之后的规则符是~,这样控制流会跳出循环,而flags依旧是初始值0,同时函数返回C_OK,指示命令成功执行。
该漏洞的成因是,处理逻辑没有考虑到边界条件,在未对flags赋值之前就可以跳出循环。
所以针对该边界条件,补丁对其进行了检测,增加了对flags的非零判断。
披露时间:2025年1月6日
复现版本: 7.4.1
补丁版本: 7.4.2

在分析上一个漏洞成因时,是否发现了ACLSetSelector其中还潜伏着一个漏洞?
这次PoC更简单。
回顾ACLSetSelector的处理逻辑,如果我们构造一个只有%的规则,会发生什么?结果是直接跳出for循环,触发panic。
该漏洞成因是,补丁注意到了flags的非零判断,但不多。于是二次补丁将flags的非零判断后移到了for循环之后。
披露时间:2025年3月
复现版本:8.0.2 (valkey)
补丁版本:8.0.3 (valkey)

Valkey是Redis7.2.4的开源fork,目前21k stars,经历几个小版本的迭代,在某些模块中已经和Redis和较大的改动。
PoC来自issue#1832 [2],复制一个server,作为replica。在replica中执行ACL LOAD命令会触发crash。
issue中提到,Redis中不存在该漏洞。我们可以提出假设,Valkey在ACL LOAD命令函数中进行了改动,改动的代码引发了issue中的问题。
diff二者的acl.c文件,在ACLLoadFromFile函数中,可以看到漏洞的成因。删除的if语句明确注释到,user在某种状况下可能为NULL,接下来需要验证,这项改动是否是成因。
注释Redis的这行代码,进行验证。
ASAN追踪漏洞,可以验证Valkey的不严谨改动导致了crash。
Valkey直接对c->user进行判空。
既然这个漏洞是Valkey对fork代码进行改动而引入的,那么还有没有类似的缺陷?diff二者的acl.c文件,发现除了上述的ACLLoadFromFile函数有明显改动,aclCatWithFlags函数中也有类似改动。
在执行ACL CAT [category] 命令时,Redis跳过了模块引入的命令,Valkey则删除了if判断。查看函数的逻辑,初步判断存在整型溢出。
当模块引入超过2^31的命令时,arraylen会溢出。这里的arraylen是一个指针,是否会影响后续代码呢?回到aclCatWithFlags函数的调用位置,arraylen作为参数传递给setDeferredArrayLen函数。
可惜的是,传递时作了类型扩展,以long的大小传递,这样无法达到溢出为负值的效果。同时模块需要创建2^31个命令,这个条件也不好达成。
[1] a68K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6J5k6h3c8A6M7#2)9J5c8Y4u0W2k6r3W2K6i4K6u0r3M7$3g2U0N6i4u0A6N6s2V1`.
[2] 2feK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6$3j5h3I4C8k6i4W2Q4x3X3c8A6L8#2)9J5c8Y4k6S2L8r3E0W2P5g2)9J5c8X3W2K6M7%4g2W2M7#2)9J5c8U0p5^5x3K6t1`.
make MALLOC=libc CFLAGS="-fsanitize=address -fno-omit-frame-pointer -O0 -g" LDFLAGS="-fsanitize=address" -j4make MALLOC=libc CFLAGS="-fsanitize=address -fno-omit-frame-pointer -O0 -g" LDFLAGS="-fsanitize=address" -j4语法: ACL SETUSER username [rule [rule ...]]引入自:Redis Open Source 6.0.0时间复杂度:O(N). ACL 类别:@admin, @slow, @dangerous语法: ACL SETUSER username [rule [rule ...]]引入自:Redis Open Source 6.0.0时间复杂度:O(N). ACL 类别:@admin, @slow, @dangerousACL SETUSER user %~ACL GETUSER userACL SETUSER user %~ACL GETUSER user==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==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:7360src/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);}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);}src/acl.c int ACLSetSelector(aclSelector *selector, const char* 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;}src/acl.c int ACLSetSelector(aclSelector *selector, const char* 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;}src/acl.c @@ -1051,7 +1051,7 @@ int ACLSetSelector(aclSelector *selector, const char* 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 {src/acl.c @@ -1051,7 +1051,7 @@ int ACLSetSelector(aclSelector *selector, const char* 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 {ACL SETUSER user %ACL GETUSER userACL SETUSER user %ACL GETUSER usersrc/acl.c int ACLSetSelector(aclSelector *selector, const char* 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;}src/acl.c int ACLSetSelector(aclSelector *selector, const char* 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;