捕鱼达人——钓鱼基础设施的应用分析

阅读量    153039 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

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的邮箱系统搭建。首先,申请域名,解析信息配置如下:

12

同时在VPS上配置主机的域名信息:

29

配置好域名,在服务器上安装iRedMail。

iRedMail是一个开源、免费的邮件服务器项目,其核心组件及其对应的功能主要有:Postfix: SMTP 服务器,Dovecot: POP3/IMAP/Managesieve 服务器,Apache: Web 服务器,MySQL: 用于存储其它程序的数据,也可用于存储邮件帐号。

搭建过程及注意事项,可参考iRedMail官网手册,本文不展开陈述。

注意在安装的过程中,会提示到是否使用由iRedMail提供的防火墙规则,这里根据个人情况选择即可。但从安全的角度出发,还是一定要给服务器上防火墙规则。本文仅做测试学习,暂不启用防火墙策略。

30

安装完成后,进入邮箱管理员界面,添加两名用户:

1

2

登录邮箱界面,即可实现基本邮件发送功能:

35

注:通过cobalt strike发送邮件,要开启邮箱服务器smtp的sasl身份验证。通过修改邮件服务器postfix配置文件的smtpd_sasl_auth_enable = yes即可:

34

测试通过telnet smtp和pop3发送接收邮件:

36

33

 

0x02 钓鱼初试

下面演示基于Cobalt Strike的钓鱼邮件功能

cobalt strike中集成的邮件钓鱼功能模块如图:

10

要配置的信息包括:

1. Targets : 钓鱼的目标

这里以jason1为钓鱼目标对象

13

2. Targets : 钓鱼邮件模板

在真实钓鱼环境中,为使邮件具备以假乱真的欺骗性,通常结合社会工程学手段,精心构造邮件内容。本文以演示为目的,构造一个简单的邮件,并将其内容存储为mail-template.eml文件。

31

32

3. Attachment:邮件附件,一般为恶意文档

可利用cobalt strike 的attack工具包,生成 macro 宏,插入到文档中,制作简易的恶意文档,或采取其他多种方式构造。

4. Embed URL:嵌入的钓鱼链接

可利用cobalt strike的attack工具包,克隆网站,构造钓鱼网站。这里随意找一个网站的登录页面,克隆到自己的服务器上。

3

4

在真实环境中,一般是针对目标的关键敏感信息构造钓鱼页面,包括邮箱身份信息、银行卡信息、通讯社交软件信息等,或是在已经突破进入目标内网后,想要获取内网办公系统、企业内网资源管理系统等信息。

5. Mail Server是邮件服务器的地址和身份信息,Bounce TO模仿发件人,正常填写即可

7

9

8

如图所示,目标接收到钓鱼邮件。邮件内记录到,邮件是从cobalt strike服务器发送出去的。且邮件内的连接地址已经替换为克隆后的服务器链接,当用户跳转进入该页面,输入登录信息,cobalt strike控制台就能观察到其输入信息。

38

还可以结合cobalt strike的listener,采用向邮件中添加恶意附件、替换链接为恶意程序等方式,让目标Beacon上线,此部分非本文演示的重点。

6

 

0x03 再上层楼

cobalt strike虽然集成了邮件钓鱼功能,但在某些情况下,显然不是最佳选项。正所谓,用专业的工具做专业的事。针对邮件钓鱼,也有不少专门的工具,例如Gophish,Gophish项目地址

Gophish是为企业和渗透测试人员设计的开源网络钓鱼工具包。 它提供快速、简易地设置和执行网络钓鱼攻击的能力。

Gophish安装完成后,存在两个页面,一个是钓鱼页面,一个是后台管理控制页面。

本文的实验环境将Gophish直接安装在邮件服务器上(fxxkxxxxxx.xxxxx),但在真实的环境中,应尽量遵循功能分割的原则

钓鱼页面(85端口,可通过配置文件更改):

20

后台管理控制页面(3333端口,可通过配置文件更改):

21

下面将介绍如何基于Gophish来实现邮件钓鱼

1-Sending Profile:设置发件策略

其主要内容是,把用来执行发送邮件操作的发送者邮箱配置信息提供给Gophish。

25

