Passman
5 minutes to read
We are given a website like this:
We also have the source code in Node.js.
Source code analysis
The web application is built with Express JS. A relevant file is routes/index.js
:
const express = require('express');
const router = express.Router();
const { graphqlHTTP } = require('express-graphql');
const AuthMiddleware = require('../middleware/AuthMiddleware');
const GraphqlSchema = require('../helpers/GraphqlHelper');
router.get('/', (req, res) => {
return res.render('login.html');
});
router.get('/register', (req, res) => {
return res.render('register.html');
});
router.use('/graphql', AuthMiddleware, graphqlHTTP({
schema: GraphqlSchema,
graphiql: false
}));
router.get('/dashboard', AuthMiddleware, async (req, res, next) => {
return res.render('dashboard.html', {user: req.user});
});
router.get('/logout', (req, res) => {
res.clearCookie('session');
return res.redirect('/');
});
module.exports = () => {
return router;
};
It is very interesting that it uses GraphQL to manage the interaction with the database.
GraphQL analysis
The file that implements the GraphQL interface is helpers/GraphqlHelper.js
:
const JWTHelper = require('./JWTHelper');
const {
GraphQLObjectType,
GraphQLSchema,
GraphQLNonNull,
GraphQLID,
GraphQLString,
GraphQLList,
GraphQLError
} = require('graphql');
const response = data => ({ message: data });
const PhraseSchema = new GraphQLObjectType({
name: 'Phrases',
fields: {
id: { type: GraphQLID },
owner: { type: GraphQLString },
type: { type: GraphQLString },
address: { type: GraphQLString },
username: { type: GraphQLString },
password: { type: GraphQLString },
note: { type: GraphQLString }
}
});
const ResponseType = new GraphQLObjectType({
name: 'Response',
fields: {
message: { type: GraphQLString },
token: { type: GraphQLString }
}
});
const queryType = new GraphQLObjectType({
name: 'Query',
fields: {
getPhraseList: {
type: new GraphQLList(PhraseSchema),
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
if (!request.user) return reject(new GraphQLError('Authentication required!'));
db.getPhraseList(request.user.username)
.then(rows => resolve(rows))
.catch(err => reject(new GraphQLError(err)))
});
}
}
}
});
const mutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
RegisterUser: {
type: ResponseType,
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
db.registerUser(args.email, args.username, args.password)
.then(() => resolve(response("User registered successfully!")))
.catch(err => reject(new GraphQLError(err)));
});
}
},
LoginUser: {
type: ResponseType,
args: {
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
db.loginUser(args.username, args.password)
.then(async (user) => {
if (user.length) {
let token = await JWTHelper.sign( user[0] );
resolve({
message: "User logged in successfully!",
token: token
});
};
reject(new Error("Username or password is invalid!"));
})
.catch(err => reject(new GraphQLError(err)));
});
}
},
UpdatePassword: {
type: ResponseType,
args: {
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
if (!request.user) return reject(new GraphQLError('Authentication required!'));
db.updatePassword(args.username, args.password)
.then(() => resolve(response("Password updated successfully!")))
.catch(err => reject(new GraphQLError(err)));
});
}
},
AddPhrase: {
type: ResponseType,
args: {
recType: { type: new GraphQLNonNull(GraphQLString) },
recAddr: { type: new GraphQLNonNull(GraphQLString) },
recUser: { type: new GraphQLNonNull(GraphQLString) },
recPass: { type: new GraphQLNonNull(GraphQLString) },
recNote: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
if (!request.user) return reject(new GraphQLError('Authentication required!'));
db.addPhrase(request.user.username, args)
.then(() => resolve(response("Phrase added successfully!")))
.catch(err => reject(new GraphQLError(err)));
});
}
},
}
});
module.exports = new GraphQLSchema({
query: queryType,
mutation: mutationType
});
We can see that there are three types (data that will be sent/returned to/from GraphQL):
Phrases
Response
Query
And four mutations (functions that can modify the state of the database):
RegisterUser
LoginUser
UpdatePassword
AddPhrase
The one that is vulnerable is UpdatePassword
, because it only requires the username and the new password. Therefore, any user can modify any user’s password, just knowing their username. This vulnerability is known as IDOR (Insecure Direct Object Reference), and it is very common in APIs that are consumed by JavaScript using AJAX.
IDOR exploitation
Let’s see what user will be the victim for the account takeover. If we inspect entrypoint.sh
, there is a user named admin
that will have the flag as a saved password in the database (password.saved_passwords
table):
#!/bin/ash
# Secure entrypoint
chmod 600 /entrypoint.sh
# Initialize & Start MariaDB
mkdir -p /run/mysqld
chown -R mysql:mysql /run/mysqld
mysql_install_db --user=mysql --ldata=/var/lib/mysql
mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 &
# Wait for mysql to start
while ! mysqladmin ping -h'localhost' --silent; do echo "not up" && sleep .2; done
function genPass() {
echo -n $RANDOM | md5sum | head -c 32
}
mysql -u root << EOF
CREATE DATABASE passman;
CREATE TABLE passman.users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(256) UNIQUE NOT NULL,
password VARCHAR(256) NOT NULL,
email VARCHAR(256) UNIQUE NOT NULL,
is_admin INT NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
INSERT INTO passman.users (username, password, email, is_admin)
VALUES
('admin', '$(genPass)', 'admin@passman.htb', 1),
('louisbarnett', '$(genPass)', 'louis_p_barnett@mailinator.com', 0),
('ninaviola', '$(genPass)', 'ninaviola57331@mailinator.com', 0),
('alvinfisher', '$(genPass)', 'alvinfisher1979@mailinator.com', 0);
CREATE TABLE IF NOT EXISTS passman.saved_passwords (
id INT NOT NULL AUTO_INCREMENT,
owner VARCHAR(256) NOT NULL,
type VARCHAR(256) NOT NULL,
address VARCHAR(256) NOT NULL,
username VARCHAR(256) NOT NULL,
password VARCHAR(256) NOT NULL,
note VARCHAR(256) NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO passman.saved_passwords (owner, type, address, username, password, note)
VALUES
('admin', 'Web', 'igms.htb', 'admin', 'HTB{f4k3_fl4g_f0r_t3st1ng}', 'password'),
('louisbarnett', 'Web', 'spotify.com', 'louisbarnett', 'YMgC41@)pT+BV', 'student sub'),
('louisbarnett', 'Email', 'dmail.com', 'louisbarnett@dmail.com', 'L-~I6pOy42MYY#y', 'private mail'),
('ninaviola', 'Web', 'office365.com', 'ninaviola1', 'OfficeSpace##1', 'company email'),
('alvinfisher', 'App', 'Netflix', 'alvinfisher1979', 'efQKL2pJAWDM46L7', 'Family Netflix'),
('alvinfisher', 'Web', 'twitter.com', 'alvinfisher1979', '7wYz9pbbaH3S64LG', 'old twitter account');
GRANT ALL ON passman.* TO 'passman'@'%' IDENTIFIED BY 'passman' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EOF
/usr/bin/supervisord -c /etc/supervisord.conf
So, we need to become admin
.
Performing GraphQL requests
Remember that we need to use a GraphQL mutation to change admin
’s password. First of all, let’s register a valid user while capturing the request with Burp Suite:
With this, we know the syntax of a mutation. We can take a look at LoginUser
as well when logging in:
Now we are logged in:
We will send this request to the Repeater tab and modify it to perform the UpdatePassword
mutation. We will also need to set the session cookie in the request:
And now we can log in as admin
using asdf
as password:
Flag
And here we have the flag:
HTB{1d0r5_4r3_s1mpl3_4nd_1mp4ctful!!}