验证Exchange邮箱用户是否存在

君子藏器于身待时而动,安全不露圭角覆盂之安。

——AnonySec

https://payloads.cn

SMTP

简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)是在Internet传输电子邮件的事实标准。
SMTP是一个相对简单的基于文本的协议。在其之上指定了一条消息的一个或多个接收者(在大多数情况下被确认是存在的),然后消息文本会被传输。可以很简单地通过telnet程序来测试一个SMTP服务器。SMTP使用TCP端口25。要为一个给定的域名决定一个SMTP服务器,需要使用MX (Mail eXchange) DNS。
——摘自 百度百科

原理

使用 SMTP RCPT TO:返回250状态码,证明邮箱地址存在;返回550状态码,证明邮箱地址不存在。

注意

有些公司将邮箱服务设置为Catch-all,这意味该域名下的每个邮箱地址,都会被认为是存在的。

Catch-all

Catch-all邮箱:全域设置 (以该邮箱为后缀的任何邮箱名,该邮箱服务器都会接收)
例如这个域microsci.com,会设置一个Catch-all邮箱地址如: info@microsci.com,该地址可以用来接收拼错或者无效的来自microsci.com域的邮箱。
如sales1@microsci.com是不存在无效的账户。当给这个邮箱sales1@microsci.com发邮件时,由于sales1@microsci.com邮箱不存在就会把邮件自动转到info@microsci.com (Catch-all邮箱里)。
这样的话,就造成检测该域的邮箱都是通过的。因为该域的邮箱会接受该域下的所有邮箱。然而也有这样的情况,收件人邮件服务器可能会默默地放弃该邮件或事后发送退回邮件。
[ 如何配置catch-all,官网文档 配置 catch-all 邮箱 ]

验证过程

查找MX记录

1
nslookup -type=MX mail.com

找到两条MX记录,SMTP服务器的地址分别为:
mail.com mail exchanger = 10 mx2.mail.com.
mail.com mail exchanger = 10 mx.mail.com.

image-20210707152940360

建立连接

SMTP默认是TCP端口25,使用telnet命令进行TCP的连接。

1
2
3
4
5
❯ telnet mx.mail.com 25
Trying 180.xx.xx.199...
Connected to mx.mail.com.
Escape character is '^]'.
220 mx.mail.com ESMTP

响应码220,证明连接成功。

HELO/EHLO

向服务器表明邮件发送的服务器

1
2
3
4
5
6
7
8
❯ HELO mx.mail.com
250 mx.mail.com

❯ EHLO mx.mail.com
250-mx.mail.com
250-8BITMIME
250-SIZE 68157440
250 STARTTLS

响应码250,证明成功。

MAIL FROM

表明发件人

1
2
MAIL FROM:<test@mail.com>
250 sender <test@mail.com> ok

RCPT TO

如果 RCPT TO 的响应码是250或者251都表示邮件地址存在,如果响应码是5xx,则表明邮件地址不存在,如果是4xx则代表无法确认。

1
2
3
4
5
6
7
8
❯ MAIL FROM:<test@mail.com>
250 sender <test@mail.com> ok

❯ RCPT TO:<mailsec@mail.com>
250 recipient <mailsec@mail.com> ok

❯ RCPT TO:<mail@mail.com>
550 #5.1.0 Address rejected.

工具化

Python

image-20210707152952506

Metasploit

开篇提到过 Catch-all 。实际上,MAIL服务器如果做了这种配置,任何邮箱地址通过SMTP这种方法都会校验通过。
image-20210707153003280
可以使用MSF的 auxiliary/scanner/msmail/onprem_enum 模块进行验证,该模块利用OWA (Outlook Webapp)基于时间的用户枚举。
[ 作者文档 onprem_enum.md ]
image-20210707153013756


附: Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# -*- coding: utf-8 -*-
import dns.resolver
import argparse
from socket import *
import random

def ColorPrint(string="", flag="", verbose=""):
colors = {
u"gray": "2",
u"red": "31",
u"green": "32",
u"yellow": "33",
u"blue": "34",
u"pink": "35",
u"cyan": "36",
u"white": "37",
}
flags = {
u"error": "[-] ",
u"warning": "[!] ",
u"info": "[*] ",
u"success": "[+] ",
u"debug": ">>> ",
u"echo": ">>> "
}
try:
if flag == 'error':
print(u"\033[%sm%s%s" % (colors[u"red"], flags[flag], string))
if flag =="info":
print(u"\033[%sm%s%s" % (colors[u"white"], flags[flag], string))
if flag == 'echo' or flag == '' or flag == 0:
print(u"\033[%sm%s%s" % (colors[u"white"], flags[flag], string))
if flag == 'success':
print(u"\033[%sm%s%s" % (colors[u"green"], flags[flag], string))
if verbose == 1:
if flag == 'warning':
print(u"\033[%sm%s%s" % (colors[u"yellow"], flags[flag], string))
if flag == 'debug':
print(u"\033[%sm%s%s" % (colors[u"white"], flags[flag], string))
except:
return 0

