Mitigating Password Recovery Weaknesses with ModSecurity and Lua

Fairly often applications include a password recovery feature that uses extremely weak questions. Password recovery mechanisms are problematic as they can be used as a secondary means of authentication which are not subject to the same strict criteria as the primary means of authentication. A perfect example of this is when U.S. Vice Presidential candidate Sarah Palin's private Yahoo account became compromised when the son of a Tennessee state representative reset Palin's password using the password recovery mechanism which required her birth date, ZIP code and information about where she met her spouse. All this information was publicly available and gathered within 45 minutes.

An out-of-band method should be used to allow users to recover their passwords. SMS can be a cheap method to notify users of their new passwords. A free alternative is to send an email containing a unique, time-limited, un-guessable single-use recovery URL. The email should be sent to the address that the user provided during registration. This post describes how you can use ModSecurity and Lua to accomplish this. The process is as follows:

  1. Initialize the SESSION collection based on the session token.
  2. Detect that the password recovery page was requested and redirect the user to our psuedo password recovery script instead.
  3. Create a one time token and tie it to the session
  4. Detect whether the one time URL tied to the session was already accessed.
  5. Use a Lua script to lookup the user's email account and send the user an email containing their one time password recovery URL
  6. Verify that the submitted token matches the stored value

The Implementation

 

Initialize the Session

SecRule REQUEST_COOKIES:PHPSESSID ^(.+)$ \
  "phase:2,capture,log,pass,chain,\
  msg:'Initializing session: %{TX.0}',setsid:%{TX.0}"
  SecRule &REQUEST_COOKIES:PHPSESSID "@eq 1"

SecRule SESSION:IS_NEW "@eq 1" \
    "phase:1,nolog,pass,\
    setvar:SESSION.TIMEOUT=1200"
    The above rules creates a session collection based on the PHPSESSID cookie only when a single cookie is found in the request headers. The session timeout is set to 20 minutes to ensure that the one time url is time limited. Notice, we set the timeout by checking the IS_NEW variable. This way the timeout does not get reset on subsequent requests that contain the PHPSESSID cookie.

Detect the Password Recovery Request and Redirect the User

SecRule REQUEST_URI "/PasswordRecovery.php" "t:none,log, \
  msg:'Password Recovery Page Detected',chain,redirect:/passwd.html"
  SecRule &SESSION:PASS "!@eq 1"

    The above rule detects whether the password recovery page was requested, redirects the user to our psuedo password recovery script, and finally the script only matches if the SESSION.PASS variable is not set. We set the SESSION.PASS variable next if the user sends a request with the proper one time token.

    The /passwd.html simply contains a form where users submit there username via the RecoverUserId parameter.

Create a one time token and tie it to the session

# Only run once
SecRule ARGS:RecoverUserId ^(.+)$ \
  "phase:2,capture,log,chain, \
  pass,setvar:SESSION.USERNAME=%{TX.0}, \
  setvar:SESSION.TOKEN=SuperSecretString%{TX.0}-%{TIME_EPOCH}, \
  msg:'Initializing User Collection with %{ARGS.RecoverUserId}'"
  SecRule &ARGS:RecoverUserId "@eq 1" chain
    SecRule &SESSION:RANDOMTOKEN "@eq 0" chain
      SecRule SESSION:TOKEN ^(.+)$ "capture,t:none,t:sha1,t:hexEncode, \
        setvar:SESSION.RANDOMTOKEN=%{TX.1}"

    The rule above detects whether a single RecoverUserId parameter was submitted. If so, parse out the submitted username and set the SESSION.USERNAME variable.

    Next, we create a SESSION.TOKEN variable that is made up of a random secret, the submitted username and the unix epoch. This is obviously a poor man's hack to create a one time token in ModSecurity rules. Ideally, we could create the one time token in a Lua script and set the token in a collection variable, but unfortunetely ModSecurity does not currently support this.

    If the SESSION.RANDOM variable has not been set yet we create a sha1 hex encoded hash of the SESSION.TOKEN variable. We only want to run this rule once to prevent an attacker from continuously changing the valid one time hash effectively creating a denial of service condition preventing the user from regaining control of their account.

Use a Lua script to lookup the user's email account and send the user an email containing their one time password recovery URL

