浅析Python中的“斜体字符”绕过

什么是“斜体字符”?

  简单来说,在 Unicode 标准中,有一组专门用于数学排版的字符,例如:

  • 普通字母:a, b, c
  • 数学斜体小写字母:𝑎, 𝑏, 𝑐(U+1D44E–U+1D467)
  • 数学粗体、斜体、花体等变体也都有独立编码

  这些字符看起来是斜体,但它们是独立的 Unicode 码点,不是通过格式化得到的。

  本文所说的“斜体字”是泛指这一类特殊变体字符,而不单单只是斜的那几个,但为了方便叙述,以下都统一叫做斜体字吧。

  斜体字绕过,本质上是一种特殊的unicode编码绕过技巧。

  ‍

“斜体字符”为什么可以被利用?

  这本质上是统一码兼容性的特性,它确保在可能具有不同视觉外观或行为的字符或字符序列之间,表示相同的抽象字符。具体的表现,就是将一些斜体字符转化为普通的常见字符,例如上文中𝓼​实际代替了s​,这是一种规范化处理。

  ‍

  可参照文档:https://www.unicode.org/reports/tr15/

  这种规范化形式有四种:

形式 描述 举例
Normalization Form Canonical Decomposition(NFD) 分解规范形式 é → e + ◌́
Normalization Form Canonical Composition(NFC) 合成规范形式 e + ◌́ → é
Normalization Form Compatibility Decomposition(NFKD) 兼容分解形式 ffi → f + f + i
Normalization Form Compatibility Composition(NFKC) 兼容合成形式 fi → f + i

  简单解释一下这四种规范化形式:

NFD

  • 将所有可分解的字符按规范等价分解为基本字符 + 组合标记
  • 不重新排序组合标记(保持原始顺序,但符合规范组合类规则)

  比如说:“é”(U+00E9) → “e”(U+0065) + “◌́”(U+0301)

  就是把一个复杂字符拆分成多个简单字符,是一种完全分解

  ‍

NFC

  • 先执行 NFD(完全分解),然后按规范等价尽可能合成为预组合字符
  • 是 Web 和操作系统中最常用的规范化形式
  • 使用最少的码位构成等价的字符串

  比如说:“e” + “◌́” → “é”(如果存在对应的预组合字符)

  ‍

NFKD

  • 不仅进行规范分解,还进行兼容性分解(compatibility decomposition)。
  • 会将“看起来不同但语义相近”的字符也展开(如全角/半角、上标数字、连字等)。

  比如说:

  • “ffi”(U+FB03,连字) → “f” + “f” + “i”
  • “①”(U+2460,带圈数字) → “1”
  • “A”(全角 A, U+FF21) → “A”(半角 A, U+0041)

  ‍

NFKC

  • 先执行 NFKD(兼容分解),然后像 NFC 一样尽可能合成。
  • 结果是兼容等价下的最紧凑形式。

  比如说:

  • “①” → “1”(无法再合成)
  • “fi”(U+FB01) → “f” + “i” → 若有预组合则合成(但“fi”无预组合字符,所以保持分开)

  ‍

  总结来说,KFD和KFC都是规范等价的,视觉和语义完全相同;而NFKD和NFKC是兼容等价的,可能改变语义或外观。

  ‍

  这是python3当中对于Unicode编码的说明:https://docs.python.org/zh-cn/3/howto/unicode.html

  通过以下代码我们可以进行测试:

1
2
3
4
5
6
import unicodedata
payload = "ʰᵉ𝓵𝓵ℴ︐ʷℴ𝘳𝓵𝑑"
print ('NFD: ' + unicodedata.normalize('NFD', payload))
print ('NFC: ' + unicodedata.normalize('NFC', payload))
print ('NFKD: ' + unicodedata.normalize('NFKD', payload))
print ('NFKC: ' + unicodedata.normalize('NFKC', payload))

image

  可以看到KFC和KFD保持了原有信息,而NFKC和NFKD则是被转成了简单字符的形式。

  在这种情况下,如果WAF仅仅只是进行了关键词匹配的过滤,就存在被绕过的可能,有点像是一种弱类型比较。

  ‍

为什么需要有Unicode规范化?

