#! /usr/bin/env python

# This code is released under the BSD License.

# This code was originally written for The Shmoo Group's (TSG)
# Rainbow Tables project.

# Copyright (c) 2006, 2007 Holt Sorenson <hso@nosneros.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:

#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above
#     copyright notice, this list of conditions and the following
#     disclaimer in the documentation and/or other materials provided with
#     the distribution.
#   * Neither the name of the <ORGANIZATION> nor the names of its
#     contributors may be used to endorse or promote products derived from
#     this software without specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# $Id: shg.py 250 2007-02-07 22:52:37Z hso $

import sys
import getopt

import re
import random
import smbpasswd
import traceback

RCSID="$Id: shg.py 250 2007-02-07 22:52:37Z hso $"

__version__ = "0.1." + "" + RCSID.split()[2] + ""


def usage():
    """function: usage()

    Prints usage.

    Returns:
    None
    """

    print "\nusage for " + sys.argv[0] + " v" + __version__ + ":\n\n", \
          "\t-u, --uid\tinitial numeric user id\n", \
          "\t-f, --file\toutput save file\n", \
          "\t-i, --in\tplaintext password file\n", \
          "\t-l, --len\tgenerated password length\n", \
          "\t-m, --mode\tmode (1...5)\n", \
          "\t          \t1: alpha\n", \
          "\t          \t2: alpha-numeric\n", \
          "\t          \t3: alpha-numeric-symbol14\n", \
          "\t          \t4: alpha-numeric-symbol32space\n", \
          "\t          \t5: un?reversible psuedo-hash\n", \
          "\t-n, --num\tprint cleartext password\n", \
          "\t-p, --prtpass\tprint cleartext password\n"


# sanCheck
def sanCheck(ret_opt, defaults):
    """function: sanCheck(dict ret_opt, dict defaults)

    Sanity checks CLI options in dict ret_opt, setting to values in 
    dict defaults where necessary.

    Returns:
    dict ret_opt - program options 
    """

    # iterate through defaults dict setting any missing
    # keys in the ret_opt dict
    for k in defaults.keys():
        ret_opt.setdefault(k, defaults[k])

    # check mode
    if ret_opt["mode"] < 1 or ret_opt["mode"] > 5:
        print "invalid mode: " + str(ret_opt["mode"]) + \
              " setting to default: " + str(defaults["mode"])
        ret_opt["mode"] = defaults["mode"]

    # mode 0 is reserved for reading from a file and shouldn't
    # ever be set by the user via -m or --mode. it trumps other
    # modes (if a user requests input from a file and other
    # options on the commandline, including mode, input from a 
    # file wins).
    if ret_opt["infile"] != "@@_##-unset-##_@@":
        ret_opt["mode"] = 0

    # check uid, num, and len
    for k in ["uid", "num", "len"]:
        if ret_opt[k] < 1:
            print "invalid " + k + ": " + str(ret_opt[k]) + \
                  " setting to default: " + str(defaults[k])
            ret_opt[k] = defaults[k]

    return ret_opt


