【二进制漏洞-Redis漏洞分析,ACL篇】此文章归类为:二进制漏洞。
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] c1dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6J5k6h3c8A6M7#2)9J5c8Y4u0W2k6r3W2K6i4K6u0r3M7$3g2U0N6i4u0A6N6s2V1`.
[2] fefK9s2c8@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"
-
j4
make 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, @dangerous
ACL SETUSER user
%
~
ACL GETUSER user
ACL SETUSER user
%
~
ACL GETUSER user
=
=
36101
=
=
ERROR: AddressSanitizer: unknown
-
crash on address
0x0000800f7000
at pc
0x7f8835527956
bp
0x7ffe2f073940
sp
0x7ffe2f073100
READ 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
0x7ffe2f073100
READ 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
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 user
ACL SETUSER user
%
ACL GETUSER user
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;
}
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;
更多【二进制漏洞-Redis漏洞分析,ACL篇】相关视频教程:www.yxfzedu.com