SecRule &ARGS:RecoverUserId "@eq 1" "phase:2,redirect:/passwd1.html, \
  exec:/opt/modsecurity/etc/rules/PasswordRecovery.lua"

    This rule verifies again that only one RecoverUserId parameter was submitted, executes our password recovery script and redirects the user to /passwd1.html. The main advantage to using a Lua script (besides for efficiency) is that Lua can access all of the ModSecurity variables including our collection data.

    I imagine the redirection rules seems a bit peculiar. As mentioned before, the /passwd.html web page was a simple HTML form the submitted the client's username to allow ModSecurity to parse the value and set it in a session collection. Instead of actually creating a proper script we use ModSecurity to detect that RecoverUserId was submitted and then redirect the user to a generic web page simulating an actual script. The generic web page simply states if the username submitted was valid, an email will be sent shortly containing further instructions.

     

The Lua script

-- Requires the following Debian packages
-- liblua5.1-md5-0 liblua5.1-socket2 liblua5.1-rex-pcre0
-- lua-apr
function main()

  local smtp = require "socket.smtp"
  local http = require "socket.http"
  local rex = require "rex_pcre"

  -- Retrieve global user value 
  local username = m.getvar("SESSION.USERNAME");
  local token = m.getvar("SESSION.RANDOMTOKEN");
  
  -- Create URL variable to query
  local url = "http://private.example.com/?user_id=" .. username
  
  -- Query the user's email
  -- html = http.request(url);

  -- Parse the email address from the response
  local email = rex.match(html, "(.*?)")

  -- Create one time short lived random email
  local randomurl = "http://example.com/?PasswordRecoveryToken="
..  token

  r, e = smtp.send {
    from = 'admin@example.com',
    rcpt = email,
    server = 'localhost',
    source = smtp.message{
        headers = {
            to = email,
            From = "admin@example.com",
            subject = "Password Recovery Instructions"
        },
        body = "To reset your account please click " .. 
        "on the following link: " .. randomurl
    }
  }

return nil;
end

    Every Lua rule needs to have an entry point that ModSecurity can find, which is the main() function. Next we include various packages and create the username and token parameters. These use the m.getvar() function to retrieve ModSecurity variables. In this case, we were able to query a private web app created for this purpose to lookup user's email addresses. Another option would be to store the mapping in a flat file and have Lua parse that file instead. After querying the email address, we parse out the value from the XML formatted response and create the randomurl variable that contains the URL with the one time token. Finally we send an email to the user that includes further instructions.

     

Verify that the submitted token matches the stored value

SecRule &ARGS:PasswordRecoveryToken "@eq 1" \
  "phase:2,chain,t:none,redirect:/PasswordRecovery.php,log, \
  msg:'Enabling SESSION.PASS %{SESSION:RANDOMTOKEN} eq
  %{ARGS.PasswordRecoveryToken}'"
  SecRule ARGS:PasswordRecoveryToken "@eq %{SESSION.RANDOMTOKEN}" \
    setvar:SESSION.PASS=1

    In the rule above, we check that there is only one PasswordRecoveryToken parameter submitted, to prevent brute force attacks. If there is then we compare the value of the PasswordRecoveryToken parameter to the value previously saved in the SESSION collection. If they match then we set the SESSION.PASS parameter to 1 and redirect the user to /PasswordRecovery.php. This time around, however, our previous rule looking for requests for this URL wont match due to the SESSION.PASS parameter set to 1.

Post new comment

The content of this field is kept private and will not be shown publicly.

Most Popular List

06/05/2011 | Written By Gordon Maddern | 62,348 Hits
About a month ago I was chatting on skype to a colleague about a payload for...
15/10/2011 | Written By Ty Miller | 17,595 Hits
Lets say that at some point you decided to adhere to security best practices...
28/06/2011 | Written By Sandeep Nain | 15,471 Hits
Coming from a family of civil engineers, I always knew that it is a rigorous...
24/05/2011 | Written By Gordon Maddern | 8,504 Hits
Skype has patched and released the fix for the Skype bug we found so we can d...

Most Recent Posts List

19/05/2013 | Written By Josh Zlatin | 3,220 Hits
Often when implementing customised ModSecurity solutions we need to...
07/05/2013 | Written By Richard Brown | 347 Hits
The term ‘ethical hacker’ is often misrepresented as the keywords...
05/04/2013 | Written By Gordon Maddern | 483 Hits
I recently had to go in to bat for a client who was told by their PCI auditor...
04/03/2013 | Written By Ty Miller | 2,308 Hits
  If you are anything like me, when you hear "Hacking in the Year 2...