# parse_args
def handleArgs(argv):
    """function: handleArgs(list argv)

    Sets program options based on CLI arguments passed in using list argv.

    Returns:
    dict ret_opt - program options 
    list ret_args - arguments not processed by getopt
    """

    ret_opt = {}
    ret_args = {}

    getopt_ok = 1
    flag_opt = ""

    defaults = {"mode": int(1), \
                "uid": int(1000), \
                "num": int(8), \
                "len": int(7), \
                "file": "@@_##-unset-##_@@", \
                "infile": "@@_##-unset-##_@@", \
                "prtpass": int(0)
               }

    flag_opts = {"-p": "prtpass", "--prtpass": "prtpass", \
                 "-f": "file", "--file": "file", \
                 "-i": "infile", "--in": "infile", \
                 "-l": "len", "--len": "len", \
                 "-m": "mode", "--mode": "mode", \
                 "-n": "num", "--num": "num", \
                 "-u": "uid", "--uid": "uid" \
                }

    flag_vals = {"prtpass": 0, \
                 "file": "s", \
                 "infile": "s", \
                 "uid": "i", \
                 "len": "i", \
                 "mode": "i", \
                 "num": "i" \
                }

    try:
        (opts, args) = \
            getopt.gnu_getopt(argv, "pf:i:l:m:n:u:", \
            ["file=", "in=", "uid=", "len=", "mode=", "num=", "prtpass"])

        if len(opts) > 0:
            # set remaining arguments that getopt didn't recognize
            # so they can be passed back to caller
            ret_args = args

            # process options returned from gnu_getopt
            for anopt in opts:
                # resolve the flag to the option name
                try:
                    flag_opt = flag_opts[anopt[0]]
                except KeyError:
                    flag_opt = None

                if flag_opt != None:
                    # handle flags passing in a string value
                    if flag_vals[flag_opt] == "s":
                        ret_opt[flag_opt] = anopt[1]
                    elif flag_vals[flag_opt] == "i":
                        # handle flags passing in an integer value
                        try:
                            ret_opt[flag_opt] = int(anopt[1])
                        except ValueError:
                            print "invalid " + flag_opt + ": " + str(anopt[1]) \
                                  + " setting to default: " + \
                                  str(defaults[flag_opt])
                            ret_opt[flag_opt] = defaults[flag_opt]
                    else:
                        # handle flags that don't pass in a value
                        ret_opt[flag_opt] = 1

    # if getopt throws a GetoptError exception and there were
    # arguments then getopt found an invalid argument. print
    # usage and return empty ret_opt and ret_args so that program
    # will exit
    except getopt.GetoptError:
        if len(argv) > 0:
            getopt_ok = 0
            usage()
            ret_opt = {}
            ret_args = {}

    # set any unset defaults and check sanity
    if getopt_ok:
        ret_opt = sanCheck(ret_opt, defaults)

    # return empty args if argv was empty
    if len(argv) < 1:
        ret_args = {}

    return ret_opt, ret_args
        

def getRndStr(mode, len):
    """function: getRndStr(int mode, int len)

    generates a ridicously low entropy psuedo-random string of length int
    len with contents determined by int mode.

    Returns:
    string rstr
    """

    # mode isn't checked here, it's checked in handleArgs so
    # if a bogus mode is somehow passed from somewher else,
    # you'll have minor problems (like getRndStr hitting maxlen
    # and failing an assertion)...
    valid = 0

    maxlen = 10000
    curlen = 1
    broken = 0

    rstr = ""

    aRe = "^[A-Z]$"
    anRe = "^[A-Z0-9]$"
    ans14Re = "^[A-Z0-9!@#$%^&*()-_+=]$"
    ans32spRe = "^[\x20-\x60\x7b-\x7e]$"

    while len > 0:
        if mode == 5:
            ch = str(hex(random.randint(0,255))).upper().lstrip('0X').zfill(2)
        else:
            while not valid:
                ch = chr(random.randint(32, 126))
                # alpha
                if mode == 1 and re.compile(aRe).match(ch):
                    valid = 1
                # alpha, num
                elif mode == 2 and re.compile(anRe).match(ch):
                    valid = 1
                # alpha, num, sym14
                elif mode == 3 and re.compile(ans14Re).match(ch):
                    valid = 1
                # alpha, num, sym32, space is included in 32...126
                elif mode == 4 and re.compile(ans32spRe).match(ch):
                    valid = 1
                # something's weird, random strings shouldn't get to 10000
                # perhaps we've been passed an invalid mode?
                if curlen > maxlen:
                    broken = 1
                    break
                curlen += 1

        len -= 1
        rstr += ch

    try:
        assert(not broken)
    except AssertionError:
        print "\nError: something wicked this way came" + \
              " while trying to generate a random string:\n" \
              "mode: " + str(mode) + " curlen: " + str(curlen)\
              + " maxlen: " + str(maxlen) + " len: " + str(len) + "\n"
        traceback.print_exc(file=sys.stderr)
        print ""
        sys.exit(3)

    return rstr


