Coverage Report

Created: 2026-11-11 06:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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