Automne's Shadow.

RedPwnCTF 2019 blueprint WriteUp

2019/11/09 Share

Web

失踪人口回归,要补的东西太多了

不忘初心,牢记使命

JavaScript的原型链污染攻击已经火了挺久了,结合这道ctf题学习一下。

在JavaScript里万物皆对象(只有一种数据结构:对象),每个实例对象都有一个原型对象,而原型对象则引申出其对应的原型对象,经过一层层的链式调用,就构成了我们常说的"原型链" 。

实例对象可以通过 proto 访问其原型对象,如下图

automne

经过不断的调用,最终的原型对象会调用到null

automne

上图的调用链:

1
leon -> Leon.prototype -> object.prototype->null

OK,回到题目本身

提供了blueprint.js和package.json,使用

1
npm install

下载指定版本的依赖

审计blueprint.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
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
const crypto = require('crypto')
const http = require('http')
const mustache = require('mustache')
const getRawBody = require('raw-body')
const _ = require('lodash')

//这里我做了修改,require的时候一直在出错
//直接把flag以常量的方式定义出来
//const flag = require('./flag')
const flag = "flag{8lu3pr1nTs_aRe_tHe_hiGh3s1_quA11tY_pr0t()s}"
//console.log(flag)

const indexTemplate = `
<!doctype html>
<style>
body {
background: #172159;
}
* {
color: #fff;
}
</style>
<h1>your public blueprints!</h1>
<i>(in compliance with military-grade security, we only show the public ones. you must have the unique URL to access private blueprints.)</i>
<br>
{{#blueprints}}
{{#public}}
<div><br><a href="/blueprints/{{id}}">blueprint</a>: {{content}}<br></div>
{{/public}}
{{/blueprints}}
<br><a href="/make">make your own blueprint!</a>
`

const blueprintTemplate = `
<!doctype html>
<style>
body {
background: #172159;
color: #fff;
}
</style>
<h1>blueprint!</h1>
{{content}}
`

const notFoundPage = `
<!doctype html>
<style>
body {
background: #172159;
color: #fff;
}
</style>
<h1>404</h1>
`

const makePage = `
<!doctype html>
<style>
body {
background: #172159;
color: #fff;
}
</style>
<div>content:</div>
<textarea id="content"></textarea>
<br>
<span>public:</span>
<input type="checkbox" id="public">
<br><br>
<button id="submit">create blueprint!</button>
<script>
submit.addEventListener('click', () => {
fetch('/make', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
content: content.value,
public: public.checked,
})
}).then(res => res.text()).then(id => location='/blueprints/' + id)
})
</script>
`

// very janky, but it works
const parseUserId = (cookies) => {
if (cookies === undefined) {
return null
}
const userIdCookie = cookies.split('; ').find(cookie => cookie.startsWith('user_id='))
if (userIdCookie === undefined) {
return null
}
return decodeURIComponent(userIdCookie.replace('user_id=', ''))
}

const makeId = () => crypto.randomBytes(16).toString('hex')

// list of users and blueprints
const users = new Map()

http.createServer((req, res) => {
let userId = parseUserId(req.headers.cookie)
let user = users.get(userId)
if (userId === null || user === undefined) {
// create user if one doesnt exist
userId = makeId()
user = {
blueprints: {
[makeId()]: {
content: flag,
},
},
}
users.set(userId, user)
}

// send back the user id
res.writeHead(200, {
'set-cookie': 'user_id=' + encodeURIComponent(userId) + '; Path=/',
})

if (req.url === '/' && req.method === 'GET') {
// list all public blueprints
res.end(mustache.render(indexTemplate, {
blueprints: Object.entries(user.blueprints).map(([k, v]) => ({
id: k,
content: v.content,
public: v.public,
})),
}))
} else if (req.url.startsWith('/blueprints/') && req.method === 'GET') {
// show an individual blueprint, including private ones
const blueprintId = req.url.replace('/blueprints/', '')
if (user.blueprints[blueprintId] === undefined) {
res.end(notFoundPage)
return
}
res.end(mustache.render(blueprintTemplate, {
content: user.blueprints[blueprintId].content,
}))
} else if (req.url === '/make' && req.method === 'GET') {
// show the static blueprint creation page
res.end(makePage)
} else if (req.url === '/make' && req.method === 'POST') {
// API used by the creation page
getRawBody(req, {
limit: '1mb',
}, (err, body) => {
if (err) {
throw err
}
let parsedBody
try {
// default values are easier to do than proper input validation
parsedBody = _.defaultsDeep({
publiс: false, // default private
cоntent: '', // default no content
}, JSON.parse(body))
} catch (e) {
res.end('bad json')
return
}

// make the blueprint
const blueprintId = makeId()
user.blueprints[blueprintId] = {
content: parsedBody.content,
public: parsedBody.public,
}

res.end(blueprintId)
})
} else {
res.end(notFoundPage)
}
}).listen(80, () => {
console.log('listening on port 80')
})

接着分析核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 let userId = parseUserId(req.headers.cookie)
console.log(userId)//添加用于调试
let user = users.get(userId)
console.log(user)//添加用于调试
if (userId === null || user === undefined) {
// create user if one doesnt exist
userId = makeId()
console.log(userId)//添加用于调试
user = {
blueprints: {
[makeId()]: {
content: flag,
},
},
}
users.set(userId, user)
}

这一段,首先从cookie里取出当前随机生成的userId,然后当执行let user = users.get(userId)时,user的值是undefined的,所以进入到条件语句中,也就是在这里,重新设置了cookie的值,并且把flag生成到一个没有public属性的blueprints里。如下图

automne

重置完cookie后,再新建一个私密的blueprint,打印的日志如下图所示

automne

再看下面的核心代码

1
2
3
4
5
6
7
8
9
if (req.url === '/' && req.method === 'GET') {
// list all public blueprints
res.end(mustache.render(indexTemplate, {
blueprints: Object.entries(user.blueprints).map(([k, v]) => ({
id: k,
content: v.content,
public: v.public,
})),
}))

可以看到这里只列出公共属性的blueprint,所以flag看不到

最后看下这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if (req.url === '/make' && req.method === 'POST') {
// API used by the creation page
getRawBody(req, {
limit: '1mb',
}, (err, body) => {
if (err) {
throw err
}
let parsedBody
try {
// default values are easier to do than proper input validation
parsedBody = _.defaultsDeep({
publiс: false, // default private
cоntent: '', // default no content
}, JSON.parse(body))
} catch (e) {
res.end('bad json')
return
}

默认将public设置为false,而且这里的_.defaultsDeep是从lodash里引入的,根据package.json里的版本信息可知4.17.11是漏洞版本

automne

所以利用方式就有了,POST新建一个blueprint然后抓包

automne

添加payload:

"constructor": {"prototype": {"public": true}}

automne

最终回到首页刷新即可得到flag

automne

CATALOG