首页
社区
课程
招聘
[原创]Redis漏洞分析,ACL篇
发表于: 2025-5-6 23:13 2446

[原创]Redis漏洞分析,ACL篇

2025-5-6 23:13
2446

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] 53bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6J5k6h3c8A6M7#2)9J5c8Y4u0W2k6r3W2K6i4K6u0r3M7$3g2U0N6i4u0A6N6s2V1`.
[2] 38dK9s2c8@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;

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 1
支持
分享
最新回复 (1)
雪    币: 1593
活跃值: (1599)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
好,多发,爱看
2025-5-9 09:38
0
游客
登录 | 注册 方可回帖
返回