# 第一步: 获取域名MX 记录
def DNSQuery(mailaddr):
dns_resolver = dns.resolver.Resolver(configure=False)
dns_resolver.timeout = 5
dns_resolver.lifetime = 5
dns_resolver.nameservers = ['119.29.29.29']
record = "MX"
domain = mailaddr[mailaddr.find(u"@") + 1:]
ColorPrint("query MX of DNS for %s" % domain, flag= "echo", verbose=verbose)
try:
MX = dns_resolver.resolve(domain, record)
m = random.randint(0, len(MX))
mx = MX[0].exchange
strMx = str(mx)[:-1]
assert strMx != u""
except Exception as e:
ColorPrint("query MX of %s failed: %s" % (strMx, e), flag="error", verbose=verbose)
return 0
ColorPrint("MX Server: %s" % strMx, flag="info", verbose=verbose)
return strMx

# 第二步: 请求mail服务器
def smtpsend(server, port=20, mail_rcptTo=""):
mailserver = (server, port)
clientSocket = socket(AF_INET, SOCK_STREAM)
helloDomain = server[server.find(u".") + 1:]
mail_from = "mail@gmail.com"
ColorPrint("connect to %s:%.f" % (server, port), flag="debug", verbose=verbose)
try:
clientSocket.connect(mailserver)
recv = clientSocket.recv(1024)
recv = recv.decode()
if recv[0:3] != '220':
ColorPrint("info: " + recv, flag="debug", verbose=verbose)
return 0
except Exception as e:
ColorPrint("Error: %s" % e, flag="error", verbose=verbose)
ColorPrint("Done. " , flag="info", verbose=verbose)
return 0
ColorPrint("Message after connection request: \n" + recv, flag="debug", verbose=verbose)
hello_command = "EHLO %s\r\n" % helloDomain
clientSocket.send(hello_command.encode())
recv1 = clientSocket.recv(1024)
recv1 = recv1.decode()
ColorPrint("Message after EHLO command:\n" + recv1, flag="debug", verbose=verbose)
if recv1[:3] != '250':
ColorPrint("250 reply not received from server." + recv1, flag="error", verbose=verbose)
return 0
mail_from_command = "MAIL FROM:<%s>\r\n" % mail_from
clientSocket.send(mail_from_command.encode())
recv2 = clientSocket.recv(1024)
recv2 = recv2.decode()
ColorPrint("After MAIL FROM command: " + recv2, flag="debug", verbose=verbose)
rcptTo = "RCPT TO:<%s>\r\n" % mail_rcptTo
clientSocket.send(rcptTo.encode())
recv3 = clientSocket.recv(1024)
recv3 = recv3.decode()
if recv3[:3] == '550':
ColorPrint("Account: %s does not exist." % mail_rcptTo, flag="error", verbose=verbose)
ColorPrint("Done. ", flag="info", verbose=verbose)
return 0
ColorPrint("Account: %s exists." % mail_rcptTo, flag="success", verbose=verbose)
ColorPrint("After RCPT TO command: %s" % recv3, flag="debug", verbose=verbose)
quit = "QUIT\r\n"
clientSocket.send(quit.encode())
recv4 = clientSocket.recv(1024)
clientSocket.close()
ColorPrint("Done. " , flag="info", verbose=verbose)
return mail_rcptTo


if __name__ == "__main__":
'''
port = 25
verbose = False #详细输出,可输出debug信息
mailaddr = "xxxx@mail.com" # 需要验证的邮箱地址
mail_rcptTo = mailaddr
server = DNSQuery(mailaddr)
smtpsend(server, port, mail_rcptTo)
'''
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--email",
help="Verification Email Address.", required=True)
parser.add_argument("-s", "--server",
help="Smtp Server.")
parser.add_argument("-p", "--port",
help="Smtp Server Port.", default=25, type=int)
parser.add_argument("-v", "--verbose",
help="verbose info (choice in [True, False])", default=False, type=bool)

args = parser.parse_args()

port = args.port
verbose = args.verbose
mailaddr = args.email
mail_rcptTo = mailaddr
server = args.server
if server == None:
server = DNSQuery(mailaddr)
smtpsend(server, port, mail_rcptTo)

Reference

验证邮箱用户是否存在