Over the last few months I've had increasing SPAM traffic coming to my site, usually from a few select IP address groups. Most of this site spamming crap that I actually catch tries to simply POST to every possible URL on the site apparently that contains a FORM tag, spewing forth a plethora of links. Now typically this isn't much of a problem because the POST operatoins tend to fail in ASP.NET either with a ViewState violation or more commonly dangerous request content which is simply refused.
However, I do see the content in my error logs which report any errors that occur in various applications and while I could simply ignore ViewState and RequestValidation errors I prefer not to have to look at this SPAM crap.
So I broke down and created a little application to start adding IP Addresses to the IIS restriction list and until recently I used this list manually. The list is now something like 500 addresses long and it's cut back significantly on the amount of POST spam that comes into the site as a whole. While it may seem like a lot of this stuff is coming from random IP addresses much of it appears comes from a repeating pool of addresses so with a bit of diligence it's possible to make a dent in this.
It's certainly not an end all solution as IP spoofing can be used to generate a never ending supply of IP addresses, but this simple approach has resulted in a significant cutbackof the offending traffic.
Blocking IP Addresses
Blocking IP Addresses in IIS involves setting up an IP Deny list, which if you're using IIS 6 can be set up from the Directory Security | IP Address and Domain Name Restrictions dialog. You can basically set up IP addresses to allow or restrict:
The focus for what I needed to do is block IP addresses so you set all computers have access and the create individual Deny entries for each IP Address or IP Address block to deny.
To deny a block of addresses you need to apply Subnet Masks - the mask basicly allows telling how many addresses in a subnet block to block. A Subnet mask of 255.255.255.255 maps exactly one IP address. While 255.255.255.0 will map an entire block when using 192.168.1.* (the 0 in the subnet mask basically says no mask on the final block so include all 255 entries).
Internally IIS stores these values in IPSecurity section of the metabase/configuration where it stores both the IP Address and subnet mask. For example:
192.168.1.101,255.255.255.255
64.44.123.0,255.255.255.0
where the first will block exactly one IP address, where the latter will block all in 64.44.123.*.
Interesting note on IIS 7 - it appears that the IP Address restriction list is not available through the IIS UI any longer - any additions to this interface have to be made programmatically or via the command line tool. I couldn't even find my entries in the ApplicationHost.config file - not quite sure where these values are actually stored.
Creating a quick and dirty Web based Tool
You can get at this info with the IISAdmin objects quite easily and in order to work with this I created a class to handle the logic to retireve a list of blocked IP Addresses and to set it in a more simplified fashion with a Web interface.
The idea is that I can quickly view and add IP Addresses to the list and I can copy the list between the various Web sites on this server simply by cutting and pasting. Again this isn't an end all solution but I've had a number of occasions where blocking several IPAddresses quickly (usually with a Robot gone wild) can stop a log jam on the server.
The key code to do this is the class below that handles the interaction with the IP Security functionality in IIS. Note that you'll need to run as an Admin or SYSTEM in order to be able to execute any of this code (more on this below).
/// <summary>
/// Allows reading and setting the IIS Blocked IP List for a given Web site
///
/// Note:
/// These routines only handle simple IP addressing rules. It handles
/// the IPDeny list only and works only with full IP Addresses or
/// Subnet blocked wildcard IP Addresses. If you use domain name
/// lookups don't use these routines to update!
///
/// These routines require ADMIN or System level permissions in
/// order to set IIS configuration settings.
///
/// Disclaimer:
/// Use at your own risk. These routines modify the IIS metabase
/// and therefore have the potential to muck up your configuration.
/// YOU ARE FULLY RESPONSIBLE FOR ANY PROBLEMS THAT THESE ROUTINES
/// MIGHT CAUSE TO YOUR CONFIGURATION!
/// </summary>
public class IISBlockedIpList : IDisposable
{
public string MetaBasePath
{
get { return _MetaBasePath; }
set { _MetaBasePath = value; }
}
private string _MetaBasePath = "IIS://localhost/W3SVC/1/ROOT";
private DirectoryEntry IIS;
public IISBlockedIpList(string metaBasePath)
{
if (!string.IsNullOrEmpty(metaBasePath))
this.MetaBasePath = metaBasePath;
}
/// <summary>
/// Returns a list of Ips as a plain string returning just the
/// IP Addresses, leaving out the subnet mask values.
///
/// Any wildcarded IP Addresses will return .0 for the
/// wild card characters.
/// </summary>
/// <returns></returns>
public string[] GetIpList()
{
this.Open();
// *** Grab the IP List
object IPSecurity = IIS.Properties["IPSecurity"].Value;
// retrieve the IPDeny list from the IPSecurity object. Note: Strings as objects
//Array origIPDenyList = (Array)wwUtils.GetPropertyCom(IPSecurity, "IPDeny");
Array origIPDenyList = (Array)
IPSecurity.GetType().InvokeMember("IPDeny",
BindingFlags.Public |
BindingFlags.Instance | BindingFlags.GetProperty,
null, IPSecurity, null);
this.Close();
// *** Format and Extract into a string list
List<string> Ips = new List<string>();
foreach (string IP in origIPDenyList)
{
// *** Strip off the subnet-mask - we'll use .0 or .* to represent
string TIP = IP.Substring(0, IP.IndexOf(",") ); //wwUtils.ExtractString(IP, "", ",");
Ips.Add(TIP);
}
return Ips.ToArray();
}
/// <summary>
/// Allows you to pass an array of strings that contain the IP Addresses
/// to block.
///
/// Wildcard IPs should use .* or .0 to indicate blocks.
///
/// Note this string list should contain ALL IP addresses to block
/// not just new and added ones (ie. use GetList first and then
/// add to the list.
/// </summary>
/// <param name="IPStrings"></param>
public void SetIpList(string[] IPStrings)
{
this.Open();
object IPSecurity = IIS.Properties["IPSecurity"].Value;
// *** IMPORTANT: This list MUST be object or COM call will fail!
List<object> newIpList = new List<object>();
foreach (string Ip in IPStrings)
{
string newIp;
if (Ip.EndsWith(".*.*.*") || Ip.EndsWith(".0.0.0"))
newIp = Ip.Replace(".*", ".0") + ",255.0.0.0";
else if (Ip.EndsWith(".*.*") || Ip.EndsWith(".0.0"))
newIp = Ip.Replace(".*", ".0") + ",255.255.0.0";
else if (Ip.EndsWith(".*") || Ip.EndsWith(".0"))
{
// *** Wildcard requires different IP Mask
newIp = Ip.Replace(".*", ".0") + ",255.255.255.0";
}
else
newIp = Ip + ", 255.255.255.255";
// *** Check for dupes - nasty but required because
// *** object -> string comparison can't do BinarySearch
bool found = false;
foreach (string tempIp in newIpList)
{
if (newIp == tempIp)
{
found = true;
break;
}
}
if (!found)
newIpList.Add(newIp);
}
//wwUtils.SetPropertyCom(this.IPSecurity, "GrantByDefault", true);
IPSecurity.GetType().InvokeMember("GrantByDefault",
BindingFlags.Public |
BindingFlags.Instance | BindingFlags.SetProperty,
null, IPSecurity, new object[] { true });
object[] ipList = newIpList.ToArray();
// *** Apply the new list
//wwUtils.SetPropertyCom(this.IPSecurity, "IPDeny",ipList);
IPSecurity.GetType().InvokeMember("IPDeny",
BindingFlags.Public |
BindingFlags.Instance | BindingFlags.SetProperty,
null, IPSecurity, new object[] { ipList });
IIS.Properties["IPSecurity"].Value = IPSecurity;
IIS.CommitChanges();
IIS.RefreshCache();
this.Close();
}
/// <summary>
/// Adds IP Addresses to the existing IP Address list
/// </summary>
/// <param name="IPString"></param>
public void AddIpList(string[] newIps)
{
string[] origIps = this.GetIpList();
List<string> Ips = new List<string>(origIps);
foreach (string ip in newIps)
{
Ips.Add(ip);
}
this.SetIpList(Ips.ToArray());
}
/// <summary>
/// Returns a list of all IIS Sites on the server
/// </summary>
/// <returns></returns>
public IIsWebSite[] GetIIsWebSites()
{
// *** IIS://Localhost/W3SVC/
string iisPath = this.MetaBasePath.Substring(0,this.MetaBasePath.ToLower().IndexOf("/w3svc/")) + "/W3SVC";
DirectoryEntry root = new DirectoryEntry(iisPath);
List<IIsWebSite> Sites = new List<IIsWebSite>();
foreach (DirectoryEntry Entry in root.Children)
{
System.DirectoryServices.PropertyCollection Properties = Entry.Properties;
try
{
IIsWebSite Site = new IIsWebSite();
Site.SiteName = (string)Properties["ServerComment"].Value;
// *** Skip over non site entries
if (Site.SiteName == null || Site.SiteName == "")
continue;
Site.IISPath = Entry.Path;
Sites.Add(Site);
}
catch { ; }
}
root.Close();
return Sites.ToArray();
}
private void Open()
{
this.Open(this.MetaBasePath);
}
private void Open(string IISMetaPath)
{
if (this.IIS == null)
this.IIS = new DirectoryEntry(IISMetaPath);
}
private void Close()
{
if (IIS != null)
{
this.IIS.Close();
this.IIS = null;
}
}
#region IDisposable Members
public void Dispose()
{
if (this.IIS != null)
IIS.Close();
}
#endregion
}
/// <summary>
/// Container class that holds information about an IIS Web site
/// </summary>
public class IIsWebSite
{
/// <summary>
/// The display name of the Web site
/// </summary>
public string SiteName
{
get { return _SiteName; }
set { _SiteName = value; }
}
private string _SiteName = "";
/// <summary>
/// The IIS Metabase path
/// </summary>
public string IISPath
{
get { return _IISPath; }
set { _IISPath = value; }
}
private string _IISPath = "";
}
This code is pretty easy to use with just a few lines of code. Here are the two methods in the ASP.NET form that use the class:
private void ShowBlockedIps()
{
string[] ipDenyList = IpList.GetIpList();
StringBuilder sb = new StringBuilder();
int count = 0;
foreach (string IP in ipDenyList)
{
sb.AppendLine(IP);
}
this.lblIpCount.Text = (ipDenyList.Length * 10).ToString() + " blocked addresses";
this.txtAddresses.Text = sb.ToString();
}
protected void btnUpdate_Click(object sender, EventArgs e)
{
string enteredIps = this.txtAddresses.Text;
string[] ipStrings = enteredIps.Replace("\n", "").Split(new char[1] { '\r' }, StringSplitOptions.RemoveEmptyEntries);
this.IpList.SetIpList(ipStrings);
this.ShowBlockedIps();
}
The two functions GetIpList and SetIpList work with string arrays and simply retrieve or set the IPDenyList.
The class takes a few liberties with the formatting of the values to set - it basically allows using .* or .0 as wildcard characters which trigger the subnetmask to be updated so you can enter things like this:
192.168.1.101
201.123.123.*
201.2.*.*
There's also a method used to retrieve a list of Web sites and their Metabase paths so you can quickly switch to another site and apply the same set of IP restrictions.
Security
As mentioned above in order for any of this to work you need to make sure you run as an Administrator or under SYSTEM and that your application is running in Full Trust. Without full trust you can't access DirectoryServices/IISAdmin. So this is hardly a general purpose utility, but something you'd have to confine to an admin area of the site.
What I do is use Windows Authentication and programmatic Impersonation to force an Admin account to process these requests. I had talked about this before (to some criticism), but using programmatic Impersonation is a great way to raise the underlying permissions of a single request rather than the entire Web application (which is the only way you can do this with ASP.NET's native configuration). Programmatic Impersonation lets you essentially impersonate the logged on Windows Authentication user for just a single request without having to run the entire site with Impersonation enabled. It's pretty simple to do with this code:
void Impersonate()
{
if (!string.IsNullOrEmpty(this.User.Identity.Name) && this.User.IsInRole("Administrators"))
{
WindowsIdentity id = this.User.Identity as WindowsIdentity;
WindowsImpersonationContext context = id.Impersonate();
}
return;
}
Do this at the beginning of the request and off you are (as long as Windows Authentication is enabled and Full Trust is used).
More interesting Uses
The Web page interface is useful, but it would be even nicer if you could do something like this more automatically. For example, when the Web site generates a dangerous request or an Invalid ViewState error and you can check whether the requests is likely a SPAM type entry, you can automatically add entries to this list.
You probably wouldn't want to add these entries directly as part of your application because of the overhead and more importantly because of the security requirements - a regular request is DEFINITELY not going to have the rights to write into the Metabase nor should it. So what I did instead is dump out the invalid IP address requests in a text file, which I can periodically access and cut and paste into the list. This actually has made it much easier to get a list of IPs to block rather quickly.
You can find the source code for all of this here:
http://www.west-wind.com/files/tools/misc/IpAddressBlock.zip
Other Posts you might also like