​ 因为同一个字符在 Unicode 中可能有多种不同的编码表示(例如 “é” 可以是一个预组字符,也可以是 “e” 加一个重音符号),虽然显示效果一样,但程序会认为它们不相等。
Unicode 规范化就是把不同表示统一成一种标准形式(如 NFC),确保:

  • 字符串比较、查找、去重等操作结果正确;

  • 不同来源的文本能一致处理。

    简言之:让“看起来一样”的文本,在程序里也“真的相等”。

在Python中怎么利用?

  有以下这样的简单测试:

1
2
3
4
5
6
7
8
import unicodedata
payload = "prinᵗ(1)"
print ('NFD: ' + unicodedata.normalize('NFD', payload))
print ('NFC: ' + unicodedata.normalize('NFC', payload))
print ('NFKD: ' + unicodedata.normalize('NFKD', payload))
print ('NFKC: ' + unicodedata.normalize('NFKC', payload))

exec(payload)

image

  ​​就是t​的一种变体字符,它可以被解析当成t​使用,

1
2
3
4
5
6
7
8
import unicodedata
payload = "prinᵗ('ʰᵉ𝓵𝓵ℴ︐ʷℴ𝘳𝓵𝑑')"
print ('NFD: ' + unicodedata.normalize('NFD', payload))
print ('NFC: ' + unicodedata.normalize('NFC', payload))
print ('NFKD: ' + unicodedata.normalize('NFKD', payload))
print ('NFKC: ' + unicodedata.normalize('NFKC', payload))

exec(payload)

image

  而字符串类型数据在实际表现上则采用NFD/NFC​进行处理,内容没有被改变。

  ‍

  具体解释来说,在python3当中,我要执行一个函数,python3会按照LEGB​规则在各作用域找到对应的函数名去进行调用,也就是通过名字来找函数对象。而在python3当中,我们可以认为,设置的函数名在实际表现上就相当于经过了NFKC/NFKD​处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A():
def tesᵗ():
pass

class B():
def test():
pass

methods = [name for name in dir(A) if not name.startswith('__')]
print("[A]Actual method names:", methods)

for name in methods:
print(f"'{name}' -> {[hex(ord(c)) for c in name]}")

methods = [name for name in dir(B) if not name.startswith('__')]
print("[B]Actual method names:", methods)

for name in methods:
print(f"'{name}' -> {[hex(ord(c)) for c in name]}")

image

  结果是一样的,在python看来,函数名tesᵗ​和test​表示的就是一个意思,因为他们经过了NFKC/NFKD​处理后就是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A():
def test():
print(1)
def tesᵗ():
print(2)

methods = [name for name in dir(A) if not name.startswith('__')]
print("[A]Actual method names:", methods)

for name in methods:
print(f"'{name}' -> {[hex(ord(c)) for c in name]}")

A.test()
A.tesᵗ()

  的运行结果是:

image

  也足以说明这一点。需要额外补充的是,这种字符替换并不适用于关键词,比如class​,def​,标识语法的特殊符号等。

  斜体字可通过这几个网站进行获取:

  ‍

  此外,利用NFKD/NFKC​不仅可以进行字符替换,在某些时候,还可以绕过某些字符串长度限制,比如以下这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import unicodedata

def task_VIII():
print("OK")

payload = "task_Ⅷ()"
print(payload)
print(len(payload))

print ('NFD: ' + unicodedata.normalize('NFD', payload))
print ('NFC: ' + unicodedata.normalize('NFC', payload))
print ('NFKD: ' + unicodedata.normalize('NFKD', payload))
print ('NFKC: ' + unicodedata.normalize('NFKC', payload))
print(len(unicodedata.normalize('NFKC', payload)))

if len(payload) > 8:
print("fail")
else:
exec(payload)

image

  可以看到是减少了3个字符的,但是由于这种字符组合词比较少,所有这种利用方式并没有多少实战价值,算是提供一种新思路吧。

  通过以下代码,简单遍历了一下可用的连体字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import string
import unicodedata

characters = list(string.ascii_letters + string.digits)

def valid_unicode_points():
for code in range(0x110000): # 0x110000 = 1,114,112
if 0xD800 <= code <= 0xDFFF:
continue # 跳过 UTF-16 代理区(非法字符)
yield code

replace_dict = {}
for cp in valid_unicode_points():
ori_char = chr(cp)
check_char = unicodedata.normalize('NFKC', ori_char)
if len(check_char) < 2:
continue

