Passman
5 minutos de lectura
Se nos proporciona un sitio web como este:

También tenemos el código fuente en Node.js.
Análisis de código fuente
La aplicación web está construida con Express JS. Un archivo relevante es 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;
};
Es muy interesante que se use GraphQL para administrar la interacción con la base de datos.
Análisis de GraphQL
El archivo que implementa la interfaz de GraphQL es 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
});
Podemos ver que hay tres tipos (datos que se enviarán/devolverán a/desde GraphQL):
PhrasesResponseQuery
Y cuatro mutaciones (funciones que pueden modificar el estado de la base de datos):
RegisterUserLoginUserUpdatePasswordAddPhrase
El que es vulnerable es UpdatePassword, porque solo requiere el nombre de usuario y la nueva contraseña. Por lo tanto, cualquier usuario puede modificar la contraseña de cualquier usuario, solo conociendo su nombre de usuario. Esta vulnerabilidad se conoce como IDOR (Insecure Direct Object Reference), y es muy común en las API que se consumen con JavaScript y AJAX.
Explotación de IDOR
Veamos qué usuario será la víctima de la modificación de la cuenta. Si inspeccionamos entrypoint.sh, hay un usuario llamado admin que tendrá la flag como una contraseña guardada en la base de datos (tabla password.saved_passwords):
#!/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
Entonces, necesitamos convertirnos en admin.
Realización de consultas GraphQL
Recordemos que necesitamos usar una mutación GraphQL para cambiar la contraseña de admin. En primer lugar, registraremos a un usuario válido para capturar la petición con Burp Suite:


Con esto, ya conocemos la sintaxis de una mutación. Podemos echar un vistazo a LoginUser también al iniciar sesión:

Ahora estamos conectados:

Enviaremos esta petición a la pestaña Repeater y la modificaremos para realizar la mutación UpdatePassword. También necesitaremos establecer la cookie de sesión en la petición:

Y ahora podemos iniciar sesión como admin usando asdf como contraseña:

Flag
Y aquí tenemos la flag:
HTB{1d0r5_4r3_s1mpl3_4nd_1mp4ctful!!}
