My website went down for around 1:30 to 2 hours today, which was a little odd. After it came back up I noticed the 15min load was > 30. Turns out someone tried to log into WordPress which isn’t that odd. It happens all the time, so much so that I run Limit Login Attempts plugin to apply a 20min timeout a IP address after 4 incorrect attempts (and then a 24 hour timeout after 16 attempts). This time though, it was a kind of a large attack.

In 118 minutes, 2337 different IP addresses tried to log into WordPress. Most were timed-out after 4 attempts, but due to oom-killer sometimes the timeout details weren’t added to the database and more than 4 attempts were made (Two ips made > 26 attempts each, though only 8 IPs made 7 or more attempts). Using a Geo IP database we can quickly get a breakdown of where the IPs came from:

Country Attempts
Russia 1786
Ukraine 1225
Vietnam 909
Thailand 817
Taiwan 613
Romania 380
Turkey 379
Bulgaria 342
Iran 278
Belarus 276
India 241
Poland 164
Serbia 159
Egypt 146
Hungary 137
Brazil 122
United States of America 121
Canada 115
South Africa 115
Other (73 other unique countries) 1762

The user agent for every attempt was “Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20100101 Firefox/19.0″ which is a valid user agent for FF19 running Windows 7. I’m guessing it was someone’s small botnet. Though the distribution of the countries seems a bit off? Maybe it’s a compromised FF addon? If it continues I may need to switch to a whitelist for logins, or move the admin login page elsewhere as current login limit plugin isn’t really suited to an attack from such a large number of IP addresses.

Code

__author__ = 'Matthew'

import pygeoip

ACCESS_FILENAME = 'access.log'

GEOIP = pygeoip.Database('GeoIP.dat')

ip_breakdown = {}
country_breakdown = {}

for line in open(ACCESS_FILENAME):
    if 'wp-login' not in line:
        continue

    ip = line[0:line.find(' ')]

    if not ip_breakdown.has_key(ip):
        ip_breakdown[ip] = 0

    ip_breakdown[ip] += 1

    ########

    info = GEOIP.lookup(ip)
    if not info.country:
        country = 'unknown'
    else:
        country = info.country

    if not country_breakdown.has_key(country):
        country_breakdown[country] = 0

    country_breakdown[country] += 1


print 'Number ips:', len(ip_breakdown.keys())
print 'Number countries:', len(country_breakdown.keys())

for country in sorted(country_breakdown, key=country_breakdown.get, reverse=True):
    print country, country_breakdown[country]

Output

Number ips: 2337
Number countries: 92
RU 1786
UA 1225
VN 909
TH 817
TW 613
RO 380
TR 379
BG 342
IR 278
BY 276
IN 241
PL 164
RS 159
EG 146
HU 137
BR 122
US 121
CN 115
SA 115
PK 93
HK 91
CZ 90
ID 82
KZ 78
GE 74
GB 73
SK 63
MD 59
DE 57
AE 56
GR 53
AZ 50
IQ 45
ES 44
BD 43
IL 43
CA 39
AR 36
NL 34
CO 34
CL 32
LV 32
MY 30
AT 29
IT 28
KG 22
BE 21
DK 19
JP 18
QA 17
ZA 15
LK 15
AU 15
LT 14
MX 13
MO 12
KH 12
PH 11
NO 11
PS 10
SE 10
MA 9
MN 9
CY 9
AM 9
BA 8
PY 8
FR 8
GH 7
EE 7
YE 7
DZ 7
PT 6
A2 6
OM 4
HR 4
EC 4
NZ 4
LB 4
LA 3
JO 2
PE 2
MK 2
MZ 2
BH 1
AO 1
ET 1
UZ 1
NP 1
CD 1
SY 1
SD 1