浏览器是本地主机网关:使用WebAssembly和Go进行客户端端口扫描
网站倾向于从浏览器扫描用户的开放端口,以更好地识别新用户/回访用户。
浏览器能否滥用"localhost"? 能否通过WebAssembly实现? 实时演示可在 http://ports.sh 或 https://ports.sh 查看, 代码可在 https://github.com/avilum/portsscan 获取, 欢迎贡献代码!
在本文中,我将演示如何滥用浏览器攻击本地主机服务——渗透组织或从浏览器运行远程代码。
当我们访问网页时,每个人都有一个独特的指纹,这已不是秘密。客户端指纹识别帮助网站在众多其他网站上跟踪我们的活动。
指纹是关于您设备、浏览器、屏幕尺寸、IP地址和许多其他变量的多种因素的组合。当这些因素结合在一起时,使我们能够被网站高度识别。
关于客户端代码的说明
诸如eBay之类的网站在我们计算机上无限期地运行代码。 它们过去仅使用JavaScript实现这一点。 多年来,前端技术不断发展——JavaScript(ECMAScript)、TypeScript、Deno(用Rust编写的安全快速JavaScript运行时)等等…
浏览器的JavaScript API一直被恶意软件滥用,因为它是浏览器支持的唯一语言。随着近年来正在开发的下一代Web技术,如WebAssembly,下一代恶意软件可能更加复杂。
WebAssembly(WASM)运行时允许将语言编译成二进制代码,浏览器WebAssembly运行时可以在访问网页时在浏览器中执行低级代码。除了更快(编译代码通常比JS快,但不总是)之外,WASM将编程语言解耦,专注于"做什么"。
WebAssembly运行时非常棒——它为浏览器带来了许多新的API和功能,但没有免费的午餐——WASM是安全研究人员和黑客的重要目标。毕竟,尽管它是公开的,但WASM有大量代码只有极少数人审查过。
这就是为什么许多新语言如Rust、Go和Deno都提供对WebAssembly作为目标架构/运行时的开箱即用支持。
截至目前,您可以用Go、Rust等语言编写代码,而不是使用JavaScript派生的语言。因此,系统编程语言在开发人员中变得广泛适用,他们希望能够超越我们曾经视为"跨平台"的范畴。
Python现在不那么"跨平台"了,是吧?(开玩笑——您可能会发现 https://pyodide.org/en/stable/console.html 很有趣)。
研究WebAssembly运行时(使用Go)
当用户访问我的网站时,从浏览器上下文映射主机上的开放端口(活动、监听服务)有多容易?我能否用低级语言实现这一点?
作为网站所有者,假设我想识别用户是否是开发人员。 端口扫描技术每秒都在发现资产和服务器。“开放端口"字面上指的是绑定到NIC设备特定端口的服务器。某些服务必须手动定义,但许多操作系统在启动时运行服务,暴露许多应用程序API,可在localhost上使用,如IPC、SMB/Samba、SSH、SMTP、FTP等。
多亏了这一点,很容易找到易受1天漏洞(公开但未披露)攻击的资产。这只是谁先到达的问题,以及是否报告(和修复)的问题。
自从多年前第一次听说WASM以来,我一直相信它。我开始尝试使用它。我选择Go是因为其简单的Net/Socket和HTTP标准库API。顺便说一句,Rust也获得了广泛的WebAssembly编译支持。
理解流程
- 用户访问网页
- 现在,用户的浏览器将初始化WebAssembly运行时
- 然后,它将自动运行我的Go端口扫描器,该扫描器已编译成WebAssembly二进制文件
开始:编写一个(不简单的)端口扫描器
我使用Go的’syscalls/js’绑定遵循了Javascript API。 我发现浏览器喜欢我使用HTTP Go API,它与(可能未完全实现的)’net’ Go API配合不佳。
浏览器中尚不支持WebAssembly原始TCP会话和UDP。社区也在研究新的WASI标准——这最终将要求浏览器实现原始TCP/UDP会话。
在尝试了几种滥用WebAssembly原始TCP会话的方法以接收原始TCP响应后,我理解到使用本机TCP会话扫描进行编程是行不通的。出于安全原因(如CORS和不安全端口),Chrome代理(有时阻止)WebAssembly请求和响应。
让我们定义端口扫描器:
然后,我们将定义端口扫描函数:
Go的"http” API有一些很好的优势,比如用于HTTP跟踪的’GotFirstResponseByte’处理程序。我希望它能工作,但它没有——想象一下那会有多容易。
最好利用Go著名的"http"包,毕竟——浏览器都是关于应用程序的。使用像HTTP这样的应用协议应该更容易。
理解响应
如何使用高级HTTP会话一致地扫描端口? 获取响应是一回事。正确理解它们并进行分类是一个挑战,因为我们无法按原样查看响应。端口扫描器通常使用原始数据包和会话对网络进行分类。在撰写本文时,在WebAssembly运行时中这是不可能的。
我们可以积极细分以下响应:
- 连接被拒绝(端口已关闭)
- 超时(端口可能开放或关闭)
- HTTP响应(开放——另一端有有效的HTTP服务器!)
我发现相对较小的超时非常可靠。
随着进展,我遇到了更多问题。浏览器阻止了缺少CORS头的有效HTTP响应,将其呈现为通用"fetch错误",掩盖了错误的真实原因,并将请求标记为"不安全"。浏览器甚至没有将错误内容转发到WASM运行时——它只是失败,没有进一步细节。
迭代常见错误。将它们视为"开放端口"。
CORS怎么样?
localhost HTTP服务通常缺少"Cross-Origin-****“头。 默认提供跨源保护(与window.location不同的主机+端口组合),因此没有这些头的HTTP请求将失败。在WebAssembly上下文中,您无法知道请求是否因CORS而失败。
我必须以某种方式克服这一点, 在经典JS中,只需在fetch()请求中添加’no-cors’模式。在研究了一点并思考覆盖JS的fetch() API调用的方法后,我找到了一个配方——一个相当简单的——效果很好!
我从未想过在我的请求中添加这样的HTTP头。这是一种使用Go语言修改浏览器行为(系统调用)的奇怪方式。
因此,对于演示来说,CORS不再是问题,但这并不意味着它将在明确指定’Access-Control-Allow-Origin’头的网站上工作。
我们无法解决这种情况。尽管如此,世界各地有大量网站将成功运行此WASM代码。
支持TLS/SSL服务的端口
没有SSL握手,我们"跳过"浏览器默认激活的许多SSL安全功能。这帮助我找到任何TCP开放端口,而不仅仅是接受SSL传输的服务。
我们必须记住,WebAssembly运行时的TCP堆栈由Chrome自己的TCP堆栈代理。如果证书不匹配,浏览器将终止我们的SSL请求,而不说明原因——它将看起来像典型的"连接被拒绝”。SSL增加了复杂性和安全功能,因此我继续使用纯文本HTTP。
定义启动函数:
请注意我使用syscalls/js库注入的JS DOM元素,这允许使用Go代码修改DOM。我对简单性感到惊讶。
编译代码:
定义加载WASM的网页 现在让我们看看客户端的计算机。哪些端口当前正在监听(例如"开放")?
注意wasm_exec.js脚本标签,这是WebAssembly Go运行时绑定。
启动本地演示:
python3 -m http.server 5000
访问时运行本地端口扫描:
访问者启动时看到的内容(index.html)——HTML本身在Go加载时注入!这有多酷?
让我们看看访问者的网络数据包
网络数据包在localhost(环回)网络接口上捕获。数据包在左侧编号:数据包10-20,是index.html请求和响应,包括脚本。数据包22之后,浏览器开始端口扫描。它发送(TCP SYN)数据包,并接收(RST,ACK)数据包作为响应,意味着端口已关闭。
现在让我们看看开放端口指示器。
如果套接字接收HTTP超时,而没有收到ConnectionRefused错误,那么该端口上必须有某种监听(通常视为’已过滤’)接受连接,返回重置/确认响应。
注意有效的HTTP/TCP响应(蓝色/紫色)。有人在监听!
并且,将所有内容放在一起:
在访问者浏览器WebAssembly运行时运行端口扫描的HTML页面
将结果与实际情况比较
我使用netstat和nmap验证了我看到的内容。 我期望我的结果相同,它们确实如此。
NetStat输出(当前操作系统上监听的开放端口)与NMap输出(从攻击者角度TCP扫描这些端口)对比。我在WASM端口扫描器中找到的每个端口都得到了这些结果的支持。
Localhost毕竟不是本地的,而是远程的
今天在配置或处理个人计算机时,我们将Localhost视为"安全"或"受控"环境。 曾经似乎攻击本地主机服务要困难得多。
我们安装的持久应用程序通常分配并在localhost上监听端口。 操作系统倾向于在启动时在localhost网络接口上打开端口以工作。无论是Windows、Mac还是Linux——您的计算机都在localhost上监听某些内容。正如我所展示的——Javascript或WebAssembly应用程序可以轻松扫描这些本地主机服务。它们也可以滥用它们。
场景1:通过Linux客户端上的RPC漏洞拒绝服务,使用单次访问
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8779
RPC-BOMB是一个示例漏洞,使攻击者能够通过过时的Linux内置RPC服务(rpcbind)使用恶意TCP数据包对操作系统造成拒绝服务攻击。
想象一下:您访问一个网站。 一秒钟后——您的计算机卡住或重置,销毁您最新的未保存工作。
- 查找该易受攻击端口是否开放(111)
- 向localhost:111发送特制TCP数据包
- 您已成功关闭访问者的计算机。
场景2:Windows打印后台处理程序远程代码执行漏洞(CVE-2021-34527):
当Windows打印后台处理程序服务不正确地执行特权文件操作时,存在远程代码执行漏洞。成功利用此漏洞的攻击者可以使用SYSTEM权限运行任意代码。然后,攻击者可以安装程序;查看、更改或删除数据;或创建具有完全用户权限的新帐户。
由于浏览器可以访问localhost、0.0.0.0、192.168.1.1等,此漏洞可用于对抗数百万用户,利用不同设备。
我们许多人使用Windows,在启动时运行此易受攻击的服务。绑定在0.0.0.0上,可通过网络访问,黑客可以使用此漏洞渗透组织。
开发人员(如我自己)经常在localhost上运行本地服务器/容器。 我也经常忘记关闭它们(例如,当运行具有restart=always的docker容器时)。除了利用漏洞,开发人员可以被识别(例如通过StackOverflow/Linkedin/Facebook)以了解我们在开发中使用哪些技术。
依我拙见,随着更多功能的实现,浏览器WASM攻击面将随时间推移被更多滥用。
在本文中,我演示了网页如何与用户本地主机网络上的服务通信和映射,涉及浏览器的安全功能。尽管目前原始TCP/UDP会话的使用尚不可能,但可以用编译成WebAssembly的多种语言实现。
我举例说明了一个场景,其中Linux主机可能通过从浏览器访问网页而经历DOS——以及如何利用Microsoft的新漏洞CVE-2021-34527。
WASI非常棒。 WebAssembly很棒。 浏览器并不那么棒——随着时间推移,它们将始终对我们所有人构成严重风险和攻击向量,因为它们变成操作系统,支持越来越多的WASI规范功能。
实时演示
使用WebAssembly的实时演示: http://ports.sh 或 https://ports.sh http://ports.sh 将查找localhost上的所有TCP服务。 https://ports.sh 将仅查找localhost上的HTTPS服务。
代码
代码可在 https://github.com/avilum/portsscan 获取,欢迎贡献。