Application Security and Development STIG Logging Requirements with IIS

The Defense Information Systems Agency (DISA) releases Security Technical Implementation Guides (STIGs). These are often-lengthy lists of recommended secure configurations. U.S. Department of Defense agencies are required to implement them.

Most STIGs are product-specific and detail very specific configuration settings (ex. set this Windows Server Group Policy setting to a specific value). The Application Security and Development (ASD) STIG is a broader and more generic STIG that applies to any application not covered by a product-specific STIG. It is generally used by developers inside the DoD when creating custom applications.

Dozens of the ASD STIG requirements relate to application logging. For web applications being hosted by IIS, I recommend leveraging the built-in IIS logging features. This accomplishes a few things:

  • Reduces the overall amount of code you have to write
  • Minimizes the risk of logging failures by separating logging from the application itself
  • If you aren’t the developer, it gives you a way to provide logging without having to request code changes from the vendor
  • Relies on functionality that the IIS STIG is already requiring you to have enabled anyway

If you have properly implemented the IIS STIG, you will be collecting at least the following fields:

  • Date
  • Time
  • Client IP Address
  • User Name
  • Method
  • URI Query
  • Protocol Status
  • Referrer
  • Request Header >> Connection
  • Request Header >> Warning
  • Request Header >> Authorization
  • Response Header >> Content-Type

Let’s take a look at a log entry and see how this information can help us meet our ASD STIG requirements:

2023-11-01 09:15:32 W3SVC1 MYSERVER 192.168.0.200 POST /BestWebAppEver/Offices/Edit/156143 - 443 EXAMPLE\johnnie.doe 192.168.15.12 HTTP/1.1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/119.0.0.0+Safari/537.36 ASP.NET_SessionId=blahblahblah;+__RequestVerificationToken_blahblahblah /BestWebAppEver/Offices/Show/156143 myserver.example.com 200 0 0 72650 2903 491 keep-alive - - text/html;+charset=utf-8

If you are following best practices with our web app, we can learn a whole lot based on just the HTTP method, URI, and returned status code. For example, the above tells me:

  • POST means the user was submitting something that would result in a data change
  • The URI tells me that the user was editing the “Office” object with an ID of 156143
  • The “200” tells me the app returned an HTTP 200, which means the action was successful

The usefulness of these logs is based in large part on how consistent and descriptive the URIs are. If the application is based on an MVC framework, the URI queries/stems will usually tell you everything you need to know. In the above example, we were able to tell exactly what record was edited based purely on the URI. The username field told us who executed the action, and the HTTP response code told us it was successful.

A good web app will return an HTTP 403 if somebody does something their account forbids. If you are ingesting your IIS logs into a SIEM (and you really should), you can now build dashboards and processes for failed actions in your web apps simply by filtering for HTTP 403s. For example, imagine an automated alert that triggers if any given user triggers three or more HTTP 403s in a ten minute timespan. You will be able to easily see from these IIS logs exactly what the user was trying.

All of the above means I can satisfy at least 39 ASD STIG requirements just via IIS logging:

STIG-IDSTIG Quick Description
APSC-DV-000520Privileged functions
APSC-DV-000610Centralized control over what to audit
APSC-DV-000670Event must include date and time
APSC-DV-000680HTTP User-Agent, Referer, and Method headers
APSC-DV-000690Client IP address
APSC-DV-000700Username
APSC-DV-000710Grant privileges
APSC-DV-000720Access security objects
APSC-DV-000730Access security levels
APSC-DV-000740Access categories of information
APSC-DV-000750Modify privileges
APSC-DV-000760Modify security objects
APSC-DV-000770Modify security levels
APSC-DV-000780Modify categories of information
APSC-DV-000790Delete privileges
APSC-DV-000800Delete security levels
APSC-DV-000810Delete application database security objects
APSC-DV-000820Delete categories of information
APSC-DV-000830Logon attempts
APSC-DV-000840Privileged activities
APSC-DV-000860Access to objects
APSC-DV-000910Initiate session auditing on startup
APSC-DV-000960Data access
APSC-DV-000970Data modification
APSC-DV-000980Date and time
APSC-DV-000990Relevant component
APSC-DV-001000Relevant application and client IP
APSC-DV-001010Success or failure
APSC-DV-001020Username
APSC-DV-001030Detailed logging for privileged requests
APSC-DV-001050Centralized control over what to audit
APSC-DV-001250Must use OS clock for logging
APSC-DV-001260Timestamps map to UTC
APSC-DV-001270Timestamps granular to one second
APSC-DV-001280Audit data access restrictions
APSC-DV-001290Audit data modification restrictions
APSC-DV-001300Audit data deletion restrictions
APSC-DV-001420Audit configuration changes
APSC-DV-003360Log concurrent logons from separate systems

ModSecurity Configuration for Passbolt

ModSecurity is a module for Apache that acts as an “application firewall.” It uses rulesets to limit what kinds of actions are allowed when accessing Apache. Potentially dangerous HTTP actions are blocked, and exceptions are made for the actions required for the application to function. ModSecurity is powered by a large collection of rules that try to filter out anything that could potentially be dangerous. Most web applications will require some rule exceptions to function. ModSecurity rules are intentionally heavy by default.

This post assumes you already have knowledge about administering Apache with ModSecurity installed. If you are running Passbolt, this configuration in your VirtualHost block should allow Passbolt to function correctly:

<Location "/auth/verify.json">
        SecRuleRemoveById 200004 942100
</Location>

<Location "/import/resources.json">
        SecRuleRemoveById 942100
</Location>

<Location "/resources.json">
        SecRuleRemoveById 942100
</Location>

<LocationMatch "^/resources/.*">
        SecRuleRemoveById 911100 980130 942100
</LocationMatch>

<LocationMatch "^/users/.*">
        SecRuleRemoveById 911100
</LocationMatch>

<LocationMatch "^/setup/completeRecovery/.*\.json">
        SecRuleRemoveById 980130 911100 949110
</LocationMatch>

Disabling Insecure Protocols and Ciphers in Apache

You will find a lot of recommended Apache configurations to disable insecure ciphers in Apache, especially to try to make the Qualsys SSL Labs test happy. It took me a while to figure out one that isn’t over-complicated:

SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!SHA1:!SHA256:!SHA384:!RSA

When searching, you will generally find SSLProtocol settings that disable all protocols and then explicitly enable TLS 1.2 and TLS 1.3. I prefer to explicitly disable so that as Apache upgrades make new TLS versions available, they will automatically become available. This is obviously a rare event, so it’s all about what works best for you. You could argue that you shouldn’t make new protocols available without consciously deciding to. I see good and bad to both methods.

Editing PDFs in PowerShell

iText is an excellent library for editing PDFs, available for both Java and .NET. PowerShell is capable of loading and using .NET libraries, so with a little magic, we can utilize the .NET iText library (licensed under AGPL) to edit PDFs in PowerShell

Add-Type -Path "$PSScriptRoot\Common.Logging.Core.dll"
Add-Type -Path "$PSScriptRoot\Common.Logging.dll"
Add-Type -Path "$PSScriptRoot\itext.io.dll"
Add-Type -Path "$PSScriptRoot\itext.kernel.dll"
Add-Type -Path "$PSScriptRoot\itext.forms.dll"
Add-Type -Path "$PSScriptRoot\itext.layout.dll"
Add-Type -Path "$PSScriptRoot\BouncyCastle.Crypto.dll"

The above DLLs include the iText libraries and dependencies. Here is a convenient ZIP file with the required DLL files:

There are additional iText DLLs you may want to load for additional features. The full AGPL iText library and links to dependencies are available at the following URL:

https://www.nuget.org/packages/itext7/

The full documentation is available at the following URL:

https://itextpdf.com/en/resources/api-documentation/itext-7-net

I won’t go into full detail on how to use this library in this post, but I will cover some basics and give a few examples to show how to use a .NET library in a PowerShell script.

First, we initialize a PdfReader object for accessing the PDF:

$Reader = [iText.Kernel.Pdf.PdfReader]::new("C:\temp\example.pdf")

Optionally, if you want to ignore password protection on a PDF, you can add the following line after:

$Reader.SetUnethicalReading($True)

Next, we initialize a PdfWriter object to write our modified PDF to:

$Writer = [iText.Kernel.Pdf.PdfWriter]::new("C:\example\new_example.pdf")

Now we generate a PdfDocument object to tie our PdfReader and PdfWriter together and allow us to start our work:

$PdfDoc = [iText.Kernel.Pdf.PdfDocument]::new($Reader, $Writer)

For this example, let’s find a form field and fill it with data. The PdfAcroForm class works with static PDF forms. We’ll start by collecting the form and all of the fields that belong to it:

$Form = [iText.Forms.PdfAcroForm]::getAcroForm($PdfDoc, $True)
$Fields = $Form.getFormFields()

getFormFields() returns a dictionary where the key is the name of the field and the value is the actual PdfFormField object. If we want to work with the field named “EmployeeName”, we can easily do so:

$Field = ($Fields | Where-Object {$_.key -eq "EmployeeName"}).Value

Now that we have the field object, we can easily set its value with the SetValue function:

$Field.SetValue("DOE, JOHN")

Once we’re done working with the document, committing our changes is as simple as calling the Close function on the PdfDocument object:

$PdfDoc.Close()

The file opened with the PdfWriter object will now exist with the changes.

fail2ban Jail for WordPress Logins

Failed WordPress logins do not result in an HTTP 400-series status code. There are some very good reasons for this, and it is a conscious decision on the part of the WordPress developers to return an HTTP 200. There is a long and interesting discussion on this on the WordPress bug tracker.

While this makes it slightly more difficult to programmatically detect failed logins, it is still possible without altering your WordPress install. WordPress login attempts are submitted via POST request to wp-login.php or xmlrpc.php. A failed login results in an HTTP 200 that returns the user to the same login page. A successful login results in an HTTP 302 redirect to the originally requested page or the admin dashboard. Ironically, this means we can rely on HTTP 200 as an indicator of a failed login.

To create a jail using this method, simply add a .conf file to /etc/fail2ban/filter.d. I chose to name mine wordpress-login.conf. The contents will look like this:

[Definition]
failregex = ^<HOST> -.*"POST /(wp-login|xmlrpc)\.php.* HTTP/.*" 200 .*$

Note that this regular expression is written specifically for the Apache access log. If you are using another web server, alter the expression accordingly. Just ensure you are looking for POSTs to wp-login.php or xmlrpc.php that result in an HTTP 200.

Once you’ve added your filter, update your jail.local to enable it:

[wordpress-login]
enabled = true
port = http,https
logpath = %(apache_access_log)s

Restart fail2ban for the new configuration to take effect.

You can test by failing a login and then checking /var/log/fail2ban.log for the detection:

INFO    [wordpress-login] Found 192.168.0.15 - 2020-01-29 20:58:01