diff --git a/controllers/api.go b/controllers/api.go index 8c009f11..88fe51bc 100644 --- a/controllers/api.go +++ b/controllers/api.go @@ -334,16 +334,11 @@ func API_Pages_Id(w http.ResponseWriter, r *http.Request) { JSONResponse(w, models.Response{Success: false, Message: "/:id and /:page_id mismatch"}, http.StatusBadRequest) return } - err = p.Validate() - if err != nil { - JSONResponse(w, models.Response{Success: false, Message: "Invalid attributes given"}, http.StatusBadRequest) - return - } p.ModifiedDate = time.Now() p.UserId = ctx.Get(r, "user_id").(int64) err = models.PutPage(&p) if err != nil { - JSONResponse(w, models.Response{Success: false, Message: "Error updating page"}, http.StatusInternalServerError) + JSONResponse(w, models.Response{Success: false, Message: "Error updating page: " + err.Error()}, http.StatusInternalServerError) return } JSONResponse(w, p, http.StatusOK) diff --git a/db/migrations/20160225173824_0.1.2_capture_credentials.sql b/db/migrations/20160225173824_0.1.2_capture_credentials.sql new file mode 100644 index 00000000..eff45de5 --- /dev/null +++ b/db/migrations/20160225173824_0.1.2_capture_credentials.sql @@ -0,0 +1,9 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +ALTER TABLE pages ADD COLUMN capture_credentials BOOLEAN; +ALTER TABLE pages ADD COLUMN capture_passwords BOOLEAN; + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back + diff --git a/models/models_test.go b/models/models_test.go index ec84a5b6..16811ae8 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -1,8 +1,10 @@ package models import ( + "strings" "testing" + "github.com/PuerkitoBio/goquery" "github.com/gophish/gophish/config" "gopkg.in/check.v1" ) @@ -61,6 +63,79 @@ func (s *ModelsSuite) TestPostGroupNoTargets(c *check.C) { c.Assert(err, check.Equals, ErrNoTargetsSpecified) } +func (s *ModelsSuite) TestPostPage(c *check.C) { + html := ` + +
+ + +
+ ` + p := Page{ + Name: "Test Page", + HTML: html, + } + // Check the capturing credentials and passwords + p.CaptureCredentials = true + p.CapturePasswords = true + err := PostPage(&p) + c.Assert(err, check.Equals, nil) + d, err := goquery.NewDocumentFromReader(strings.NewReader(p.HTML)) + c.Assert(err, check.Equals, nil) + forms := d.Find("form") + forms.Each(func(i int, f *goquery.Selection) { + // Check the action has been set + a, _ := f.Attr("action") + c.Assert(a, check.Equals, "") + // Check the password still has a name + _, ok := f.Find("input[type=\"password\"]").Attr("name") + c.Assert(ok, check.Equals, true) + // Check the username is still correct + u, ok := f.Find("input").Attr("name") + c.Assert(ok, check.Equals, true) + c.Assert(u, check.Equals, "username") + }) + // Check what happens when we don't capture passwords + p.CapturePasswords = false + p.HTML = html + err = PutPage(&p) + c.Assert(err, check.Equals, nil) + d, err = goquery.NewDocumentFromReader(strings.NewReader(p.HTML)) + c.Assert(err, check.Equals, nil) + forms = d.Find("form") + forms.Each(func(i int, f *goquery.Selection) { + // Check the action has been set + a, _ := f.Attr("action") + c.Assert(a, check.Equals, "") + // Check the password still has a name + _, ok := f.Find("input[type=\"password\"]").Attr("name") + c.Assert(ok, check.Equals, false) + // Check the username is still correct + u, ok := f.Find("input").Attr("name") + c.Assert(ok, check.Equals, true) + c.Assert(u, check.Equals, "username") + }) + // Finally, check when we don't capture credentials + p.CaptureCredentials = false + p.HTML = html + err = PutPage(&p) + c.Assert(err, check.Equals, nil) + d, err = goquery.NewDocumentFromReader(strings.NewReader(p.HTML)) + c.Assert(err, check.Equals, nil) + forms = d.Find("form") + forms.Each(func(i int, f *goquery.Selection) { + // Check the action has been set + a, _ := f.Attr("action") + c.Assert(a, check.Equals, "") + // Check the password still has a name + _, ok := f.Find("input[type=\"password\"]").Attr("name") + c.Assert(ok, check.Equals, false) + // Check the username is still correct + _, ok = f.Find("input").Attr("name") + c.Assert(ok, check.Equals, false) + }) +} + func (s *ModelsSuite) TestPutUser(c *check.C) { u, err := GetUser(1) u.Username = "admin_changed" diff --git a/models/page.go b/models/page.go index 23af64c3..0132aacc 100644 --- a/models/page.go +++ b/models/page.go @@ -2,27 +2,71 @@ package models import ( "errors" + "strings" "time" + + "github.com/PuerkitoBio/goquery" ) // Page contains the fields used for a Page model type Page struct { - Id int64 `json:"id" gorm:"column:id; primary_key:yes"` - UserId int64 `json:"-" gorm:"column:user_id"` - Name string `json:"name"` - HTML string `json:"html" gorm:"column:html"` - ModifiedDate time.Time `json:"modified_date"` + Id int64 `json:"id" gorm:"column:id; primary_key:yes"` + UserId int64 `json:"-" gorm:"column:user_id"` + Name string `json:"name"` + HTML string `json:"html" gorm:"column:html"` + CaptureCredentials bool `json:"capture_credentials" gorm:"column:capture_credentials"` + CapturePasswords bool `json:"capture_passwords" gorm:"column:capture_passwords"` + ModifiedDate time.Time `json:"modified_date"` } // ErrPageNameNotSpecified is thrown if the name of the landing page is blank. var ErrPageNameNotSpecified = errors.New("Page Name not specified") +// 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 { + passwordFields := f.Find("input[type=\"password\"]") + passwordFields.Each(func(j int, pass *goquery.Selection) { + pass.RemoveAttr("name") + }) + } + } 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 } - return nil + // If the user specifies to capture passwords, + // we automatically capture credentials + if p.CapturePasswords && !p.CaptureCredentials { + p.CaptureCredentials = true + } + return p.parseHTML() } // GetPages returns the pages owned by the given user. @@ -74,7 +118,8 @@ func PostPage(p *Page) error { // PutPage edits an existing Page in the database. // Per the PUT Method RFC, it presumes all data for a page is provided. func PutPage(p *Page) error { - err := db.Where("id=?", p.Id).Save(p).Error + err := p.Validate() + err = db.Where("id=?", p.Id).Save(p).Error if err != nil { Logger.Println(err) } diff --git a/static/css/main.css b/static/css/main.css index 309d6e4b..35bf33cd 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -458,3 +458,6 @@ td.details-control{ #refresh_message{ display:none; } +#capture_passwords { + display:none; +} diff --git a/static/js/app/landing_pages.js b/static/js/app/landing_pages.js index 2a6103df..e845bfdd 100644 --- a/static/js/app/landing_pages.js +++ b/static/js/app/landing_pages.js @@ -9,7 +9,10 @@ var pages = [] function save(idx) { var page = {} page.name = $("#name").val() - page.html = CKEDITOR.instances["html_editor"].getData(); + editor = CKEDITOR.instances["html_editor"] + page.html = editor.getData() + page.capture_credentials = $("#capture_credentials_checkbox").prop("checked") + page.capture_passwords = $("#capture_passwords_checkbox").prop("checked") if (idx != -1) { page.id = pages[idx].id api.pageId.put(page) @@ -36,6 +39,8 @@ function dismiss() { $("#modal\\.flashes").empty() $("#name").val("") $("#html_editor").val("") + $("#newLandingPageModal").find("input[type='checkbox']").prop("checked", false) + $("#capture_passwords").hide() $("#newLandingPageModal").modal('hide') } @@ -79,6 +84,11 @@ function edit(idx) { page = pages[idx] $("#name").val(page.name) $("#html_editor").val(page.html) + $("#capture_credentials_checkbox").prop("checked", page.capture_credentials) + $("#capture_passwords_checkbox").prop("checked", page.capture_passwords) + if (page.capture_credentials){ + $("#capture_passwords").show() + } } } @@ -177,5 +187,8 @@ $(document).ready(function() { } }, this)); }; + $("#capture_credentials_checkbox").change(function(){ + $("#capture_passwords").toggle() + }) load() }) diff --git a/templates/landing_pages.html b/templates/landing_pages.html index 6efe51f8..172079ee 100644 --- a/templates/landing_pages.html +++ b/templates/landing_pages.html @@ -80,6 +80,17 @@ +
+ + +
+
+ + +
+ Warning: Credentials are currently not encrypted. This means that captured passwords are stored in the database as cleartext. Be careful with this! +
+