valid = True
for ch in check_char:
if ch not in characters:
valid = False
break
if not valid:
continue

replace_dict[check_char] = ori_char

print(replace_dict)
1
{'IJ': 'IJ', 'ij': 'ij', 'LJ': 'LJ', 'Lj': 'Lj', 'lj': 'lj', 'NJ': 'NJ', 'Nj': 'Nj', 'nj': 'nj', 'DZ': 'DZ', 'Dz': 'Dz', 'dz': 'dz', 'Rs': '₨', 'No': '№', 'SM': '℠', 'TEL': '℡', 'TM': '™', 'FAX': '℻', 'II': 'Ⅱ', 'III': 'Ⅲ', 'IV': 'Ⅳ', 'VI': 'Ⅵ', 'VII': 'Ⅶ', 'VIII': 'Ⅷ', 'IX': 'Ⅸ', 'XI': 'Ⅺ', 'XII': 'Ⅻ', 'ii': 'ⅱ', 'iii': 'ⅲ', 'iv': 'ⅳ', 'vi': 'ⅵ', 'vii': 'ⅶ', 'viii': 'ⅷ', 'ix': 'ⅸ', 'xi': 'ⅺ', 'xii': 'ⅻ', '10': '⑩', '11': '⑪', '12': '⑫', '13': '⑬', '14': '⑭', '15': '⑮', '16': '⑯', '17': '⑰', '18': '⑱', '19': '⑲', '20': '⑳', 'PTE': '㉐', '21': '㉑', '22': '㉒', '23': '㉓', '24': '㉔', '25': '㉕', '26': '㉖', '27': '㉗', '28': '㉘', '29': '㉙', '30': '㉚', '31': '㉛', '32': '㉜', '33': '㉝', '34': '㉞', '35': '㉟', '36': '㊱', '37': '㊲', '38': '㊳', '39': '㊴', '40': '㊵', '41': '㊶', '42': '㊷', '43': '㊸', '44': '㊹', '45': '㊺', '46': '㊻', '47': '㊼', '48': '㊽', '49': '㊾', '50': '㊿', 'Hg': '㋌', 'erg': '㋍', 'eV': '㋎', 'LTD': '㋏', 'hPa': '㍱', 'da': '㍲', 'AU': '㍳', 'bar': '㍴', 'oV': '㍵', 'pc': '㍶', 'dm': '㍷', 'dm2': '㍸', 'dm3': '㍹', 'IU': '㍺', 'pA': '㎀', 'nA': '㎁', 'mA': '㎃', 'kA': '㎄', 'KB': '㎅', 'MB': '㎆', 'GB': '㎇', 'cal': '㎈', 'kcal': '㎉', 'pF': '㎊', 'nF': '㎋', 'mg': '㎎', 'kg': '㎏', 'Hz': '㎐', 'kHz': '㎑', 'MHz': '㎒', 'GHz': '㎓', 'THz': '㎔', 'ml': '㎖', 'dl': '㎗', 'kl': '㎘', 'fm': '㎙', 'nm': '㎚', 'mm': '㎜', 'cm': '㎝', 'km': '㎞', 'mm2': '㎟', 'cm2': '㎠', 'm2': '㎡', 'km2': '㎢', 'mm3': '㎣', 'cm3': '㎤', 'm3': '㎥', 'km3': '㎦', 'Pa': '㎩', 'kPa': '㎪', 'MPa': '㎫', 'GPa': '㎬', 'rad': '㎭', 'ps': '㎰', 'ns': '㎱', 'ms': '㎳', 'pV': '㎴', 'nV': '㎵', 'mV': '㎷', 'kV': '㎸', 'MV': '🅋', 'pW': '㎺', 'nW': '㎻', 'mW': '㎽', 'kW': '㎾', 'MW': '㎿', 'Bq': '㏃', 'cc': '㏄', 'cd': '㏅', 'dB': '㏈', 'Gy': '㏉', 'ha': '㏊', 'HP': '㏋', 'in': '㏌', 'KK': '㏍', 'KM': '㏎', 'kt': '㏏', 'lm': '㏐', 'ln': '㏑', 'log': '㏒', 'lx': '㏓', 'mb': '㏔', 'mil': '㏕', 'mol': '㏖', 'PH': '㏗', 'PPM': '㏙', 'PR': '㏚', 'sr': '㏛', 'Sv': '㏜', 'Wb': '㏝', 'gal': '㏿', 'ff': 'ff', 'fi': 'fi', 'fl': 'fl', 'ffi': 'ffi', 'ffl': 'ffl', 'st': 'st', 'CD': '🄭', 'WZ': '🄮', 'HV': '🅊', 'SD': '🅌', 'SS': '🅍', 'PPV': '🅎', 'WC': '🅏', 'MC': '🅪', 'MD': '🅫', 'MR': '🅬', 'DJ': '🆐'}

  在不同环境下(例如web框架flask),可能对于这些变体字符有提前处理,所以可替换字符不一定通用。

  ‍

exec() 执行环境

  在exec()​默认执行环境下,一些已测可用的字符替换:

原始字符 变体字符
0 0,𝟎,𝟘,𝟢,𝟬,𝟶,🯰
1 1,𝟏,𝟙,𝟣,𝟭,𝟷,🯱
2 2,𝟐,𝟚,𝟤,𝟮,𝟸,🯲
3 3,𝟑,𝟛,𝟥,𝟯,𝟹,🯳
4 4,𝟒,𝟜,𝟦,𝟰,𝟺,🯴
5 5,𝟓,𝟝,𝟧,𝟱,𝟻,🯵
6 6,𝟔,𝟞,𝟨,𝟲,𝟼,🯶
7 7,𝟕,𝟟,𝟩,𝟳,𝟽,🯷
8 8,𝟖,𝟠,𝟪,𝟴,𝟾,🯸
9 9,𝟗,𝟡,𝟫,𝟵,𝟿,🯹
A ᴬ,A,𝐀,𝐴,𝑨,𝒜,𝓐,𝔄,𝔸,𝕬,𝖠,𝗔,𝘈,𝘼,𝙰
B ᴮ,ℬ,B,𝐁,𝐵,𝑩,𝓑,𝔅,𝔹,𝕭,𝖡,𝗕,𝘉,𝘽,𝙱
C ℂ,ℭ,Ⅽ,C,𝐂,𝐶,𝑪,𝒞,𝓒,𝕮,𝖢,𝗖,𝘊,𝘾,𝙲
D ᴰ,ⅅ,Ⅾ,D,𝐃,𝐷,𝑫,𝒟,𝓓,𝔇,𝔻,𝕯,𝖣,𝗗,𝘋,𝘿,𝙳
E ᴱ,ℰ,E,𝐄,𝐸,𝑬,𝓔,𝔈,𝔼,𝕰,𝖤,𝗘,𝘌,𝙀,𝙴
F ℱ,F,𝐅,𝐹,𝑭,𝓕,𝔉,𝔽,𝕱,𝖥,𝗙,𝘍,𝙁,𝙵
G ᴳ,G,𝐆,𝐺,𝑮,𝒢,𝓖,𝔊,𝔾,𝕲,𝖦,𝗚,𝘎,𝙂,𝙶
H ᴴ,ℋ,ℌ,ℍ,H,𝐇,𝐻,𝑯,𝓗,𝕳,𝖧,𝗛,𝘏,𝙃,𝙷
I ᴵ,ℐ,ℑ,Ⅰ,I,𝐈,𝐼,𝑰,𝓘,𝕀,𝕴,𝖨,𝗜,𝘐,𝙄,𝙸
J ᴶ,J,𝐉,𝐽,𝑱,𝒥,𝓙,𝔍,𝕁,𝕵,𝖩,𝗝,𝘑,𝙅,𝙹
K ᴷ,K,K,𝐊,𝐾,𝑲,𝒦,𝓚,𝔎,𝕂,𝕶,𝖪,𝗞,𝘒,𝙆,𝙺
L ᴸ,ℒ,Ⅼ,L,𝐋,𝐿,𝑳,𝓛,𝔏,𝕃,𝕷,𝖫,𝗟,𝘓,𝙇,𝙻
M ᴹ,ℳ,Ⅿ,M,𝐌,𝑀,𝑴,𝓜,𝔐,𝕄,𝕸,𝖬,𝗠,𝘔,𝙈,𝙼
N ᴺ,ℕ,N,𝐍,𝑁,𝑵,𝒩,𝓝,𝔑,𝕹,𝖭,𝗡,𝘕,𝙉,𝙽
O ᴼ,O,𝐎,𝑂,𝑶,𝒪,𝓞,𝔒,𝕆,𝕺,𝖮,𝗢,𝘖,𝙊,𝙾
P ᴾ,ℙ,P,𝐏,𝑃,𝑷,𝒫,𝓟,𝔓,𝕻,𝖯,𝗣,𝘗,𝙋,𝙿
Q ℚ,Q,𝐐,𝑄,𝑸,𝒬,𝓠,𝔔,𝕼,𝖰,𝗤,𝘘,𝙌,𝚀
R ᴿ,ℛ,ℜ,ℝ,R,𝐑,𝑅,𝑹,𝓡,𝕽,𝖱,𝗥,𝘙,𝙍,𝚁
S S,𝐒,𝑆,𝑺,𝒮,𝓢,𝔖,𝕊,𝕾,𝖲,𝗦,𝘚,𝙎,𝚂
T ᵀ,T,𝐓,𝑇,𝑻,𝒯,𝓣,𝔗,𝕋,𝕿,𝖳,𝗧,𝘛,𝙏,𝚃
U ᵁ,U,𝐔,𝑈,𝑼,𝒰,𝓤,𝔘,𝕌,𝖀,𝖴,𝗨,𝘜,𝙐,𝚄
V Ⅴ,ⱽ,V,𝐕,𝑉,𝑽,𝒱,𝓥,𝔙,𝕍,𝖁,𝖵,𝗩,𝘝,𝙑,𝚅
W ᵂ,W,𝐖,𝑊,𝑾,𝒲,𝓦,𝔚,𝕎,𝖂,𝖶,𝗪,𝘞,𝙒,𝚆
X Ⅹ,X,𝐗,𝑋,𝑿,𝒳,𝓧,𝔛,𝕏,𝖃,𝖷,𝗫,𝘟,𝙓,𝚇
Y Y,𝐘,𝑌,𝒀,𝒴,𝓨,𝔜,𝕐,𝖄,𝖸,𝗬,𝘠,𝙔,𝚈
Z ℤ,ℨ,Z,𝐙,𝑍,𝒁,𝒵,𝓩,𝖅,𝖹,𝗭,𝘡,𝙕,𝚉
_ ︳,︴,﹍,﹎,﹏,_
a ª,ᵃ,ₐ,a,𝐚,𝑎,𝒂,𝒶,𝓪,𝔞,𝕒,𝖆,𝖺,𝗮,𝘢,𝙖,𝚊
b ᵇ,b,𝐛,𝑏,𝒃,𝒷,𝓫,𝔟,𝕓,𝖇,𝖻,𝗯,𝘣,𝙗,𝚋
c ᶜ,ⅽ,c,𝐜,𝑐,𝒄,𝒸,𝓬,𝔠,𝕔,𝖈,𝖼,𝗰,𝘤,𝙘,𝚌
d ᵈ,ⅆ,ⅾ,d,𝐝,𝑑,𝒅,𝒹,𝓭,𝔡,𝕕,𝖉,𝖽,𝗱,𝘥,𝙙,𝚍
e ᵉ,ₑ,ℯ,ⅇ,e,𝐞,𝑒,𝒆,𝓮,𝔢,𝕖,𝖊,𝖾,𝗲,𝘦,𝙚,𝚎
f ᶠ,f,𝐟,𝑓,𝒇,𝒻,𝓯,𝔣,𝕗,𝖋,𝖿,𝗳,𝘧,𝙛,𝚏
g ᵍ,ℊ,g,𝐠,𝑔,𝒈,𝓰,𝔤,𝕘,𝖌,𝗀,𝗴,𝘨,𝙜,𝚐
h ʰ,ₕ,ℎ,h,𝐡,𝒉,𝒽,𝓱,𝔥,𝕙,𝖍,𝗁,𝗵,𝘩,𝙝,𝚑
i ᵢ,ⁱ,ℹ,ⅈ,ⅰ,i,𝐢,𝑖,𝒊,𝒾,𝓲,𝔦,𝕚,𝖎,𝗂,𝗶,𝘪,𝙞,𝚒
j ʲ,ⅉ,ⱼ,j,𝐣,𝑗,𝒋,𝒿,𝓳,𝔧,𝕛,𝖏,𝗃,𝗷,𝘫,𝙟,𝚓
k ᵏ,ₖ,k,𝐤,𝑘,𝒌,𝓀,𝓴,𝔨,𝕜,𝖐,𝗄,𝗸,𝘬,𝙠,𝚔
l ˡ,ₗ,ℓ,ⅼ,l,𝐥,𝑙,𝒍,𝓁,𝓵,𝔩,𝕝,𝖑,𝗅,𝗹,𝘭,𝙡,𝚕
m ᵐ,ₘ,ⅿ,m,𝐦,𝑚,𝒎,𝓂,𝓶,𝔪,𝕞,𝖒,𝗆,𝗺,𝘮,𝙢,𝚖
n ⁿ,ₙ,n,𝐧,𝑛,𝒏,𝓃,𝓷,𝔫,𝕟,𝖓,𝗇,𝗻,𝘯,𝙣,𝚗
o º,ᵒ,ₒ,ℴ,o,𝐨,𝑜,𝒐,𝓸,𝔬,𝕠,𝖔,𝗈,𝗼,𝘰,𝙤,𝚘
p ᵖ,ₚ,p,𝐩,𝑝,𝒑,𝓅,𝓹,𝔭,𝕡,𝖕,𝗉,𝗽,𝘱,𝙥,𝚙
q q,𝐪,𝑞,𝒒,𝓆,𝓺,𝔮,𝕢,𝖖,𝗊,𝗾,𝘲,𝙦,𝚚
r ʳ,ᵣ,r,𝐫,𝑟,𝒓,𝓇,𝓻,𝔯,𝕣,𝖗,𝗋,𝗿,𝘳,𝙧,𝚛
s ſ,ˢ,ₛ,s,𝐬,𝑠,𝒔,𝓈,𝓼,𝔰,𝕤,𝖘,𝗌,𝘀,𝘴,𝙨,𝚜
t ᵗ,ₜ,t,𝐭,𝑡,𝒕,𝓉,𝓽,𝔱,𝕥,𝖙,𝗍,𝘁,𝘵,𝙩,𝚝
u ᵘ,ᵤ,u,𝐮,𝑢,𝒖,𝓊,𝓾,𝔲,𝕦,𝖚,𝗎,𝘂,𝘶,𝙪,𝚞
v ᵛ,ᵥ,ⅴ,v,𝐯,𝑣,𝒗,𝓋,𝓿,𝔳,𝕧,𝖛,𝗏,𝘃,𝘷,𝙫,𝚟
w ʷ,w,𝐰,𝑤,𝒘,𝓌,𝔀,𝔴,𝕨,𝖜,𝗐,𝘄,𝘸,𝙬,𝚠
x ˣ,ₓ,ⅹ,x,𝐱,𝑥,𝒙,𝓍,𝔁,𝔵,𝕩,𝖝,𝗑,𝘅,𝘹,𝙭,𝚡
y ʸ,y,𝐲,𝑦,𝒚,𝓎,𝔂,𝔶,𝕪,𝖞,𝗒,𝘆,𝘺,𝙮,𝚢
z ᶻ,z,𝐳,𝑧,𝒛,𝓏,𝔃,𝔷,𝕫,𝖟,𝗓,𝘇,𝘻,𝙯,𝚣

  ‍

bottle框架(前端传参时):

  由于URL编码问题,这些变体字符往往是几个url编码串联在一起,但是bottle在处理时是单个url编码来看的,因此可用的变体字符极大受限,详情参考LamentXu的《聊聊bottle框架中由斜体字引发的模板注入(SSTI)waf bypass》:https://www.cnblogs.com/LAMENTXU/articles/18805019

原始字符 变体字符
a %aa
o %ba

  ‍

json.loads

  ​json.loads​会解析unicode格式的数据,如果在json.loads​解析前进行waf​检测,可使用unicode​绕过,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import json

def waf(check_str):
if "os" in check_str:
return True
return False

a = '{"payload": "__import__(\'\\u006F\\u0073\').system(\'whoami\')"}'
print(a)

print(waf(a))

b = json.loads(a)
print(b)

image

  ‍

参考文章

  ‍