10

One of my clients has asked me to modify the login page that their board members use to access their materials. The problem here is that the person previously responsible for this has left no documentation behind and the passwords are encrypted with some (seemingly simple) algorithm.

I have access to the hashes, and I know what the passwords are. The javascript is using the hash as their password. My thought is that if I can figure out what the algorithm is that I can create new accounts to suit their request.

Is there a way I can check to see what the algorithm is?

The user is prompted to select their name from a drop down menu and their password is associated with the hash beside their name in the HTML code. The form option looks like this (note that N denotes a numerical value and L denotes a letter).

<option value='Username|NNNNN|LLLLLLLL'>Username

The actual script that parses the value looks like this

<SCRIPT LANGUAGE="JavaScript">
<!-- Begin
var params = new Array(4);
var alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI";

function check(form) {
    which = form.memlist.selectedIndex;
    choice = form.memlist.options[which].value + "|";
    if (choice == "x|") {
        alert("Please Select Your Name From The List");
        return;
    }
    p = 0;
    for (i = 0; i < 3; i++) {
        a = choice.indexOf("|", p);
        params[i] = choice.substring(a, p);
        p = a + 1;
    }
    h1 = makehash(form.pass.value, 3);
    h2 = makehash(form.pass.value, 10) + " ";
    if (h1 != params[1]) {
        alert("Incorrect Password!");
        return;
    };
    var page = "";
    for (var i = 0; i < 8; i++) {
        letter = params[2].substring(i, i + 1)
        ul = letter.toUpperCase();
        a = alpha.indexOf(ul, 0);
        a -= (h2.substring(i, i + 1) * 1);
        if (a < 0) a += 26;
        page += alpha.substring(a, a + 1);
    };
    top.location = page.toLowerCase() + ".html";
}

function makehash(pw, mult) {
    pass = pw.toUpperCase();
    hash = 0;
    for (i = 0; i < 8; i++) {
        letter = pass.substring(i, i + 1);
        c = alpha.indexOf(letter, 0) + 1;
        hash = hash * mult + c;
    }
    return (hash);
}
// End -->
</script>

Is there anyway I can reverse engineer this so that I can create new user accounts?

Ilmari Karonen
  • 4,406
  • 20
  • 28
