How LazyOwn Attack Surface Visualization Works with Bloodhound Data Integration
LazyOwn transforms raw Bloodhound Active Directory exports into interactive attack surface visualizations by parsing ZIP archives server-side, extracting nodes and privilege relationships, and rendering them through a Vis.js frontend with curated lists of critical attack vectors.
The LazyOwn C2 framework (available at grisuno/lazyown) provides Red Team operators with a streamlined approach to analyzing Active Directory environments through integrated attack surface visualization with Bloodhound data integration. This capability bridges the gap between raw Bloodhound collection data and actionable security insights, converting complex AD privilege relationships into an explorable graph interface.
Bloodhound Data Processing Pipeline
The visualization workflow follows a strict three-stage pipeline implemented in lazyc2.py. First, the /upload_zip endpoint (lines 3985-4040) accepts Bloodhound export archives with validation and security checks. Second, the process_bloodhound_zip function (lines 508-610) parses the JSON collections and constructs graph primitives. Third, extract_attack_vectors analyzes these structures to identify privileged accounts and dangerous permission edges before the frontend renders the interactive network.
Secure File Upload and Validation
The upload handler implements security-hardened file processing to prevent directory traversal and ensure data integrity. Located in lazyc2.py, the upload_zip_file function generates UUID-based temporary filenames to prevent collisions and canonicalizes all paths using os.path.abspath to verify files remain within the configured UPLOAD_FOLDER.
@app.route('/upload_zip', methods=['POST'])
def upload_zip_file():
"""Handles the file upload, processes the BloodHound ZIP, and prepares data for visualization."""
if 'file' not in request.files:
return render_template('index.html', error="No file part")
file = request.files['file']
if file.filename == '' or not file.filename.lower().endswith('.zip'):
return render_template('index.html', error="Only ZIP files are allowed")
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
unique_filename = f"{uuid.uuid4().hex}.zip"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
# Security check – ensure the resolved path stays inside the upload folder
if not os.path.abspath(filepath).startswith(os.path.abspath(app.config['UPLOAD_FOLDER'])):
return render_template('index.html', error="Invalid file path")
file.save(filepath)
nodes, edges, error_message, ad_data = process_bloodhound_zip(filepath)
try: os.remove(filepath)
except Exception: pass
if error_message:
return render_template('index.html', error=error_message)
return render_template('surface.html', nodes=nodes, edges=edges, ad_data=ad_data)
The function strictly validates file extensions, enforces path containment, and guarantees cleanup of temporary ZIP files regardless of parsing success or failure.
Extracting Nodes and Edges from Bloodhound Exports
Parsing ZIP Contents
The process_bloodhound_zip function in lazyc2.py (lines 508-610) orchestrates the extraction of Active Directory objects from Bloodhound's multi-file JSON format. It iterates through every *.json file within the archive—typically including users.json, groups.json, computers.json, and relationship files—building a comprehensive graph representation.
def process_bloodhound_zip(zip_filepath):
"""
Processes a BloodHound ZIP file to extract nodes and edges for graph visualization.
Returns: (nodes, edges, error_message, ad_data)
"""
nodes_data = {}
edges_data = []
error_message = None
try:
with zipfile.ZipFile(zip_filepath, 'r') as zip_ref:
json_files = [n for n in zip_ref.namelist() if n.endswith('.json')]
if not json_files:
return [], [], "No JSON files found in the ZIP", {}
for name in json_files:
with zip_ref.open(name) as f:
data = json.load(f)
items = data.get('data', []) if isinstance(data, dict) else data
for item in items:
# Node extraction
if 'ObjectIdentifier' in item and 'Properties' in item:
node_id = item['ObjectIdentifier']
if node_id not in nodes_data:
props = item['Properties']
nodes_data[node_id] = {
'id': node_id,
'label': props.get('name', node_id),
'title': json.dumps(props, indent=2),
'type': name.split('_')[1].replace('.json', '')
}
# Edge extraction from ACEs and Group Memberships
if 'Aces' in item and isinstance(item['Aces'], list):
for ace in item['Aces']:
if 'PrincipalSID' in ace and 'RightName' in ace:
edges_data.append({
'from': item['ObjectIdentifier'],
'to': ace['PrincipalSID'],
'label': ace['RightName']
})
if 'PrimaryGroupSID' in item and item['PrimaryGroupSID']:
edges_data.append({
'from': item['ObjectIdentifier'],
'to': item['PrimaryGroupSID'],
'label': 'MemberOf'
})
except FileNotFoundError:
error_message = f"File not found: {zip_filepath}"
except zipfile.BadZipFile:
error_message = f"Invalid or corrupted ZIP file: {zip_filepath}"
except Exception:
error_message = "An unexpected error occurred while processing the ZIP"
ad_data = extract_attack_vectors(list(nodes_data.values()), edges_data)
return list(nodes_data.values()), edges_data, error_message, ad_data
Building the Graph Structure
For each Bloodhound object, the parser constructs nodes using the ObjectIdentifier as a unique ID and populates properties including human-readable labels, JSON-formatted tooltips (title), and object types derived from filenames. Edges are constructed from two primary relationship sources:
- ACE relationships: Access Control Entries defining permissions like
GenericAll,WriteDacl, andWriteOwner - Group memberships:
PrimaryGroupSIDrelationships indicatingMemberOfassociations
After building the raw graph, the function invokes extract_attack_vectors to derive high-value security insights.
Identifying Critical Attack Vectors
The extract_attack_vectors function transforms raw graph data into actionable intelligence by identifying privileged accounts and dangerous permissions according to the LazyOwn source code. It searches for high-value AD groups (Domain Admins, Enterprise Admins, Administrators) and flags edges containing high-risk rights defined in the dangerous_rights list.
def extract_attack_vectors(nodes, edges):
"""
Analyses BloodHound nodes and edges to extract critical attack vectors for AD compromise.
Returns a dict with privileged_accounts, dangerous_permissions, potential_attack_paths, misconfigurations.
"""
ad_data = {
'privileged_accounts': [],
'dangerous_permissions': [],
'potential_attack_paths': [],
'misconfigurations': []
}
# Privileged accounts detection
for node in nodes:
if node.get('type') in ['User', 'Group'] and node.get('label', '').lower() in [
'domain admins', 'enterprise admins', 'administrators'
]:
ad_data['privileged_accounts'].append({
'id': node['id'],
'label': node['label'],
'type': node['type'],
'details': node['title']
})
# Dangerous permissions detection
dangerous_rights = ['GenericAll', 'WriteDacl', 'WriteOwner',
'Owns', 'AllExtendedRights', 'DCSync']
for edge in edges:
if edge['label'] in dangerous_rights:
src = next((n for n in nodes if n['id'] == edge['from']), None)
dst = next((n for n in nodes if n['id'] == edge['to']), None)
if src and dst:
ad_data['dangerous_permissions'].append({
'from': src['label'],
'to': dst['label'],
'right': edge['label'],
'source_type': src['type'],
'target_type': dst['type']
})
return ad_data
The function specifically flags GenericAll, WriteDacl, WriteOwner, Owns, AllExtendedRights, and DCSync permissions as critical attack vectors, resolving source and target node labels to provide human-readable context for security operators.
Interactive Frontend Visualization
The Vis.js network library powers the client-side rendering in templates/surface.html and templates/graph.html. The Flask backend injects JSON-encoded node and edge datasets directly into the Jinja2 templates using the tojson filter, eliminating the need for additional API calls.
{% set nodes_json = nodes|tojson %}
{% set edges_json = edges|tojson %}
{% set ad_data_json = ad_data|tojson %}
The JavaScript initialization creates interactive DataSets that support real-time filtering, physics-based layouts, and clustering:
const nodes = new vis.DataSet({{ nodes_json }});
const edges = new vis.DataSet({{ edges_json }});
const container = document.getElementById('mynetwork');
const data = { nodes, edges };
const options = {/* filters, clustering, physics, etc. */};
const network = new vis.Network(container, data, options);
The surface.html template renders an attack vector accordion displaying the four categories from ad_data: privileged accounts, dangerous permissions, potential attack paths, and misconfigurations. UI controls allow operators to filter by node type, edge relationship, and depth, while statistical panels update dynamically as the graph changes.
Practical Implementation Examples
Uploading Bloodhound Data via cURL
Operators can programmatically upload Bloodhound exports using standard HTTP POST requests:
curl -X POST http://localhost:5000/upload_zip \
-F "file=@/path/to/BloodHound-Data.zip"
Programmatic Parsing in Python
The parsing functions can be imported directly for custom analysis workflows:
from lazyc2 import process_bloodhound_zip
zip_path = "samples/BloodHound-Data.zip"
nodes, edges, err, ad_data = process_bloodhound_zip(zip_path)
if err:
print(f"Error: {err}")
else:
print(f"Found {len(nodes)} nodes and {len(edges)} edges")
print("Privileged accounts:")
for acct in ad_data['privileged_accounts']:
print(f" * {acct['label']} ({acct['type']})")
Rendering in Custom Flask Views
The visualization components integrate seamlessly into custom route handlers:
@app.route('/demo')
def demo():
nodes, edges, _, ad_data = process_bloodhound_zip('sample.zip')
return render_template('surface.html',
nodes=nodes,
edges=edges,
ad_data=ad_data)
Summary
LazyOwn's attack surface visualization with Bloodhound data integration provides a comprehensive pipeline for Active Directory security analysis:
- Secure upload handling in
lazyc2.pyvalidates ZIP archives and prevents path traversal through UUID-based filenames and path canonicalization - Bloodhound JSON parsing extracts nodes from
ObjectIdentifierfields and builds relationship edges from ACE and group membership data - Attack vector extraction identifies privileged accounts and dangerous permissions including GenericAll, WriteDacl, and DCSync rights
- Vis.js frontend rendering in
templates/surface.htmldelivers interactive graph exploration with real-time filtering and curated security insights - Jinja2 data injection eliminates frontend API dependencies by embedding parsed graph data directly into the rendered HTML
Frequently Asked Questions
How does LazyOwn prevent directory traversal during Bloodhound ZIP uploads?
The upload_zip_file function in lazyc2.py resolves absolute paths using os.path.abspath and validates that the target filepath starts with the absolute path of the configured UPLOAD_FOLDER. This canonicalization check ensures uploaded files cannot escape the designated storage directory regardless of malicious filename inputs.
What specific Bloodhound relationship types does LazyOwn extract for visualization?
According to the source code in lazyc2.py, the parser extracts two primary relationship categories: ACE edges from the Aces array (representing permissions like GenericAll, WriteDacl, and WriteOwner) and group membership edges from PrimaryGroupSID fields (rendered as MemberOf relationships). These cover the critical privilege escalation pathways in Active Directory environments.
Can the attack vector detection logic be extended to identify custom misconfigurations?
Yes. The extract_attack_vectors function structure in lazyc2.py includes placeholder arrays for potential_attack_paths and misconfigurations specifically reserved for custom heuristics. Security teams can extend this function to implement GPO analysis, Kerberoasting detection, or domain-specific privilege anomalies while maintaining the existing visualization pipeline through surface.html.
What frontend technologies render the interactive attack surface graph?
The visualization layer combines Jinja2 templating (for server-side data injection), Vis.js Network (for the interactive graph canvas), and Bootstrap components (for the sidebar controls and attack vector accordion). These are defined in templates/surface.html and templates/graph.html, with the Vis.js DataSets receiving pre-processed node and edge arrays directly from the Flask backend.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →