This weekend I was doing some HTB machines to prepare for the OSWE certification. One of the recommended machines was
Writeup. This machine is vulnerable to CVE-2019-9053 which has a corresponding exploit on Exploit-DB.
While reading through the exploit code, I noticed that the payload variable (Shown at line 13 below) was used multiple times in different functions and always started with the prefix
a,b,1,5)). While the rest of the payload variable’s value was easy to understand, I didn’t understand why the prefix was needed. I searched the internet for answers but struggled to find any. So I decided to dig into the source code of CMS made simple and figure it out. The only information I had from the internet was that the the vulnerability was a blind sql injection located in the News module of CMS Made Simple version 2.2.10 and earlier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def dump_username(): global flag global db_name global output ord_db_name = "" ord_db_name_temp = "" while flag: flag = False for i in range(0, len(dictionary)): temp_db_name = db_name + dictionary[i] ord_db_name_temp = ord_db_name + hex(ord(dictionary[i]))[2:] beautify_print_try(temp_db_name) payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+username+like+0x" + ord_db_name_temp + "25+and+user_id+like+0x31)+--+" url = url_vuln + "&m1_idlist=" + payload start_time = time.time() r = session.get(url) elapsed_time = time.time() - start_time if elapsed_time >= TIME: flag = True break if flag: db_name = temp_db_name ord_db_name = ord_db_name_temp output += '\n[+] Username found: ' + db_name flag = True
I started by downloading the CMS Made Simple source code from the HTB machine after exploiting it, by transfering all the files from the
/var/www/html/ directory to my local computer. Next, I opened the top-most directory in the text editor VSCode. In VSCode, I searched for the parameter named
m1_idlist since this was the parameter being exploited in the exploit code. However, this search had no results. As such, I searched for “idlist” since I thought that the underscore character might just be a delimiter. Among the results, shown below, the entries from the action.default.php file seemed interesting.
In this code, shown below, the value of the idlist parameter is assigned to the
$idlist variable (Line 60). Then, it is ensured that the
$idlist variable is a string (Line 61). Next, the
explode function is called to split the
$idlist string into an array by splitting on the
, character. Then, a for loop is used to ensure that each element in the
$tmp array is an integer (Line 63 - 66). Afterwards, the
$idlist variable is assigned to the filtered
$tmp array and any duplicates are removed (Line 67). Finally, the filtered integers in the
$idlist variable are placed in a string which is appended to the
$query1 variable (line 68). This
$query1 string is then evaluated by the database later on in the code.
The problem in this code is the usage of the
unset functions in the for loop. The
count function counts the element in an array while the
unset function removes an element from an array. At the first line of the for loop, the element at index
$i of the
$tmp variable is casted to an int. What is important to understand here is that php does not throw an error if the string to cast to an int could not be casted to an int. Instead, it returns the integer
0. As such, the next line tries to filter out any strings which could not be converted to an integer (Probably negative values were unwanted as well, hence the expression
< 1 instead of
=== 0 ). The problem here, however, is that after removing an item with the
unset function, the length of the array is decreased by one, meaning that the result of executing
count($tmp) will be decreased by one as well. As such, the filter can be bypassed by sending a negative number followed by a comma character and a payload!
To make this clear, consider the value
$idlist = ",evil". At the first iteration of the for loop at line 63, we have
$i = 0,
$tmp = ["","evil"] and
count($tmp) = 2. Since
$i < count($tmp), we enter the body of the for loop. At line 64, the empty string
“” is converted to an integer, resulting in the integer
0. Then, this element is removed from the array since
0 < 1 is true. Finally, the end of the for loop is reached and
$i is incremented by one. The for loop condition
$i < count($tmp) is then checked again. At this point, the values are
$i = 1,
$tmp = ["evil"] and
count($tmp) = 1 which means that the body of the for loop is not entered. As such, the last element can evade the filter and reach the query string, resulting in the blind sqli vulnerability!
The last thing worth noticing is the format of the string which is appended to the query. Since the array
$idlist only contains the element
“evil”, the expression
implode(',',$idlist) simply results in the string
“evil”. As such, we can change this element to
[SQLI_Payload] is an sql injection payload, since this would create a valid SQL query. In conclusion, this means that a blind sql injection attack can be performed by setting the
idlist variable to
,1))[SQLI_Payload] (Note the
, character). This will result in the expression
'(mn.news_id IN (1)) [SQLI_payload] )) AND ' at line 68.
Proof of Concept
A simple exploitation example is to force the database software to sleep for a couple of seconds by using the built-in
sleep function. This can be performed by setting the value of the idlist variable to
,1)) and sleep(5)#. URL encoding this payload and placing it in the URL results in the following proof of concept URL (where
[IP] is the ip address of the HTB machine named “writeup”):
As can be seen in the picture below, the
curl command can be used to check that the proof of concept URL works as expected.