Google CTFには年齢の関係で参加できなかったので1人で問題は解いていたがDreamhackに参加し、4位でした。

Hello, go!
first blood!!

GoのWebサイトです。
Go
package main
import (
"html/template"
"net/http"
"bytes"
"fmt"
"strings"
"github.com/labstack/echo/v4"
)
func greetHandler(c echo.Context) error {
name := c.QueryParam("name")
if name == "" {
name = "go"
}
if strings.Contains(strings.ToLower(name),"flag"){
return c.String(http.StatusBadRequest, "flag is not allowed.")
}
t, err := template.New("page").Parse(
fmt.Sprintf(
<html>
<body>
<h1>Hello, %s!</h1>
</body>
</html>, name))
if err != nil {
return c.String(http.StatusInternalServerError, "Template parse error: "+err.Error())
}
buf := new(bytes.Buffer)
err = t.Execute(buf, c)
if err != nil {
return c.String(http.StatusInternalServerError, "Template execution error: "+err.Error())
}
return c.HTMLBlob(http.StatusOK, buf.Bytes())
}
func main() {
e := echo.New()
e.GET("/", greetHandler)
e.Start(":8000")
}
fmt.Sprintf
で動的に組み立てたテンプレート文字列をそのまま template.Parse
→ Execute
に流しているため、SSTIが存在する。
Execute(buf, c)
の際、テンプレート中のドット(.
)は echo.Context
インスタンス c
を指すため、.QueryParam
や .File
といった任意メソッドの呼び出しが可能となっている。
一応strings.Contains(strings.ToLower(name),"flag"){とflagという文字列を含むものをはじいているが、print (“../f” “lag”)のように動的に生成してやるとよい。
よって、nameに{{.File (print “../f” “lag”)}}を入れると、Execute(buf,c)実行時に.
が echo.Context
インスタンスを参照して、フラグが取得できる。
photographer
もうrevではchatGPTに勝てません!!!
Common things between us
Python
from Crypto.Util.number import getPrime, GCD
import os
import random
random.seed(os.urandom(10))
with open("flag", "rb") as f:
flag = f.read()
assert 1 < len(flag) < 128
plaintexts = [os.urandom(len(flag)) for _ in range(999)]
last_pt = list(flag)
for i in range(999):
for j in range(len(flag)):
last_pt[j] ^= plaintexts[i][j]
plaintexts.append(bytes(last_pt))
mods = [[] for _ in range(1000)]
select = list(range(1000))
e = 0x10001
for _ in range(1000):
while True:
prime = getPrime(256)
if GCD(e, prime - 1) == 1:
break
random.shuffle(select)
choices = select[:4]
for ch in choices:
mods[ch].append(prime)
if len(mods[ch]) == 4:
select.remove(ch)
mods = [a[0] * a[1] * a[2] * a[3] for a in mods]
for pt, mod in zip(plaintexts, mods):
pt = int.from_bytes(pt, "big")
ct = pow(pt, e, mod)
print(ct, mod)
1000個の平文すべてのXORを取ることでフラグを得ることができる。
各平文はそれぞれ異なる法でRSA暗号化されています。
各法は4つの素数の積で作られていますが、その素数は複数の法の間で共有されているのが脆弱性です。
よって、output.txtに出力されているmod同士をGCDして共通の因数を取り出します。
まれに、2つの法で複数の素因数が共通している場合があるのに注意が必要です。
各法の4つの素因数がわかったらRSAを複合して、p_iを得ることができます。
Python
from Crypto.Util.number import GCD
def solve():
with open("output.txt", "r") as f:
lines = f.readlines()
ct = [int(line.split()[0]) for line in lines]
n = [int(line.split()[1]) for line in lines]
#素因数分解
factors = [set() for _ in range(len(n))]
for i in range(len(n)):
for j in range(i + 1, len(n)):
g = GCD(n[i], n[j])
if g > 1:
factors[i].add(g)
factors[j].add(g)
for i in range(len(n)):
minimal_factors = {f1 for f1 in factors[i] if all(f1 == f2 or f1 % f2 != 0 for f2 in factors[i])}
if minimal_factors:
prod = 1
for p in minimal_factors: prod *= p
last_factor = n[i] // prod
if last_factor > 1: minimal_factors.add(last_factor)
factors[i] = minimal_factors
plaintexts = []
for i, f_set in enumerate(factors):
if len(f_set) == 4:
p1, p2, p3, p4 = list(f_set)
phi = (p1 - 1) * (p2 - 1) * (p3 - 1) * (p4 - 1)
try:
d = pow(0x10001, -1, phi)
pt = pow(ct[i], d, n[i])
plaintexts.append(pt)
except ValueError:
continue
byte_list = [p.to_bytes((p.bit_length() + 7) // 8, 'big') for p in plaintexts]
max_len = max(len(b) for b in byte_list if b)
flag = bytearray(max_len)
for pt_bytes in byte_list:
padded_pt = pt_bytes.rjust(max_len, b'\x00')
for i in range(max_len):
flag[i] ^= padded_pt[i]
print(f"{flag.decode()}")
solve()