# main
def mkPassList(opt):
    """function: mkPassList(argv)

    invoked by main to generate a list of passwords to be hashed
    based on options passed in via dict opt

    Returns:
    list pass_list
    """

    pass_list = []
    pass_str = ""
    len = 0
    togen = 0

    if opt["infile"] != "@@_##-unset-##_@@":
        # read passwords from file
        try:
            f = open(opt["infile"], 'r')
            for line in f:
                pass_list.append(line.rstrip())
            f.close()
        except IOError:
            pass_list = []
    else:
        # generate passwords
        togen = opt["num"]
        while togen > 0:
            # generate bogus hashes
            if opt["mode"] == 5:
                pass_list.append( \
                                    getRndStr(opt["mode"], 16) + \
                                    ":" + getRndStr(opt["mode"], 16) \
                                )
            else:
                len = opt["len"]
                # generate hashes based on random chars concatenated
                # to pass_str
                while len > 0:
                    pass_str += getRndStr(opt["mode"], 1)
                    len -= 1
                pass_list.append(pass_str)

            pass_str = ""
            togen -=1

    return pass_list


# main
def main(argv=None):
    """function: main(argv)

    invoked by main to execute program based on CLI args passed
    in using list argv.

    Returns:
    int ret
    """

    ret = 0

    pass_str = ""
    out_str = ""
    opt = {}
    args = []
    passwords = []

    # init argv if we're just getting started
    if argv is None:
        argv = sys.argv

    # process command line args (if any) and set defaults
    (opt, args) = handleArgs(sys.argv[1:])

    # opt[ions] are never zero unless something has gone wrong since
    # they get set to defaults
    if len(opt) == 0:
        ret = 1
    else:
        #print >> sys.stderr, "DEBUG n: " + str(opt["num"])
        #print >> sys.stderr, "DEBUG i: " + str(opt["infile"])
        #print >> sys.stderr, "DEBUG f: " + str(opt["file"])
        #print >> sys.stderr, "DEBUG m: " + str(opt["mode"])
        #print >> sys.stderr, "DEBUG u: " + str(opt["uid"])
        #print >> sys.stderr, "DEBUG l: " + str(opt["len"])

        togen = opt["num"]
        uid = opt["uid"]

        # open save file for appending, if requested and if possible
        if opt["file"] == "@@_##-unset-##_@@":
            out_stream = sys.stdout
        else:
            try:
                out_stream = open(opt["file"], "a")
            except IOError:
                sys.stderr.write("Unable to open" + opt["file"] + \
                                 ". Using stdout.\n")
                out_stream = sys.stdout

        # read or generate passwords
        passwords = mkPassList(opt)

        # check to see if any passwords were read or generated
        if len(passwords) <= 0:
            if opt["infile"] != "@@_##-unset-##_@@":
                print >> sys.stderr, "Error reading from file: '" + \
                                     opt["infile"] + "'. Unable to continue."
            else:
                print >> sys.stderr, "Error generating passwords. Unable" \
                                     " to continue."
            ret = 1
        else:
            # generate number of passwords requested by user or set by
            # default
            for p in passwords:
                # generate bogus hashes
                if opt["mode"] == 5:
                    out_str = "pwn" + str(uid) + ":" + str(uid) + ":" + p + ":::"
                    p = "!!UNPRINTABLE_CANARD"
                else:
                    # generate hashes based on random chars concatenated
                    # to pass_str
                    out_str = "pwn" + str(uid) + ":" + str(uid) + ":" + \
                              smbpasswd.lmhash(p) + \
                              ":" + smbpasswd.nthash(p) + ":::"

                # print the password string if requested by user
                if opt["prtpass"]:
                    print >> out_stream, "; " + str(opt["mode"]) + ": '" + p + "'"

                # print the hashes
                print >> out_stream, out_str

                pass_str = ""
                togen -= 1
                uid += 1

    return ret 


# call the main function if we're in __main__ and exit
# with whatever the main function returns
if __name__ == "__main__":
    sys.exit(main())
