LoginSignup
1
1

Pythonはマルチバイト文字で変数/関数を宣言できるし、全角半角を区別しない。

Posted at

TL;DR

  • Pythonは変数・関数を全角文字やその他さまざまな文字で宣言できる
  • Pythonは変数や関数名を正規化してから解釈する e.g. foo == foo # True
  • evalなどでユーザの入力に従いPythonのコードが実行可能な場合で、入力値の制限が十分でない場合と悪用が成立するかもしれない

きっかけ

Pearl CTFというCTFに "b4by_jail" という問題がありました。問題はPythonのソースファイルが添付されていて、netcatでつないでやり取りするという形式でした。
問題のコードは以下のようなものです。

問題のコード(一部抜粋)
#!/usr/local/bin/python
flag="pearl{f4k3_fl4g}"
blacklist=list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`![]{},<>/123456789")

def check_blocklist(string):
    for i in string:
        if i in blacklist:
            return(0)
    return(1)
def main():
    cmd=input(">>> ")
    if(check_blocklist(cmd)):
        try:
            print(eval(cmd))
        except:
            print("Sorry no valid output to show.")
    else:
        print("Your sentence has been increased by 2 years for attempted escape.")

main()

入力として"flag"という文字列を与えればeval(cmd)が評価されてflag変数を参照できそうですが、[a-zA-Z~![]{},<>/1-9]`が含まれる場合は拒否されてしまうようです。

最初はblacklistに含まれない[0()%+-]などをうまく使って jsfuck のようなことをできないかなと調べて pyjsfuck というプロジェクトのREADME.mdを読み込んでいたのですが、どうもこの文字セットだけで文字列を表現するのは難しそうでした。

しばらく悩んでいるとふと、asciiの印刷可能文字に限定して考える必要ないのではないかと思いました。そう言えば Python って変数名にascii以外も使えた気がするなと思い、Pythonインタプリタを開き全角文字を使い色々試していると、以下のような動作が確認できました。

Pythonインタプリタ
>>> b= 'b'
>>> b
'b'
>>> print('hi')
hi

半角のbで宣言した変数bを全角の"b" で参照できています。また、全角文字で関数 print()を呼び出せています。

ならばとこの問題に全角で"flag"と入力し、フラグを取得することができました。

検証

どの文字が使えるかということに関しては既存の記事があったのでそれを参照することをおすすめします。

全角文字以外にも同一の文字として解釈される文字がありそうだなということで総当りで試すコードを書きました。

検証用のコード
import unicodedata
import csv

def check_normalization_equal(c1, c2):
    define = f"a{c1} = 'a'"
    check = f"a{c2}"
    result = False
    try:
        exec(define)
        result = (eval(check) == 'a')
    except:
        pass
    return result

def main():
    
    utf8 = []
    for i in range(0, 0x10FFFF):
        try:
            utf8 += [chr(i)]
        except:
            pass
    alphameric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"

    f = open("./python-bypass-ascii.csv", 'w')
    writer = csv.writer(f)
    writer.writerow(['ASCII', 'UTF-8', 'Codepoint','Unicode Name'])
    for an in alphameric:
        for u8 in utf8:
            if check_normalization_equal(an, u8):
                print(an, u8, unicodedata.name(u8))
                writer.writerow([an, u8, f'U+{ord(u8):02X}', unicodedata.name(u8)])
    f.close()


if __name__ == '__main__':
    main()

main()check_normalization_equal()では以下のような処理をしています。

  • [a-zA-Z0-9_]とUTF-8の全文字の組み合わせを列挙する
  • exec()aa, a0, a_ などの変数をそれぞれ宣言
  • eval('aª'), eval('a0'), eval(a﹏)などを評価
  • 前者と後者の値が一致すれば出力を行う

a0のような形で2文字目になっているのは、2文字目以降しか使えない文字 [0-9] が存在するからです。

結果

結果は以下のようになりました。以下のような文字が同一のものと解釈されるようです。

ASCII 対応するUnicodeの文字名(一部省略)
A-Za-z0-9_ FULLWIDTH Aa0_
A-Za-z0-9 MATHEMATICAL BOLD 𝐀𝐚𝟎
A-Za-z0-9 MATHEMATICAL DOUBLE-STRUCK 𝕒𝔸𝟘
A-Za-z0-9 MATHEMATICAL SANS-SERIF 𝖠𝖺𝟢
A-Za-z0-9 MATHEMATICAL SANS-SERIF BOLD 𝗮𝗔𝟬
A-Za-z0-9 MATHEMATICAL MONOSPACE 𝚊𝙰𝟶
A-Za-z MATHEMATICAL ITALIC 𝑎𝐴
A-Za-z MATHEMATICAL BLOD ITALIC 𝒂𝑨
A-Za-z MATHEMATICAL SCRIPT 𝒶𝒜
A-Za-z MATHEMATICAL BOLD SCRIPT 𝓪𝓐
A-Za-z MATHEMATICAL FRAKTUR 𝔞𝔄
A-Za-z MATHEMATICAL SANS-SERIF ITALIC 𝘢𝘈
A-Za-z MATHEMATICAL SANS-SERIF BOLD ITALIC 𝙖𝘼
A-RT-Wa-rt-z MODIFIER LETTER ᵃᴬ
aehijklmnoprtuvx SUBSCRIPT SMALL ₐₑₕᵢⱼₖₗₘₙₒₚᵣₜᵤᵥₓ
cdilmvxCDILMVX ROMAN NUMERIC ⅽⅾⅰⅼⅿⅴⅹⅭⅮⅠⅬⅯⅤⅩ
BEFHILMReglo SCRIPT CAPITAL/SMALL ℬℰℱℋℐℒℳℛℯℊℓℴ
deijCDHNPQRZ DOUBLE-STRUCK CAPITAL ⅆⅇⅈⅉℂⅅℍℕℙℚℝℤ
0-9 SEGMENTED DIGIT 🯰🯱🯲🯳🯴🯵🯶🯷🯸🯹
CHIRZ BLACL-LETTER ℭℌℑℜℨ
in SUPERSCRIPT ⁱⁿ
ao FEMININE/MASCULINE ORDINAL INDICATOR ªº
i INFORMATION SOURCE
K KELVIN SIGN
h PLANCK CONSTANT
_ PRESENTATION FORM FOR VERTICAL LOW LINE
_ PRESENTATION FORM FOR VERTICAL WAVY LOW LINE
_ DASHED LOW LINE
_ CENTRELINE LOW LINE
_ WAVY LOW LINE

数学関連のものが多く、他にも7セグメント数字や一部の特殊な記号などが含まれています。アンダーバー"_"と縦棒"︳"が同一の文字として解釈されるのは面白いですね。

これがどのような手順で正規化されたかは私の知識だとよくわからないです。

unicodedata.normalize() で使用可能な方法で正規化してみましたが、結果に丸数字①が含まれるなど、同じ結果にはなりませんでした。

まとめ

PythonはUnicode文字を変数名として命名すると、正規化してから解釈されるようです。この正規化によって変数・関数に命名可能な文字[a-zA-Z0-9_]と同等と解釈されるものには、全角文字のほか数学関連の文字や一部の特殊な文字などが含まれているようでした。

eval()などが使われていると、一見安全にフィルタしている様に見えてもこのような工夫によってバイパスできてしまうことがあります。evalのような関数はどの言語でも基本使わないようにしましょう。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1