Skip to content

Commit

Permalink
feat: add Outlook HTTP API validation
Browse files Browse the repository at this point in the history
- check the validity of Outlook/Office 365 email addresses via the
  method outlined
  [here](https://www.trustedsec.com/blog/achieving-passive-user-enumeration-with-onedrive/).
- run only via the `--outlook-use-api` flags (defaulting to `false`.)

relates reacherhq#937
  • Loading branch information
PsypherPunk committed Oct 6, 2022
1 parent 49c8f5c commit 0373295
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 2 deletions.
6 changes: 6 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ pub struct Cli {
#[clap(long, env, default_value = "false", parse(try_from_str))]
pub gmail_use_api: bool,

/// For Outlook/Office 365 email addresses, use Outlook's API instead of
/// connecting directly to their SMTP servers.
#[clap(long, env, default_value = "false", parse(try_from_str))]
pub outlook_use_api: bool,

/// Whether to check if a gravatar image is existing for the given email.
#[clap(long, env, default_value = "false", parse(try_from_str))]
pub check_gravatar: bool,
Expand All @@ -87,6 +92,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.set_smtp_port(CONF.smtp_port)
.set_yahoo_use_api(CONF.yahoo_use_api)
.set_gmail_use_api(CONF.gmail_use_api)
.set_outlook_use_api(CONF.outlook_use_api)
.set_check_gravatar(CONF.check_gravatar);
if let Some(proxy_host) = &CONF.proxy_host {
input.set_proxy(CheckEmailInputProxy {
Expand Down
66 changes: 64 additions & 2 deletions core/src/smtp/hotmail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,24 @@ use fantoccini::{
ClientBuilder, Locator,
};
use futures::TryFutureExt;
use reqwest::Error as ReqwestError;
use serde::Serialize;
use serde_json::Map;

use super::SmtpDetails;
use crate::util::ser_with_display::ser_with_display;
use crate::LOG_TARGET;
use crate::{
smtp::http_api::create_client, util::ser_with_display::ser_with_display, CheckEmailInput,
};

#[derive(Debug, Serialize)]
pub enum HotmailError {
#[serde(serialize_with = "ser_with_display")]
Cmd(CmdError),
#[serde(serialize_with = "ser_with_display")]
NewSession(NewSessionError),
#[serde(serialize_with = "ser_with_display")]
ReqwestError(ReqwestError),
}

impl From<CmdError> for HotmailError {
Expand All @@ -50,6 +55,12 @@ impl From<NewSessionError> for HotmailError {
}
}

impl From<ReqwestError> for HotmailError {
fn from(error: ReqwestError) -> Self {
HotmailError::ReqwestError(error)
}
}

/// Check if a Hotmail/Outlook email exists by connecting to the password
/// recovery page https://account.live.com/password/reset using a headless
/// browser. Make sure you have a WebDriver server running locally before
Expand Down Expand Up @@ -140,9 +151,52 @@ pub async fn check_password_recovery(
})
}

/// Convert an email address to its corresponding OneDrive URL.
fn get_onedrive_url(email_address: &str) -> String {
let (username, domain) = email_address
.split_once('@')
.expect("Email address syntax already validated.");
let (tenant, _) = domain
.split_once('.')
.expect("Email domain syntax already validated.");

format!(
"https://{}-my.sharepoint.com/personal/{}_{}/_layouts/15/onedrive.aspx",
tenant,
username.replace('.', "_"),
domain.replace('.', "_"),
)
}

/// Use HTTP request to verify if an Outlook email address exists.
/// See: <https://www.trustedsec.com/blog/achieving-passive-user-enumeration-with-onedrive/>
pub async fn check_outlook_api(
to_email: &EmailAddress,
input: &CheckEmailInput,
) -> Result<SmtpDetails, HotmailError> {
let url = get_onedrive_url(to_email.as_ref());

let response = create_client(input, "outlook")?.head(url).send().await?;

let email_exists = response.status() == 403;

log::debug!(
target: LOG_TARGET,
"[email={}] outlook response: {:?}",
to_email,
response
);

Ok(SmtpDetails {
can_connect_smtp: true,
is_deliverable: email_exists,
..Default::default()
})
}

#[cfg(test)]
mod tests {
use super::check_password_recovery;
use super::{check_password_recovery, get_onedrive_url};
use async_smtp::EmailAddress;
use async_std::prelude::FutureExt;
use std::str::FromStr;
Expand Down Expand Up @@ -193,4 +247,12 @@ mod tests {
let f = f1.try_join(f2).await;
assert!(f.is_ok(), "{:?}", f);
}

#[test]
fn test_onedrive_url() {
let email_address = "[email protected]";
let expected = "https://acmecomputercompany-my.sharepoint.com/personal/lightmand_acmecomputercompany_com/_layouts/15/onedrive.aspx";

assert_eq!(expected, get_onedrive_url(email_address));
}
}
5 changes: 5 additions & 0 deletions core/src/smtp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ pub async fn check_smtp(
.await
.map_err(|err| err.into());
}
if input.outlook_use_api && host_lowercase.ends_with(".outlook.com.") {
return hotmail::check_outlook_api(to_email, input)
.await
.map_err(|err| err.into());
}
#[cfg(feature = "headless")]
if let Some(webdriver) = &input.hotmail_use_headless {
if host_lowercase.contains("outlook") {
Expand Down
13 changes: 13 additions & 0 deletions core/src/util/input_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ pub struct CheckEmailInput {
///
/// Defaults to false.
pub gmail_use_api: bool,
/// For Outlook/Office 365 email addresses, use Outlook's API instead of
/// connecting directly to their SMTP servers.
///
/// Defaults to false.
pub outlook_use_api: bool,
// Whether to check if a gravatar image is existing for the given email.
//
// Defaults to false
Expand Down Expand Up @@ -132,6 +137,7 @@ impl Default for CheckEmailInput {
smtp_timeout: None,
yahoo_use_api: true,
gmail_use_api: false,
outlook_use_api: false,
check_gravatar: false,
retries: 2,
}
Expand Down Expand Up @@ -247,6 +253,13 @@ impl CheckEmailInput {
self
}

/// Set whether to use Outlook's API or connecting directly to their SMTP
/// servers. Defaults to false.
pub fn set_outlook_use_api(&mut self, use_api: bool) -> &mut CheckEmailInput {
self.outlook_use_api = use_api;
self
}

/// Whether to check if a gravatar image is existing for the given email.
/// Defaults to false.
pub fn set_check_gravatar(&mut self, check_gravatar: bool) -> &mut CheckEmailInput {
Expand Down

0 comments on commit 0373295

Please sign in to comment.