0x00 前言
邮件钓鱼攻击是一种常见的网络攻击方法,无论是广撒网式的“大海捞鱼”,还是极具精准化、定制化的鱼叉式钓鱼,都越来越普遍。
在红蓝模拟对抗的场景下,邮件钓鱼攻击也逐渐演变为一种精准、高效的打点手段,或通过恶意链接窃取敏感信息数据,或结合恶意附件夺控目标权限。
本文将从红队基础设施构建的角度出发,结合Cobalt Strike、iRedMail、Gophish等工具,探索邮件钓鱼基本架构的应用。
本文旨在交流技术,严禁从事不正当活动。网络不是法外之地,争做遵纪守法好公民。
0x01 基本设置
基本环境信息:
- mail vps: xx.xx.xx.52
- domain: fxxkxxxxxx.xxxxx
- cobalt strike vps: xx.xx.xx.155
- 本地局域网出口:xx.xx.xx.35
发送钓鱼邮件的第一步,是先拥有发送者邮箱,常见的如Gmail、Hotmail、163等第三方邮箱。但在真实的钓鱼场景中,一般不会使用此类邮箱。其原因一是容易暴露身份,且邮箱的欺骗性较低,效果不好;二是第三方邮箱通常会有多种限制、严格的过滤规则等,在发送大批量邮件时受限。
因此,常见的模式是基于目标的信息搜集,申请近似的域名,并使用自己的服务器搭建邮箱系统来发送邮件。
如何让申请的域名更具欺骗性、迷惑性,也深有讲究,本文不多赘述,仅申请一无关域名做测试学习使用。
下面简介基于iRedMail的邮箱系统搭建。首先,申请域名,解析信息配置如下:
同时在VPS上配置主机的域名信息:
配置好域名,在服务器上安装iRedMail。
iRedMail是一个开源、免费的邮件服务器项目,其核心组件及其对应的功能主要有:Postfix: SMTP 服务器,Dovecot: POP3/IMAP/Managesieve 服务器,Apache: Web 服务器,MySQL: 用于存储其它程序的数据,也可用于存储邮件帐号。
搭建过程及注意事项,可参考iRedMail官网手册,本文不展开陈述。
注意在安装的过程中,会提示到是否使用由iRedMail提供的防火墙规则,这里根据个人情况选择即可。但从安全的角度出发,还是一定要给服务器上防火墙规则。本文仅做测试学习,暂不启用防火墙策略。
安装完成后,进入邮箱管理员界面,添加两名用户:
登录邮箱界面,即可实现基本邮件发送功能:
注:通过cobalt strike发送邮件,要开启邮箱服务器smtp的sasl身份验证。通过修改邮件服务器postfix配置文件的smtpd_sasl_auth_enable = yes
即可:
测试通过telnet smtp和pop3发送接收邮件:
0x02 钓鱼初试
下面演示基于Cobalt Strike的钓鱼邮件功能
cobalt strike中集成的邮件钓鱼功能模块如图:
要配置的信息包括:
1. Targets : 钓鱼的目标
这里以jason1
为钓鱼目标对象
2. Targets : 钓鱼邮件模板
在真实钓鱼环境中,为使邮件具备以假乱真的欺骗性,通常结合社会工程学手段,精心构造邮件内容。本文以演示为目的,构造一个简单的邮件,并将其内容存储为mail-template.eml
文件。
3. Attachment:邮件附件,一般为恶意文档
可利用cobalt strike 的attack工具包,生成 macro 宏,插入到文档中,制作简易的恶意文档,或采取其他多种方式构造。
4. Embed URL:嵌入的钓鱼链接
可利用cobalt strike的attack工具包,克隆网站,构造钓鱼网站。这里随意找一个网站的登录页面,克隆到自己的服务器上。
在真实环境中,一般是针对目标的关键敏感信息构造钓鱼页面,包括邮箱身份信息、银行卡信息、通讯社交软件信息等,或是在已经突破进入目标内网后,想要获取内网办公系统、企业内网资源管理系统等信息。
5. Mail Server是邮件服务器的地址和身份信息,Bounce TO模仿发件人,正常填写即可
如图所示,目标接收到钓鱼邮件。邮件内记录到,邮件是从cobalt strike服务器发送出去的。且邮件内的连接地址已经替换为克隆后的服务器链接,当用户跳转进入该页面,输入登录信息,cobalt strike控制台就能观察到其输入信息。
还可以结合cobalt strike的listener,采用向邮件中添加恶意附件、替换链接为恶意程序等方式,让目标Beacon上线,此部分非本文演示的重点。
0x03 再上层楼
cobalt strike虽然集成了邮件钓鱼功能,但在某些情况下,显然不是最佳选项。正所谓,用专业的工具做专业的事。针对邮件钓鱼,也有不少专门的工具,例如Gophish,Gophish项目地址。
Gophish是为企业和渗透测试人员设计的开源网络钓鱼工具包。 它提供快速、简易地设置和执行网络钓鱼攻击的能力。
Gophish安装完成后,存在两个页面,一个是钓鱼页面,一个是后台管理控制页面。
本文的实验环境将Gophish直接安装在邮件服务器上(fxxkxxxxxx.xxxxx),但在真实的环境中,应尽量遵循功能分割的原则
钓鱼页面(85端口,可通过配置文件更改):
后台管理控制页面(3333端口,可通过配置文件更改):
下面将介绍如何基于Gophish来实现邮件钓鱼
1-Sending Profile:设置发件策略
其主要内容是,把用来执行发送邮件操作的发送者邮箱配置信息提供给Gophish。
其中几个关键字段:
- Interface Type:接口类型,默认为smtp且不可更改,因此发送邮件的邮箱需要开启SMTP服务。
- Host: SMTP服务器地址
- Username: SMTP服务器认证的用户名
- Password: SMTP服务器认证的口令
- Email Headers:自定义邮件头部信息,可按需填写。
填写好相应信息后,可通过Send Test Email
发送测试邮件,确认发件邮箱能否正常发送邮件:
2-Landing Pages:目标钓鱼页面
主要内容是设置钓鱼邮件内,超链接指向的钓鱼网页,比如银行账号登录页面、社交账号登录页面等。
Import Site
导入超链接,会自动解析html,我们以随便搜到的某一个后台登录页面为例。
勾选Capture Submitted Data
,即捕获受害者在钓鱼页面登录时的提交数据。
最下端的Redirect to
,填入提交数据后跳转的页面,为给受害者营造一种输错账号密码的假象,可填入登录页面的连接,并在URL后填入一个报错参数,https://xxxxx/account/login?errorcode=1
3-Email Templates:邮件模板
主要是发送给受害者的邮件内容模板。通常是在社会工程学的基础上,分析目标特征,发送具有针对性的内容,这里简单构造一个公司发送内部福利的邮件内容。
直接通过Import Email
导入邮件模板,记得勾选Change Links to Point to Landing Page
选项,将邮件内容里的超链接替换为钓鱼页面链接。
Add Tracking Image
在邮件末尾添加一个跟踪图像,可以掌握受害者是否打开了钓鱼邮件。
Add Files
可添加附件,增强邮件的欺骗性,同时可结合免杀木马使用。
4-Users and Groups:目标用户和组
可直接导入csv格式的文件,批量导入用户;也可手工添加。这里以jason1和jason2两名同志为目标。
5-Campaign:执行钓鱼行动
Campaign
部分是整合上述各个子环节,综合起来执行钓鱼行动。
依次填入邮件模板、目标钓鱼页面、发件策略、目标用户信息。
其中,需要注意的是URL
选项。在上文中提到了,gophish启动运行后,我们配置了85端口运行钓鱼页面,3333端口运行后台管理控制页面。当我们启动Campaign事件,85端口会运行我们在Landing Pages
部分配置的钓鱼页面,即某后台登录页面。因此,此处的URL
选项填入运行gophish的服务器地址。此外,还需保证此地址对于目标受害者的网络环境来说是可访问的。当填写公网vps的IP地址或域名时,需确保目标内网环境能够访问公网vps,即需要注意防火墙策略等。当填写内网ip地址时,则意味着钓鱼目标页面只能够被目标内网访问,外部网络环境无法访问。当然,也可以输入其他方式搭建的钓鱼服务器地址。
而后受害者jason1就会接受到钓鱼邮件:
需要注意的是,在邮件内有提示To protect your privacy remote resources have been blocked
,字面意思很简单,就是为了保护隐私,远程资源被禁掉了。出现提示的原因,是因为我们在Email Template
部分,启用了Add Tracking Image
选项,会在邮件末尾追加一个远程图像资源<img>,通过受害者请求加载资源来判定目标是否打开了邮件:
如图所示,在邮件最后有一个<img>,其位于gophish服务器端,并通过rid
来识别受害者。
点击邮件内的超链接,将跳转到我们在Landing Page
部分布局的钓鱼后台登录页面:
输入用户名、口令,登录后,页面将跳转进入真实的后台登录页面,并在url处可看到我们添加的参数,errorcode=1
,让受害者以为输错了登录信息,增强欺骗性。
此时,gophish控制后台已经捕捉记录到受害者的信息:
上图显示,两个目标中,有一个打开了邮件,并点击了钓鱼超链接,输入了登录信息,此人即是jason1:
jason1的结果中,成功记录到了提交的用户名和口令信息。
0x04 写在文末
有几点个人思考和分析,同大家分享:
1. 服务器SMTP出口
无论是采用cobalt strike还是gophish,在真实的环境中,其实都应该遵循功能独立分割的原则。即每台服务器只完成特定的功能,其目的就是在于提高红队基础设施的健壮性、可扩展性、弹性恢复能力。
具体而言,cs server和 gophish server都会通过SMTP协议与mail server连接并发送邮件。即cs server、gophish server、mail server都需要连接到目标的25端口SMTP服务,cs server、gophish server、mail server需放行25出站端口。
部分的VPS或者ISP会默认封禁25出站端口,其目的在于防止大量发送垃圾邮件。例如某厂商的vps,其官方文档中有提到,对于某些服务器实例,将会封禁smtp出站端口,且其屏蔽SMTP的技术不是屏蔽25端口,而是只要是SMTP出站包都会被ban掉。可以向其官方提交工单,申请开放25出站端口,来解决此问题。
2. 钓鱼页面的数据捕获问题
利用gophish来clone制作钓鱼页面,仅仅通过Import难以实现完美克隆,可能无法满足钓鱼的功能需求,通常需要手动修正。
最为常见的问题就是:无法捕获受害者在钓鱼页面提交的数据。
首先看一下gophish
的源码中,对钓鱼页面部分的处理过程
在Landing Page
功能部分,通过Import
导入钓鱼页面后,其解析处理html页面代码如下:
// parseHTML parses the page HTML on save to handle the
// capturing (or lack thereof!) of credentials and passwords
func (p *Page) parseHTML() error {
d, err := goquery.NewDocumentFromReader(strings.NewReader(p.HTML))
if err != nil {
return err
}
forms := d.Find("form")
forms.Each(func(i int, f *goquery.Selection) {
// We always want the submitted events to be
// sent to our server
f.SetAttr("action", "")
if p.CaptureCredentials {
// If we don't want to capture passwords,
// find all the password fields and remove the "name" attribute.
if !p.CapturePasswords {
inputs := f.Find("input")
inputs.Each(func(j int, input *goquery.Selection) {
if t, _ := input.Attr("type"); strings.EqualFold(t, "password") {
input.RemoveAttr("name")
}
})
} else {
// If the user chooses to re-enable the capture passwords setting,
// we need to re-add the name attribute
inputs := f.Find("input")
inputs.Each(func(j int, input *goquery.Selection) {
if t, _ := input.Attr("type"); strings.EqualFold(t, "password") {
input.SetAttr("name", "password")
}
})
}
} else {
// Otherwise, remove the name from all
// inputs.
inputFields := f.Find("input")
inputFields.Each(func(j int, input *goquery.Selection) {
input.RemoveAttr("name")
})
}
})
p.HTML, err = d.Html()
return err
}
// Validate ensures that a page contains the appropriate details
func (p *Page) Validate() error {
if p.Name == "" {
return ErrPageNameNotSpecified
}
// If the user specifies to capture passwords,
// we automatically capture credentials
if p.CapturePasswords && !p.CaptureCredentials {
p.CaptureCredentials = true
}
if err := ValidateTemplate(p.HTML); err != nil {
return err
}
if err := ValidateTemplate(p.RedirectURL); err != nil {
return err
}
return p.parseHTML()
}
// PostPage creates a new page in the database.
func PostPage(p *Page) error {
err := p.Validate()
if err != nil {
log.Error(err)
return err
}
// Insert into the DB
err = db.Save(p).Error
if err != nil {
log.Error(err)
}
return err
}
其整体流程是,先Validate
验证各个字段的详细情况,然后parseHTML
解析目标页面,并将结果db.Save(p)
保存到数据库中。
sqlite数据库中的记录如下:
需要注意的是,此时存入数据库的html页面,是经过parseHTML()
函数处理的页面。例如f.SetAttr("action", "")
代码会将form表单的action
属性置空
上图即为处理前后的html源码对比,可以观察到action=""
。即表示,当受害者在钓鱼页面post
提交数据,把数据提交到当前钓鱼页面,而非真实的登录页面,否则钓鱼后台将无法捕获到用户提交的数据。
当受害者在钓鱼页面输入了身份信息并提交后,其后台处理代码主流程如下:
// PhishHandler handles incoming client connections and registers the associated actions performed
// (such as clicked link, etc.)
func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) {
r, err := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
if err != ErrInvalidRequest && err != ErrCampaignComplete {
log.Error(err)
}
http.NotFound(w, r)
return
}
w.Header().Set("X-Server", config.ServerName) // Useful for checking if this is a GoPhish server (e.g. for campaign reporting plugins)
var ptx models.PhishingTemplateContext
// Check for a preview
if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
ptx, err = models.NewPhishingTemplateContext(&preview, preview.BaseRecipient, preview.RId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
p, err := models.GetPage(preview.PageId, preview.UserId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
renderPhishResponse(w, r, ptx, p)
return
}
rs := ctx.Get(r, "result").(models.Result)
rid := ctx.Get(r, "rid").(string)
c := ctx.Get(r, "campaign").(models.Campaign)
d := ctx.Get(r, "details").(models.EventDetails)
// Check for a transparency request
if strings.HasSuffix(rid, TransparencySuffix) {
ps.TransparencyHandler(w, r)
return
}
p, err := models.GetPage(c.PageId, c.UserId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
switch {
case r.Method == "GET":
err = rs.HandleClickedLink(d)
if err != nil {
log.Error(err)
}
case r.Method == "POST":
err = rs.HandleFormSubmit(d)
if err != nil {
log.Error(err)
}
}
ptx, err = models.NewPhishingTemplateContext(&c, rs.BaseRecipient, rs.RId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
}
renderPhishResponse(w, r, ptx, p)
}
// renderPhishResponse handles rendering the correct response to the phishing
// connection. This usually involves writing out the page HTML or redirecting
// the user to the correct URL.
func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.PhishingTemplateContext, p models.Page) {
// If the request was a form submit and a redirect URL was specified, we
// should send the user to that URL
if r.Method == "POST" {
if p.RedirectURL != "" {
redirectURL, err := models.ExecuteTemplate(p.RedirectURL, ptx)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
}
// Otherwise, we just need to write out the templated HTML
html, err := models.ExecuteTemplate(p.HTML, ptx)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
w.Write([]byte(html))
}
// HandleFormSubmit updates a Result in the case where the recipient submitted
// credentials to the form on a Landing Page.
func (r *Result) HandleFormSubmit(details EventDetails) error {
event, err := r.createEvent(EventDataSubmit, details)
if err != nil {
return err
}
r.Status = EventDataSubmit
r.ModifiedDate = event.Time
return db.Save(r).Error
}
func (r *Result) createEvent(status string, details interface{}) (*Event, error) {
e := &Event{Email: r.Email, Message: status}
if details != nil {
dj, err := json.Marshal(details)
if err != nil {
return nil, err
}
e.Details = string(dj)
}
AddEvent(e, r.CampaignId)
return e, nil
}
// AddEvent creates a new campaign event in the database
func AddEvent(e *Event, campaignID int64) error {
e.CampaignId = campaignID
e.Time = time.Now().UTC()
whs, err := GetActiveWebhooks()
if err == nil {
whEndPoints := []webhook.EndPoint{}
for _, wh := range whs {
whEndPoints = append(whEndPoints, webhook.EndPoint{
URL: wh.URL,
Secret: wh.Secret,
})
}
webhook.SendAll(whEndPoints, e)
} else {
log.Errorf("error getting active webhooks: %v", err)
}
return db.Save(e).Error
}
其主要流程是是,通过HandleFormSubmit()->createEvent()->AddEvent()
将用户的相关数据存入数据库中的event,然后renderPhishResponse()->http.Redirect(w, r, redirectURL, http.StatusFound)
将重定向页面(通常是真实的登录页面)返回给受害者。
这里想要说明的是,红队人员通过Import
导入钓鱼页面后,parseHTML()
函数解析钓鱼页面时,其对目标钓鱼页面有一定的规则、格式上的要求。否则,会导致无法捕获受害者提交的表单数据,或数据不全。
参考此篇博文中的说明:
比如:导入的前端源码,必须存在严格存在
<form method="post" ···><input name="aaa" ··· /> ··· <input type="submit" ··· /></form>
结构,即表单(POST方式)— Input标签(具有name属性)Input标签(submit类型)— 表单闭合的结构。再如:对于需要被捕获的表单数据,除了input标签需要被包含在
<form>
中,还需满足该<input>
存在name属性。例如<input name="username">
,否则会因为没有字段名而导致value被忽略。
仅以此文作技术交流,恪守职业操守,严禁非法之用。
发表评论
您还未登录,请先登录。
登录