Skip to content

Commit 3b465f8

Browse files
committed
normalize: HTML escape
1 parent 6fd8f7f commit 3b465f8

File tree

9 files changed

+134
-44
lines changed

9 files changed

+134
-44
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
unisec (0.0.5)
4+
unisec (0.0.6)
55
ctf-party (~> 3.0)
66
dry-cli (~> 1.0)
77
paint (~> 2.3)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ A CLI tool and library to play with Unicode security.
2626
- **Hexdump**
2727
- UTF-8, UTF-16, UTF-32 hexadecimal dumps
2828
- **Normalization**
29-
- NFC, NFKC, NFD, NFKD normalization forms
29+
- NFC, NFKC, NFD, NFKD normalization forms, HTML escape bypass for XSS
3030
- **Properties**
3131
- Get all properties of a given Unicode character
3232
- List code points matching a Unicode property

docs/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## [unreleased]
22

3+
## [0.0.6]
4+
5+
**Features**
6+
7+
- _Prepare a XSS payload for HTML escape bypass (HTML escape followed by NFKC / NFKD normalization)_
8+
- Rename CLI command `normalize` into `normalize all`
9+
- Add a new method `replace_bypass` in the class `Unisec::Normalization`
10+
- Add a new CLI command `normalize replace` (using the new `replace_bypass` method)
11+
312
## [0.0.5]
413

514
**Features**