其中几个关键字段:

  • Interface Type:接口类型,默认为smtp且不可更改,因此发送邮件的邮箱需要开启SMTP服务。
  • Host: SMTP服务器地址
  • Username: SMTP服务器认证的用户名
  • Password: SMTP服务器认证的口令
  • Email Headers:自定义邮件头部信息,可按需填写。

填写好相应信息后,可通过Send Test Email发送测试邮件,确认发件邮箱能否正常发送邮件:

27

2-Landing Pages:目标钓鱼页面

主要内容是设置钓鱼邮件内,超链接指向的钓鱼网页,比如银行账号登录页面、社交账号登录页面等。

19

Import Site导入超链接,会自动解析html,我们以随便搜到的某一个后台登录页面为例。

勾选Capture Submitted Data,即捕获受害者在钓鱼页面登录时的提交数据。

最下端的Redirect to,填入提交数据后跳转的页面,为给受害者营造一种输错账号密码的假象,可填入登录页面的连接,并在URL后填入一个报错参数,https://xxxxx/account/login?errorcode=1

3-Email Templates:邮件模板

主要是发送给受害者的邮件内容模板。通常是在社会工程学的基础上,分析目标特征,发送具有针对性的内容,这里简单构造一个公司发送内部福利的邮件内容。

26

直接通过Import Email导入邮件模板,记得勾选Change Links to Point to Landing Page选项,将邮件内容里的超链接替换为钓鱼页面链接。

Add Tracking Image在邮件末尾添加一个跟踪图像,可以掌握受害者是否打开了钓鱼邮件。

Add Files可添加附件,增强邮件的欺骗性,同时可结合免杀木马使用。

4-Users and Groups:目标用户和组

28

可直接导入csv格式的文件,批量导入用户;也可手工添加。这里以jason1和jason2两名同志为目标。

5-Campaign:执行钓鱼行动

Campaign部分是整合上述各个子环节,综合起来执行钓鱼行动。

17

依次填入邮件模板、目标钓鱼页面、发件策略、目标用户信息。

其中,需要注意的是URL选项。在上文中提到了,gophish启动运行后,我们配置了85端口运行钓鱼页面,3333端口运行后台管理控制页面。当我们启动Campaign事件,85端口会运行我们在Landing Pages部分配置的钓鱼页面,即某后台登录页面。因此,此处的URL选项填入运行gophish的服务器地址。此外,还需保证此地址对于目标受害者的网络环境来说是可访问的。当填写公网vps的IP地址或域名时,需确保目标内网环境能够访问公网vps,即需要注意防火墙策略等。当填写内网ip地址时,则意味着钓鱼目标页面只能够被目标内网访问,外部网络环境无法访问。当然,也可以输入其他方式搭建的钓鱼服务器地址。

而后受害者jason1就会接受到钓鱼邮件:

18

需要注意的是,在邮件内有提示To protect your privacy remote resources have been blocked,字面意思很简单,就是为了保护隐私,远程资源被禁掉了。出现提示的原因,是因为我们在Email Template部分,启用了Add Tracking Image选项,会在邮件末尾追加一个远程图像资源<img>,通过受害者请求加载资源来判定目标是否打开了邮件:

22

如图所示,在邮件最后有一个<img>,其位于gophish服务器端,并通过rid来识别受害者。

点击邮件内的超链接,将跳转到我们在Landing Page部分布局的钓鱼后台登录页面:

16

输入用户名、口令,登录后,页面将跳转进入真实的后台登录页面,并在url处可看到我们添加的参数,errorcode=1,让受害者以为输错了登录信息,增强欺骗性。

15

此时,gophish控制后台已经捕捉记录到受害者的信息:

23

上图显示,两个目标中,有一个打开了邮件,并点击了钓鱼超链接,输入了登录信息,此人即是jason1:

24

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出站端口,来解决此问题。

39

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数据库中的记录如下:

37

需要注意的是,此时存入数据库的html页面,是经过parseHTML()函数处理的页面。例如f.SetAttr("action", "")代码会将form表单的action属性置空

5

上图即为处理前后的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)将重定向页面(通常是真实的登录页面)返回给受害者。

14

11

这里想要说明的是,红队人员通过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被忽略。

仅以此文作技术交流,恪守职业操守,严禁非法之用。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多