Fix stored-HTML-injection: escape AD data before it hits Markdown tables
Every table cell fed from directory content (descriptions, sAMAccountName, OS strings, DNs, object classes) was interpolated into the Markdown report raw. python-markdown doesn't escape inline HTML by default, so a directory-controlled value like an OU description containing <script>... would render live once the report was converted to HTML with md_to_html.py -- and the underlying data is attacker-influenceable, not just operator-authored. Added md_escape() (Python) / ConvertTo-MdSafe (PowerShell), applied at every table-row interpolation in both scripts. Escapes &, <, > to HTML entities and | plus embedded newlines to keep the table structure intact. Raw JSON dumps are left untouched -- this only affects the Markdown/HTML presentation layer. Verified end-to-end with <script>, <img onerror=...>, embedded &, and embedded | payloads across every affected table in both scripts; confirmed no live tags reach the rendered HTML and no double-escaping occurs.
This commit is contained in:
+25
-8
@@ -79,6 +79,23 @@ function ConvertTo-Rfc3339($DateTime) {
|
||||
return $DateTime.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
}
|
||||
|
||||
# AD attributes (descriptions, sAMAccountName, OS strings, DNs, ...) are
|
||||
# directory content, not report-generated text -- they can contain anything
|
||||
# a writer or an attacker put there, including raw HTML. Markdown doesn't
|
||||
# escape inline HTML by default, so an unescaped '<script>' in an OU
|
||||
# description would render live when the report is viewed as HTML. This also
|
||||
# neutralizes '|' and embedded newlines, which would otherwise corrupt the
|
||||
# table row itself. Call on every AD-sourced value before it goes into a
|
||||
# Markdown table cell.
|
||||
function ConvertTo-MdSafe($Value) {
|
||||
if ($null -eq $Value) { return "" }
|
||||
$text = [string]$Value
|
||||
$text = $text.Replace("&", "&").Replace("<", "<").Replace(">", ">")
|
||||
$text = $text.Replace("|", "\|")
|
||||
$text = ($text -replace "\s*[\r\n]+\s*", " ").Trim()
|
||||
return $text
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Object type counts (whole subtree)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -301,7 +318,7 @@ if ($findings.Count -gt 0) {
|
||||
Add-Line ""
|
||||
Add-Line "| Severity | Finding | Count | Notes |"
|
||||
Add-Line "|---|---|---|---|"
|
||||
foreach ($f in $findings) { Add-Line "| $($f.Severity) | $($f.Title) | $($f.Rows.Count) | $($f.Notes) |" }
|
||||
foreach ($f in $findings) { Add-Line "| $($f.Severity) | $(ConvertTo-MdSafe $f.Title) | $($f.Rows.Count) | $(ConvertTo-MdSafe $f.Notes) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "### Finding Detail Lists"
|
||||
@@ -309,9 +326,9 @@ if ($findings.Count -gt 0) {
|
||||
foreach ($f in $findings) {
|
||||
Add-Line "#### [$($f.Severity)] $($f.Title)"
|
||||
Add-Line ""
|
||||
Add-Line "| $($f.Cols[0]) | $($f.Cols[1]) | $($f.Cols[2]) |"
|
||||
Add-Line "| $(ConvertTo-MdSafe $f.Cols[0]) | $(ConvertTo-MdSafe $f.Cols[1]) | $(ConvertTo-MdSafe $f.Cols[2]) |"
|
||||
Add-Line "|---|---|---|"
|
||||
foreach ($row in $f.Rows) { Add-Line "| $($row.Col0) | $($row.Col1) | $($row.DN) |" }
|
||||
foreach ($row in $f.Rows) { Add-Line "| $(ConvertTo-MdSafe $row.Col0) | $(ConvertTo-MdSafe $row.Col1) | $(ConvertTo-MdSafe $row.DN) |" }
|
||||
Add-Line ""
|
||||
}
|
||||
} else {
|
||||
@@ -323,7 +340,7 @@ Add-Line "## Object Type Counts"
|
||||
Add-Line ""
|
||||
Add-Line "| Object Class | Count |"
|
||||
Add-Line "|---|---|"
|
||||
foreach ($row in $objectCounts) { Add-Line "| $($row.ObjectClass) | $($row.Count) |" }
|
||||
foreach ($row in $objectCounts) { Add-Line "| $(ConvertTo-MdSafe $row.ObjectClass) | $($row.Count) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Organizational Units"
|
||||
@@ -333,7 +350,7 @@ Add-Line "- Maximum nesting depth: $maxDepth"
|
||||
Add-Line ""
|
||||
Add-Line "| OU DN | Depth | Description |"
|
||||
Add-Line "|---|---|---|"
|
||||
foreach ($o in ($ous | Sort-Object DN)) { Add-Line "| $($o.DN) | $($o.Depth) | $($o.Description) |" }
|
||||
foreach ($o in ($ous | Sort-Object DN)) { Add-Line "| $(ConvertTo-MdSafe $o.DN) | $($o.Depth) | $(ConvertTo-MdSafe $o.Description) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Users"
|
||||
@@ -362,7 +379,7 @@ Add-Line ""
|
||||
Add-Line "| Operating System | Count |"
|
||||
Add-Line "|---|---|"
|
||||
$osGroups = $computers | Group-Object -Property { if ($_.OS) { $_.OS } else { "Unknown" } } | Sort-Object Count -Descending
|
||||
foreach ($g in $osGroups) { Add-Line "| $($g.Name) | $($g.Count) |" }
|
||||
foreach ($g in $osGroups) { Add-Line "| $(ConvertTo-MdSafe $g.Name) | $($g.Count) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Groups"
|
||||
@@ -375,7 +392,7 @@ Add-Line ""
|
||||
Add-Line "| Scope | Count |"
|
||||
Add-Line "|---|---|"
|
||||
$scopeGroups = $groups | Group-Object -Property Scope | Sort-Object Count -Descending
|
||||
foreach ($g in $scopeGroups) { Add-Line "| $($g.Name) | $($g.Count) |" }
|
||||
foreach ($g in $scopeGroups) { Add-Line "| $(ConvertTo-MdSafe $g.Name) | $($g.Count) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "### Largest groups (top 15 by member count)"
|
||||
@@ -383,7 +400,7 @@ Add-Line ""
|
||||
Add-Line "| Group | Type | Scope | Members |"
|
||||
Add-Line "|---|---|---|---|"
|
||||
foreach ($g in ($groups | Sort-Object -Property MemberCount -Descending | Select-Object -First 15)) {
|
||||
Add-Line "| $($g.SamAccountName) | $($g.Type) | $($g.Scope) | $($g.MemberCount) |"
|
||||
Add-Line "| $(ConvertTo-MdSafe $g.SamAccountName) | $(ConvertTo-MdSafe $g.Type) | $(ConvertTo-MdSafe $g.Scope) | $($g.MemberCount) |"
|
||||
}
|
||||
Add-Line ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user