Este artículo explica cómo hackear una base de datos de MediaWiki para alterar la contraseña de un usuario, sin recurrir al método de restablecimiento por correo electrónico. Durante el proceso se repasan algunos conceptos interesantes como búsqueda de archivos por nombre y contenido; chroot, acceso a bases de datos SQLite, consultas SQL, codificación en Base64, generación de claves utilizando la función PBKDF2, ejecución de código PHP en modo interactivo, y más...



Problema: dado un sitio MediaWiki, se desea restablecer la contraseña de un usuario sin recurrir al método por correo electrónico, sino alterando directamente el campo contraseña en la tabla de usuarios de la base de datos.

Se parte de la idea que no se tiene conocimiento alguno acerca de la instalación. Sólo se sabe que corre sobre un servidor OpenBSD 5.9 con httpd y una base de datos SQLite:

# uname -a
OpenBSD wiki.linuxito.com 5.9 GENERIC#1761 amd64
# ps ax | grep http
18319 ??  Isp     0:00.01 /usr/sbin/httpd
23518 ??  Ip      0:00.08 httpd: server (httpd)
32513 ??  Ip      0:00.02 httpd: server (httpd)
13080 ??  Ip      0:00.02 httpd: server (httpd)
15236 ??  Ip      0:00.01 httpd: logger (httpd)
30989 p0  S+p     0:00.00 grep http

Manos a la obra

El primer paso consiste en determinar dónde (en qué archivo) se encuentra la base de datos SQLite. Para ello, es necesario revisar el archivo de configuración de MediaWiki:

# locate LocalSettings.php
/var/www/htdocs/wiki/LocalSettings.php

En la configuración de MediaWiki, la variable $wgSQLiteDataDir apunta al directorio donde se encuentra la base de datos (archivo my_wiki.sqlite):

# grep SQLiteDataDir /var/www/htdocs/wiki/LocalSettings.php
$wgSQLiteDataDir    = "/var/databases";

httpd está desarrollado por la comunidad de OpenBSD y está orientado a la seguridad (como todo producto derivado de OpenBSD), por ello corre dentro de un chroot. Del manual de configuración de httpd (man httpd.conf):

     chroot directory
             Set the chroot(2) directory.  If not specified, it defaults to
             /var/www, the home directory of the www user.

Esto significa que la base de datos no se encuentra en /var/databases, sino en /var/www/var/databases.

Acceder a la base de datos SQLite utilizando el cliente sqlite3:

# sqlite3 /var/www/var/databases/my_wiki.sqlite
SQLite version 3.9.2 OpenBSD
Enter ".help" for usage hints.
sqlite>

El comando .help lista todos los comandos disponibles:

sqlite> .help
.backup ?DB? FILE      Backup DB (default "main") to FILE
.bail on|off           Stop after hitting an error.  Default OFF
.binary on|off         Turn binary output on or off.  Default OFF
.clone NEWDB           Clone data into NEWDB from the existing database
.databases             List names and files of attached databases
.dbinfo ?DB?           Show status information about the database
.dump ?TABLE? ...      Dump the database in an SQL text format
                         If TABLE specified, only dump tables matching
                         LIKE pattern TABLE.
.echo on|off           Turn command echo on or off
.eqp on|off            Enable or disable automatic EXPLAIN QUERY PLAN
.exit                  Exit this program
.explain ?on|off?      Turn output mode suitable for EXPLAIN on or off.
                         With no args, it turns EXPLAIN on.
.fullschema            Show schema and the content of sqlite_stat tables
.headers on|off        Turn display of headers on or off
.help                  Show this message
.import FILE TABLE     Import data from FILE into TABLE
.indexes ?TABLE?       Show names of all indexes
                         If TABLE specified, only show indexes for tables
                         matching LIKE pattern TABLE.
.limit ?LIMIT? ?VAL?   Display or change the value of an SQLITE_LIMIT
.load FILE ?ENTRY?     Load an extension library
.log FILE|off          Turn logging on or off.  FILE can be stderr/stdout
.mode MODE ?TABLE?     Set output mode where MODE is one of:
                         ascii    Columns/rows delimited by 0x1F and 0x1E
                         csv      Comma-separated values
                         column   Left-aligned columns.  (See .width)
                         html     HTML <table> code
                         insert   SQL insert statements for TABLE
                         line     One value per line
                         list     Values delimited by .separator strings
                         tabs     Tab-separated values
                         tcl      TCL list elements
