Repository: Mailu/Mailu

Issue:
Question:  Is login possible via port 25
If only port 25 is open to the world, is it possible for a brute force password attack to occur?

I'm seeing messages but someone told me the errors can't be trusted, that there is no actual way to log in via port 25.  But if this were true why would the error message indicate different?

```
2022/03/02 06:04:22 [info] 13#13: *89499 client 31.210.20.189:60509 connected to 0.0.0.0:25
2022/03/02 06:04:22 [error] 13#13: *89499 31.210.20.189 could not be resolved (3: Host not found) while in resolving client address, client: 31.210.20.189, server: 0.0.0.0:25
127.0.0.1 - - [02/Mar/2022:06:04:23 +0000] "GET /auth/email HTTP/1.0" 200 0 "-" "-"
2022/03/02 06:04:23 [info] 13#13: *89499 client login failed: "Authentication credentials invalid" while in http auth state, client: 31.210.20.189, server: 0.0.0.0:25, login: "database"
```


Discussion:
Can you fetch the corresponding logs from the admin container?

https://github.com/Mailu/Mailu/blob/2e9b14d536b69d4c7b5fe0cd4fb7de3d1c069126/core/admin/mailu/internal/views/auth.py#L15 is what should prevent login on port 25... The attacker may be using a method that isn't "plain"... but last I have checked the nginx module was "normalizing" all attempts. We could probably drop the second condition in any case
I'm experiencing similar logs. However, there's no new messages in the admin container since startup. None of the other containers show anything relavant, either.
I will need a packet capture and the exact version of what you are running (``cat /version``) to debug this... You can generate one using ``tcpdump -w /tmp/25.pcap -n -s 0 tcp port 25``. Assuming the attacker is always using the same source IPs you can add something like `` and net 31.210.20.0/24`` to filter out unrelated traffic.

From what I can see:
1) no AUTH method is advertised on port 25 (so no RFC compliant client should attempt to AUTH)
2) we reject any "valid" attempt

Of course there are a zillion of corner cases (like if the attacker does STARTTLS, uses something that isn't AUTH/PLAIN, sends invalid data) so that's why a packet capture would help.

PLAIN using valid credentials:
```
$nc -C test.mailu.io 25
220 test.mailu.io ESMTP ready
EHLO client.example.com
250-test.mailu.io
250-PIPELINING
250-SIZE
250-2000000
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 STARTTLS
AUTH PLAIN
334 
AGFkbWluQHRlc3QubWFpbHUuaW8AbGV0bWVpbg==
502 5.5.1 AUTH not supported
QUIT

$nc -C test.mailu.io 25
220 test.mailu.io ESMTP ready
EHLO client.example.com
250-test.mailu.io
250-PIPELINING
250-SIZE
250-2000000
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 STARTTLS
AUTH PLAIN AGFkbWluQHRlc3QubWFpbHUuaW8AbGV0bWVpbg==
502 5.5.1 AUTH not supported
QUIT
```

LOGIN using valid credentials:
```
$nc -C test.mailu.io 25
220 test.mailu.io ESMTP ready
EHLO client.example.com
250-test.mailu.io
250-PIPELINING
250-SIZE
250-2000000
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 STARTTLS
AUTH LOGIN
334 VXNlcm5hbWU6
YWRtaW5AdGVzdC5tYWlsdS5pbwo=
334 UGFzc3dvcmQ6
bGV0bWVpbgo=
502 5.5.1 AUTH not supported
QUIT
```

Resolving patch:
diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py
--- a/core/admin/mailu/internal/nginx.py
+++ b/core/admin/mailu/internal/nginx.py
@@ -93,8 +93,8 @@ def handle_authentication(headers):
             app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
         else:
             try:
-                user = models.User.query.get(user_email)
-                is_valid_user = True
+                user = models.User.query.get(user_email) if '@' in user_email else None
+                is_valid_user = user is not None
             except sqlalchemy.exc.StatementError as exc:
                 exc = str(exc).split('\n', 1)[0]
                 app.logger.warn(f'Invalid user {user_email!r}: {exc}')
diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py
--- a/core/admin/mailu/internal/views/auth.py
+++ b/core/admin/mailu/internal/views/auth.py
@@ -12,7 +12,7 @@ def nginx_authentication():
     """
     client_ip = flask.request.headers["Client-Ip"]
     headers = flask.request.headers
-    if headers["Auth-Port"] == '25' and headers['Auth-Method'] == 'plain':
+    if headers["Auth-Port"] == '25':
         response = flask.Response()
         response.headers['Auth-Status'] = 'AUTH not supported'
         response.headers['Auth-Error-Code'] = '502 5.5.1'
diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py
--- a/core/admin/mailu/internal/views/postfix.py
+++ b/core/admin/mailu/internal/views/postfix.py
@@ -5,6 +5,7 @@
 import flask
 import idna
 import re
+import sqlalchemy.exc
 import srslib
 
 @internal.route("/postfix/dane/<domain_name>")
@@ -158,18 +159,13 @@ def postfix_sender_rate(sender):
 def postfix_sender_access(sender):
     """ Simply reject any sender that pretends to be from a local domain
     """
-    if not is_void_address(sender):
-        localpart, domain_name = models.Email.resolve_domain(sender)
-        return flask.jsonify("REJECT") if models.Domain.query.get(domain_name) else flask.abort(404)
-    else:
-        return flask.abort(404)
-
-
-def is_void_address(email):
-    '''True if the email is void (null) email address.
-    '''
-    if email.startswith('<') and email.endswith('>'):
-        email = email[1:-1]
-    # Some MTAs use things like '<MAILER-DAEMON>' instead of '<>'; so let's
-    # consider void any such thing.
-    return '@' not in email
+    if '@' in sender:
+        if sender.startswith('<') and sender.endswith('>'):
+            sender = sender[1:-1]
+        try:
+            localpart, domain_name = models.Email.resolve_domain(sender)
+            if models.Domain.query.get(domain_name):
+                return flask.jsonify("REJECT")
+        except sqlalchemy.exc.StatementError:
+            pass
+    return flask.abort(404)


Test patch: