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):
Phrases
Response
Query
Y cuatro mutaciones (funciones que pueden modificar el estado de la base de datos):
RegisterUser
LoginUser
UpdatePassword
AddPhrase
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 APIs 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{n0_acc3ss_c0ntr0ls_id0r_pwn4g3!!}