docs/pages/usage.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ Options:
5858
- [Randomize](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Confusables/Randomize)
5959
- [Grep](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Grep)
6060
- [Hexdump](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Hexdump)
61-
- [Normalize](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Normalize)
61+
- **Normalize**
62+
- [All](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Normalize/All)
63+
- [Replace](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Normalize/Replace)
6264
- **Properties**
6365
- [Char](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Properties/Char)
6466
- [Codepoints](https://acceis.github.io/unisec/yard/Unisec/CLI/Commands/Properties/Codepoints)

lib/unisec/cli/cli.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ module Commands
2424
register 'confusables randomize', Confusables::Randomize
2525
register 'grep', Grep
2626
register 'hexdump', Hexdump
27-
register 'normalize', Normalize
27+
register 'normalize all', Normalize::All
28+
register 'normalize replace', Normalize::Replace
2829
register 'properties char', Properties::Char
2930
register 'properties codepoints', Properties::Codepoints
3031
register 'properties list', Properties::List

lib/unisec/cli/normalization.rb

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,77 @@ module Unisec
88
module CLI
99
module Commands
1010
# CLI sub-commands `unisec normalize xxx` for the class {Unisec::Normalization} from the lib.
11-
#
12-
# Command `unisec normalize "example"`
13-
#
14-
# Example:
15-
#
16-
# ```plaintext
17-
# ➜ unisec normalize ẛ̣
18-
# Original: ẛ̣
19-
# U+1E9B U+0323
20-
# NFC: ẛ̣
21-
# U+1E9B U+0323
22-
# NFKC: ṩ
23-
# U+1E69
24-
# NFD: ẛ̣
25-
# U+017F U+0323 U+0307
26-
# NFKD: ṩ
27-
# U+0073 U+0323 U+0307
28-
#
29-
# ➜ unisec normalize ẛ̣ --form nfkd
30-
# ṩ
31-
# ```
32-
class Normalize < Dry::CLI::Command
33-
desc 'Normalize in all forms'
34-
35-
argument :input, required: true,
36-
desc: 'String input. Read from STDIN if equal to -.'
37-
38-
option :form, default: nil, values: %w[nfc nfkc nfd nfkd],
39-
desc: 'Output only in the specified normalization form.'
40-
41-
# Normalize in all forms
42-
# @param input [String] Input string to normalize
43-
def call(input: nil, **options)
44-
input = $stdin.read.chomp if input == '-'
45-
if options[:form].nil?
46-
puts Unisec::Normalization.new(input).display
47-
else
48-
# using send() is safe here thanks to the value whitelist
49-
puts Unisec::Normalization.send(options[:form], input)
11+
module Normalize
12+
# Command `unisec normalize all "example"`
13+
#
14+
# Example:
15+
#
16+
# ```plaintext
17+
# ➜ unisec normalize all ẛ̣
18+
# Original: ẛ̣
19+
# U+1E9B U+0323
20+
# NFC: ẛ̣
21+
# U+1E9B U+0323
22+
# NFKC: ṩ
23+
# U+1E69
24+
# NFD: ẛ̣
25+
# U+017F U+0323 U+0307
26+
# NFKD: ṩ
27+
# U+0073 U+0323 U+0307
28+
#
29+
# ➜ unisec normalize all ẛ̣ --form nfkd
30+
# ṩ
31+
# ```
32+
class All < Dry::CLI::Command
33+
desc 'Normalize in all forms'
34+
35+
argument :input, required: true,
36+
desc: 'String input. Read from STDIN if equal to -.'
37+
38+
option :form, default: nil, values: %w[nfc nfkc nfd nfkd],
39+
desc: 'Output only in the specified normalization form.'
40+
41+
# Normalize in all forms
42+
# @param input [String] Input string to normalize
43+
def call(input: nil, **options)
44+
input = $stdin.read.chomp if input == '-'
45+
if options[:form].nil?
46+
puts Unisec::Normalization.new(input).display
47+
else
48+
# using send() is safe here thanks to the value whitelist
49+
puts Unisec::Normalization.send(options[:form], input)
50+
end
51+
end
52+
end
53+
54+
# Command `unisec normalize replace "example"`
55+
#
56+
# Example:
57+
#
58+
# ```plaintext
59+
# ➜ unisec normalize replace "<svg onload=\"alert('XSS')\">"
60+
# Original: <svg onload="alert('XSS')">
61+
# U+003C U+0073 U+0076 U+0067 U+0020 U+006F U+006E U+006C U+006F U+0061 U+0064 U+003D U+0022 U+0061 U+006C U+0065 U+0072 U+0074 U+0028 U+0027 U+0058 U+0053 U+0053 U+0027 U+0029 U+0022 U+003E
62+
# Bypass payload: ﹤svg onload="alert('XSS')"﹥
63+
# U+FE64 U+0073 U+0076 U+0067 U+0020 U+006F U+006E U+006C U+006F U+0061 U+0064 U+003D U+FF02 U+0061 U+006C U+0065 U+0072 U+0074 U+0028 U+FF07 U+0058 U+0053 U+0053 U+FF07 U+0029 U+FF02 U+FE65
64+
# NFKC: <svg onload="alert('XSS')">
65+
# U+003C U+0073 U+0076 U+0067 U+0020 U+006F U+006E U+006C U+006F U+0061 U+0064 U+003D U+0022 U+0061 U+006C U+0065 U+0072 U+0074 U+0028 U+0027 U+0058 U+0053 U+0053 U+0027 U+0029 U+0022 U+003E
66+
# NFKD: <svg onload="alert('XSS')">
67+
# U+003C U+0073 U+0076 U+0067 U+0020 U+006F U+006E U+006C U+006F U+0061 U+0064 U+003D U+0022 U+0061 U+006C U+0065 U+0072 U+0074 U+0028 U+0027 U+0058 U+0053 U+0053 U+0027 U+0029 U+0022 U+003E
68+
#
69+
# ➜ echo -n "<svg onload=\"alert('XSS')\">" | unisec normalize replace -
70+
# ```
71+
class Replace < Dry::CLI::Command
72+
desc 'Prepare a XSS payload for HTML escape bypass (HTML escape followed by NFKC / NFKD normalization)'
73+
74+
argument :input, required: true,
75+
desc: 'String input. Read from STDIN if equal to -.'
76+
77+
# Prepare a XSS payload for HTML escape bypass (HTML escape followed by NFKC / NFKD normalization)
78+
# @param input [String] Input string to normalize
79+
def call(input: nil, **_options)
80+
input = $stdin.read.chomp if input == '-'
81+
puts Unisec::Normalization.new(input).display_replace
5082
end
5183
end
5284
end

lib/unisec/normalization.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
module Unisec
66
# Normalization Forms
77
class Normalization
8+
# HTML escapable characters mapped with their Unicode counterparts that will
9+
# cast to themself after applying normalization forms using compatibility mode.
10+
HTML_ESCAPE_BYPASS = {
11+
'<' => ['﹤', '<'],
12+
'>' => ['﹥', '>'],
13+
'"' => ['"'],
14+
"'" => ['''],
15+
'&' => ['﹠', '&']
16+
}.freeze
17+
818
# Original input
919
# @return [String] untouched input
1020
attr_reader :original
@@ -64,6 +74,25 @@ def self.nfkd(str)
6474
str.unicode_normalize(:nfkd)
6575
end
6676

77+
# Replace HTML escapable characters with their Unicode counterparts that will
78+
# cast to themself after applying normalization forms using compatibility mode.
79+
# Usefull for XSS, to bypass HTML escape.
80+
# If several values are possible, one is picked randomly.
81+
# @param str [String] the target string
82+
# @return [String] escaped input
83+
def self.replace_bypass(str)
84+
str = str.dup
85+
HTML_ESCAPE_BYPASS.each do |k, v|
86+
str.gsub!(k, v.sample)
87+
end
88+
str
89+
end
90+
91+
# Instance version of {Normalization.replace_bypass}.
92+
def replace_bypass
93+
Normalization.replace_bypass(@original)
94+
end
95+
6796
# Display a CLI-friendly output summurizing all normalization forms
6897
# @return [String] CLI-ready output
6998
# @example
@@ -90,5 +119,19 @@ def display
90119
colorize.call('NFD', @nfd) +
91120
colorize.call('NFKD', @nfkd)
92121
end
122+
123+
# Display a CLI-friendly output of the XSS payload to bypass HTML escape and
124+
# what it does once normalized in NFKC & NFKD.
125+
def display_replace
126+
colorize = lambda { |form_title, form_attr|
127+
"#{Paint[form_title.to_s, :underline,
128+
:bold]}: #{form_attr}\n #{Paint[Unisec::Properties.chars2codepoints(form_attr), :red]}\n"
129+
}
130+
payload = replace_bypass
131+
colorize.call('Original', @original) +
132+
colorize.call('Bypass payload', payload) +
133+
colorize.call('NFKC', Normalization.nfkc(payload)) +
134+
colorize.call('NFKD', Normalization.nfkd(payload))
135+
end
93136
end
94137
end

lib/unisec/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
module Unisec
44
# Version of unisec library and app
5-
VERSION = '0.0.5'
5+
VERSION = '0.0.6'
66
end

test/test_normalization.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ def test_unisec_normalization
1010
assert_equal("\u{017F 0323 0307}", Unisec::Normalization.nfd("\u{1E9B 0323}"))
1111
assert_equal("\u{0073 0323 0307}", Unisec::Normalization.nfkd("\u{1E9B 0323}"))
1212
assert_equal("\u{2126}", Unisec::Normalization.new("\u{2126}").original)
13+
14+
payload = "<svg onload=\"alert('XSS')\">"
15+
assert_equal(payload, Unisec::Normalization.replace_bypass(payload).unicode_normalize(:nfkc))
1316
end
1417
end

0 commit comments

Comments
 (0)