Skip to content

Commit 1d2450d

Browse files
committed
new: precompiling --http-success expression to achieve better performances during evaluation
1 parent 5790d9e commit 1d2450d

3 files changed

Lines changed: 89 additions & 86 deletions

File tree

docs/plugins/http.md

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,6 @@ The `--http-success` parameter accepts a boolean expression that is evaluated to
6060
- `!` - Logical NOT
6161
- Parentheses for grouping: `(status == 200 || status == 201) && contains(body, "ok")`
6262

63-
### Variable Interpolation
64-
65-
The following placeholders can be used in expressions and will be replaced with the current credential values:
66-
- `{$username}` - Current username being tested
67-
- `{$password}` - Current password being tested
68-
- `{$payload}` - Current payload/username for enumeration plugins
69-
7063
For a list of all the operators and builtin functions [refer to this documentation](https://docs.rs/evalexpr/latest/evalexpr/index.html).
7164

7265
### Expression Examples
@@ -96,8 +89,14 @@ For a list of all the operators and builtin functions [refer to this documentati
9689
# Verify API response
9790
--http-success 'status == 200 && content_type == "application/json" && contains(body, "\"authenticated\": true")'
9891

99-
# Use interpolation to check for username in response
100-
--http-success 'status == 200 && contains(body, "{$username}")'
92+
# Check for username in response
93+
--http-success 'status == 200 && contains(body, username)'
94+
95+
# Check for password in response
96+
--http-success 'status == 200 && contains(body, password)'
97+
98+
# Check for single payload (for http.enum)
99+
--http-success 'status == 200 && contains(body, payload)'
101100
```
102101

103102
## Plugin Usage Examples
@@ -210,17 +209,6 @@ file?filename=..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c.
210209
...
211210
```
212211

213-
Google Suite / GMail valid accounts enumeration:
214-
215-
```sh
216-
legba http.enum \
217-
--payloads data/employees-names.txt \
218-
--http-success-string "COMPASS" \
219-
--http-success "status == 204" \
220-
--quiet \
221-
--target "https://mail.google.com/mail/gxlu?email={PAYLOAD}@broadcom.com"
222-
```
223-
224212
### Misc HTTP Requests
225213

226214
HTTP Post Request (Wordpress wp-login.php page):
@@ -282,4 +270,4 @@ legba http \
282270
--http-method POST \
283271
--http-payload 'destination=https://exchange-server/&flags=4&username={USERNAME}&password={PASSWORD}' \
284272
--http-success 'status == 302 && set_cookie != ""'
285-
```
273+
```

src/plugins/http/http_test.rs

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
1-
// TODO: add more tests
21
#[cfg(test)]
32
mod tests {
4-
use reqwest::header::{CONTENT_TYPE, HeaderValue};
5-
6-
use crate::{
7-
creds::Credentials,
8-
options::Options,
9-
plugins::{
10-
Plugin,
11-
http::{HTTP_PASSWORD_VAR, HTTP_PAYLOAD_VAR, HTTP_USERNAME_VAR},
12-
},
13-
};
14-
153
use crate::plugins::http::{HTTP, Strategy};
4+
use crate::{creds::Credentials, options::Options, plugins::Plugin};
5+
use reqwest::header::{CONTENT_TYPE, HeaderValue};
166

177
#[test]
188
fn test_get_target_url_adds_default_schema_and_path() {
@@ -546,7 +536,7 @@ mod tests {
546536
let mut http = HTTP::new(Strategy::Enumeration);
547537
// Set the expression directly with the placeholder
548538
http.success_expression =
549-
format!("status == 200 && contains(body, \"{}\")", HTTP_USERNAME_VAR);
539+
evalexpr::build_operator_tree("status == 200 && contains(body, username)").unwrap();
550540

551541
let response = http
552542
.client
@@ -580,7 +570,7 @@ mod tests {
580570
let mut http = HTTP::new(Strategy::Enumeration);
581571
// Set the expression directly with the placeholder
582572
http.success_expression =
583-
format!("status == 200 && contains(body, \"{}\")", HTTP_PASSWORD_VAR);
573+
evalexpr::build_operator_tree("status == 200 && contains(body, password)").unwrap();
584574

585575
let response = http
586576
.client
@@ -614,7 +604,7 @@ mod tests {
614604
let mut http = HTTP::new(Strategy::Enumeration);
615605
// Set the expression directly with the placeholder
616606
http.success_expression =
617-
format!("status == 200 && contains(body, \"{}\")", HTTP_PAYLOAD_VAR);
607+
evalexpr::build_operator_tree("status == 200 && contains(body, payload)").unwrap();
618608

619609
let response = http
620610
.client
@@ -749,7 +739,7 @@ mod tests {
749739
});
750740

751741
let mut http = HTTP::new(Strategy::Request);
752-
http.success_expression = "status == 200".to_owned();
742+
http.success_expression = evalexpr::build_operator_tree("status == 200").unwrap();
753743
let opts = Options {
754744
target: Some(server.base_url()),
755745
..Options::default()
@@ -773,7 +763,7 @@ mod tests {
773763
});
774764

775765
let mut http = HTTP::new(Strategy::Request);
776-
http.success_expression = "status == 200".to_owned();
766+
http.success_expression = evalexpr::build_operator_tree("status == 200").unwrap();
777767
let opts = Options {
778768
target: Some(server.base_url()),
779769
..Options::default()
@@ -888,7 +878,7 @@ mod tests {
888878
});
889879

890880
let mut http = HTTP::new(Strategy::Request);
891-
http.success_expression = "status == 200".to_owned();
881+
http.success_expression = evalexpr::build_operator_tree("status == 200").unwrap();
892882
let opts = Options {
893883
target: Some(server.base_url()),
894884
..Options::default()
@@ -918,7 +908,7 @@ mod tests {
918908
});
919909

920910
let mut http = HTTP::new(Strategy::Request);
921-
http.success_expression = "status == 200".to_owned();
911+
http.success_expression = evalexpr::build_operator_tree("status == 200").unwrap();
922912
let opts = Options {
923913
target: Some(server.base_url()),
924914
..Options::default()
@@ -973,7 +963,7 @@ mod tests {
973963
});
974964

975965
let mut http = HTTP::new(Strategy::Request);
976-
http.success_expression = "status == 200".to_owned();
966+
http.success_expression = evalexpr::build_operator_tree("status == 200").unwrap();
977967
let opts = Options {
978968
target: Some(server.base_url()),
979969
..Options::default()
@@ -999,7 +989,7 @@ mod tests {
999989
});
1000990

1001991
let mut http = HTTP::new(Strategy::Request);
1002-
http.success_expression = "status == 200".to_owned();
992+
http.success_expression = evalexpr::build_operator_tree("status == 200").unwrap();
1003993
let opts = Options {
1004994
target: Some(server.base_url()),
1005995
..Options::default()
@@ -1591,7 +1581,7 @@ mod tests {
15911581
let mut http = HTTP::new(Strategy::Enumeration);
15921582
// Set the expression directly with the placeholder
15931583
http.success_expression =
1594-
format!("status == 200 && contains(body, \"{}\")", HTTP_USERNAME_VAR);
1584+
evalexpr::build_operator_tree("status == 200 && contains(body, username)").unwrap();
15951585

15961586
let response = http
15971587
.client

src/plugins/http/mod.rs

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ mod payload;
2626
mod placeholders;
2727
mod ua;
2828

29-
// Placeholders used for interpolating --http-success
30-
const HTTP_USERNAME_VAR: &str = "{$username}";
31-
const HTTP_PASSWORD_VAR: &str = "{$password}";
32-
const HTTP_PAYLOAD_VAR: &str = "{$payload}";
33-
3429
const HTTP_LOWERCASE_PLACEHOLDERS: &[&str] = &["{payload}", "{username}", "{password}"];
3530
const HTTP_UPPERCASE_PLACEHOLDERS: &[&str] = &["{PAYLOAD}", "{USERNAME}", "{PASSWORD}"];
3631

@@ -79,7 +74,7 @@ pub(crate) struct HTTP {
7974
real_target: Option<String>,
8075
user_agent: Option<String>,
8176

82-
success_expression: String,
77+
success_expression: evalexpr::Node<evalexpr::DefaultNumericTypes>,
8378

8479
enum_ext: String,
8580
enum_ext_placeholder: String,
@@ -103,8 +98,8 @@ impl HTTP {
10398
csrf: None,
10499
domain: String::new(),
105100
workstation: String::new(),
106-
success_expression: String::new(),
107101
enum_ext: String::new(),
102+
success_expression: evalexpr::build_operator_tree("").unwrap(),
108103
enum_ext_placeholder: String::new(),
109104
method: Method::GET,
110105
headers: HeaderMap::default(),
@@ -238,11 +233,11 @@ impl HTTP {
238233
builder
239234
}
240235

241-
async fn is_success_response(
236+
async fn build_response_context(
242237
&self,
243238
creds: &Credentials,
244239
response: Response,
245-
) -> Option<Success> {
240+
) -> Result<(u16, String, usize, HashMapContext<DefaultNumericTypes>), Error> {
246241
let status = response.status().as_u16();
247242

248243
let mut context = HashMapContext::<DefaultNumericTypes>::new();
@@ -265,14 +260,14 @@ impl HTTP {
265260

266261
context
267262
.set_value(header_var_name, Value::from(value.to_str().unwrap()))
268-
.unwrap();
263+
.map_err(|e| e.to_string())?;
269264
}
270265

271266
// always set content_type
272267
if !content_type_set {
273268
context
274269
.set_value(String::from("content_type"), Value::from(""))
275-
.unwrap();
270+
.map_err(|e| e.to_string())?;
276271
}
277272

278273
// add response status, body, size and content type to the context
@@ -281,13 +276,13 @@ impl HTTP {
281276

282277
context
283278
.set_value(String::from("status"), Value::from_int(status as i64))
284-
.unwrap();
279+
.map_err(|e| e.to_string())?;
285280
context
286281
.set_value(String::from("body"), Value::from(body))
287-
.unwrap();
282+
.map_err(|e| e.to_string())?;
288283
context
289284
.set_value(String::from("size"), Value::from_int(size as i64))
290-
.unwrap();
285+
.map_err(|e| e.to_string())?;
291286

292287
// the builtin contains function is for searching a string within a tuple of strings,
293288
// let's override it to something that makes more sense
@@ -305,37 +300,61 @@ impl HTTP {
305300
}
306301
}),
307302
)
308-
.unwrap();
309-
310-
// replace placeholders with actual values
311-
let success_expression = self
312-
.success_expression
313-
.replace(HTTP_USERNAME_VAR, &creds.username)
314-
.replace(HTTP_PASSWORD_VAR, &creds.password)
315-
.replace(HTTP_PAYLOAD_VAR, creds.single());
316-
317-
match eval_boolean_with_context(&success_expression, &context) {
318-
Ok(result) => {
319-
if result {
320-
Some(Success {
321-
status,
322-
content_type,
323-
size,
324-
})
325-
} else {
303+
.map_err(|e| e.to_string())?;
304+
305+
// set placeholders with actual values
306+
context
307+
.set_value(
308+
String::from("username"),
309+
Value::from(creds.username.clone()),
310+
)
311+
.map_err(|e| e.to_string())?;
312+
context
313+
.set_value(
314+
String::from("password"),
315+
Value::from(creds.password.clone()),
316+
)
317+
.map_err(|e| e.to_string())?;
318+
context
319+
.set_value(String::from("payload"), Value::from(creds.single()))
320+
.map_err(|e| e.to_string())?;
321+
322+
Ok((status, content_type, size, context))
323+
}
324+
325+
async fn is_success_response(
326+
&self,
327+
creds: &Credentials,
328+
response: Response,
329+
) -> Option<Success> {
330+
let built = self.build_response_context(creds, response).await;
331+
if let Ok((status, content_type, size, context)) = built {
332+
match self.success_expression.eval_boolean_with_context(&context) {
333+
Ok(result) => {
334+
if result {
335+
Some(Success {
336+
status,
337+
content_type,
338+
size,
339+
})
340+
} else {
341+
None
342+
}
343+
}
344+
Err(e) => {
345+
#[cfg(test)]
346+
println!(
347+
"error evaluating expression '{}': {}",
348+
self.success_expression, e
349+
);
350+
#[cfg(not(test))]
351+
log::error!("error evaluating success expression: {}", e);
326352
None
327353
}
328354
}
329-
Err(e) => {
330-
#[cfg(test)]
331-
println!(
332-
"error evaluating expression '{}': {}",
333-
success_expression, e
334-
);
335-
#[cfg(not(test))]
336-
log::error!("error evaluating success expression: {}", e);
337-
None
338-
}
355+
} else {
356+
log::error!("error building response context: {}", built.err().unwrap());
357+
None
339358
}
340359
}
341360

@@ -865,7 +884,13 @@ impl Plugin for HTTP {
865884
None
866885
};
867886

868-
self.success_expression = opts.http.http_success.clone();
887+
self.success_expression =
888+
evalexpr::build_operator_tree(&opts.http.http_success).map_err(|e| {
889+
format!(
890+
"error parsing success expression '{}': {}",
891+
opts.http.http_success, e
892+
)
893+
})?;
869894

870895
self.enum_ext = opts.http.http_enum_ext.clone();
871896
self.enum_ext_placeholder = opts.http.http_enum_ext_placeholder.clone();

0 commit comments

Comments
 (0)