DKNUCKLES
  • 9,247
  • 2
  • 37
  • 48
  • 4
    Your code was close to unreadable, so I ran it through [jsbeautifier.org](http://jsbeautifier.org/) to fix the indentation and spacing. I do understand that you wanted to post the original code exactly as it was, but that doesn't help much if it takes too much effort to read. – Ilmari Karonen Jul 26 '12 at 11:47
  • 1
    Oh my gosh, this is an amazing candidate for DailyWTF. – fluffy Jul 29 '12 at 16:42

3 Answers3

25

The algorithm here is:

function makehash(pw, mult) { // Password and... multiplier?
    pass = pw.toUpperCase(); // Case insensitivity
    var hash = 0;
    for (i = 0; i < Math.min(pass.length, 8); i++) { // 8 char passwords max...
        c = pass.charCodeAt(i) - 63; // A = 2, B = 3, etc.
        hash *= mult;
        hash += c;
    }

    return hash;
}

I cleaned the code up a bit and added some comments. Whoever wrote this is utterly incompetent in the fields coding, security and mathematics. Anyway, it is no "official" algorithm like MD5 or AES, but homebrew and incredibly fault-intolerant. It accepts only letters, is case-insensitive, and ignores all characters after the first 8.

I would highly recommend upgrading everyone's password hash. See also: How to securely hash passwords?

By the way, here is the rest of the code with some formatting:

var params=new Array(4);
var alpha="ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI";

function check(form) {
    which = form.memlist.selectedIndex;
    choice = form.memlist.options[which].value + "|";
    if (choice == "x|") {
        alert("Please Select Your Name From The List");
        return;
    }

    p = 0;
    for (i = 0; i < 3; i++) {
        a = choice.indexOf("|", p);
        params[i] = choice.substring(a, p);
        p = a + 1;
    }
    h1 = makehash(form.pass.value, 3);
    h2 = makehash(form.pass.value, 10) + " ";
    if (h1 != params[1]) {
        alert("Incorrect Password!");
        return;
    }

    var page = "";
    for (var i = 0; i < 8; i++) {
        letter = params[2].substring(i, i + 1)
        ul = letter.toUpperCase();
        a = alpha.indexOf(ul, 0);
        a -= h2.substring(i, i + 1) * 1; // multiplying by one? Seriously?
        if (a<0)
            a+=26;
        page += alpha.substring(a, a + 1);
    };
    top.location=page.toLowerCase() + ".html";
}

I would add comments, but I'm not sure if it's worth it to find any reason in this mess.

Luc
  • 32,378
  • 8
  • 75
  • 137
  • 2
    Yeah this 8 letter stuff is really crazy. – Andrew Smith Jul 25 '12 at 20:24
  • Thank you for that! I'm no coder but even I've been able to figure out that this is not the best code. Despite all that I find myself confused how to reverse engineer this :( – DKNUCKLES Jul 25 '12 at 20:30
  • 1
    Is this script server sided? If it's not, have a look a t the serversided code, because someone might be able to dump right about anything in your DB otherwise. – Lucas Kauffman Jul 25 '12 at 21:12
  • 1
    @LucasKauffman - You assume too much--I don't even think there IS a database. This is just a script to redirect you to a "secret" 8-character page if you type the right 8-character password. Or you can just guess what that page is from looking at the hash and seeing its one of just 10^8 possible values and brute force it. – dr jimbob Jul 26 '12 at 13:09
  • 3
    Thanks for your great feedback - I think it's safe to say that the best course of action is to re-do this page completely. – DKNUCKLES Jul 30 '12 at 20:29
16

Searching for a section of code yields the page this was taken from:

http://www.yaldex.com/FSPassProtect/LoginCoder.htm

among many other locations. This should tell you how to add another entry.

However, this is an extremely insecure authentication mechanism that can be trivially bypassed by anyone remotely competent as all you have to do is guess the page (case-insensitive 8 character string ending in .html) that you will redirected to if you enter the password correctly. And really its won't take a quick script trying 26^8 different URLs; all you have to do is look at the encoded value 'BAFJFFCI' and try up to 10^8 9^7 if there's only one user; if there are multiple users going to the same page, you can reduce the space significantly.


Ok, I've deconstructed how this works. Every user in the login form has an option with a value like "User name|35240|BAFJFFCI". The user name is not used anywhere and is irrelevant. The next hash, called h1, is generated by calling make_hash on the entered password string with a multiplier of 3.

def make_hash(pw, mult):
    hash = 0
    for char in pw:
        char_ind = ord(char) - ord('A') +1 # A=1, B=2, C=3, ...
        hash *= mult
        hash += char_ind
    return hash

So if your password is 'insecure', that gets translated into numbers (I=9, n=14, s=19, ...) [9,14,19,5,3,21,18,5], so your hash is 3*(3*(3*(3*(3*(3*(3*(9)+14)+19)+5)+3)+21)+18)+5, which works out to be 35240. Note its case-insensitive. So what's the second value? Well if you type a valid password, it will forward you to a secret page. In my case the secret page was 'AAAAAAAA' which was will be decoded from the field 'BAFJFFCI'

To decode they generate another hash with a multiplier of 10. h2=make_hash(pw, 10) which in my case works out to be h2=105955285. Each digit starting from the left, tells me how many characters to shift by. E.g., 'AAAAAAAA' if you shift the first character forward one letter gives 'B', second character forward 0 gives 'A', shift 'A' forward 5 characters 'F', forward 9 characters 'J', ... and similar to recover 'AAAAAAAA' from 'BAFJFFCI' with the h2 (generated from the password) you simply subtract the letters.

As an attacker this means seeing the hash of each user in the password field there are only ten possible letters for the source page. So 10^8 possible webpages to try and check. If multiple users all get sent forward to the same page, you can easily figure out the destination page, and just go there yourself bypassing the entire authentication procedure.

For example, both 'GCDDGDAJ' and 'BAFJFFCI' are shifted version pointing back to aaaaaaaa.html. By assuming both go to the same page, and seeing 'G', 'B' both originate shifted by 0-9 characters from the same letter, I know the first letter is either A,B,X,Y,Z. I can easily see if its possible for two h2 to forward to the same page by making sure that no letters are further than 9 characters apart (e.g., if in one password there's an 'A' and in the other password in the same spot there's a K,L,M,N,O,P, or Q then the passwords can't go to the same page) and the distance apart also limits the values for the real spot.


And for more crypto-analysis:

Also you can find collisions for this hash very simply. For simplicity, I'll analyze a three character password generated with this scheme. So now h1 = 3*(3*c1+c2)+c3). Taking the modulus (remainder after division with symbol %) with respect to 3, you see that h1 % 3 = c3 % 3. Thus if h1 % 3 = 0 the last-letter is in CFILORUX; if h1 % 3 = 1, its in ADGJMPSVY; if h1 % 3 = 2 its in BEHKNQTWZ. You can use this procedure to reduce a three character password down to at most 9^2 = 81 possibilities.

For example, let's say the pw is 'THE' and hash was 3*(3*20+8)+5=209. As 209 % 3 = 2; we know the last character is in BEHKNQTWZ. We can try all 9 choices subtract off the last character, divide by 3 and repeat again. E.g., if the last letter was B, then (209-2)/3 = 69 which means and 69 % 3 = 0, so the middle letter is in CFILORUX. If it was a C, then we get the first letter must be (69-3)=22 = V; hence VCB has a hash of 209. Repeating this for other choices: UFB, TIB, SLB, ROB, QRB, PUB, OXB. If last letter was E; we'd have VBE, UEE, THE, SKE, RNE, QQE, PTE, OWE, NZE. This is fairly easy to iterate through; so you'd have a max of 9^2 = 81 choices (each letter besides the last which is determined by what's left-over; has either 8 or 9 choices). In our case there are 78 choices:

VCB UFB TIB SLB ROB QRB PUB OXB VBE UEE THE SKE RNE QQE PTE OWE NZE VAH UDH TGH SJH RMH QPH PSH OVH NYH UCK TFK SIK RLK QOK PRK OUK NXK UBN TEN SHN RKN QNN PQN OTN NWN MZN UAQ TDQ SGQ RJQ QMQ PPQ OSQ NVQ MYQ TCT SFT RIT QLT POT ORT NUT MXT TBW SEW RHW QKW PNW OQW NTW MWW LZW TAZ SDZ RGZ QJZ PMZ OPZ NSZ MVZ LYZ

These have 78 unique mult=10 hashes that you'd have to try to get the URL (I've sorted them):

1476 1483 1546 1553 1560 1567 1574 1616 1623 1630 1637 1644 1651 1658 1665 1686 1693 1700 1707 1714 1721 1728 1735 1742 1756 1763 1770 1777 1784 1791 1798 1805 1812 1826 1833 1840 1847 1854 1861 1868 1875 1882 1896 1903 1910 1917 1924 1931 1938 1945 1952 1966 1973 1980 1987 1994 2001 2008 2015 2022 2036 2043 2050 2057 2064 2071 2078 2085 2092 2127 2134 2141 2148 2155 2162 2218 2225 2232

Note, that only the first three digits of the mult-10 hash matter in altering the URL so really there were only 62 different values. Or for simplicity I could just find the largest/smallest mod hash, and check the first three digits for everything in between (checking 77 values; after finding VCB and LYZ).

Note, they the full mult-10 hashes all have the same value mod 7 (all 6). This pattern (having the same remainder divided by 10-3=7) will be true in general. So you could also just figure out the first and last allowable password 'VCB' giving mult=10 hash 2232, and 'LYZ' giving mult-10 hash of 1476 and try all ((2232-1476)/7)+1=109 mult-10 hashes in that range separated by 7 and only use the first n-digits from the mult-10 hash.

Again, regardless of the method you'll have a few million URLs to check, which at a rate of 100/second should take under a day.

dr jimbob
  • 38,936
  • 8
  • 92
  • 162
  • Addition (since I can't edit my previous anymore): Two errors in your code: the password isn't uppercased, and the comment should be like in my code. A=2, not 1. – Luc Jul 26 '12 at 00:23
  • @Lucb1e - You had a mistake in your code. It is A = 1 as javascript is zero indexed. Copy alpha and the makehash function into a javascript console, and type `makehash('AAAAAAAA',3)` and you get 3280. Type into a calculator: `3*(3*(3*(3*(3*(3*(3*(1)+1)+1)+1)+1)+1)+1)+1` and get 3280. Or notice "ABC".indexOf('A') gives 0. (I didn't point this out in your answer as its subtle and a small implementation detail in your otherwise great answer). I also refactored out the capitalization from the hash function for simplicity and put case-insensitive in the text. – dr jimbob Jul 26 '12 at 13:00
  • (E.g., so if your password was 'insecure' you call `makehash("INSECURE", 3)` and `makehash("INSECURE", 10)` to get the actual (unsalted) hash and recover the eight-char forwarding URL; again its a minor detail and doesn't change the fact that the best attack is still to just guess all the possible 10^8 URLs you may be redirected to). – dr jimbob Jul 26 '12 at 13:06
  • It's even worse than that actually, an attacker does not need to check 10^8 pages, but only 26063 possible pages. – Lie Ryan Jul 26 '12 at 16:49
3

This hashing algorithm is so insecure that it takes about 2 minutes to calculate all possible passwords and page combination. It generates a list of 20000 to 2500000 possible secret pages that you need to actually check with the server. For @drjimbob's example of 'INSECURE' to 'aaaaaaaa.html' for example, the you need only to check only 26063 possible secret pages. The others fared better, but not really by much. In fact this password scheme actually weakens the security compared to just prompting the user for the URL to the html page. In that case an attacker had to check 26^8 possible pages, however due to the password scheme, an attacker only need to check 0.00122% of all those 26^8 possibilities.

>>> # "insecure", "aaaaaaaa.html"
>>> len(set(y for x,y in find_pages(35240, 'BAFJFFCI')))
3248895

>>> # "password", "mainpage.html"
>>> len(set(y for x,y in find_pages(42691, 'NGLOQEMM')))
2988569

>>> # "theirpwd", "endpages.html"
>>> len(set(y for x,y in find_pages(52219, 'GNLVAPMV')))
3035974

>>> # "asdfvcxz", "nowheres.html"
>>> len(set(y for x,y in find_pages(18215, 'PXAPGWKY')))
2382856

>>> # "zaqxswde", "logintop.html"
>>> len(set(y for x,y in find_pages(64403, 'NUIRTURT')))
2792596




import string
chars = [(c, ord(c) - ord('A') + 1) for c in string.uppercase]
chars = chars[2::3], chars[::3], chars[1::3]
chars = [list(reversed([(a,b,c) for a,(b,c) in enumerate(chs)])) for chs in chars]

def reverse_make_hash(hash, num=8, cur=''):
    """ generates a list of pw such that `make_hash(pw, 3) == hash` """
    if num <= 0 or hash <= 0: return
    if num == 1 and hash > 26: return
    if num == 1 and hash <= 26: 
        yield chr(ord('A') + hash - 1) + cur

    mod = hash % 3
    for i,c,v in chars[mod]:
        n = (hash - v) / 3
        for pot in reverse_make_hash(n, num-1, c + cur): yield pot

# from dr jimbob
def make_hash(pw, mult):
    hash = 0
    for char in pw:
        char_ind = ord(char) - ord('A') +1 # A=1, B=2, C=3, ...
        hash *= mult
        hash += char_ind
    return hash

def find_pages(hash, page):
    results = []
    for p in reverse_make_hash(hash): 
        page_url = ''.join(chr((ord(p)-int(h)-ord('A')) % 26 +ord('A')) for p,h in zip(page, str(make_hash(p, 10))))
        if page_url.isalpha():
            print p, page_url
            results.append((p, page_url.lower() + '.html'))
    return results

a = find_pages(35240, 'BAFJFFCI')  # "insecure", "aaaaaaaa.html"
b = find_pages(42691, 'NGLOQEMM')  # "password", "mainpage.html"
c = find_pages(52219, 'GNLVAPMV')  # "theirpwd", "endpages.html"
d = find_pages(18215, 'PXAPGWKY')  # "asdfvcxz", "nowheres.html"
e = find_pages(64403, 'NUIRTURT')  # "zaqxswde", "logintop.html"
Benoit Esnard
  • 13,979
  • 7
  • 65
  • 65
Lie Ryan
  • 31,279
  • 6
  • 69
  • 93
  • Awesome answer, but I think you somewhat undercounted when saying only 26063 choices. For pw="INSECURE"; with hash=35240, your method gets 3,812,714 by my count with `len(set(list(reverse_make_hash(35240,8))))`. Much more consistent with my updated rough estimate of at most 9^7 (somewhere between 9^7 and 8^7). And this is a bit of an overestimate as only the first 8 decimal digits of the mult=10 hash matter, so they'll be some repeats. – dr jimbob Jul 26 '12 at 17:28
  • Oh now I see. The difference in our counts is you assume that if you can't shift "B" backwards two spaces. But I believe their scheme is smart enough for that; look at the original JS code for the part `if (a < 0) a += 26;`. (Shifting 'B' backwards two places goes 'Z'.) But regardless, its still quite weak. – dr jimbob Jul 26 '12 at 17:45
  • 1
    @drjimbob: fixed that up, the code and the counts as well. The number is more reasonable now, but yeah, it's still very weak regardless. – Lie Ryan Jul 29 '12 at 18:19