Just in time for the holiday season, a new Monero cryptocurrency mining campaign has kicked off during the first weeks of December. Unlike the plethora of other mining malware thus far in 2017, this new campaign is a sophisticated multi-staged attack that leverages NSA-attributed EternalBlue and EternalSynergy attacks to opportunistically spread and 'worm' across victim networks. A recent F5 Labs report dubbed the campaign “Zealot” based on the name of the zip file containing the python scripts with the NSA-attributed exploits.
Given the nature of the campaign, it wasn't surprising to see Zealot activity hit RSA FirstWatch infrastructure. As noted in the F5 Labs report, clear Apache Struts CVE-2017-5638 exploit attempts were observed.
This 'apache struts exploit attempt' activity was detected and registered as meta in the Indicator of Compromise field of NetWitness Packets.
Interestingly enough, the expected DotNetNuke (DNN) vulnerability (CVE-2017-9822) exploits were not observed against our infrastructure; however, FirstWatch did observe previously unreported 'HTTPServerILServlet.java in JMS over HTTP Invocation Layer of JbossMQ' (CVE-2017-7504) exploit attempts in conjunction with the Zealot Campaign.
On the Windows side of these exploits, the payload observed was 'xmrig.exe', a popular Monero miner. (Note: Aeon mining was also noted in conjunction with Zealot activity).
This activity is detected by NetWitness Packets and 'monero mining' is registered as meta within the Indicators of Compromise field.
I was kind of curious about this and wanted to get some meta around these attempts. I wrote a parser that was working in my environment to both identify these java_post attempts as well as attempt to extract the command the attacker was trying to include in the POST.
I don't think it's perfect, but gave me enough to get started. The alert meta would go into the 'ioc' meta key as 'java_post_*****' where it would call out powershell, wget, curl, cmd, or bash. Since I had the command meta reporting into 'command' meta key which is not indexed, I created a report to give me that when the IOC hit.
Report rule looked like this:
And the report output looked like this:
Since 'Text' formatted meta keys have a limit of 256 bytes, its likely the command could be truncated in some of the cases, however I found I had enough to get a good indication of what it was doing. Furthermore, if I needed the entire command, I could just go back to the session if it was still available.
While the exploit attempts seem to be trying to deliver a crypto-coin miner as a payload, I suspect that payload could be changed for something else down the road. I suppose the commands could be examined for extracting additional IOC's, such as IP addresses, hostnames, downloaders and maybe even hashes, but I haven't automated that.
Parser attached.
lua_post_badness.lua
=============================================
-- Step 1 - Create parser
local lua_post_badness = nw.createParser("lua_post_badness", "Identify suspicious HTTP POSTs", "80")
--[[
DESCRIPTION
Identify suspicious HTTP POSTs
VERSION
2017-12-19 - Initial development
2017-12-29 - Added function to attempt command extraction
AUTHOR
christopher.ahearn@rsa.com
DEPENDENCIES
None
META KEYS
None
NOTES
None
--]]
-- Step 2 - Define meta keys to write meta into
-- declare the meta keys we'll be registering meta with
lua_post_badness:setKeys({
nwlanguagekey.create("ioc", nwtypes.Text),
nwlanguagekey.create("command", nwtypes.Text),
})
-- Step 4 - Do SOMETHING once your token matched
function lua_post_badness:sessionBegin()
-- reset global variables
foundpost = nil
end
function lua_post_badness:tokenPOST(token, first, last)
if nw.isRequestStream() then
foundpost = 1
end
end
function lua_post_badness:tokenJAVALang(token, first, last)
if foundpost == 1 then
if nw.isRequestStream() then
-- set position to byte match
current_position = last + 1
local payload = nw.getPayload(current_position, current_position + 4096)
local find_powershell = payload:find("powershell", 1, -1)
if find_powershell then
nw.createMeta(self.keys["ioc"], "java_post_powershell")
local cmdfind = payload:find("execuq", find_powershell, -1)
if cmdfind then
local foundcmd = payload:tostring(find_powershell, cmdfind -4)
if foundcmd then
local normalmsg = string.gsub(foundcmd, "\x74\x00", "\x20")
if normalmsg then
nw.createMeta(self.keys["command"], normalmsg)
end
end
end
return
end
local find_wget = payload:find("wget", 1, -1)
if find_wget then
nw.createMeta(self.keys["ioc"], "java_post_wget")
local cmdfind = payload:find("execuq", find_wget, -1)
if cmdfind then
local foundcmd = payload:tostring(find_wget, cmdfind -4)
if foundcmd then
local normalmsg = string.gsub(foundcmd, "\x74\x00", "\x20")
if normalmsg then
nw.createMeta(self.keys["command"], normalmsg)
end
end
end
return
end
local find_curl = payload:find("curl", 1, -1)
if find_curl then
nw.createMeta(self.keys["ioc"], "java_post_curl")
local cmdfind = payload:find("execuq", find_curl, -1)
if cmdfind then
local foundcmd = payload:tostring(find_curl, cmdfind -4)
if foundcmd then
local normalmsg = string.gsub(foundcmd, "\x74\x00", "\x20")
if normalmsg then
nw.createMeta(self.keys["command"], normalmsg)
end
end
end
return
end
local find_cmd = payload:find("cmd", 1, -1)
if find_cmd then
nw.createMeta(self.keys["ioc"], "java_post_cmd")
local cmdfind = payload:find("execuq", find_cmd, -1)
if cmdfind then
local foundcmd = payload:tostring(find_cmd, cmdfind -4)
if foundcmd then
--nw.logInfo("*** FOUND CMD: " .. foundcmd .. " ***")
local normalmsg = string.gsub(string.gsub(string.gsub(foundcmd, "\x74\x00", "\x20"),"\x02", ""),"cmd.exet../ct.", "cmd.exe /c ")
if normalmsg then
--nw.logInfo("*** NORMAL MSG: " .. normalmsg .. " ***")
nw.createMeta(self.keys["command"], normalmsg)
end
end
end
return
end
local find_bash = payload:find("bash", 1, -1)
if find_bash then
nw.createMeta(self.keys["ioc"], "java_post_bash")
local cmdfind = payload:find("execuq", find_bash, -1)
if cmdfind then
local foundcmd = payload:tostring(find_bash, cmdfind -4)
if foundcmd then
local normalmsg = string.gsub(foundcmd, "\x74\x00", "\x20")
if normalmsg then
nw.createMeta(self.keys["command"], normalmsg)
end
end
end
return
end
end
end
end
function lua_post_badness:tokenCMDString(token, first, last)
if foundpost == 1 then
if nw.isRequestStream() then
-- set position to byte match
current_position = last + 1
--nw.logInfo("*** POST COMMAND STRING TOKEN MATCH ***")
local payload = nw.getPayload(current_position, current_position + 1024)
local findstringf, findstringl = payload:find("<string>", 1, -1)
if findstringl then
current_position = findstringl + 1
local foundendf, foundendl = payload:find("</string>\10", current_position, -1)
if foundendf then
--nw.logInfo("*** FOUND END ***")
local foundcmd = payload:tostring(current_position, foundendf -1)
if foundcmd then
--nw.logInfo("*** FOUND CMD ***")
--nw.logInfo("*** " .. foundcmd .. " ***")
local findstring = string.find(foundcmd, "</string><string>")
if findstring then
--nw.logInfo("*** FOUND STRING ***")
local mycmd = string.gsub(foundcmd, "</string><string>", " ")
if mycmd then
--nw.logInfo("*** MY COMMAND " .. mycmd .. " ***")
nw.createMeta(self.keys["command"], mycmd)
end
else
nw.createMeta(self.keys["command"], foundcmd)
end
end
end
end
end
end
end
function lua_post_badness:tokenSTRINGMod(token, first, last)
if foundpost == 1 then
if nw.isRequestStream() then
-- set position to byte match
current_position = first + 8
local payload = nw.getPayload(current_position, current_position + 1024)
local findendstringf, findendstringl = payload:find("</string>", 1, -1)
if findendstringf then
local foundcmd = payload:tostring(1, findendstringf -1)
if foundcmd then
if #foundcmd > 10 then
local findstring = string.find(foundcmd, "</string><string>")
if findstring then
local mycmd = string.gsub(foundcmd, "</string><string>", " ")
if mycmd then
nw.createMeta(self.keys["command"], mycmd)
end
else
nw.createMeta(self.keys["command"], foundcmd)
end
end
end
end
end
end
end
function lua_post_badness:tokenARRAY(token, first, last)
if foundpost == 1 then
if nw.isRequestStream() then
-- set position to byte match
current_position = last + 1
--nw.logInfo("*** ARRAY TOKEN MATCH ***")
local payload = nw.getPayload(current_position, current_position + 8096)
local startoffieldf, startoffield = payload:find("<string>", 1 -1)
local endoffield = payload:find("</array>", 1, -1)
if startoffield and endoffield then
local mylist = payload:tostring(startoffield +1, endoffield -1)
if mylist then
found, foundpos = {}, 0
cmd = {}
repeat
loopagain = false
foundpos = string.find(mylist, "</string>", foundpos)
if foundpos then
foundpos = foundpos +1
table.insert(found, foundpos)
loopagain = true
end
until loopagain == false
start = 1
for i,v in ipairs(found) do
local mycmd = string.sub(mylist, start, v -1)
table.insert(cmd, mycmd)
start = string.find(mylist, "<string>", v, -1)
end
if #cmd >= 1 then
local mycommand = string.gsub(table.concat(cmd, " "),"< <string>", " ")
if mycommand then
nw.createMeta(self.keys["command"], mycommand)
--nw.logInfo("*** COMMAND: " .. mycommand .. " ***")
end
end
end
end
end
end
end
-- Step 3 - Define tokens that get you close to what you want
-- declare what tokens and events we want to match.
-- These do not have to be exact matches but just get you close to the data you want.
lua_post_badness:setCallbacks({
[nwevents.OnSessionBegin] = lua_post_badness.sessionBegin,
["^POST "] = lua_post_badness.tokenPOST, -- the carat ^ indicates beginning of line
["java.lang.String"] = lua_post_badness.tokenJAVALang,
["\60\99\111\109\109\97\110\100\62\10"] = lua_post_badness.tokenCMDString,
["<string>cmd"] = lua_post_badness.tokenSTRINGMod,
["<string>powershell"] = lua_post_badness.tokenSTRINGMod,
["<string>curl"] = lua_post_badness.tokenSTRINGMod,
["<string>wget"] = lua_post_badness.tokenSTRINGMod,
["<string>bash"] = lua_post_badness.tokenSTRINGMod,
["<string>/bin/bash"] = lua_post_badness.tokenSTRINGMod,
["<array\32class=\34java.lang.String\34"] = lua_post_badness.tokenARRAY,
})
=============================================