diff --git a/src/AppContainer.jsx b/src/AppContainer.jsx index 4b64651d3..f53e0f72a 100644 --- a/src/AppContainer.jsx +++ b/src/AppContainer.jsx @@ -83,7 +83,12 @@ const fullEdgeList = [ 'AddKeyCredentialLink', 'DumpSMSAPassword', 'DCSync', - 'SyncLAPSPassword' + 'SyncLAPSPassword', + 'Enroll', + 'AutoEnroll', + 'ManageCa', + 'ManageCertificates', + 'EnabledBy' ]; export default class AppContainer extends Component { diff --git a/src/components/RawQuery.jsx b/src/components/RawQuery.jsx index 8f9240e49..77a20b80b 100644 --- a/src/components/RawQuery.jsx +++ b/src/components/RawQuery.jsx @@ -24,12 +24,14 @@ const RawQuery = () => { const onKeyDown = (e) => { let key = e.keyCode ? e.keyCode : e.which; - if (key === 13) { + if ((key == 10 || key == 13) && e.ctrlKey) { emitter.emit('query', query); } }; const onChange = (e) => { + e.target.style.height = 'inherit'; + e.target.style.height = `${e.target.scrollHeight}px`; setQueryFromEvent(e.target.value); }; @@ -59,7 +61,7 @@ const RawQuery = () => { transition={{ duration: 0.25 }} animate={open ? 'open' : 'closed'} > - { className={clsx(styles.input, 'form-control')} autoComplete='off' placeholder='Enter a cypher query. Your query must return nodes or paths.' - /> + /> */} + ); diff --git a/src/components/SearchContainer/EdgeFilter/EdgeFilter.jsx b/src/components/SearchContainer/EdgeFilter/EdgeFilter.jsx index 68f702626..d1edb653b 100644 --- a/src/components/SearchContainer/EdgeFilter/EdgeFilter.jsx +++ b/src/components/SearchContainer/EdgeFilter/EdgeFilter.jsx @@ -112,6 +112,21 @@ const EdgeFilter = ({ open }) => { + + + + +
{ switch (type) { case 'Group': - icon.className = 'fa fa-users'; + if (item.hasOwnProperty("bl-icon")){ + icon.className = 'fa '+ item["bl-icon"]; + }else{ + icon.className = 'fa fa-users'; + } break; case 'User': icon.className = 'fa fa-user'; @@ -43,6 +47,12 @@ const SearchRow = ({ item, search }) => { case 'OU': icon.className = 'fa fa-sitemap'; break; + case 'CA': + icon.className = 'fa fa-university'; + break; + case 'CertificateTemplate': + icon.className = 'fa fa-id-card'; + break; case 'Container': icon.className = 'fa fa-box' break @@ -104,8 +114,12 @@ const SearchRow = ({ item, search }) => { icon.className = 'fa fa-window-restore' break default: - icon.className = 'fa fa-question'; - type = 'Base'; + if (item.hasOwnProperty("bl-icon")){ + icon.className = 'fa '+ item["bl-icon"]; + }else{ + icon.className = 'fa fa-question'; + type = 'Base'; + } break; } diff --git a/src/components/SearchContainer/TabContainer.jsx b/src/components/SearchContainer/TabContainer.jsx index 3fa5b51d3..5d97b5be2 100644 --- a/src/components/SearchContainer/TabContainer.jsx +++ b/src/components/SearchContainer/TabContainer.jsx @@ -8,6 +8,8 @@ import ComputerNodeData from './Tabs/ComputerNodeData'; import DomainNodeData from './Tabs/DomainNodeData'; import GpoNodeData from './Tabs/GPONodeData'; import OuNodeData from './Tabs/OUNodeData'; +import CaNodeData from './Tabs/CANodeData'; +import TemplateNodeData from './Tabs/TemplateNodeData'; import AZGroupNodeData from './Tabs/AZGroupNodeData'; import AZUserNodeData from './Tabs/AZUserNodeData'; import AZContainerRegistryNodeData from './Tabs/AZContainerRegistryNodeData'; @@ -48,6 +50,8 @@ class TabContainer extends Component { domainVisible: false, gpoVisible: false, ouVisible: false, + caVisible: false, + templateVisible: false, containerVisible: false, azGroupVisible: false, azUserVisible: false, @@ -92,6 +96,10 @@ class TabContainer extends Component { this._domainNodeClicked(); } else if (type === 'OU') { this._ouNodeClicked(); + } else if (type === 'CA') { + this._caNodeClicked(); + } else if (type === 'CertificateTemplate') { + this._templateNodeClicked(); } else if (type === 'GPO') { this._gpoNodeClicked(); } else if (type === 'AZGroup') { @@ -225,6 +233,22 @@ class TabContainer extends Component { }); } + _caNodeClicked() { + this.clearVisible() + this.setState({ + caVisible: true, + selected: 2 + }); + } + + _templateNodeClicked() { + this.clearVisible() + this.setState({ + templateVisible: true, + selected: 2 + }); + } + _azGroupNodeClicked() { this.clearVisible() this.setState({ @@ -405,6 +429,8 @@ class TabContainer extends Component { !this.state.domainVisible && !this.state.gpoVisible && !this.state.ouVisible && + !this.state.caVisible && + !this.state.templateVisible && !this.state.azGroupVisible && !this.state.azUserVisible && !this.state.azContainerRegistryVisible && @@ -436,6 +462,8 @@ class TabContainer extends Component { + + diff --git a/src/components/SearchContainer/Tabs/CANodeData.jsx b/src/components/SearchContainer/Tabs/CANodeData.jsx new file mode 100644 index 000000000..8e4c6ef7b --- /dev/null +++ b/src/components/SearchContainer/Tabs/CANodeData.jsx @@ -0,0 +1,171 @@ +/****************************************************************************************** +* The credit goes to https://github.com/ly4k/BloodHound. Thank you for the excellent work! +*/ +import clsx from 'clsx'; +import React, { useContext, useEffect, useState } from 'react'; +import { Table } from 'react-bootstrap'; +import { AppContext } from '../../../AppContext'; +import CollapsibleSection from './Components/CollapsibleSection'; +import ExtraNodeProps from './Components/ExtraNodeProps'; +import MappedNodeProps from './Components/MappedNodeProps'; +import NodeCypherLink from './Components/NodeCypherLink'; +import NodePlayCypherLink from './Components/NodePlayCypherLink'; +import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink'; +import styles from './NodeData.module.css'; + +const CANodeData = () => { + const [visible, setVisible] = useState(false); + const [objectid, setObjectid] = useState(null); + const [label, setLabel] = useState(null); + const [domain, setDomain] = useState(null); + const [nodeProps, setNodeProps] = useState({}); + const [blocksInheritance, setBlocksInheritance] = useState(false); + const context = useContext(AppContext); + + useEffect(() => { + emitter.on('nodeClicked', nodeClickEvent); + + return () => { + emitter.removeListener('nodeClicked', nodeClickEvent); + }; + }, []); + + const nodeClickEvent = (type, id, blocksinheritance, domain) => { + if (type === 'CA') { + setVisible(true); + setObjectid(id); + setDomain(domain); + setBlocksInheritance(blocksinheritance); + let session = driver.session(); + session + .run(`MATCH (n:CA {objectid: $objectid}) RETURN n AS node`, { + objectid: id, + }) + .then((r) => { + let props = r.records[0].get('node').properties; + setNodeProps(props); + setLabel(props.name); + session.close(); + }); + } else { + setObjectid(null); + setVisible(false); + } + }; + + const displayMap = { + objectid: 'Object ID', + 'CA Name': 'CA Name', + }; + + return objectid === null ? ( +
+ ) : ( +
+
+
{label || objectid}
+ + +
+ + + + + +
+
+
+ +
+ + + +
+ + + +
+ + +
+ + + + (u1:CA {objectid: $objectid}) WHERE r.isacl=true' + } + end={label} + distinct + /> + (g:Group)-[r1:ManageCa|ManageCertificates|Auditor|Operator|Owns]->(u:CA {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' + } + end={label} + distinct + /> + +
+
+
+ +
+ + +
+ + + + (u1:CA {objectid: $objectid}) WHERE r.isacl=true' + } + end={label} + distinct + /> + (g:Group)-[r1:Read|Enroll]->(u:CA {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' + } + end={label} + distinct + /> + +
+
+
+ +
+
+
+ ); +}; + +CANodeData.propTypes = {}; +export default CANodeData; diff --git a/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx b/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx index a411c2829..17e3176bb 100644 --- a/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx +++ b/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx @@ -113,6 +113,20 @@ const DatabaseDataDisplay = () => { index={index} label={'Computers'} /> + + (n:CertificateTemplate)) WHERE g<>n and n.`Enrollee Supplies Subject` = true and n.`Client Authentication` = true and n.`Enabled` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates']) return p" + } + ] + }, + { + "name": "Find Misconfigured Certificate Templates (ESC2)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH (n:CertificateTemplate) WHERE n.`Enabled` = true and n.`Any Purpose` = true RETURN n" + } + ] + }, + { + "name": "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC2)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:CertificateTemplate)) WHERE g<>n and n.`Enabled` = true and n.`Any Purpose` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates']) return p" + } + ] + }, + { + "name": "Find Enrollment Agent Templates (ESC3)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH (n:CertificateTemplate) WHERE n.`Enabled` = true and n.`Enrollment Agent` = true RETURN n" + } + ] + }, + { + "name": "Shortest Paths to Enrollment Agent Templates from Owned Principals (ESC3)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:CertificateTemplate)) WHERE g<>n and n.`Enabled` = true and n.`Enrollment Agent` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates']) return p" + } + ] + }, + { + "name": "Shortest Paths to Vulnerable Certificate Template Access Control (ESC4)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=shortestPath((g)-[r*1..]->(n:CertificateTemplate)) WHERE g<>n and n.`Enabled` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates','Enroll','AutoEnroll']) RETURN p" + } + ] + }, + { + "name": "Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:CertificateTemplate)) WHERE g<>n and n.Enabled = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates','Enroll','AutoEnroll']) return p" + } + ] + }, + { + "name": "Find Certificate Authorities with User Specified SAN (ESC6)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH (n:CA) WHERE n.`User Specified SAN` = 'Enabled' RETURN n" + } + ] + }, + { + "name": "Shortest Paths to Vulnerable Certificate Authority Access Control (ESC7)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g)-[r*1..]->(n:CA)) WHERE g<>n and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','Enroll','AutoEnroll']) RETURN p" + } + ] + }, + { + "name": "Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:CA)) WHERE g<>n and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','Enroll','AutoEnroll']) RETURN p" + } + ] + }, + { + "name": "Find Certificate Authorities with HTTP Web Enrollment (ESC8)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH (n:CA) WHERE n.`Web Enrollment` = 'Enabled' RETURN n" + } + ] + }, + { + "name": "Find Unsecured Certificate Templates (ESC9)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH (n:CertificateTemplate) WHERE 'NoSecurityExtension' in n.`Enrollment Flag` and n.`Enabled` = true RETURN n" + } + ] + }, + { + "name": "Shortest Paths to Unsecured Certificate Templates from Owned Principals (ESC9)", + "category": "PKI", + "queryList": [ + { + "final": true, + "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:CertificateTemplate)) WHERE g<>n and 'NoSecurityExtension' in n.`Enrollment Flag` and n.`Enabled` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates']) return p" + } + ] } ] } diff --git a/src/components/SearchContainer/Tabs/PrebuiltQueryNode.jsx b/src/components/SearchContainer/Tabs/PrebuiltQueryNode.jsx index 7aa7853f3..68c24580e 100644 --- a/src/components/SearchContainer/Tabs/PrebuiltQueryNode.jsx +++ b/src/components/SearchContainer/Tabs/PrebuiltQueryNode.jsx @@ -1,7 +1,37 @@ -import React, { Component } from 'react'; +import React, { Component, useContext } from 'react'; import './PrebuiltQueries.module.css'; +import { motion } from 'framer-motion'; +import { AppContext } from '../../../AppContext'; + +import GlyphiconSpan from '../../GlyphiconSpan'; +import Icon from '../../Icon'; +import { withAlert } from 'react-alert'; + export default class PrebuiltQueryNode extends Component { + static contextType = AppContext; + + constructor(props) { + super(props) + + this.state = { + hovered: false + } + } + + enterQuery() { + this.setState({ + hovered: true + }) + }; + + + exitQuery() { + this.setState({ + hovered: false + }) + }; + render() { let c; @@ -14,11 +44,77 @@ export default class PrebuiltQueryNode extends Component { } }.bind(this); + const copyQuery = (e) => { + let containsProps = false; + let queries = [] + let queryList = this.props.info.queryList + + let containsMultipleQueries = queryList.length > 1; + + for (var i = 0; i < queryList.length; i++) { + let query = queryList[i].query; + + if (queries.length > 0) { + queries.push(""); + } + + let props = queryList[i].props; + if (props) { + containsProps = true; + for (const [key, value] of Object.entries(props)) { + query = query.replace(`\$${key}`, JSON.stringify(value)) + } + } + queries.push(query); + } + + navigator.clipboard.writeText(queries.join("\n")) + + this.props.alert.info("Copied query") + + if (containsProps) { + this.props.alert.show(WARNING Query contains props. These might not be properly formatted, { type: 'error' }) + } + + if (containsMultipleQueries) { + this.props.alert.show(WARNING Query contains multiple queries, { type: 'error' }) + } + }; + + const variants = { + open: { height: 'auto', opacity: 1 }, + closed: { height: 0, opacity: 0 }, + }; + return ( - - {this.props.info.name} - - + + {this.props.info.name} + + + copyQuery()} + > + + + + + + + ); } } +//export default withAlert()(PrebuiltQueryNode) diff --git a/src/components/SearchContainer/Tabs/TemplateNodeData.jsx b/src/components/SearchContainer/Tabs/TemplateNodeData.jsx new file mode 100644 index 000000000..572dbe226 --- /dev/null +++ b/src/components/SearchContainer/Tabs/TemplateNodeData.jsx @@ -0,0 +1,171 @@ +/****************************************************************************************** +* The credit goes to https://github.com/ly4k/BloodHound. Thank you for the excellent work! +*/ +import clsx from 'clsx'; +import React, { useContext, useEffect, useState } from 'react'; +import { Table } from 'react-bootstrap'; +import { AppContext } from '../../../AppContext'; +import CollapsibleSection from './Components/CollapsibleSection'; +import ExtraNodeProps from './Components/ExtraNodeProps'; +import MappedNodeProps from './Components/MappedNodeProps'; +import NodeCypherLink from './Components/NodeCypherLink'; +import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink'; +import styles from './NodeData.module.css'; + +const TemplateNodeData = () => { + const [visible, setVisible] = useState(false); + const [objectid, setObjectid] = useState(null); + const [label, setLabel] = useState(null); + const [domain, setDomain] = useState(null); + const [nodeProps, setNodeProps] = useState({}); + const [blocksInheritance, setBlocksInheritance] = useState(false); + const context = useContext(AppContext); + + useEffect(() => { + emitter.on('nodeClicked', nodeClickEvent); + + return () => { + emitter.removeListener('nodeClicked', nodeClickEvent); + }; + }, []); + + const nodeClickEvent = (type, id, blocksinheritance, domain) => { + if (type === 'CertificateTemplate') { + setVisible(true); + setObjectid(id); + setDomain(domain); + setBlocksInheritance(blocksinheritance); + let session = driver.session(); + session + .run(`MATCH (n:CertificateTemplate {objectid: $objectid}) RETURN n AS node`, { + objectid: id, + }) + .then((r) => { + let props = r.records[0].get('node').properties; + setNodeProps(props); + setLabel(props.name); + session.close(); + }); + } else { + setObjectid(null); + setVisible(false); + } + }; + + const displayMap = { + objectid: 'Object ID', + 'Template Name': 'Template Name', + 'Display Name': 'Display Name', + }; + + return objectid === null ? ( +
+ ) : ( +
+
+
{label || objectid}
+ + +
+ + + + + +
+
+
+ +
+ + + +
+ + + +
+ + +
+ + + + (u1:CertificateTemplate {objectid: $objectid}) WHERE r.isacl=true' + } + end={label} + distinct + /> + (g:Group)-[r1:AllExtendedRights|GenericAll|GenericWrite|WriteDacl|WriteOwner|WriteProperty|Owns]->(u:CertificateTemplate {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' + } + end={label} + distinct + /> + +
+
+
+ +
+ + +
+ + + + (u1:CertificateTemplate {objectid: $objectid}) WHERE r.isacl=true' + } + end={label} + distinct + /> + (g:Group)-[r1:AutoEnroll|Enroll|GenericAll]->(u:CertificateTemplate {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' + } + end={label} + distinct + /> + +
+
+
+ +
+
+
+ ); +}; + +TemplateNodeData.propTypes = {}; +export default TemplateNodeData; diff --git a/src/index.js b/src/index.js index 03e5815a4..a5df54f48 100644 --- a/src/index.js +++ b/src/index.js @@ -142,6 +142,18 @@ global.appStore = { scale: 1.25, color: '#FFAA00', }, + CA: { + font: "'Font Awesome 5 Free'", + content: '\uF19C', + scale: 1.25, + color: '#FFAA00', + }, + CertificateTemplate: { + font: "'Font Awesome 5 Free'", + content: '\uF2C2', + scale: 1.25, + color: '#73E6A1', + }, Container: { font: "'Font Awesome 5 Free'", content: '\uF466', diff --git a/src/js/newingestion.js b/src/js/newingestion.js index 9e91bfddb..4002bbde4 100644 --- a/src/js/newingestion.js +++ b/src/js/newingestion.js @@ -20,6 +20,8 @@ export const ADLabels = { Computer: 'Computer', OU: 'OU', GPO: 'GPO', + CertificateTemplate: 'CertificateTemplate', + CA: 'CA', Domain: 'Domain', Container: 'Container', MemberOf: 'MemberOf', @@ -34,6 +36,7 @@ export const ADLabels = { Contains: 'Contains', GPLink: 'GPLink', TrustedBy: 'TrustedBy', + EnabledBy: 'EnabledBy', DumpSMSAPassword: 'DumpSMSAPassword', }; @@ -224,6 +227,65 @@ export function buildGroupJsonNew(chunk) { return queries; } +/** + * + * @param {Array.} chunk + * @returns {{}} + */ +export function buildTemplateJsonNew(chunk) { + let queries = {}; + + queries.properties = {}; + queries.properties.statement = PROP_QUERY.format(ADLabels.CertificateTemplate); + queries.properties.props = []; + + for (let template of chunk) { + let properties = template.Properties; + let identifier = template.ObjectIdentifier; + let aces = template.Aces; + let cas = template.cas_ids; + + queries.properties.props.push({ objectid: identifier, map: properties }); + + processAceArrayNew(aces, identifier, ADLabels.CertificateTemplate, queries); + + if (cas) { + let format = [ADLabels.CertificateTemplate, ADLabels.CA, ADLabels.EnabledBy, NON_ACL_PROPS]; + let props = cas.map((ca) => { + return { source: identifier, target: ca }; + }); + insertNew(queries, format, props); + } + } + + return queries; +} + +/** + * + * @param {Array.} chunk + * @returns {{}} + */ +export function buildCaJsonNew(chunk) { + let queries = {}; + + queries.properties = {}; + queries.properties.statement = PROP_QUERY.format(ADLabels.CA); + queries.properties.props = []; + + for (let ca of chunk) { + let properties = ca.Properties; + let identifier = ca.ObjectIdentifier; + let aces = ca.Aces; + + queries.properties.props.push({ objectid: identifier, map: properties }); + + processAceArrayNew(aces, identifier, ADLabels.CA, queries); + } + + return queries; +} + /** * * @param {Array.} chunk