diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 4d10e4ca..a7eb4e85 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -394,7 +394,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx"] + target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] steps: - uses: actions/checkout@v3 - name: Retrieve global variables @@ -439,7 +439,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx"] + target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] steps: - uses: actions/checkout@v3 - name: Retrieve global variables diff --git a/core/base/requirements-dev.txt b/core/base/requirements-dev.txt index 92d34fdb..35e1cc9b 100644 --- a/core/base/requirements-dev.txt +++ b/core/base/requirements-dev.txt @@ -46,6 +46,10 @@ watchdog # core/postfix postfix-mta-sts-resolver +# core/oletools +python-magic +oletools + # optional/fetchmail requests diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 716f848e..db6f2b9b 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -42,6 +42,7 @@ marshmallow==3.18.0 marshmallow-sqlalchemy==0.28.1 multidict==6.0.2 mysql-connector-python==8.0.31 +oletools==0.60.1 packaging==21.3 passlib==1.7.4 podop @ file:///app/libs/podop @@ -52,7 +53,9 @@ pycares==4.2.2 pycparser==2.21 Pygments==2.13.0 pyOpenSSL==22.1.0 -pyparsing==3.0.9 +pyparsing==2.4.7 +python-dateutil==2.8.2 +python-magic==0.4.27 python-dateutil==2.8.2 pytz==2022.6 PyYAML==6.0 diff --git a/core/oletools/Dockerfile b/core/oletools/Dockerfile new file mode 100644 index 00000000..8bb98cd9 --- /dev/null +++ b/core/oletools/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile-upstream:1.4.3 + +# oletools image +FROM base + +ARG VERSION=local +LABEL version=$VERSION + +RUN set -euxo pipefail \ + ; apk add --no-cache netcat-openbsd libmagic libffi \ + ; curl -sLo olefy.py https://raw.githubusercontent.com/HeinleinSupport/olefy/f8aac6cc55283886d153e89c8f27fae66b1c24e2/olefy.py \ + ; chmod 755 olefy.py + +RUN echo $VERSION >/version + +HEALTHCHECK --start-period=60s CMD echo PING|nc -q1 127.0.0.1 11343|grep "PONG" +EXPOSE 11343/tcp + +USER nobody:nobody + +ENV \ + OLEFY_BINDADDRESS="0.0.0.0" \ + OLEFY_BINDPORT="11343" \ + OLEFY_OLEVBA_PATH="/app/venv/bin/olevba" \ + OLEFY_PYTHON_PATH="/app/venv/bin/python3" \ + OLEFY_TMPDIR="/dev/shm/" \ + OLEFY_MINLENGTH="300" \ + OLEFY_DEL_TMP="1" \ + OLEFY_DEL_TMP_FAILED="1" + +CMD /app/olefy.py diff --git a/core/rspamd/conf/composites.conf b/core/rspamd/conf/composites.conf new file mode 100644 index 00000000..d7031520 --- /dev/null +++ b/core/rspamd/conf/composites.conf @@ -0,0 +1,12 @@ +OLETOOLS_MACRO_MRAPTOR { + expression = "(OLETOOLS_A & OLETOOLS_W) | (OLETOOLS_A & OLETOOLS_X) | (OLETOOLS_W & OLETOOLS_X)"; + message = "Rejected (malicious macro - mraptor)"; + policy = "leave"; + score = 20.0; +} +OLETOOLS_MACRO_SUSPICIOUS { + expression = "OLETOOLS & OLETOOLS_SUSPICIOUS"; + message = "Rejected (malicious macro)"; + policy = "leave"; + score = 20.0; +} diff --git a/core/rspamd/conf/external_services.conf b/core/rspamd/conf/external_services.conf new file mode 100644 index 00000000..2a918caa --- /dev/null +++ b/core/rspamd/conf/external_services.conf @@ -0,0 +1,61 @@ +oletools { + # default olefy settings + servers = "{{ OLETOOLS_ADDRESS }}" + + # needs to be set explicitly for Rspamd < 1.9.5 + scan_mime_parts = true; + extended = true; + max_size = 3145728; + timeout = 20.0; + retransmits = 1; + + patterns { + OLETOOLS_MACRO = '^.....M..$'; + OLETOOLS_AUTOEXEC = '^A....M..$'; + OLETOOLS_SUSPICIOUS = '^.....MS.$'; +# see https://github.com/decalage2/oletools/blob/master/oletools/mraptor.py + OLETOOLS_A = '(?i)\b(?:Auto(?:Exec|_?Open|_?Close|Exit|New)|Document(?:_?Open|_Close|_?BeforeClose|Change|_New)|NewDocument|Workbook(?:_Open|_Activate|_Close|_BeforeClose)|\w+_(?:Painted|Painting|GotFocus|LostFocus|MouseHover|Layout|Click|Change|Resize|BeforeNavigate2|BeforeScriptExecute|DocumentComplete|DownloadBegin|DownloadComplete|FileDownload|NavigateComplete2|NavigateError|ProgressChange|PropertyChange|SetSecureLockIcon|StatusTextChange|TitleChange|MouseMove|MouseEnter|MouseLeave|OnConnecting))|Auto_Ope\b'; + OLETOOLS_W = '(?i)\b(?:FileCopy|CopyFile|Kill|CreateTextFile|VirtualAlloc|RtlMoveMemory|URLDownloadToFileA?|AltStartupPath|WriteProcessMemory|ADODB\.Stream|WriteText|SaveToFile|SaveAs|SaveAsRTF|FileSaveAs|MkDir|RmDir|SaveSetting|SetAttr)\b|(?:\bOpen\b[^\n]+\b(?:Write|Append|Binary|Output|Random)\b)'; + OLETOOLS_X = '(?i)\b(?:Shell|CreateObject|GetObject|SendKeys|RUN|CALL|MacScript|FollowHyperlink|CreateThread|ShellExecuteA?|ExecuteExcel4Macro|EXEC|REGISTER|SetTimer)\b|(?:\bDeclare\b[^\n]+\bLib\b)'; + } + + # mime-part regex matching in content-type or filename + mime_parts_filter_regex { + #UNKNOWN = "application\/octet-stream"; + DOC2 = "application\/msword"; + DOC3 = "application\/vnd\.ms-word.*"; + XLS = "application\/vnd\.ms-excel.*"; + PPT = "application\/vnd\.ms-powerpoint.*"; + GENERIC = "application\/vnd\.openxmlformats-officedocument.*"; + } + # mime-part filename extension matching (no regex) + mime_parts_filter_ext { + doc = "doc"; + dot = "dot"; + docx = "docx"; + dotx = "dotx"; + docm = "docm"; + dotm = "dotm"; + xls = "xls"; + xlt = "xlt"; + xla = "xla"; + xlsx = "xlsx"; + xltx = "xltx"; + xlsm = "xlsm"; + xltm = "xltm"; + xlam = "xlam"; + xlsb = "xlsb"; + ppt = "ppt"; + pot = "pot"; + pps = "pps"; + ppa = "ppa"; + pptx = "pptx"; + potx = "potx"; + ppsx = "ppsx"; + ppam = "ppam"; + pptm = "pptm"; + potm = "potm"; + ppsm = "ppsm"; + slk = "slk"; + } +} diff --git a/core/rspamd/start.py b/core/rspamd/start.py index 37de1df9..b285ca03 100755 --- a/core/rspamd/start.py +++ b/core/rspamd/start.py @@ -14,6 +14,7 @@ log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) os.environ["REDIS_ADDRESS"] = system.get_host_address_from_environment("REDIS", "redis") os.environ["ADMIN_ADDRESS"] = system.get_host_address_from_environment("ADMIN", "admin") +os.environ["OLETOOLS_ADDRESS"] = system.get_host_address_from_environment("OLETOOLS", "oletools:11343") if os.environ.get("ANTIVIRUS") == 'clamav': os.environ["ANTIVIRUS_ADDRESS"] = system.get_host_address_from_environment("ANTIVIRUS", "antivirus:3310") diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index b6c99ca5..773fddd2 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -103,16 +103,33 @@ services: - {{ dns }} {% endif %} + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-{{ version }}} + hostname: oletools + restart: always + networks: + - noinet + depends_on: + {% if resolver_enabled %} + - resolver + dns: + - {{ dns }} + {% endif %} + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-{{ version }}} hostname: antispam restart: always env_file: {{ env }} + networks: + - default + - noinet volumes: - "{{ root }}/filter:/var/lib/rspamd" - "{{ root }}/overrides/rspamd:/etc/rspamd/override.d:ro" depends_on: - front + - oletools {% if resolver_enabled %} - resolver dns: @@ -199,3 +216,6 @@ networks: {% if ipv6_enabled %} - subnet: {{ subnet6 }} {% endif %} + noinet: + driver: bridge + internal: true diff --git a/tests/build.hcl b/tests/build.hcl index d657cbb7..f5893b8c 100644 --- a/tests/build.hcl +++ b/tests/build.hcl @@ -34,6 +34,7 @@ group "default" { "antispam", "front", "imap", + "oletools", "smtp", "webmail", @@ -152,6 +153,15 @@ target "front" { tags = tag("nginx") } +target "oletools" { + inherits = ["defaults"] + context = "core/oletools/" + contexts = { + base = "target:base" + } + tags = tag("oletools") +} + target "imap" { inherits = ["defaults"] context = "core/dovecot/" diff --git a/tests/compose/core/docker-compose.yml b/tests/compose/core/docker-compose.yml index 1f9d6730..8d56a443 100644 --- a/tests/compose/core/docker-compose.yml +++ b/tests/compose/core/docker-compose.yml @@ -65,10 +65,20 @@ services: depends_on: - front + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-local} + hostname: oletools + restart: always + networks: + - noinet + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local} restart: always env_file: mailu.env + networks: + - default + - noinet volumes: - "/mailu/filter:/var/lib/rspamd" - "/mailu/dkim:/dkim" @@ -96,3 +106,6 @@ networks: driver: default config: - subnet: 192.168.203.0/24 + noinet: + driver: bridge + internal: true diff --git a/tests/compose/fetchmail/docker-compose.yml b/tests/compose/fetchmail/docker-compose.yml index c1a1a55c..067532fa 100644 --- a/tests/compose/fetchmail/docker-compose.yml +++ b/tests/compose/fetchmail/docker-compose.yml @@ -65,10 +65,20 @@ services: depends_on: - front + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-local} + hostname: oletools + restart: always + networks: + - noinet + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local} restart: always env_file: mailu.env + networks: + - default + - noinet volumes: - "/mailu/filter:/var/lib/rspamd" - "/mailu/dkim:/dkim" @@ -103,3 +113,6 @@ networks: driver: default config: - subnet: 192.168.203.0/24 + noinet: + driver: bridge + internal: true diff --git a/tests/compose/filters/docker-compose.yml b/tests/compose/filters/docker-compose.yml index 41908a40..3eb2d84c 100644 --- a/tests/compose/filters/docker-compose.yml +++ b/tests/compose/filters/docker-compose.yml @@ -65,10 +65,20 @@ services: depends_on: - front + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-local} + hostname: oletools + restart: always + networks: + - noinet + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local} restart: always env_file: mailu.env + networks: + - default + - noinet volumes: - "/mailu/filter:/var/lib/rspamd" - "/mailu/dkim:/dkim" @@ -102,3 +112,6 @@ networks: driver: default config: - subnet: 192.168.203.0/24 + noinet: + driver: bridge + internal: true diff --git a/tests/compose/webdav/docker-compose.yml b/tests/compose/webdav/docker-compose.yml index 5dfa6bc8..1391b68d 100644 --- a/tests/compose/webdav/docker-compose.yml +++ b/tests/compose/webdav/docker-compose.yml @@ -65,10 +65,20 @@ services: depends_on: - front + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-local} + hostname: oletools + restart: always + networks: + - noinet + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local} restart: always env_file: mailu.env + networks: + - default + - noinet volumes: - "/mailu/filter:/var/lib/rspamd" - "/mailu/dkim:/dkim" @@ -103,3 +113,6 @@ networks: driver: default config: - subnet: 192.168.203.0/24 + noinet: + driver: bridge + internal: true diff --git a/tests/compose/webmail/docker-compose.yml b/tests/compose/webmail/docker-compose.yml index 14d1dae9..5e106105 100644 --- a/tests/compose/webmail/docker-compose.yml +++ b/tests/compose/webmail/docker-compose.yml @@ -65,10 +65,20 @@ services: depends_on: - front + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-local} + hostname: oletools + restart: always + networks: + - noinet + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local} restart: always env_file: mailu.env + networks: + - default + - noinet volumes: - "/mailu/filter:/var/lib/rspamd" - "/mailu/dkim:/dkim" @@ -104,3 +114,6 @@ networks: driver: default config: - subnet: 192.168.203.0/24 + noinet: + driver: bridge + internal: true diff --git a/towncrier/newsfragments/2510.feature b/towncrier/newsfragments/2510.feature new file mode 100644 index 00000000..a6ad675b --- /dev/null +++ b/towncrier/newsfragments/2510.feature @@ -0,0 +1 @@ +Implement OLETools and block bad macros in office documents