.nullvalue STRING      Use STRING in place of NULL values
.once FILENAME         Output for the next SQL command only to FILENAME
.open ?FILENAME?       Close existing database and reopen FILENAME
.output ?FILENAME?     Send output to FILENAME or stdout
.print STRING...       Print literal STRING
.prompt MAIN CONTINUE  Replace the standard prompts
.quit                  Exit this program
.read FILENAME         Execute SQL in FILENAME
.restore ?DB? FILE     Restore content of DB (default "main") from FILE
.save FILE             Write in-memory database into FILE
.scanstats on|off      Turn sqlite3_stmt_scanstatus() metrics on or off
.schema ?TABLE?        Show the CREATE statements
                         If TABLE specified, only show tables matching
                         LIKE pattern TABLE.
.separator COL ?ROW?   Change the column separator and optionally the row
                         separator for both the output mode and .import
.shell CMD ARGS...     Run CMD ARGS... in a system shell
.show                  Show the current values for various settings
.stats on|off          Turn stats on or off
.system CMD ARGS...    Run CMD ARGS... in a system shell
.tables ?TABLE?        List names of tables
                         If TABLE specified, only list tables matching
                         LIKE pattern TABLE.
.timeout MS            Try opening locked tables for MS milliseconds
.timer on|off          Turn SQL timer on or off
.trace FILE|off        Output each SQL statement as it is run
.vfsname ?AUX?         Print the name of the VFS stack
.width NUM1 NUM2 ...   Set column widths for "column" mode
                         Negative values right-justify

Con el comando .tables es posible listar las tablas de la base de datos (similar a show tables en MySQL):

sqlite> .tables
archive               msg_resource          searchindex_segments
category              msg_resource_links    site_identifiers    
categorylinks         objectcache           site_stats          
change_tag            oldimage              sites               
external_user         page                  tag_summary         
externallinks         page_props            templatelinks       
filearchive           page_restrictions     text                
image                 pagelinks             transcache          
imagelinks            protected_titles      updatelog           
interwiki             querycache            uploadstash         
ipblocks              querycache_info       user                
iwlinks               querycachetwo         user_former_groups  
job                   recentchanges         user_groups         
l10n_cache            redirect              user_newtalk        
langlinks             revision              user_properties     
log_search            searchindex           valid_tag           
logging               searchindex_content   watchlist           
module_deps           searchindex_segdir  

La tabla user es la que almacena la información relacionada a los usuarios:

sqlite> select * from user;
1|Emiliano|Emiliano Linuxito|:pbkdf2:sha512:30000:64:ssss1====:kkkkkkkk1=======|y otros campos más...
2|Fulano|Fulano García|:B:ssss1:kkkk1|y otros campos más...
3|Sultano|Sultano Perez|:B:ssss2:kkkk2|y otros campos más...
4|Mengano|Mengano Saraza|:pbkdf2:sha512:30000:64:ssss2===:kkkkkkkk2=======|y otros campos más...

más filas...

A continuación es necesario interpretar cada campo de dicha tabla, recurriendo al comando .dump:

sqlite> .dump user
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE user (
 user_id INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT,
 user_name TEXT  NOT NULL default '',
 user_real_name TEXT  NOT NULL default '',
 user_password BLOB NOT NULL,
 user_newpassword BLOB NOT NULL,
 user_newpass_time BLOB,
 user_email TEXT NOT NULL,
 user_touched BLOB NOT NULL default '',
 user_token BLOB NOT NULL default '',
 user_email_authenticated BLOB,
 user_email_token BLOB,
 user_email_token_expires BLOB,
 user_registration BLOB,
 user_editcount INTEGER
 , user_password_expires BLOB DEFAULT NULL);
INSERT INTO "user" VALUES(1,'Emiliano','Emiliano Linuxito',':pbkdf2:sha512:30000:64:ssss1===:kkkkkkkk1=======',...);
INSERT INTO "user" VALUES(2,'Fulano','Fulano García',':B:ssss1:kkkk1',...);
INSERT INTO "user" VALUES(3,'Sultano','Sultano Perez',':B:ssss2:kkkk2',...);
INSERT INTO "user" VALUES(4,'Mengano','Mengano Saraza',':pbkdf2:sha512:30000:64:ssss2===:kkkkkkkk2=======',...);

etcétera...

CREATE UNIQUE INDEX user_name ON user (user_name);
CREATE INDEX user_email_token ON user (user_email_token);
CREATE INDEX user_email ON user (user_email);
COMMIT;

Se observa que el campo contraseña es de tipo BLOB. Esto significa que se trata de datos crudos almacenados tal como vienen.

Si se examina la clase User de MediaWiki (includes/User.php), es posible comprobar que utiliza un objeto de la clase PasswordFactory. Esta a su vez utiliza la clase Password definida en el archivo includes/password/Password.php para gestionar las contraseñas de los usuarios, las cuales pueden ser almacenadas utilizando diferentes formatos.

