├── README.md ├── colors ├── __pycache__ │ └── colors.cpython-38.pyc └── colors.py ├── requirements.txt └── slackhound.py /README.md: -------------------------------------------------------------------------------- 1 | # Slackhound 2 | 3 | Slackhound 2.0 Released 2-12-2024 4 | 5 | # New Features include sending messages to users, saving workspace users and profiles to a sqlite database, channel reconnaissance, snoozing notifications, and more! 6 | 7 | Slackhound is a command line tool for red and blue teams to quickly perform reconnaissance of a Slack workspace/organization. Slackhound makes collection of an organization's users, files, messages, etc. quickly searchable and large objects are written to CSV for offline review. Red Teams can use Slachound to export or lookup user/employee's directory information similar to Active Directory without the concern of detection seen with active directory reconnaissance. 8 | 9 | # Requirements 10 | You will need a Slack token for your target workspace with basic "user" level rights. Slack supports three token types (user, bot, and App). Admin level privileges are not required for Slackhound. As a red teamer I can normally find a token somewhere on the network or if you are logged into Slack from a web browser then you can find the user's token with Burp, Chrome developer tools or Curl. As a blue teamer, you can ask IT or responsible Slack team to generate a token for you. The other traditional option is to create a Slack app for the workspace and generate a token. 11 | 12 | NOTE: Save your token inside a file in the Slackhound directory as "token.txt" 13 | 14 | # Finding the token from the Web Browser 15 | 16 | Here is one simple way to locate the token if you're logged into from a web browser. 17 | 1. In Chrome, click the "three dots" in upper right-hand corner. 18 | 2. Click on "More Tools" -> "Developer Tools". 19 | 3. Select "network" and "headers" views in right-side pane of developer tools. 20 | 4. While logged into Slack, visit https://.slack.com/customize/emoji 21 | 5. Now either filter for "xoxs-" or "xoxp-" tokens OR look for the page results with a name of client.boot. This will also contain the Slack token. 22 | 6. Copy the token (starts with xoxs- or xoxp- and paste into token.txt file located in Slackhound directory. 23 | 7. That's it. 24 | 25 | # Token privilege requirements 26 | Slackhound reconnaissance functions are intended to require low user level scopes and not rely on any admin privileges. However, Slack sets up very granular OAUTH privilege scopes and depending on the organization's workspace settings, access can be very granular. Typically, any user level token will have the required scope to use "-a" Slackhound option which will export important details, such as, all email addresses, phone numbers, team Id, user id, first/last names, and profile details like titles, Listed below are the OAUTH privilege scopes and descriptions that are required for Slackhound. 27 | 28 | # Scopes needed for some key Slackhound functions 29 | 30 | Slackhound -a —> users.profile.read 31 | 32 | Slackhound -c —> users.read.email 33 | 34 | Slackhound -d —> channels:read,groups:read,mpim:read,im:read 35 | 36 | Slackhound -e —> channels:read 37 | 38 | Slackhound -i —> im:write, im:read 39 | 40 | Slackhound -j —> files:write 41 | 42 | Slackhound -k —> channels:history,groups:history,mpim:history,im:history 43 | 44 | Slackhound -l —> nothing additional 45 | 46 | Slackhound -m —> nothing additional 47 | 48 | Slackhound -n —> reminders:read, reminders:write 49 | 50 | NOTE: If python throws an error complaining about a "KeyError" this often means that your token is valid, but a needed scope isn't authorized to perform the function. 51 | 52 | # Brief description of scopes 53 | channels:history 54 | View messages and other content in a user’s public channels 55 | 56 | channels:read 57 | View basic information about public channels in a workspace 58 | 59 | files:read 60 | View files shared in channels and conversations that a user has access to 61 | 62 | search:read 63 | Search a workspace’s content 64 | 65 | usergroups:read 66 | View user groups in a workspace 67 | 68 | users.profile:read 69 | View profile details about people in a workspace 70 | 71 | users:read 72 | View people in a workspace 73 | 74 | users:read.email 75 | View email addresses of people in a workspace 76 | 77 | # Using Slackhound 78 | Usage: slackhound.py [options] 79 | 80 | # Options: 81 | -h, --help show this help message and exit 82 | 83 | -a, --dumpAllUsers dump all user info from Slack Workspace to csv file and sqlite db 84 | 85 | -b GETUSER, --getUser=GETUSER 86 | get user profile, location, and check if user is 87 | active 88 | 89 | -c SEARCHUSERBYEMAIL, --searchUserByEmail=SEARCHUSERBYEMAIL 90 | Lookup user by email address 91 | 92 | -d, --listChannels List all Slack channels an account has access to 93 | 94 | -e USERCHANNELS, --userChannels=USERCHANNELS 95 | Get channels a specific user ID belongs to 96 | 97 | -f SEARCHFILES, --searchFiles=SEARCHFILES 98 | Search files for a keyword and put results in csv 99 | 100 | -g SEARCHMESSAGES, --searchMessages=SEARCHMESSAGES 101 | Search messages for a keyword and put results in csv 102 | 103 | -i, --sendMessage send messages to channel or Slack user 104 | 105 | -j, --uploadFile upload a file to user or channel 106 | 107 | -k GETCONVERSATION, --getConversation=GETCONVERSATION 108 | show channel's conversation history 109 | 110 | -l SETSNOOZER, --setSnoozer=SETSNOOZER 111 | Turn on Do Not Distrub (in minutes) 112 | 113 | -m GETFILELIST, --getFileList=GETFILELIST 114 | Get list of files uploaded by this user 115 | 116 | -n, --sendReminder Creates a reminder to user from Slackbot 117 | 118 | -z, --checkToken check if token is valid 119 | -------------------------------------------------------------------------------- /colors/__pycache__/colors.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BojackThePillager/Slackhound/a3cba3176d8d62ad609464ebdeb565519b03a580/colors/__pycache__/colors.cpython-38.pyc -------------------------------------------------------------------------------- /colors/colors.py: -------------------------------------------------------------------------------- 1 | class Colors: 2 | HEADER = '\033[95m' 3 | OKBLUE = '\033[94m' 4 | OKGREEN = '\033[92m' 5 | WARNING = '\033[93m' 6 | FAIL = '\033[91m' 7 | ENDC = '\033[0m' 8 | BOLD = '\033[1m' 9 | UNDERLINE = '\033[4m' 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas==1.1.3 2 | SQLAlchemy==1.4.29 3 | requests==2.24.0 4 | numpy==1.19.2 5 | PyYAML==6.0.1 6 | -------------------------------------------------------------------------------- /slackhound.py: -------------------------------------------------------------------------------- 1 | # Slackhound - Slack enumeration tool 2 | # Version 2 by Brad Richardson 3 | # Use legally,responsibily and at your own risk 4 | # No guarantees or rights are provided by using this software 5 | 6 | import json 7 | import os.path 8 | from os import path 9 | from pathlib import Path 10 | import sys 11 | import argparse 12 | from optparse import OptionParser 13 | import optparse 14 | import csv 15 | import requests 16 | import pandas as pd 17 | from pandas import json_normalize 18 | import numpy as np 19 | import sqlite3 20 | import sqlalchemy 21 | import yaml 22 | from colors.colors import Colors 23 | 24 | api_url_base = 'https://slack.com/api/users.list' 25 | api_token = "" 26 | bearer_token = "" 27 | 28 | # Ensure we have and can read in and store a token 29 | if path.exists('token.txt'): 30 | with open('token.txt', 'r') as file: 31 | api_token = file.read() 32 | api_token = api_token.strip('\n') 33 | else: 34 | print("Error: token file NOT FOUND!") 35 | 36 | # set bearer token variable and headers 37 | bearer_token = 'Bearer ' + api_token 38 | api_headers = {'Authorization': bearer_token} 39 | 40 | parser = optparse.OptionParser() 41 | parser.add_option("-a", "--dumpAllUsers", action='store_true', dest='dumpAllUsers', help='dump all user info from Slack Workspace to csv file and creates a sqlite database') 42 | parser.add_option("-b", "--getUser", dest='getUser', help='get user profile, location, and check if user is active') 43 | parser.add_option("-c", "--searchUserByEmail", help="Lookup user by email address") 44 | parser.add_option("-d", "--listChannels", action='store_true', dest='listChannels', help="List all Slack channels an account has access to") 45 | parser.add_option("-e", "--userChannels", help="Get channels a specific user ID belongs to") 46 | parser.add_option("-f", "--searchFiles", help="Search files for a keyword and put results in csv") 47 | parser.add_option("-g", "--searchMessages", help="Search messages for a keyword and put results in csv") 48 | parser.add_option("-i", "--sendMessage", help="send messages to channel or Slack user", action='store_true', dest='sendSlackMessage') 49 | parser.add_option("-j", "--uploadFile", action='store_true', dest='uploadFile', help='upload a file to user or channel') 50 | parser.add_option("-k", "--getConversation", dest='getConversation', help="show channel's conversation history") 51 | parser.add_option("-l", "--setSnoozer", dest='setSnoozer', help="Turn on Do Not Distrub (in minutes)") 52 | parser.add_option("-m", "--getFileList", dest='getFileList', help="Get list of files uploaded by this user") 53 | parser.add_option("-n", "--sendReminder", dest='sendReminder', action='store_true', help="Creates a reminder to user from Slackbot") 54 | parser.add_option("-z", "--checkToken", dest='checkToken', action='store_true', help="check token") 55 | 56 | (options, args) = parser.parse_args() 57 | 58 | def dumpAllUsers(): 59 | if checkToken(): 60 | try: 61 | jsonResponse = requests.get(api_url_base, headers=api_headers) 62 | data = json.loads(jsonResponse.text) 63 | slack_data = data['members'] 64 | data_file = open('slack_objects_dump.csv', 'w', newline='') 65 | csv_writer = csv.writer(data_file) 66 | count = 0 67 | for slack_details in slack_data: 68 | if count == 0: 69 | header = slack_details.keys() 70 | csv_writer.writerow(header) 71 | count += 1 72 | csv_writer.writerow(slack_details.values()) 73 | data_file.close() 74 | 75 | print(Colors.OKGREEN + Colors.BOLD + 'slack_objects_dump.csv successfully written' + Colors.ENDC) 76 | 77 | table_name = "slackmembers" 78 | 79 | 80 | conn = sqlite3.connect('data.db') 81 | c = conn.cursor() 82 | conn.commit() 83 | 84 | df = json_normalize(data['members']) 85 | if 'profile.image_512' in df.columns: 86 | del df['profile.image_512'] 87 | if 'profile.image_192' in df.columns: 88 | del df['profile.image_192'] 89 | if 'profile.image_72' in df.columns: 90 | del df['profile.image_72'] 91 | if 'profile.image_48' in df.columns: 92 | del df['profile.image_48'] 93 | if 'profile.image_32' in df.columns: 94 | del df['profile.image_32'] 95 | if 'profile.image_24' in df.columns: 96 | del df['profile.image_24'] 97 | if 'profile.status_emoji' in df.columns: 98 | del df['profile.status_emoji'] 99 | if 'profile.status_text_canonical' in df.columns: 100 | del df['profile.status_text_canonical'] 101 | if 'profile.status_emoji_display_info' in df.columns: 102 | del df['profile.status_emoji_display_info'] 103 | if 'enterprise_user.is_owner' in df.columns: 104 | del df['enterprise_user.is_owner'] 105 | 106 | engine = sqlalchemy.create_engine('sqlite:///data.db') 107 | 108 | df.to_sql(table_name, engine, index=False, if_exists='replace') 109 | c.close() 110 | conn.close() 111 | print(Colors.OKGREEN + Colors.BOLD + 'data.db sqlite3 file successfully written' + Colors.ENDC) 112 | except requests.exceptions.RequestException as exception: 113 | print(Colors.FAIL + Colors.BOLD + "ERROR : " + str(exception) + Colors.ENDC) 114 | else: 115 | exit() 116 | 117 | def getUser(user_id): 118 | api_url_base = 'https://slack.com/api/users.getPresence?pretty=1&user=' 119 | if checkToken(): 120 | if scopeCheck(api_url_base, user_id): 121 | try: 122 | response = requests.get(api_url_base + user_id, headers=api_headers) 123 | todos = json.loads(response.text) 124 | print("Status :", todos['presence']) 125 | api_url_base = 'https://slack.com/api/users.info?pretty=1&user=' 126 | response = requests.get(api_url_base + user_id, headers=api_headers) 127 | todos = json.loads(response.text) 128 | print(get_pretty_json_string(todos)) 129 | except requests.exceptions.RequestException as exception: 130 | print(Colors.FAIL + Colors.BOLD + "ERROR : " + str(exception) + Colors.ENDC) 131 | exit() 132 | else: 133 | exit() 134 | 135 | def scopeCheck(api,element): 136 | api_check = requests.get(api + element, headers=api_headers).json() 137 | print("Checking token API permissions: ") 138 | if str(api_check['ok']) == 'True': 139 | success = True 140 | print(Colors.OKGREEN + Colors.BOLD + str(api_check['ok']) + Colors.ENDC) 141 | return success 142 | else: 143 | success = False 144 | print(Colors.FAIL + Colors.BOLD + "Failure: Error : " + str(api_check['error']) + Colors.ENDC) 145 | return success 146 | 147 | def checkToken(): 148 | tokenCheck = requests.post("https://slack.com/api/auth.test", headers=api_headers).json() 149 | print("Checking token: ") 150 | if str(tokenCheck['ok']) == 'True': 151 | success = True 152 | print(Colors.OKGREEN + Colors.BOLD + str(tokenCheck) + Colors.ENDC) 153 | return success 154 | else: 155 | success = False 156 | print(Colors.FAIL + Colors.BOLD + "Failure: Error : " + str(tokenCheck['error']) + Colors.ENDC) 157 | return success 158 | 159 | def searchUserByEmail(email_addr): 160 | api_url_base = 'https://slack.com/api/users.lookupByEmail?pretty=1&email=' 161 | if checkToken(): 162 | try: 163 | response = requests.get(api_url_base + email_addr, headers=api_headers) 164 | todos = json.loads(response.text) 165 | if str(todos['ok']) == 'True': 166 | print(f'Email, {email_addr}') 167 | print(todos) 168 | print("User ID :", todos['user']['id']) 169 | print("Team ID :", todos['user']['team_id']) 170 | print("Real Name :", todos['user']['real_name']) 171 | print("Display Name :", todos['user']['real_name']) 172 | print("Time Zone :", todos['user']['tz']) 173 | print("Time Zone Label :", todos['user']['tz_label']) 174 | print("Is Admin : ", todos['user']['is_admin']) 175 | print("Uses MFA :", todos['user']['has_2fa']) 176 | print("Last Update :", todos['user']['updated']) 177 | else: 178 | print("Error :", todos['error']) 179 | except requests.exceptions.RequestException as exception: 180 | print(str(exception)) 181 | else: 182 | exit() 183 | 184 | def listChannels(): 185 | api_url_base = 'https://slack.com/api/conversations.list?pretty=1&types=public_channel,private_channel' 186 | if checkToken(): 187 | try: 188 | response = requests.get(api_url_base, headers=api_headers).json() 189 | print(get_pretty_json_string(response)) 190 | except requests.exceptions.RequestException as exception: 191 | print(str(exception)) 192 | else: 193 | exit() 194 | 195 | def userChannels(user_id): 196 | api_url_base = 'https://slack.com/api/users.conversations?pretty=1&user=' 197 | if checkToken(): 198 | try: 199 | response = requests.get(api_url_base + user_id, headers=api_headers).json() 200 | for key, value in response.items(): 201 | print(key, ":", value) 202 | except requests.exceptions.RequestException as exception: 203 | print(str(exception)) 204 | else: 205 | exit() 206 | 207 | def searchFiles(keyword): 208 | api_url_base = 'https://slack.com/api/search.files?pretty=1&query=' 209 | if checkToken(): 210 | try: 211 | response = requests.get(api_url_base + keyword, headers=api_headers) 212 | data = json.loads(response.text) 213 | slack_data = data['files']['matches'] 214 | data_file = open('slack_files_search.csv', 'w', newline='') 215 | csv_writer = csv.writer(data_file) 216 | count = 0 217 | for slack_details in slack_data: 218 | if count == 0: 219 | header = slack_details.keys() 220 | csv_writer.writerow(header) 221 | count += 1 222 | csv_writer.writerow(slack_details.values()) 223 | data_file.close() 224 | except requests.exceptions.RequestException as exception: 225 | print(str(exception)) 226 | else: 227 | exit() 228 | 229 | def searchMessages(keyword): 230 | api_url_base = 'https://slack.com/api/search.messages?pretty=1&query=' 231 | if checkToken(): 232 | try: 233 | response = requests.get(api_url_base + keyword, headers=api_headers) 234 | data = json.loads(response.text) 235 | slack_data = data['messages']['matches'] 236 | data_file = open('slack_messages_search.csv', 'w', newline='') 237 | csv_writer = csv.writer(data_file) 238 | count = 0 239 | for slack_details in slack_data: 240 | if count == 0: 241 | header = slack_details.keys() 242 | csv_writer.writerow(header) 243 | count += 1 244 | csv_writer.writerow(slack_details.values()) 245 | data_file.close() 246 | except requests.exceptions.RequestException as exception: 247 | print(str(exception)) 248 | else: 249 | exit() 250 | 251 | def sendSlackMessage(message, channel): 252 | post_dict = {} 253 | post_dict['text'] = message 254 | post_dict['channel'] = channel 255 | post_dict['token'] = api_token 256 | api_url_base = 'https://slack.com/api/chat.postMessage?=pretty=1' 257 | if checkToken(): 258 | try: 259 | response = requests.post(api_url_base, data = post_dict, headers = api_headers) 260 | assert response.status_code == 200 261 | print("Message sent") 262 | except requests.exceptions.RequestException as exception: 263 | print(str(exception)) 264 | else: 265 | exit() 266 | 267 | def getConversation(channel): 268 | post_dict = {} 269 | post_dict['channel'] = channel 270 | post_dict['token'] = api_token 271 | api_url_base = 'https://slack.com/api/conversations.history?=pretty=1' 272 | if checkToken(): 273 | try: 274 | response = requests.post(api_url_base, data = post_dict, headers = api_headers).json() 275 | print(get_pretty_json_string(response)) 276 | except requests.exceptions.RequestException as exception: 277 | print(str(exception)) 278 | else: 279 | exit() 280 | 281 | def setSnoozer(num_minutes): 282 | post_dict = {} 283 | post_dict['num_minutes'] = num_minutes 284 | post_dict['token'] = api_token 285 | api_url_base = 'https://slack.com/api/dnd.setSnooze?=pretty=1' 286 | if checkToken(): 287 | try: 288 | response = requests.post(api_url_base, data = post_dict, headers = api_headers) 289 | print(Colors.OKGREEN + Colors.BOLD + 'Snoozing notifications for ' + num_minutes + ' minutes' + Colors.ENDC) 290 | except requests.exceptions.RequestException as exception: 291 | print(str(exception)) 292 | else: 293 | exit() 294 | 295 | def getFileList(id): 296 | post_dict = {} 297 | post_dict['user'] = id 298 | post_dict['token'] = api_token 299 | api_url_base = 'https://slack.com/api/files.list?=pretty=1' 300 | if checkToken(): 301 | try: 302 | response = requests.post(api_url_base, data = post_dict, headers = api_headers).json() 303 | print(get_pretty_json_string(response)) 304 | except requests.exceptions.RequestException as exception: 305 | print(str(exception)) 306 | else: 307 | exit() 308 | 309 | def sendReminder(user,my_text,my_time): 310 | post_dict = {} 311 | print(user) 312 | print(my_time) 313 | post_dict['user'] = user 314 | post_dict['text'] = my_text 315 | post_dict['time'] = my_time 316 | post_dict['token'] = api_token 317 | api_url_base = 'https://slack.com/api/reminders.add?' 318 | if checkToken(): 319 | try: 320 | response = requests.post(api_url_base, data = post_dict, headers = api_headers).json() 321 | print(get_pretty_json_string(response)) 322 | except requests.exceptions.RequestException as exception: 323 | print(str(exception)) 324 | else: 325 | exit() 326 | 327 | def get_pretty_json_string(value): 328 | return yaml.dump(value, sort_keys=False, default_flow_style=False) 329 | 330 | def uploadFile(file_name,channel_id,initial_comment): 331 | post_dict = {} 332 | post_dict['filename'] = file_name 333 | post_dict['file'] = file_name 334 | post_dict['channels'] = channel_id 335 | post_dict['initial_comment'] = initial_comment 336 | post_dict['token'] = api_token 337 | print(post_dict) 338 | my_file = { 'file' : (file_name, open(file_name, 'rb'), 'txt')} 339 | api_url_base = 'https://slack.com/api/files.upload?=pretty=1' 340 | path_to_file = file_name 341 | path = Path(path_to_file) 342 | if path.is_file(): 343 | if checkToken(): 344 | try: 345 | response = requests.post(api_url_base, data = post_dict, headers = api_headers, files = my_file).json() 346 | print(get_pretty_json_string(response)) 347 | print(Colors.OKGREEN + Colors.BOLD + 'File uploaded successfully' + Colors.ENDC) 348 | except requests.exceptions.RequestException as exception: 349 | print(str(exception)) 350 | else: 351 | exit() 352 | else: 353 | print("Error: Cannot find filename: " + file_name) 354 | 355 | def readlines(selArgs): 356 | if options.dumpAllUsers: 357 | dumpAllUsers() 358 | if options.searchUserByEmail: 359 | searchUserByEmail(options.searchUserByEmail) 360 | if options.getUser: 361 | getUser(options.getUser) 362 | if options.listChannels: 363 | listChannels() 364 | if options.userChannels: 365 | userChannels(options.userChannels) 366 | if options.searchFiles: 367 | searchFiles(options.searchFiles) 368 | if options.searchMessages: 369 | searchMessages(options.searchMessages) 370 | if options.sendSlackMessage: 371 | options.message = input('Enter Message Text:') 372 | options.channel = input('Enter Channel or user ID:') 373 | sendSlackMessage(options.message,options.channel) 374 | if options.getConversation: 375 | getConversation(options.getConversation) 376 | if options.setSnoozer: 377 | setSnoozer(options.setSnoozer) 378 | if options.getFileList: 379 | getFileList(options.getFileList) 380 | if options.sendReminder: 381 | options.my_text = input("Enter Reminder Message: ") 382 | options.my_time = input("Enter EPOCH time or Ex. in 15 minutes, or every Thursday to create reminder date: ") 383 | options.user = input("Enter Target User Id: ") 384 | sendReminder(options.user,options.my_text,options.my_time) 385 | if options.uploadFile: 386 | options.file_name = input("Enter filename and path: ") 387 | options.channel = input("Enter channel ID: ") 388 | options.initial_comment = input("Enter file comment: ") 389 | uploadFile(options.file_name, options.channel, options.initial_comment) 390 | if options.checkToken: 391 | checkToken() 392 | else: { print("") } 393 | readlines(options) 394 | --------------------------------------------------------------------------------