/src/suricata8/rust/src/ftp/mod.rs
Line | Count | Source |
1 | | /* Copyright (C) 2017 Open Information Security Foundation |
2 | | * |
3 | | * You can copy, redistribute or modify this Program under the terms of |
4 | | * the GNU General Public License version 2 as published by the Free |
5 | | * Software Foundation. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * version 2 along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
15 | | * 02110-1301, USA. |
16 | | */ |
17 | | |
18 | | //! FTP parser and application layer module. |
19 | | |
20 | | use nom7::bytes::complete::{tag, take_until}; |
21 | | use nom7::character::complete::{digit1, multispace0}; |
22 | | use nom7::combinator::{complete, map_res, opt, verify}; |
23 | | use nom7::sequence::{delimited, tuple}; |
24 | | use nom7::{Err, IResult}; |
25 | | use std; |
26 | | use std::str; |
27 | | use std::str::FromStr; |
28 | | |
29 | | pub mod constant; |
30 | | pub mod event; |
31 | | pub mod ftp; |
32 | | pub mod response; |
33 | | |
34 | | // We transform an integer string into a i64, ignoring surrounding whitespaces |
35 | | // We look for a digit suite, and try to convert it. |
36 | | // If either str::from_utf8 or FromStr::from_str fail, |
37 | | // we fallback to the parens parser defined above |
38 | 44.8k | fn getu16(i: &[u8]) -> IResult<&[u8], u16> { |
39 | 44.8k | map_res( |
40 | 44.8k | map_res(delimited(multispace0, digit1, multispace0), str::from_utf8), |
41 | 44.8k | FromStr::from_str, |
42 | 44.8k | )(i) |
43 | 44.8k | } |
44 | | |
45 | 33.1k | fn parse_u16(i: &[u8]) -> IResult<&[u8], u16> { |
46 | 33.1k | map_res(map_res(digit1, str::from_utf8), u16::from_str)(i) |
47 | 33.1k | } |
48 | | |
49 | | // PORT 192,168,0,13,234,10 |
50 | 32.8k | fn ftp_active_port(i: &[u8]) -> IResult<&[u8], u16> { |
51 | 32.8k | let (i, _) = tag("PORT")(i)?; |
52 | 24.3k | let (i, _) = delimited(multispace0, digit1, multispace0)(i)?; |
53 | 23.2k | let (i, _) = tuple(( |
54 | 23.2k | tag(","), |
55 | 23.2k | digit1, |
56 | 23.2k | tag(","), |
57 | 23.2k | digit1, |
58 | 23.2k | tag(","), |
59 | 23.2k | digit1, |
60 | 23.2k | tag(","), |
61 | 23.2k | ))(i)?; |
62 | 18.1k | let (i, part1) = verify(parse_u16, |&v| v <= u8::MAX as u16)(i)?; |
63 | 16.3k | let (i, _) = tag(",")(i)?; |
64 | 14.9k | let (i, part2) = verify(parse_u16, |&v| v <= u8::MAX as u16)(i)?; |
65 | 2.72k | Ok((i, part1 * 256 + part2)) |
66 | 32.8k | } |
67 | | |
68 | | // 227 Entering Passive Mode (212,27,32,66,221,243). |
69 | 16.2k | fn ftp_pasv_response(i: &[u8]) -> IResult<&[u8], u16> { |
70 | 16.2k | let (i, _) = tag("227")(i)?; |
71 | 16.2k | let (i, _) = take_until("(")(i)?; |
72 | 11.7k | let (i, _) = tag("(")(i)?; |
73 | 11.7k | let (i, _) = tuple(( |
74 | 11.7k | digit1, |
75 | 11.7k | tag(","), |
76 | 11.7k | digit1, |
77 | 11.7k | tag(","), |
78 | 11.7k | digit1, |
79 | 11.7k | tag(","), |
80 | 11.7k | digit1, |
81 | 11.7k | tag(","), |
82 | 11.7k | ))(i)?; |
83 | 5.31k | let (i, part1) = verify(getu16, |&v| v <= u8::MAX as u16)(i)?; |
84 | 4.22k | let (i, _) = tag(",")(i)?; |
85 | 2.68k | let (i, part2) = verify(getu16, |&v| v <= u8::MAX as u16)(i)?; |
86 | | // may also be completed by a final point |
87 | 1.99k | let (i, _) = tag(")")(i)?; |
88 | 1.54k | let (i, _) = opt(complete(tag(".")))(i)?; |
89 | 1.54k | Ok((i, part1 * 256 + part2)) |
90 | 16.2k | } |
91 | | |
92 | | #[no_mangle] |
93 | 32.8k | pub unsafe extern "C" fn SCFTPParsePort(input: *const u8, len: u32) -> u16 { |
94 | 32.8k | if input.is_null() { |
95 | 0 | return 0; |
96 | 32.8k | } |
97 | 32.8k | let buf = build_slice!(input, len as usize); |
98 | 32.8k | match ftp_active_port(buf) { |
99 | 2.72k | Ok((_, dport)) => { |
100 | 2.72k | return dport; |
101 | | } |
102 | 0 | Err(Err::Incomplete(_)) => { |
103 | 0 | SCLogDebug!("port incomplete: '{:?}'", buf); |
104 | 0 | } |
105 | 30.0k | Err(_) => { |
106 | 30.0k | SCLogDebug!("port error on '{:?}'", buf); |
107 | 30.0k | } |
108 | | } |
109 | 30.0k | return 0; |
110 | 32.8k | } |
111 | | |
112 | | #[no_mangle] |
113 | 16.2k | pub unsafe extern "C" fn SCFTPParsePortPasv(input: *const u8, len: u32) -> u16 { |
114 | 16.2k | if input.is_null() { |
115 | 0 | return 0; |
116 | 16.2k | } |
117 | 16.2k | let buf = build_slice!(input, len as usize); |
118 | 16.2k | match ftp_pasv_response(buf) { |
119 | 1.54k | Ok((_, dport)) => { |
120 | 1.54k | return dport; |
121 | | } |
122 | 0 | Err(Err::Incomplete(_)) => { |
123 | 0 | SCLogDebug!("pasv incomplete: '{:?}'", String::from_utf8_lossy(buf)); |
124 | 0 | } |
125 | 14.7k | Err(_) => { |
126 | 14.7k | SCLogDebug!("pasv error on '{:?}'", String::from_utf8_lossy(buf)); |
127 | 14.7k | } |
128 | | } |
129 | 14.7k | return 0; |
130 | 16.2k | } |
131 | | |
132 | | // 229 Entering Extended Passive Mode (|||48758|). |
133 | 33.8k | pub fn ftp_epsv_response(i: &[u8]) -> IResult<&[u8], u16> { |
134 | 33.8k | let (i, _) = tag("229")(i)?; |
135 | 33.8k | let (i, _) = take_until("|||")(i)?; |
136 | 29.4k | let (i, _) = tag("|||")(i)?; |
137 | 29.4k | let (i, port) = getu16(i)?; |
138 | 3.40k | let (i, _) = tag("|)")(i)?; |
139 | 2.33k | let (i, _) = opt(complete(tag(".")))(i)?; |
140 | 2.33k | Ok((i, port)) |
141 | 33.8k | } |
142 | | |
143 | | // EPRT |2|2a01:e34:ee97:b130:8c3e:45ea:5ac6:e301|41813| |
144 | 17.2k | fn ftp_active_eprt(i: &[u8]) -> IResult<&[u8], u16> { |
145 | 17.2k | let (i, _) = tag("EPRT")(i)?; |
146 | 14.3k | let (i, _) = take_until("|")(i)?; |
147 | 10.1k | let (i, _) = tag("|")(i)?; |
148 | 10.1k | let (i, _) = take_until("|")(i)?; |
149 | 9.19k | let (i, _) = tag("|")(i)?; |
150 | 9.19k | let (i, _) = take_until("|")(i)?; |
151 | 7.35k | let (i, _) = tag("|")(i)?; |
152 | 7.35k | let (i, port) = getu16(i)?; |
153 | 4.48k | let (i, _) = tag("|")(i)?; |
154 | 3.10k | Ok((i, port)) |
155 | 17.2k | } |
156 | | |
157 | | #[no_mangle] |
158 | 17.2k | pub unsafe extern "C" fn SCFTPParsePortEprt(input: *const u8, len: u32) -> u16 { |
159 | 17.2k | if input.is_null() { |
160 | 0 | return 0; |
161 | 17.2k | } |
162 | 17.2k | let buf = build_slice!(input, len as usize); |
163 | 17.2k | match ftp_active_eprt(buf) { |
164 | 3.10k | Ok((_, dport)) => { |
165 | 3.10k | return dport; |
166 | | } |
167 | 0 | Err(Err::Incomplete(_)) => { |
168 | 0 | SCLogDebug!("eprt incomplete: '{:?}'", String::from_utf8_lossy(buf)); |
169 | 0 | } |
170 | 14.1k | Err(_) => { |
171 | 14.1k | SCLogDebug!("epsv incomplete: '{:?}'", String::from_utf8_lossy(buf)); |
172 | 14.1k | } |
173 | | } |
174 | 14.1k | return 0; |
175 | 17.2k | } |
176 | | #[no_mangle] |
177 | 33.8k | pub unsafe extern "C" fn SCFTPParsePortEpsv(input: *const u8, len: u32) -> u16 { |
178 | 33.8k | if input.is_null() { |
179 | 0 | return 0; |
180 | 33.8k | } |
181 | 33.8k | let buf = build_slice!(input, len as usize); |
182 | 33.8k | match ftp_epsv_response(buf) { |
183 | 2.33k | Ok((_, dport)) => { |
184 | 2.33k | return dport; |
185 | | } |
186 | 0 | Err(Err::Incomplete(_)) => { |
187 | 0 | SCLogDebug!("epsv incomplete: '{:?}'", String::from_utf8_lossy(buf)); |
188 | 0 | } |
189 | 31.4k | Err(_) => { |
190 | 31.4k | SCLogDebug!("epsv incomplete: '{:?}'", String::from_utf8_lossy(buf)); |
191 | 31.4k | } |
192 | | } |
193 | 31.4k | return 0; |
194 | 33.8k | } |
195 | | |
196 | | #[cfg(test)] |
197 | | mod test { |
198 | | use super::*; |
199 | | |
200 | | #[test] |
201 | | fn test_pasv_response_valid() { |
202 | | let port = |
203 | | ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,221,243).".as_bytes()); |
204 | | assert_eq!(port, Ok((&b""[..], 56819))); |
205 | | let port_notdot = |
206 | | ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,221,243)".as_bytes()); |
207 | | assert_eq!(port_notdot, Ok((&b""[..], 56819))); |
208 | | |
209 | | let port_epsv_dot = |
210 | | ftp_epsv_response("229 Entering Extended Passive Mode (|||48758|).".as_bytes()); |
211 | | assert_eq!(port_epsv_dot, Ok((&b""[..], 48758))); |
212 | | let port_epsv_nodot = |
213 | | ftp_epsv_response("229 Entering Extended Passive Mode (|||48758|)".as_bytes()); |
214 | | assert_eq!(port_epsv_nodot, Ok((&b""[..], 48758))); |
215 | | } |
216 | | |
217 | | #[test] |
218 | | fn test_active_eprt_valid() { |
219 | | let port = |
220 | | ftp_active_eprt("EPRT |2|2a01:e34:ee97:b130:8c3e:45ea:5ac6:e301|41813|".as_bytes()); |
221 | | assert_eq!(port, Ok((&b""[..], 41813))); |
222 | | } |
223 | | |
224 | | #[test] |
225 | | fn test_active_port_valid() { |
226 | | let port = ftp_active_port("PORT 192,168,0,13,234,10".as_bytes()); |
227 | | assert_eq!(port, Ok((&b""[..], 59914))); |
228 | | } |
229 | | |
230 | | // A port that is too large for a u16. |
231 | | #[test] |
232 | | fn test_pasv_response_too_large() { |
233 | | let port = |
234 | | ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,257,243).".as_bytes()); |
235 | | assert!(port.is_err()); |
236 | | |
237 | | let port = |
238 | | ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,255,65535).".as_bytes()); |
239 | | assert!(port.is_err()); |
240 | | } |
241 | | |
242 | | #[test] |
243 | | fn test_active_eprt_too_large() { |
244 | | let port = |
245 | | ftp_active_eprt("EPRT |2|2a01:e34:ee97:b130:8c3e:45ea:5ac6:e301|81813|".as_bytes()); |
246 | | assert!(port.is_err()); |
247 | | } |
248 | | |
249 | | #[test] |
250 | | fn test_active_port_too_large() { |
251 | | let port = ftp_active_port("PORT 212,27,32,66,257,243".as_bytes()); |
252 | | assert!(port.is_err()); |
253 | | |
254 | | let port = ftp_active_port("PORT 212,27,32,66,255,65535".as_bytes()); |
255 | | assert!(port.is_err()); |
256 | | } |
257 | | } |