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