/src/zeek/src/analyzer/protocol/ftp/FTP.cc
Line | Count | Source |
1 | | // See the file "COPYING" in the main distribution directory for copyright. |
2 | | |
3 | | #include "zeek/analyzer/protocol/ftp/FTP.h" |
4 | | |
5 | | #include <cstdlib> |
6 | | |
7 | | #include "zeek/Base64.h" |
8 | | #include "zeek/RuleMatcher.h" |
9 | | #include "zeek/ZeekString.h" |
10 | | #include "zeek/analyzer/Manager.h" |
11 | | #include "zeek/analyzer/protocol/ftp/events.bif.h" |
12 | | #include "zeek/analyzer/protocol/login/NVT.h" |
13 | | |
14 | | namespace zeek::analyzer::ftp { |
15 | | |
16 | 30.3k | FTP_Analyzer::FTP_Analyzer(Connection* conn) : analyzer::tcp::TCP_ApplicationAnalyzer("FTP", conn) { |
17 | 30.3k | nvt_orig = new analyzer::login::NVT_Analyzer(conn, true); |
18 | 30.3k | nvt_orig->SetIsNULSensitive(true); |
19 | 30.3k | nvt_orig->SetCRLFAsEOL(tcp::LF_as_EOL); |
20 | | |
21 | 30.3k | nvt_resp = new analyzer::login::NVT_Analyzer(conn, false); |
22 | 30.3k | nvt_resp->SetIsNULSensitive(true); |
23 | 30.3k | nvt_resp->SetCRLFAsEOL(tcp::LF_as_EOL); |
24 | | |
25 | 30.3k | nvt_resp->SetPeer(nvt_orig); |
26 | 30.3k | nvt_orig->SetPeer(nvt_resp); |
27 | | |
28 | 30.3k | AddSupportAnalyzer(nvt_orig); |
29 | 30.3k | AddSupportAnalyzer(nvt_resp); |
30 | 30.3k | } |
31 | | |
32 | 30.3k | void FTP_Analyzer::Done() { |
33 | 30.3k | analyzer::tcp::TCP_ApplicationAnalyzer::Done(); |
34 | | |
35 | 30.3k | if ( TCP() ) { |
36 | 30.3k | if ( nvt_orig->HasPartialLine() && (TCP()->OrigState() == analyzer::tcp::TCP_ENDPOINT_CLOSED || |
37 | 2.73k | TCP()->OrigPrevState() == analyzer::tcp::TCP_ENDPOINT_CLOSED) ) |
38 | | // ### should include the partial text |
39 | 294 | Weird("partial_ftp_request"); |
40 | 30.3k | } |
41 | 30.3k | } |
42 | | |
43 | 3.54M | static uint32_t get_reply_code(int len, const char* line) { |
44 | 3.54M | if ( len >= 3 && isdigit(line[0]) && isdigit(line[1]) && isdigit(line[2]) ) |
45 | 1.59M | return (line[0] - '0') * 100 + (line[1] - '0') * 10 + (line[2] - '0'); |
46 | 1.94M | else |
47 | 1.94M | return 0; |
48 | 3.54M | } |
49 | | |
50 | | // The minimal length of an FTP command is 3 characters (PWD, MKD, |
51 | | // RMD, ...) and should only contain printable ascii. |
52 | 2.27M | static bool is_ftp_cmd(int len, const char* s) { |
53 | 2.27M | if ( len < 3 ) |
54 | 890k | return false; |
55 | | |
56 | 9.21M | for ( int i = 0; i < len; i++ ) |
57 | 7.85M | if ( ! isprint(s[i]) || isspace(s[i]) ) |
58 | 23.0k | return false; |
59 | | |
60 | 1.36M | return true; |
61 | 1.38M | } |
62 | | |
63 | 6.07M | void FTP_Analyzer::DeliverStream(int length, const u_char* data, bool orig) { |
64 | 6.07M | analyzer::tcp::TCP_ApplicationAnalyzer::DeliverStream(length, data, orig); |
65 | | |
66 | 6.07M | if ( tls_active ) { |
67 | 5.00k | ForwardStream(length, data, orig); |
68 | 5.00k | return; |
69 | 5.00k | } |
70 | | |
71 | 6.07M | if ( (orig && ! ftp_request) || (! orig && ! ftp_reply) ) |
72 | 0 | return; |
73 | | |
74 | | // const char* orig_line = line; |
75 | 6.07M | const char* line = reinterpret_cast<const char*>(data); |
76 | 6.07M | const char* end_of_line = line + length; |
77 | | |
78 | 6.07M | if ( length == 0 ) |
79 | | // Could emit "ftp empty request/reply" weird, but maybe not worth it. |
80 | 964k | return; |
81 | | |
82 | 5.11M | Args vl; |
83 | | |
84 | 5.11M | EventHandlerPtr f; |
85 | 5.11M | if ( orig ) { |
86 | 2.27M | int cmd_len; |
87 | 2.27M | const char* cmd; |
88 | 2.27M | StringVal* cmd_str; |
89 | | |
90 | 2.27M | line = util::skip_whitespace(line, end_of_line); |
91 | 2.27M | util::get_word(end_of_line - line, line, cmd_len, cmd); |
92 | 2.27M | line = util::skip_whitespace(line + cmd_len, end_of_line); |
93 | | |
94 | 2.27M | if ( ! is_ftp_cmd(cmd_len, cmd) ) { |
95 | 914k | if ( AnalyzerConfirmed() ) |
96 | 845k | Weird("FTP_invalid_command"); |
97 | | |
98 | | // Ignore the whole line. |
99 | 914k | return; |
100 | 914k | } |
101 | 1.36M | else if ( BifConst::FTP::max_command_length > 0 && |
102 | 1.36M | static_cast<zeek_uint_t>(cmd_len) > BifConst::FTP::max_command_length ) { |
103 | | // If the FTP command is unusually long, log a weird if the analyzer |
104 | | // has previously been confirmed, but otherwise just ignore the whole |
105 | | // line and move on to the next. |
106 | 7.98k | if ( AnalyzerConfirmed() ) |
107 | 7.71k | LimitReachedWeird("FTP_max_command_length_exceeded", cmd_len, BifConst::FTP::max_command_length); |
108 | | |
109 | 7.98k | return; |
110 | 7.98k | } |
111 | 1.35M | else |
112 | 1.35M | cmd_str = (new StringVal(cmd_len, cmd))->ToUpper(); |
113 | | |
114 | 1.35M | vl = { |
115 | 1.35M | ConnVal(), |
116 | 1.35M | IntrusivePtr{AdoptRef{}, cmd_str}, |
117 | 1.35M | make_intrusive<StringVal>(end_of_line - line, line), |
118 | 1.35M | }; |
119 | | |
120 | 1.35M | f = ftp_request; |
121 | 1.35M | AnalyzerConfirmation(); |
122 | | |
123 | 1.35M | if ( strncmp(reinterpret_cast<const char*>(cmd_str->Bytes()), "AUTH", cmd_len) == 0 ) |
124 | 8.95k | auth_requested = std::string(line, end_of_line - line); |
125 | | |
126 | 1.35M | if ( detail::rule_matcher ) |
127 | 1.35M | Conn()->Match(zeek::detail::Rule::FTP, reinterpret_cast<const u_char*>(cmd), end_of_line - cmd, true, true, |
128 | 1.35M | true, true); |
129 | 1.35M | } |
130 | 2.83M | else { |
131 | 2.83M | uint32_t reply_code = get_reply_code(length, line); |
132 | | |
133 | 2.83M | int cont_resp; |
134 | | |
135 | 2.83M | if ( pending_reply ) { |
136 | 1.83M | if ( reply_code == pending_reply && length > 3 && line[3] == ' ' ) { |
137 | | // This is the end of the reply. |
138 | 67.0k | line = util::skip_whitespace(line + 3, end_of_line); |
139 | 67.0k | pending_reply = 0; |
140 | 67.0k | cont_resp = 0; |
141 | 67.0k | } |
142 | 1.76M | else { |
143 | 1.76M | cont_resp = 1; // not the end |
144 | 1.76M | reply_code = 0; // flag as intermediary |
145 | 1.76M | } |
146 | 1.83M | } |
147 | 998k | else { // a new reply |
148 | 998k | cont_resp = 0; |
149 | | |
150 | 998k | if ( reply_code == 0 ) { |
151 | 859k | AnalyzerViolation("non-numeric reply code", reinterpret_cast<const char*>(data), length); |
152 | 859k | return; |
153 | 859k | } |
154 | 139k | else if ( reply_code < 100 ) { |
155 | 489 | AnalyzerViolation("invalid reply code", reinterpret_cast<const char*>(data), length); |
156 | 489 | return; |
157 | 489 | } |
158 | 139k | else if ( length > 3 && line[3] == '-' ) { // a continued reply |
159 | 71.3k | pending_reply = reply_code; |
160 | 71.3k | line = util::skip_whitespace(line + 4, end_of_line); |
161 | 71.3k | cont_resp = 1; |
162 | 71.3k | } |
163 | 67.8k | else if ( length > 3 && line[3] != ' ' ) { |
164 | | // This is a proper reply code, but there's no space after |
165 | | // the reply code even though the line is long enough. |
166 | 7.35k | AnalyzerViolation("invalid reply line", reinterpret_cast<const char*>(data), length); |
167 | 7.35k | return; |
168 | 7.35k | } |
169 | 60.5k | else { // a self-contained reply |
170 | 60.5k | line += 3; |
171 | | |
172 | 60.5k | if ( line < end_of_line ) |
173 | 11.5k | line = util::skip_whitespace(line, end_of_line); |
174 | 48.9k | else |
175 | 48.9k | line = end_of_line; |
176 | 60.5k | } |
177 | 998k | } |
178 | | |
179 | 1.96M | if ( reply_code == 234 && ! auth_requested.empty() && auth_requested == "TLS" ) { |
180 | 274 | EnqueueConnEvent(ftp_starttls, ConnVal()); |
181 | 274 | Analyzer* ssl = analyzer_mgr->InstantiateAnalyzer("SSL", Conn()); |
182 | 274 | if ( ssl ) { |
183 | 274 | AddChildAnalyzer(ssl); |
184 | 274 | RemoveSupportAnalyzer(nvt_orig); |
185 | 274 | RemoveSupportAnalyzer(nvt_resp); |
186 | 274 | tls_active = true; |
187 | 274 | } |
188 | 274 | } |
189 | | |
190 | 1.96M | if ( reply_code == 334 && ! auth_requested.empty() && auth_requested == "GSSAPI" ) { |
191 | | // Server wants to proceed with an ADAT exchange and we |
192 | | // know how to analyze the GSI mechanism, so attach analyzer |
193 | | // to look for that. |
194 | 145k | Analyzer* ssl = analyzer_mgr->InstantiateAnalyzer("SSL", Conn()); |
195 | 145k | if ( ssl ) { |
196 | 145k | ssl->AddSupportAnalyzer(new FTP_ADAT_Analyzer(Conn(), true)); |
197 | 145k | ssl->AddSupportAnalyzer(new FTP_ADAT_Analyzer(Conn(), false)); |
198 | 145k | AddChildAnalyzer(ssl); |
199 | 145k | } |
200 | 145k | } |
201 | | |
202 | 1.96M | vl = {ConnVal(), val_mgr->Count(reply_code), make_intrusive<StringVal>(end_of_line - line, line), |
203 | 1.96M | val_mgr->Bool(cont_resp)}; |
204 | | |
205 | 1.96M | f = ftp_reply; |
206 | 1.96M | } |
207 | | |
208 | 3.32M | EnqueueConnEvent(f, std::move(vl)); |
209 | | |
210 | 3.32M | if ( ! tls_active ) |
211 | 3.32M | ForwardStream(length, data, orig); |
212 | 3.32M | } |
213 | | |
214 | 760k | void FTP_ADAT_Analyzer::DeliverStream(int len, const u_char* data, bool orig) { |
215 | | // Don't know how to parse anything but the ADAT exchanges of GSI GSSAPI, |
216 | | // which is basically just TLS/SSL. |
217 | 760k | if ( ! Parent()->IsAnalyzer("SSL") ) { |
218 | 0 | Parent()->Remove(); |
219 | 0 | return; |
220 | 0 | } |
221 | | |
222 | 760k | bool done = false; |
223 | 760k | const char* line = reinterpret_cast<const char*>(data); |
224 | 760k | const char* end_of_line = line + len; |
225 | | |
226 | 760k | String* decoded_adat = nullptr; |
227 | | |
228 | 760k | if ( orig ) { |
229 | 44.6k | int cmd_len; |
230 | 44.6k | const char* cmd; |
231 | 44.6k | line = util::skip_whitespace(line, end_of_line); |
232 | 44.6k | util::get_word(len, line, cmd_len, cmd); |
233 | | |
234 | 44.6k | if ( strncmp(cmd, "ADAT", cmd_len) == 0 ) { |
235 | 6.46k | line = util::skip_whitespace(line + cmd_len, end_of_line); |
236 | 6.46k | StringVal encoded(end_of_line - line, line); |
237 | 6.46k | decoded_adat = detail::decode_base64(encoded.AsString(), nullptr, Conn()); |
238 | | |
239 | 6.46k | if ( first_token ) { |
240 | | // RFC 2743 section 3.1 specifies a framing format for tokens |
241 | | // that includes an identifier for the mechanism type. The |
242 | | // framing is supposed to be required for the initial context |
243 | | // token, but GSI doesn't do that and starts right in on a |
244 | | // TLS/SSL handshake, so look for that to identify it. |
245 | 875 | const u_char* msg = nullptr; |
246 | 875 | int msg_len = 0; |
247 | | |
248 | 875 | if ( decoded_adat ) { |
249 | 756 | msg = decoded_adat->Bytes(); |
250 | 756 | msg_len = decoded_adat->Len(); |
251 | 756 | } |
252 | 119 | else |
253 | 119 | Weird("ftp_adat_bad_first_token_encoding"); |
254 | | |
255 | | // Just check that it looks like a viable TLS/SSL handshake |
256 | | // record from the first byte (content type of 0x16) and |
257 | | // that the fourth and fifth bytes indicating the length of |
258 | | // the record match the length of the decoded data. |
259 | 875 | if ( msg_len < 5 || msg[0] != 0x16 || |
260 | 706 | msg_len - 5 != ntohs(*reinterpret_cast<const uint16_t*>(msg + 3)) ) { |
261 | | // Doesn't look like TLS/SSL, so done analyzing. |
262 | 244 | done = true; |
263 | 244 | delete decoded_adat; |
264 | 244 | decoded_adat = nullptr; |
265 | 244 | } |
266 | 875 | } |
267 | | |
268 | 6.46k | first_token = false; |
269 | 6.46k | } |
270 | | |
271 | 38.2k | else if ( strncmp(cmd, "AUTH", cmd_len) == 0 ) |
272 | | // Security state will be reset by a reissued AUTH. |
273 | 398 | done = true; |
274 | 44.6k | } |
275 | | |
276 | 715k | else { |
277 | 715k | uint32_t reply_code = get_reply_code(len, line); |
278 | | |
279 | 715k | switch ( reply_code ) { |
280 | 241 | case 232: |
281 | 7.52k | case 234: |
282 | | // Indicates security data exchange is complete, but nothing |
283 | | // more to decode in replies. |
284 | 7.52k | done = true; |
285 | 7.52k | break; |
286 | | |
287 | 47.0k | case 235: |
288 | | // Security data exchange complete, but may have more to decode |
289 | | // in the reply (same format at 334 and 335). |
290 | 47.0k | done = true; |
291 | | |
292 | | // Fall-through. |
293 | | |
294 | 464k | case 334: |
295 | 480k | case 335: |
296 | | // Security data exchange still in progress, and there could be data |
297 | | // to decode in the reply. |
298 | 480k | line += 3; |
299 | 480k | if ( len > 3 && line[0] == '-' ) |
300 | 335k | line++; |
301 | | |
302 | 480k | line = util::skip_whitespace(line, end_of_line); |
303 | | |
304 | 480k | if ( end_of_line - line >= 5 && strncmp(line, "ADAT=", 5) == 0 ) { |
305 | 336k | line += 5; |
306 | 336k | StringVal encoded(end_of_line - line, line); |
307 | 336k | decoded_adat = zeek::detail::decode_base64(encoded.AsString(), nullptr, Conn()); |
308 | 336k | } |
309 | | |
310 | 480k | break; |
311 | | |
312 | | // Server isn't going to accept named security mechanism. |
313 | | // Client has to restart back at the AUTH. |
314 | 74 | case 421: |
315 | 272 | case 431: |
316 | 603 | case 500: |
317 | 729 | case 501: |
318 | 869 | case 503: |
319 | 1.51k | case 535: |
320 | | |
321 | | // If the server is sending protected replies, the security |
322 | | // data exchange must have already succeeded. It does have |
323 | | // encoded data in the reply, but 632 and 633 are also encrypted. |
324 | 1.71k | case 631: |
325 | 1.91k | case 632: |
326 | 2.94k | case 633: done = true; break; |
327 | | |
328 | 225k | default: break; |
329 | 715k | } |
330 | 715k | } |
331 | | |
332 | 760k | if ( decoded_adat ) { |
333 | 314k | ForwardStream(decoded_adat->Len(), decoded_adat->Bytes(), orig); |
334 | 314k | delete decoded_adat; |
335 | 314k | } |
336 | | |
337 | 760k | if ( done ) |
338 | 58.1k | Parent()->Remove(); |
339 | 760k | } |
340 | | |
341 | | } // namespace zeek::analyzer::ftp |