Tuesday, April 23, 2019

Server side request forgery (SSRF) attack

Server side request forgery (SSRF) attack

In a Server-Side Request Forgery (SSRF) attack, the attacker can abuse functionality on the server to read or update internal resources. The attacker can supply or a modify a URL which the code running on the server will read or submit data to, and by carefully selecting the URLs, the attacker may be able to read server configuration such as AWS metadata, connect to internal services like http enabled databases or perform post requests towards internal services which are not intended to be exposed.


source: Acunetix
You can no longer request information from internal systems, but can still make internal API-calls. Imagine the following PHP code (source Detectify):

//getimage.php
$content = file_get_contents($_GET['url']);
file_put_contents(‘image.jpg’, $content);

The above code will fetch data from a URL using PHP’s file_get_contents() function and then save it to the disk. A legitime request would then look like:

GET  /getimage.php?url=https://website.com/images/cat.jpg

And the web application would make a request to https://website.com/images/cat.jpg. This code could be exploited with SSRF. Such an attack could look something like:

GET  /getimage.php?url=http://127.0.0.1/api/v1/getuser/id/1

In this case the vulnerable web application would make a GET request to the internal REST API service, trying to access the /api/v1/getuser/id/1 endpoint. This REST API service is only accessible on the local network, but due to a SSRF vulnerability it was possible for the attacker to make such an internal request and read that response.   Sometimes you can make a request to an external server, and the request itself may contain sensitive headers. One of many examples would be HTTP basic passwords, if a proxy has been used. SSRF can therefore be carried out to both internal and external services.

Due to microservices and serverless platforms, SSRF will probably be a bigger thing in the future. Making internal requests now means that you can interact with other parts of the service, pretending to be the actual service.

Mitigation


The most robust way to avoid Server Side Request Forgery (SSRF) is to whitelist the DNS name or IP address that your application needs to access. If a whitelist approach does not suit you and you must rely on a blacklist, it’s important to validate user input properly. For example, do not allow requests to private (non-routable) IP addresses.

To prevent response data leaking to the attacker, you must ensure that the received response is as expected. Under no circumstances should the raw response body from the request sent by the server be delivered to the client.

If your application only uses HTTP or HTTPS to make requests, allow only these URL schemas. If you disable unused URL schemas, the attacker will be unable to use the web application to make requests using potentially dangerous schemas such as file:///, dict://, ftp:// and gopher://

Uncomplicated Firewall with Python

Uncomplicated Firewall with Python

Uncomplicated Firewall, is an interface to iptables that simplifyies the process of configuring a firewall. Iptables is flexible, it can be difficult for beginners to learn how to use it to properly configure a firewall. 

In the example below is a python program that makes it easy allowing and blocking various services by IP address. 

## Description :
## Generate ip-host binding list for a list of nodes, when internal DNS is missing.
## 1. For existing nodes, allow traffic from new nodes
## 2. For new nodes, allow traffic from all nodes
##
## Sample:
## python ./ufw_allow_ip.py --old_ip_list_file /tmp/old_ip_list --new_ip_list_file /tmp/new_ip_list \
## --ssh_username root --ssh_port 22 --ssh_key_file ~/.ssh/id_rsa
##
##-------------------------------------------------------------------
import os, sys
import paramiko
import argparse
# multiple threading for a list of ssh servers
import Queue
import threading
import logging
log_folder = "%s/log" % (os.path.expanduser('~'))
if os.path.exists(log_folder) is False:
os.makedirs(log_folder)
log_file = "%s/%s.log" % (log_folder, os.path.basename(__file__).rstrip('\.py'))
logging.basicConfig(filename=log_file, level=logging.DEBUG, format='%(asctime)s %(message)s')
logging.getLogger().addHandler(logging.StreamHandler())
def get_list_from_file(fname):
l = []
with open(fname,'r') as f:
for row in f:
row = row.strip()
if row.startswith('#') or row == '':
continue
l.append(row)
return l
def ufw_allow_ip_list(server_ip, ip_list, ssh_connect_args):
if len(ip_list) == 0:
print("Skip run ufw update in %s, since ip_list is empty" % (server_ip))
return("OK", "")
[ssh_username, ssh_port, ssh_key_file, key_passphrase] = ssh_connect_args
ssh_command = ""
# TODO: improve this command, by using a library
for ip in ip_list:
ssh_command = "%s && ufw allow from %s" % (ssh_command, ip)
if ssh_command.startswith(" && "):
ssh_command = ssh_command[len(" && "):]
print("Update ufw in %s. ssh_command: %s" % (server_ip, ssh_command))
output = ""
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
key = paramiko.RSAKey.from_private_key_file(ssh_key_file, password=key_passphrase)
ssh.connect(server_ip, username=ssh_username, port=ssh_port, pkey=key)
stdin, stdout, stderr = ssh.exec_command(ssh_command)
output = "\n".join(stdout.readlines())
output = output.rstrip("\n")
print("Command output in %s: %s" % (server_ip, output))
ssh.close()
except:
return ("ERROR", "Unexpected on server: %s error: %s\n" % (server_ip, sys.exc_info()[0]))
return ("OK", output)
###############################################################
if __name__ == '__main__':
# get parameters from users
parser = argparse.ArgumentParser()
parser.add_argument('--old_ip_list_file', required=True, \
help="IP list of current cluster", type=str)
parser.add_argument('--new_ip_list_file', required=True, \
help="IP list of new nodes", type=str)
parser.add_argument('--ssh_username', required=False, default="root", \
help="Which OS user to ssh", type=str)
parser.add_argument('--ssh_port', required=False, default="22", \
help="Which port to connect sshd", type=int)
parser.add_argument('--ssh_key_file', required=False, default="%s/.ssh/id_rsa" % os.path.expanduser('~'), \
help="ssh key file to connect", type=str)
parser.add_argument('--key_passphrase', required=False, default="", \
help="Which OS user to ssh", type=str)
l = parser.parse_args()
ssh_connect_args = [l.ssh_username, l.ssh_port, l.ssh_key_file, l.key_passphrase]
old_ip_list = get_list_from_file(l.old_ip_list_file)
new_ip_list = get_list_from_file(l.new_ip_list_file)
has_error = False
# TODO: speed up this process by multiple threading
for old_ip in old_ip_list:
(status, output) = ufw_allow_ip_list(old_ip, new_ip_list, ssh_connect_args)
if status != "OK":
has_error = True
print("Error in %s. errmsg: %s" % (old_ip, output))
for new_ip in new_ip_list:
(status, output) = ufw_allow_ip_list(new_ip, new_ip_list + old_ip_list, ssh_connect_args)
if status != "OK":
has_error = True
print("Error in %s. errmsg: %s" % (new_ip, output))
if has_error is True:
sys.exit(1)
#!/usr/bin/python