Supongamos que se desea modificar la contraseña del usuario cuyo user_id es igual a 4 ("Mengano Saraza"). Se observa que, para este usuario en particular, la clave está almacenada utilizando la función de derivación de claves PBKDF2 en conjunto con el algoritmo de hashing SHA-512 utilizando 30000 iteraciones y una longitud de clave de 64 bytes.

El formato del campo contraseña es interno de MediaWiki. Cada parámetro se separa con dos puntos (:) y tanto claves como salts están codificados en Base64 (notar los clásicos caracteres = de relleno de Base64). Para crear una nueva contraseña con la función PBKDF2 será necesario entonces identificar todos los parámetros y decodificar el salt.

Es posible utilizar cualquier herramienta o lenguaje que provea la funcionalidad necesaria para generar claves derivadas utilizando PBKDF2. Ya que el servidor posee PHP instalado (pues MediaWiki está implementado con PHP), por qué no hacerlo con PHP en modo interactivo:

# php-5.6 -a 
Interactive shell

php >

La salida cruda de la función PBKDF2 se debe almacenar codificada en Base64. Es posible mantener el salt originar o cambiarlo por uno nuevo. PHP posee la función hash_pbkdf2 para generar claves derivadas con PBKDF2 y base64_encode/base64_decode para codificar/decodificar Base64 respectivamente.

Lógicamente uno puede cambiar el método de almacenamiento de la contraseña, algoritmo de hashing, etc. de acuerdo a cualquiera de los soportados por MediaWiki. Este ejemplo muestra el proceso de generación de clave con PBKDF2, porque es el que resulta más interesante.

Si se desea mantener el salt original, es necesario pasárselo a hash_pbkdf2 decodificado. El resto de los parámetros se mantienen iguales: SHA-512 como algoritmo de hashing, 30000 iteraciones y clave de 64 bytes. La nueva clave es "twopala" (sin las comillas dobles, por supuesto):

php > echo(base64_encode(hash_pbkdf2("sha512","twopala",base64_decode("ssss2==="),30000,64,1)));
5lN2FuRakbapssu2TstGtnG1vfgkkmJPoYqVkVZrAGfFP2jfWgMMDXcKpjtjd4qwe/q4nZkg9nVdRHB8g8fMRA==

Notar que el último parámetro a hash_pbkdf2 es 1, pues se necesita la clave en formato binario crudo. Luego se codifica en Base64 y se imprime. De esta forma se obtiene una nueva clave derivada PBKDF2 a partir del hash SHA-512 y codificada en Base64, para reemplazar en la última parte de la cadena correspondiente a la contraseña del usuario en cuestión. Simple.

También es posible generar un nuevo salt e imprimir toda la cadena de contraseña para reemplazar, directamente desde PHP:

php > $salt=base64_encode("saraza.com.ar");
php > echo($salt);
c2FyYXphLmNvbS5hcg==
php > $salt=base64_encode("saraza.com.ar");
php > $pass=base64_encode(hash_pbkdf2("sha512","trustno1",base64_decode($salt),30000,64,1))
php > echo ":pbkdf2:sha512:30000:64:".$salt.":".$pass;
:pbkdf2:sha512:30000:64:c2FyYXphLmNvbS5hcg==:UFg1gwVzjD1fs54K0GjkYlsEt2K2hkwyF0qMyFPiZqibTSG7VSb2fG78tPqPjlEwO2ZoysqJeiixH/sabemBWg==

Esta última línea corresponde con el campo contraseña a reemplazar utilizando una sentencia SQL.

Conectarse nuevamente a la base de datos y ejecutar la siguiente sentencia para asignar la nueva clave archisegura "trustno1" al usuario cuyo user_id es igual a 4:

sqlite> update user set user_password=':pbkdf2:sha512:30000:64:c2FyYXphLmNvbS5hcg==:UFg1gwVzjD1fs54K0GjkYlsEt2K2hkwyF0qMyFPiZqibTSG7VSb2fG78tPqPjlEwO2ZoysqJeiixH/sabemBWg==' where user_id=4;
sqlite> .exit

TL;DR

No me interesan todos estos conceptos y quiero resolver el problema rápido y fácil.

Entre todos los scripts de mantenimiento de MediaWiki, existe uno para resetear contraseñas desde línea de comandos de manera simple, sin tener que comprender todos estos conceptos de seguridad y contraseñas:

Manual:changePassword.php - MediaWiki

Sin embargo, tener mucho cuidado al pasar contraseñas como parámetro, pues típicamente éstas quedan almacenadas en el historial de comandos de la consola.

Referencias

MediaWiki: Password Class Reference

Datatypes In SQLite Version 3

PBKDF2

SHA-2

Base64

PHP: hash_pbkdf2 - Manual

PHP: base64_encode - Manual

PHP: base64_decode - Manual


Tal vez pueda interesarte


Compartí este artículo